PageSourceSearch

https://vkusblagosti.ru/wp-content/themes/bono_child/js/coupon-auto-apply.js

js vkusblagosti.ru collected 2026-10-05 00:04:16 UTC 8,043 bytes, 301 lines download raw bytes

1/**
2 * Автоприменение промокода только после REST-проверки: код существует в Woo и ещё не в корзине.
3 * Blur/change не используем (нет спама apply_coupon при наборе).
4 */
5(function ($) {
6	'use strict';
7
8	/**
9	 * Код промокода в тексте уведомлений (в кавычках) — заглавными, как в поле ввода.
10	 * Дублирует PHP-фильтры: CCB вставляет HTML с ответа AJAX, иногда обходя серверную нормализацию.
11	 */
12	function uppercasePromoCodeInsideQuotes(text) {
13		if (!text || typeof text !== 'string') {
14			return text;
15		}
16		return text
17			.replace(/"([A-Za-z0-9_-]+)"/g, function (_, c) {
18				return '"' + c.toUpperCase() + '"';
19			})
20			.replace(/«([A-Za-z0-9_-]+)»/g, function (_, c) {
21				return '«' + c.toUpperCase() + '»';
22			})
23			.replace(/\u201C([A-Za-z0-9_-]+)\u201D/g, function (_, c) {
24				return '\u201C' + c.toUpperCase() + '\u201D';
25			})
26			.replace(/"([A-Za-z0-9_-]+)"/g, function (_, c) {
27				return '"' + c.toUpperCase() + '"';
28			});
29	}
30
31	function uppercaseCouponNoticeTextNodes(root) {
32		var scope = root && root.querySelectorAll ? root : document;
33		var sel =
34			'.cart-coupon-wrap.coupon-box .woocommerce-error li, .cart-coupon-wrap.coupon-box .woocommerce-message li, .cart-coupon-wrap.coupon-box .woocommerce-info li, .ccb-checkout-wrapper ul.woocommerce-error li, .ccb-checkout-wrapper ul.woocommerce-message li';
35		var lis = scope.querySelectorAll(sel);
36		var j, li, walk, node, next;
37		for (j = 0; j < lis.length; j++) {
38			li = lis[j];
39			walk = document.createTreeWalker(li, NodeFilter.SHOW_TEXT, null);
40			while ((node = walk.nextNode())) {
41				next = uppercasePromoCodeInsideQuotes(node.nodeValue);
42				if (next !== node.nodeValue) {
43					node.nodeValue = next;
44				}
45			}
46		}
47	}
48
49	function getConfig() {
50		var c = window.bonoChildCouponCheck;
51		if (!c || !c.restUrl) {
52			return { restUrl: '', nonce: '', minLength: 3, debounceMs: 800 };
53		}
54		return {
55			restUrl: c.restUrl,
56			nonce: c.nonce || '',
57			minLength: typeof c.minLength === 'number' ? c.minLength : 3,
58			debounceMs: typeof c.debounceMs === 'number' ? c.debounceMs : 800
59		};
60	}
61
62	function normalize(code) {
63		return String(code || '').trim();
64	}
65
66	function pickApplyButton($input) {
67		var $wrap = $input.closest('.cart-coupon-wrap.coupon-box');
68		if ($wrap.length) {
69			var $ccbBtn = $wrap.find('button[data-ccb-apply-coupon="1"]').first();
70			if ($ccbBtn.length) {
71				return $ccbBtn;
72			}
73		}
74
75		var $form = $input.closest('form');
76		if ($form.length) {
77			var $btn = $form.find('button[name="apply_coupon"], button[type="submit"]').first();
78			if ($btn.length) {
79				return $btn;
80			}
81		}
82
83		return $('button[data-ccb-apply-coupon="1"], form.checkout_coupon button[name="apply_coupon"], form.checkout_coupon button[type="submit"]').first();
84	}
85
86	function clearDebounceTimer($input) {
87		var t = $input.data('vbAutoApplyTimer');
88		if (t) {
89			window.clearTimeout(t);
90		}
91		$input.removeData('vbAutoApplyTimer');
92	}
93
94	function abortFetch($input) {
95		var prev = $input.data('vbCouponCheckAbort');
96		if (prev && typeof prev.abort === 'function') {
97			prev.abort();
98		}
99	}
100
101	/**
102	 * Синхронизация data-vb-last-auto-applied с фактом корзины после updated_checkout / updated_cart_totals.
103	 */
104	function syncInputWithCart($input) {
105		var cfg = getConfig();
106		if (!cfg.restUrl || !cfg.nonce) {
107			return;
108		}
109		var code = normalize($input.val());
110		if (!code) {
111			$input.removeAttr('data-vb-last-auto-applied');
112			return;
113		}
114
115		abortFetch($input);
116		var ac = new window.AbortController();
117		$input.data('vbCouponCheckAbort', ac);
118
119		var url = cfg.restUrl + (cfg.restUrl.indexOf('?') >= 0 ? '&' : '?') + 'code=' + encodeURIComponent(code);
120
121		window
122			.fetch(url, {
123				method: 'GET',
124				credentials: 'same-origin',
125				signal: ac.signal,
126				headers: {
127					'X-WP-Nonce': cfg.nonce
128				}
129			})
130			.then(function (r) {
131				if (!r.ok) {
132					return null;
133				}
134				return r.json();
135			})
136			.then(function (data) {
137				if (!data || typeof data !== 'object') {
138					return;
139				}
140				if (data.already_applied) {
141					$input.attr('data-vb-last-auto-applied', code);
142				} else {
143					$input.removeAttr('data-vb-last-auto-applied');
144				}
145			})
146			.catch(function () {
147				/* ignore abort */
148			});
149	}
150
151	function onCouponPrecheckDone($input, code, data) {
152		if (!data) {
153			return;
154		}
155
156		if (!data.exists) {
157			$input.attr('data-vb-fail-code', code);
158			return;
159		}
160
161		if (data.already_applied) {
162			$input.attr('data-vb-last-auto-applied', code);
163			return;
164		}
165
166		var $btn = pickApplyButton($input);
167		if (!$btn || !$btn.length) {
168			return;
169		}
170		if ($btn.closest('.processing').length) {
171			return;
172		}
173
174		$input.attr('data-vb-last-auto-applied', code);
175		$btn.trigger('click');
176	}
177
178	function runPrecheck($input) {
179		var cfg = getConfig();
180		if (!cfg.restUrl || !cfg.nonce) {
181			return;
182		}
183
184		var code = normalize($input.val());
185		if (code.length < cfg.minLength) {
186			return;
187		}
188
189		var fail = $input.attr('data-vb-fail-code');
190		if (fail !== undefined && code === fail) {
191			return;
192		}
193
194		var last = normalize($input.attr('data-vb-last-auto-applied'));
195		if (last && code === last) {
196			return;
197		}
198
199		abortFetch($input);
200		var ac = new window.AbortController();
201		$input.data('vbCouponCheckAbort', ac);
202
203		var url = cfg.restUrl + (cfg.restUrl.indexOf('?') >= 0 ? '&' : '?') + 'code=' + encodeURIComponent(code);
204
205		window
206			.fetch(url, {
207				method: 'GET',
208				credentials: 'same-origin',
209				signal: ac.signal,
210				headers: {
211					'X-WP-Nonce': cfg.nonce
212				}
213			})
214			.then(function (r) {
215				if (!r.ok) {
216					return null;
217				}
218				return r.json();
219			})
220			.then(function (data) {
221				onCouponPrecheckDone($input, code, data);
222			})
223			.catch(function (err) {
224				if (err && err.name === 'AbortError') {
225					return;
226				}
227			});
228	}
229
230	function onInputImmediate($input) {
231		var v = normalize($input.val());
232		var fail = $input.attr('data-vb-fail-code');
233		if (fail !== undefined && v !== fail) {
234			$input.removeAttr('data-vb-fail-code');
235		}
236		var lap = $input.attr('data-vb-last-auto-applied');
237		if (lap !== undefined && v !== lap) {
238			$input.removeAttr('data-vb-last-auto-applied');
239		}
240	}
241
242	function buildDebouncedHandler() {
243		var cfg = getConfig();
244		var wait = cfg.debounceMs;
245
246		return function (e) {
247			var el = e && e.target ? e.target : null;
248			if (!el || el.tagName !== 'INPUT' || el.name !== 'coupon_code') {
249				return;
250			}
251			var $input = $(el);
252			onInputImmediate($input);
253
254			clearDebounceTimer($input);
255			var t = window.setTimeout(function () {
256				runPrecheck($input);
257			}, wait);
258			$input.data('vbAutoApplyTimer', t);
259		};
260	}
261
262	$(function () {
263		uppercaseCouponNoticeTextNodes(document);
264
265		$(document.body).on('input', 'input[name="coupon_code"]', buildDebouncedHandler());
266
267		$(document.body).on('updated_checkout updated_cart_totals', function () {
268			$('input[name="coupon_code"]').each(function () {
269				var $i = $(this);
270				clearDebounceTimer($i);
271				syncInputWithCart($i);
272			});
273			window.setTimeout(function () {
274				uppercaseCouponNoticeTextNodes(document);
275			}, 0);
276		});
277
278		$(document.body).on('applied_coupon_in_checkout', function () {
279			window.setTimeout(function () {
280				uppercaseCouponNoticeTextNodes(document);
281			}, 0);
282		});
283
284		// Ответ apply_coupon вставляется в DOM без обязательного updated_checkout в том же тике.
285		if (typeof MutationObserver !== 'undefined') {
286			var moTimer = null;
287			var mo = new MutationObserver(function () {
288				if (moTimer) {
289					window.clearTimeout(moTimer);
290				}
291				moTimer = window.setTimeout(function () {
292					uppercaseCouponNoticeTextNodes(document);
293				}, 50);
294			});
295			var wrap = document.querySelector('.ccb-checkout-wrapper');
296			if (wrap) {
297				mo.observe(wrap, { childList: true, subtree: true });
298			}
299		}
300	});
301})(jQuery);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.