jQuery(document).ready(function ($) {
if (window.tptQuoteFormConfig === undefined || !window.tptQuoteFormConfig.forms) {
return;
}
/**
* Represents a single Request a Quote modal form.
*
* @param {jQuery} $modal
* @param {int} productId
* @param {object} config
*/
const RequestQuoteForm = function ($modal, productId, config) {
this.$modal = $modal;
this.productId = productId;
this.config = config;
this.state = {};
this.previousActiveElement = null;
this.init = function () {
this.bindEvents();
};
this.handleKeyDown = (e) => {
if (e.key === 'Escape' && this.isOpen()) {
this.close();
}
};
this.bindEvents = function () {
this.$modal.on('click', '.tpt-quote-modal-close', this.close.bind(this));
let isMouseDownOnOverlay = false;
this.$modal.on('mousedown', (e) => {
isMouseDownOnOverlay = (e.target === this.$modal[0]);
});
this.$modal.on('mouseup', (e) => {
if (isMouseDownOnOverlay && e.target === this.$modal[0]) {
this.close();
}
isMouseDownOnOverlay = false;
});
this.$modal.find('form').on('submit', this.submit.bind(this));
};
this.open = function () {
this.previousActiveElement = document.activeElement;
this.renderState();
this.$modal.show();
// Focus first input field for accessibility
setTimeout(() => {
this.$modal.find('input:visible, textarea:visible').not('[type="hidden"], .tpt-quote-modal-close').first().focus();
}, 50);
$(document).on('keydown', this.handleKeyDown);
};
this.close = function () {
this.$modal.hide();
$(document).off('keydown', this.handleKeyDown);
// Restore focus
if (this.previousActiveElement) {
this.previousActiveElement.focus();
}
};
this.updateState = function (newState) {
this.state = { ...this.state, ...newState };
if (this.isOpen()) {
this.renderState();
}
};
this.isOpen = function () {
return this.$modal.is(':visible');
};
this.renderState = function () {
const $fieldsContainer = this.$modal.find('.tpt-quote-fields-container');
if (this.state.quantity) {
const $qtyInputs = $fieldsContainer.find('input.tpt-quote-sync-quantity');
if ($qtyInputs.length) {
$qtyInputs.val(this.state.quantity);
}
}
const currentId = (this.state.variationId && this.state.variationId > 0) ? this.state.variationId : this.productId;
this.$modal.find('.tpt-quote-product-id').val(currentId);
if (this.state.priceHtml) {
this.$modal.find('.tpt-quote-product-info .price').html(this.state.priceHtml);
}
};
this.submit = function (e) {
e.preventDefault();
const $form = $(e.currentTarget);
const submitData = (token) => {
let formData = new FormData($form[0]);
if (token) {
formData.append('g-recaptcha-response', token);
}
const $submitBtn = $form.find('button[type="submit"]');
const originalBtnText = $submitBtn.text();
$.ajax({
url: window.tptQuoteFormConfig.restUrl,
method: 'POST',
data: formData,
processData: false,
contentType: false,
beforeSend: (xhr) => {
xhr.setRequestHeader('X-WP-Nonce', window.tptQuoteFormConfig.nonce);
const submittingText = window.tptQuoteFormConfig.i18n?.defaultSubmittingText || 'Submitting...';
$submitBtn.prop('disabled', true).text(submittingText);
this.$modal.find('.tpt-quote-form-message').empty();
},
success: (response) => {
const successAction = this.config.success_action || 'message';
if (successAction === 'redirect' && this.config.success_redirect_url) {
window.location.href = this.config.success_redirect_url;
} else {
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.set('tier_pricing_table_quote_success', '1');
currentUrl.searchParams.set('form_id', this.config.id);
window.location.href = currentUrl.toString();
}
},
error: (xhr) => {
const msg = xhr.responseJSON?.message || 'An error occurred.';
this.$modal.find('.tpt-quote-form-message').html(`
${msg}
`);
},
complete: () => {
$submitBtn.prop('disabled', false).text(originalBtnText);
}
});
};
if (this.config?.recaptcha_site_key && this.config?.recaptcha_secret_key && typeof grecaptcha !== 'undefined') {
grecaptcha.ready(() => {
grecaptcha.execute(this.config.recaptcha_site_key, { action: 'submit' }).then((token) => {
submitData(token);
});
});
} else {
submitData(null);
}
};
};
/**
* Manages all Request Quote forms on the page.
*/
const RequestQuoteManager = function () {
this.forms = {};
this.autoOpened = false;
this.init = function () {
this.initializeForms();
this.bindGlobalEvents();
};
this.initializeForms = function () {
$('.tpt-quote-modal').each((index, el) => {
const $modal = $(el);
const productId = $modal.find('.tpt-quote-product-id').val();
const formId = $modal.find('.tpt-quote-form-id').val();
const config = window.tptQuoteFormConfig.forms.find(f => f.id == formId);
if (!config || !productId) return;
const quoteForm = new RequestQuoteForm($modal, productId, config);
quoteForm.init();
this.forms[productId] = quoteForm;
});
};
/**
* Resolve the modal for a trigger: its own product's modal, or the shared modal of its parent
* (variations share the parent's modal so triggers inserted by AJAX still have one to open).
* Falls back to the enclosing pricing wrapper for themes overriding the button template.
*/
this.resolveForm = function ($trigger) {
const productId = $trigger.data('product-id');
if (this.forms[productId]) {
return this.forms[productId];
}
const parentId = $trigger.data('parent-id') || $trigger.closest('.tpt__tiered-pricing').data('product-id');
return parentId ? this.forms[parentId] : undefined;
};
this.openForTrigger = function ($trigger) {
const form = this.resolveForm($trigger);
if (!form) {
return;
}
const productId = $trigger.data('product-id');
// Opened from a variation's trigger: make the modal submit that variation.
if (productId && String(productId) !== String(form.productId)) {
form.updateState({ variationId: productId, parentId: form.productId });
}
form.open();
};
this.bindGlobalEvents = function () {
$(document).on('click', '.tpt-request-quote-trigger', (e) => {
e.preventDefault();
this.openForTrigger($(e.currentTarget));
});
$(document).on('tiered_price_update', (event, data) => {
if (!data || !data.__instance || !data.__instance.formatting) return;
const parentId = data.parentId;
const variationId = data.productId !== data.parentId ? data.productId : null;
const priceHtml = data.__instance.formatting.formatPrice(data.price);
const stateUpdate = {
variationId: variationId,
parentId: parentId,
quantity: data.quantity,
priceHtml: priceHtml
};
if (parentId && this.forms[parentId]) {
this.forms[parentId].updateState(stateUpdate);
if (data.quantity) {
this.handleAutoOpen(parseInt(data.quantity, 10), parentId);
}
}
if (variationId && this.forms[variationId]) {
this.forms[variationId].updateState(stateUpdate);
if (data.quantity) {
this.handleAutoOpen(parseInt(data.quantity, 10), variationId);
}
}
});
};
this.handleAutoOpen = function (qty, productId) {
if (this.autoOpened || isNaN(qty)) return;
let $trigger;
if (productId) {
$trigger = $(`.tpt-request-quote-trigger[data-product-id="${productId}"], .tpt-request-quote-trigger[data-parent-id="${productId}"]`).first();
} else {
$trigger = $('.tpt-request-quote-trigger').first();
}
if ($trigger.length) {
const form = this.resolveForm($trigger);
if (form) {
const triggerAttr = $trigger.attr('data-auto-open-quantity');
let autoOpenQty = null;
if (triggerAttr !== undefined) {
// empty string means deactivated, otherwise parse it
autoOpenQty = triggerAttr === "" ? null : triggerAttr;
} else {
// fallback if attribute doesn't exist
autoOpenQty = form.config?.auto_open_quantity || null;
}
if (autoOpenQty) {
const threshold = parseInt(autoOpenQty, 10);
if (!isNaN(threshold) && threshold > 0 && qty >= threshold) {
this.autoOpened = true;
form.open();
}
}
}
}
};
};
// Initialize Manager
const manager = new RequestQuoteManager();
manager.init();
});