| 1 |
jQuery(document).ready(function ($) { |
| 2 |
|
| 3 |
if (window.tptQuoteFormConfig === undefined || !window.tptQuoteFormConfig.forms) { |
| 4 |
return; |
| 5 |
} |
| 6 |
|
| 7 |
/** |
| 8 |
* Represents a single Request a Quote modal form. |
| 9 |
* |
| 10 |
* @param {jQuery} $modal |
| 11 |
* @param {int} productId |
| 12 |
* @param {object} config |
| 13 |
*/ |
| 14 |
const RequestQuoteForm = function ($modal, productId, config) { |
| 15 |
this.$modal = $modal; |
| 16 |
this.productId = productId; |
| 17 |
this.config = config; |
| 18 |
this.state = {}; |
| 19 |
this.previousActiveElement = null; |
| 20 |
|
| 21 |
this.init = function () { |
| 22 |
this.bindEvents(); |
| 23 |
}; |
| 24 |
|
| 25 |
this.handleKeyDown = (e) => { |
| 26 |
if (e.key === 'Escape' && this.isOpen()) { |
| 27 |
this.close(); |
| 28 |
} |
| 29 |
}; |
| 30 |
|
| 31 |
this.bindEvents = function () { |
| 32 |
this.$modal.on('click', '.tpt-quote-modal-close', this.close.bind(this)); |
| 33 |
|
| 34 |
let isMouseDownOnOverlay = false; |
| 35 |
|
| 36 |
this.$modal.on('mousedown', (e) => { |
| 37 |
isMouseDownOnOverlay = (e.target === this.$modal[0]); |
| 38 |
}); |
| 39 |
|
| 40 |
this.$modal.on('mouseup', (e) => { |
| 41 |
if (isMouseDownOnOverlay && e.target === this.$modal[0]) { |
| 42 |
this.close(); |
| 43 |
} |
| 44 |
isMouseDownOnOverlay = false; |
| 45 |
}); |
| 46 |
|
| 47 |
this.$modal.find('form').on('submit', this.submit.bind(this)); |
| 48 |
}; |
| 49 |
|
| 50 |
this.open = function () { |
| 51 |
this.previousActiveElement = document.activeElement; |
| 52 |
this.renderState(); |
| 53 |
this.$modal.show(); |
| 54 |
|
| 55 |
// Focus first input field for accessibility |
| 56 |
setTimeout(() => { |
| 57 |
this.$modal.find('input:visible, textarea:visible').not('[type="hidden"], .tpt-quote-modal-close').first().focus(); |
| 58 |
}, 50); |
| 59 |
|
| 60 |
$(document).on('keydown', this.handleKeyDown); |
| 61 |
}; |
| 62 |
|
| 63 |
this.close = function () { |
| 64 |
this.$modal.hide(); |
| 65 |
$(document).off('keydown', this.handleKeyDown); |
| 66 |
|
| 67 |
// Restore focus |
| 68 |
if (this.previousActiveElement) { |
| 69 |
this.previousActiveElement.focus(); |
| 70 |
} |
| 71 |
}; |
| 72 |
|
| 73 |
this.updateState = function (newState) { |
| 74 |
this.state = { ...this.state, ...newState }; |
| 75 |
if (this.isOpen()) { |
| 76 |
this.renderState(); |
| 77 |
} |
| 78 |
}; |
| 79 |
|
| 80 |
this.isOpen = function () { |
| 81 |
return this.$modal.is(':visible'); |
| 82 |
}; |
| 83 |
|
| 84 |
this.renderState = function () { |
| 85 |
const $fieldsContainer = this.$modal.find('.tpt-quote-fields-container'); |
| 86 |
|
| 87 |
if (this.state.quantity) { |
| 88 |
const $qtyInputs = $fieldsContainer.find('input.tpt-quote-sync-quantity'); |
| 89 |
if ($qtyInputs.length) { |
| 90 |
$qtyInputs.val(this.state.quantity); |
| 91 |
} |
| 92 |
} |
| 93 |
|
| 94 |
const currentId = (this.state.variationId && this.state.variationId > 0) ? this.state.variationId : this.productId; |
| 95 |
this.$modal.find('.tpt-quote-product-id').val(currentId); |
| 96 |
|
| 97 |
if (this.state.priceHtml) { |
| 98 |
this.$modal.find('.tpt-quote-product-info .price').html(this.state.priceHtml); |
| 99 |
} |
| 100 |
}; |
| 101 |
|
| 102 |
this.submit = function (e) { |
| 103 |
e.preventDefault(); |
| 104 |
const $form = $(e.currentTarget); |
| 105 |
|
| 106 |
const submitData = (token) => { |
| 107 |
let formData = new FormData($form[0]); |
| 108 |
if (token) { |
| 109 |
formData.append('g-recaptcha-response', token); |
| 110 |
} |
| 111 |
|
| 112 |
const $submitBtn = $form.find('button[type="submit"]'); |
| 113 |
const originalBtnText = $submitBtn.text(); |
| 114 |
|
| 115 |
$.ajax({ |
| 116 |
url: window.tptQuoteFormConfig.restUrl, |
| 117 |
method: 'POST', |
| 118 |
data: formData, |
| 119 |
processData: false, |
| 120 |
contentType: false, |
| 121 |
beforeSend: (xhr) => { |
| 122 |
xhr.setRequestHeader('X-WP-Nonce', window.tptQuoteFormConfig.nonce); |
| 123 |
const submittingText = window.tptQuoteFormConfig.i18n?.defaultSubmittingText || 'Submitting...'; |
| 124 |
$submitBtn.prop('disabled', true).text(submittingText); |
| 125 |
this.$modal.find('.tpt-quote-form-message').empty(); |
| 126 |
}, |
| 127 |
success: (response) => { |
| 128 |
const successAction = this.config.success_action || 'message'; |
| 129 |
if (successAction === 'redirect' && this.config.success_redirect_url) { |
| 130 |
window.location.href = this.config.success_redirect_url; |
| 131 |
} else { |
| 132 |
const currentUrl = new URL(window.location.href); |
| 133 |
currentUrl.searchParams.set('tier_pricing_table_quote_success', '1'); |
| 134 |
currentUrl.searchParams.set('form_id', this.config.id); |
| 135 |
window.location.href = currentUrl.toString(); |
| 136 |
} |
| 137 |
}, |
| 138 |
error: (xhr) => { |
| 139 |
const msg = xhr.responseJSON?.message || 'An error occurred.'; |
| 140 |
this.$modal.find('.tpt-quote-form-message').html(`<div style="color: red; margin-bottom: 10px;">${msg}</div>`); |
| 141 |
}, |
| 142 |
complete: () => { |
| 143 |
$submitBtn.prop('disabled', false).text(originalBtnText); |
| 144 |
} |
| 145 |
}); |
| 146 |
}; |
| 147 |
|
| 148 |
if (this.config?.recaptcha_site_key && this.config?.recaptcha_secret_key && typeof grecaptcha !== 'undefined') { |
| 149 |
grecaptcha.ready(() => { |
| 150 |
grecaptcha.execute(this.config.recaptcha_site_key, { action: 'submit' }).then((token) => { |
| 151 |
submitData(token); |
| 152 |
}); |
| 153 |
}); |
| 154 |
} else { |
| 155 |
submitData(null); |
| 156 |
} |
| 157 |
}; |
| 158 |
}; |
| 159 |
|
| 160 |
/** |
| 161 |
* Manages all Request Quote forms on the page. |
| 162 |
*/ |
| 163 |
const RequestQuoteManager = function () { |
| 164 |
this.forms = {}; |
| 165 |
this.autoOpened = false; |
| 166 |
|
| 167 |
this.init = function () { |
| 168 |
this.initializeForms(); |
| 169 |
this.bindGlobalEvents(); |
| 170 |
}; |
| 171 |
|
| 172 |
this.initializeForms = function () { |
| 173 |
$('.tpt-quote-modal').each((index, el) => { |
| 174 |
const $modal = $(el); |
| 175 |
const productId = $modal.find('.tpt-quote-product-id').val(); |
| 176 |
const formId = $modal.find('.tpt-quote-form-id').val(); |
| 177 |
|
| 178 |
const config = window.tptQuoteFormConfig.forms.find(f => f.id == formId); |
| 179 |
if (!config || !productId) return; |
| 180 |
|
| 181 |
const quoteForm = new RequestQuoteForm($modal, productId, config); |
| 182 |
quoteForm.init(); |
| 183 |
|
| 184 |
this.forms[productId] = quoteForm; |
| 185 |
}); |
| 186 |
}; |
| 187 |
|
| 188 |
this.bindGlobalEvents = function () { |
| 189 |
$(document).on('click', '.tpt-request-quote-trigger', (e) => { |
| 190 |
e.preventDefault(); |
| 191 |
const productId = $(e.currentTarget).data('product-id'); |
| 192 |
|
| 193 |
if (this.forms[productId]) { |
| 194 |
this.forms[productId].open(); |
| 195 |
} |
| 196 |
}); |
| 197 |
|
| 198 |
$(document).on('tiered_price_update', (event, data) => { |
| 199 |
if (!data || !data.__instance || !data.__instance.formatting) return; |
| 200 |
|
| 201 |
const parentId = data.parentId; |
| 202 |
const variationId = data.productId !== data.parentId ? data.productId : null; |
| 203 |
const priceHtml = data.__instance.formatting.formatPrice(data.price); |
| 204 |
|
| 205 |
const stateUpdate = { |
| 206 |
variationId: variationId, |
| 207 |
parentId: parentId, |
| 208 |
quantity: data.quantity, |
| 209 |
priceHtml: priceHtml |
| 210 |
}; |
| 211 |
|
| 212 |
if (parentId && this.forms[parentId]) { |
| 213 |
this.forms[parentId].updateState(stateUpdate); |
| 214 |
if (data.quantity) { |
| 215 |
this.handleAutoOpen(parseInt(data.quantity, 10), parentId); |
| 216 |
} |
| 217 |
} |
| 218 |
|
| 219 |
if (variationId && this.forms[variationId]) { |
| 220 |
this.forms[variationId].updateState(stateUpdate); |
| 221 |
if (data.quantity) { |
| 222 |
this.handleAutoOpen(parseInt(data.quantity, 10), variationId); |
| 223 |
} |
| 224 |
} |
| 225 |
}); |
| 226 |
}; |
| 227 |
|
| 228 |
this.handleAutoOpen = function (qty, productId) { |
| 229 |
if (this.autoOpened || isNaN(qty)) return; |
| 230 |
|
| 231 |
let $trigger; |
| 232 |
if (productId) { |
| 233 |
$trigger = $(`.tpt-request-quote-trigger[data-product-id="${productId}"]`).first(); |
| 234 |
} else { |
| 235 |
$trigger = $('.tpt-request-quote-trigger').first(); |
| 236 |
} |
| 237 |
|
| 238 |
if ($trigger.length) { |
| 239 |
const targetProductId = $trigger.data('product-id'); |
| 240 |
const form = this.forms[targetProductId]; |
| 241 |
|
| 242 |
if (form) { |
| 243 |
const triggerAttr = $trigger.attr('data-auto-open-quantity'); |
| 244 |
let autoOpenQty = null; |
| 245 |
if (triggerAttr !== undefined) { |
| 246 |
// empty string means deactivated, otherwise parse it |
| 247 |
autoOpenQty = triggerAttr === "" ? null : triggerAttr; |
| 248 |
} else { |
| 249 |
// fallback if attribute doesn't exist |
| 250 |
autoOpenQty = form.config?.auto_open_quantity || null; |
| 251 |
} |
| 252 |
|
| 253 |
if (autoOpenQty) { |
| 254 |
const threshold = parseInt(autoOpenQty, 10); |
| 255 |
if (!isNaN(threshold) && threshold > 0 && qty >= threshold) { |
| 256 |
this.autoOpened = true; |
| 257 |
form.open(); |
| 258 |
} |
| 259 |
} |
| 260 |
} |
| 261 |
} |
| 262 |
}; |
| 263 |
}; |
| 264 |
|
| 265 |
// Initialize Manager |
| 266 |
const manager = new RequestQuoteManager(); |
| 267 |
manager.init(); |
| 268 |
}); |
| 269 |
|