PluginProbe
Tiered Pricing Table for WooCommerce / 8.0.2
Tiered Pricing Table for WooCommerce v8.0.2
8.0.2 7.1.7 7.1.5 7.1.4 7.1.1 6.5.0 6.4.0 6.1.0 trunk 1.0 1.1 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.2.0 2.2.1 2.2.2 2.2.3 2.3.0 2.3.1 2.3.2 2.3.3 All 91 releases
tier-pricing-table / src / Addons / RequestAQuote / assets / js / quote-form.js

quote-form.js in Tiered Pricing Table for WooCommerce 8.0.2, at src/Addons/RequestAQuote/assets/js/quote-form.js

298 lines 8.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 /**
189 * Resolve the modal for a trigger: its own product's modal, or the shared modal of its parent
190 * (variations share the parent's modal so triggers inserted by AJAX still have one to open).
191 * Falls back to the enclosing pricing wrapper for themes overriding the button template.
192 */
193 this.resolveForm = function ($trigger) {
194 const productId = $trigger.data('product-id');
195
196 if (this.forms[productId]) {
197 return this.forms[productId];
198 }
199
200 const parentId = $trigger.data('parent-id') || $trigger.closest('.tpt__tiered-pricing').data('product-id');
201
202 return parentId ? this.forms[parentId] : undefined;
203 };
204
205 this.openForTrigger = function ($trigger) {
206 const form = this.resolveForm($trigger);
207
208 if (!form) {
209 return;
210 }
211
212 const productId = $trigger.data('product-id');
213
214 // Opened from a variation's trigger: make the modal submit that variation.
215 if (productId && String(productId) !== String(form.productId)) {
216 form.updateState({ variationId: productId, parentId: form.productId });
217 }
218
219 form.open();
220 };
221
222 this.bindGlobalEvents = function () {
223 $(document).on('click', '.tpt-request-quote-trigger', (e) => {
224 e.preventDefault();
225 this.openForTrigger($(e.currentTarget));
226 });
227
228 $(document).on('tiered_price_update', (event, data) => {
229 if (!data || !data.__instance || !data.__instance.formatting) return;
230
231 const parentId = data.parentId;
232 const variationId = data.productId !== data.parentId ? data.productId : null;
233 const priceHtml = data.__instance.formatting.formatPrice(data.price);
234
235 const stateUpdate = {
236 variationId: variationId,
237 parentId: parentId,
238 quantity: data.quantity,
239 priceHtml: priceHtml
240 };
241
242 if (parentId && this.forms[parentId]) {
243 this.forms[parentId].updateState(stateUpdate);
244 if (data.quantity) {
245 this.handleAutoOpen(parseInt(data.quantity, 10), parentId);
246 }
247 }
248
249 if (variationId && this.forms[variationId]) {
250 this.forms[variationId].updateState(stateUpdate);
251 if (data.quantity) {
252 this.handleAutoOpen(parseInt(data.quantity, 10), variationId);
253 }
254 }
255 });
256 };
257
258 this.handleAutoOpen = function (qty, productId) {
259 if (this.autoOpened || isNaN(qty)) return;
260
261 let $trigger;
262 if (productId) {
263 $trigger = $(`.tpt-request-quote-trigger[data-product-id="${productId}"], .tpt-request-quote-trigger[data-parent-id="${productId}"]`).first();
264 } else {
265 $trigger = $('.tpt-request-quote-trigger').first();
266 }
267
268 if ($trigger.length) {
269 const form = this.resolveForm($trigger);
270
271 if (form) {
272 const triggerAttr = $trigger.attr('data-auto-open-quantity');
273 let autoOpenQty = null;
274 if (triggerAttr !== undefined) {
275 // empty string means deactivated, otherwise parse it
276 autoOpenQty = triggerAttr === "" ? null : triggerAttr;
277 } else {
278 // fallback if attribute doesn't exist
279 autoOpenQty = form.config?.auto_open_quantity || null;
280 }
281
282 if (autoOpenQty) {
283 const threshold = parseInt(autoOpenQty, 10);
284 if (!isNaN(threshold) && threshold > 0 && qty >= threshold) {
285 this.autoOpened = true;
286 form.open();
287 }
288 }
289 }
290 }
291 };
292 };
293
294 // Initialize Manager
295 const manager = new RequestQuoteManager();
296 manager.init();
297 });
298