PageSourceSearch

https://3z.rent/wp-content/plugins/3z-worker-plugin-dist/assets/js/cart-merge.js?ver=1.0.106

js 3z.rent collected 2026-10-05 03:07:52 UTC 8,025 bytes, 232 lines download raw bytes

1/**
2 * Deferred account-cart merge dialog — the client half of TZR_Cart_Merge.
3 *
4 * Enqueued as '3z-rent-cart-merge' on every front-end page: the dialog opens wherever the customer is
5 * once their session exists. A cacheable render ships nothing but an empty `div.tzr-cart-merge` slot;
6 * the dialog itself arrives in the uncached cart-fragments response the picker force-triggers on
7 * every page load, so this script's job is to notice the slot being filled and drive what happens
8 * next — including applying the decision in place afterwards (`ezCartMerge.context` says which
9 * server-rendered cart block this page carries), so a login never gains a navigation.
10 *
11 * Three things here are less obvious than they look:
12 *
13 * - Opening the dialog POSTs `seen` before the customer touches anything. Display IS the decision
14 *   point: from that moment their own cart wins unless they say otherwise, and the server overwrites
15 *   the account cart so a later login cannot re-ask. Closing the dialog, or wandering off, therefore
16 *   needs no cooperation from this script to mean "keep the current list".
17 * - The token guard. WooCommerce re-applies its sessionStorage copy of the last fragments response on
18 *   the next page load, so the same dialog markup can reappear after it has been answered. Each
19 *   snapshot carries an identity and a token is only ever opened once per browser session.
20 * - The dialog is moved out of the fragment slot the instant it opens, leaving a fresh empty slot
21 *   behind. Any later fragments response then lands on that slot instead of ripping the open dialog
22 *   out from under the customer.
23 * - Markup that will not open (an already-seen token, an incomplete fragment) is scrubbed back to
24 *   the empty slot, never left in place: its fixed backdrop covers the whole viewport, and while
25 *   cart-merge.css also disables pointer events on an un-opened dialog, the slot must not carry a
26 *   full-page overlay to begin with.
27 */
28(function () {
29	'use strict';
30
31	/** Timings of the CSS in cart-merge.css the script has to outwait: the columns settling on a
32	 *  choice, the check mark drawing and resting, the dialog fading out. */
33	const DECIDE_MS = 1000;
34	const DONE_MS = 1300;
35	const CLOSE_MS = 300;
36
37	const SEEN_PREFIX = 'tzrCartMergeSeen:';
38
39	function endpoint() {
40		return window.ezCartMerge.wcAjaxUrl.replace('%%endpoint%%', 'tzr_cart_merge');
41	}
42
43	function post(choice, nonce, keepalive) {
44		return fetch(endpoint(), {
45			method: 'POST',
46			credentials: 'same-origin',
47			keepalive: !!keepalive,
48			headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
49			body: new URLSearchParams({ choice: choice, nonce: nonce }).toString(),
50		});
51	}
52
53	// sessionStorage is unavailable in some privacy modes; a throwing guard must not cost the dialog.
54	function seenAlready(token) {
55		try {
56			return window.sessionStorage.getItem(SEEN_PREFIX + token) === '1';
57		} catch (e) {
58			return false;
59		}
60	}
61
62	function markSeen(token) {
63		try {
64			window.sessionStorage.setItem(SEEN_PREFIX + token, '1');
65		} catch (e) {
66			// Nothing to do — the server-side one-shot is the authoritative one either way.
67		}
68	}
69
70	function emptySlot() {
71		const slot = document.createElement('div');
72		slot.className = 'tzr-cart-merge';
73		return slot;
74	}
75
76	/** Move the dialog to <body> and leave an empty slot behind for later fragments to overwrite. */
77	function detachFromSlot(root) {
78		if (root.parentNode) {
79			root.parentNode.insertBefore(emptySlot(), root);
80		}
81		document.body.appendChild(root);
82	}
83
84	/** Replace delivered-but-not-opening markup with the empty slot, exactly as the server ships it. */
85	function scrubSlot(root) {
86		if (root.parentNode) {
87			root.parentNode.replaceChild(emptySlot(), root);
88		}
89	}
90
91	function close(root) {
92		root.classList.remove('is-visible');
93		document.documentElement.classList.remove('tzr-cm-open');
94		window.setTimeout(function () {
95			if (root.parentNode) {
96				root.parentNode.removeChild(root);
97			}
98		}, CLOSE_MS);
99	}
100
101	/**
102	 * The cart may have changed under every server-rendered block painted from it: the mini-cart
103	 * fragments everywhere, plus WooCommerce core's cart-table re-render on the cart page and the
104	 * order-review refresh on checkout — the same three flows the duration picker re-requests.
105	 */
106	function refreshCartBlocks() {
107		if (!window.jQuery) {
108			return;
109		}
110		const body = window.jQuery(document.body);
111		body.trigger('wc_fragment_refresh');
112		const context = window.ezCartMerge.context || '';
113		if (context === 'cart') {
114			body.trigger('wc_update_cart');
115		}
116		if (context === 'checkout') {
117			body.trigger('update_checkout');
118		}
119	}
120
121	function pause(ms) {
122		return new Promise(function (resolve) {
123			window.setTimeout(resolve, ms);
124		});
125	}
126
127	function open(root) {
128		const dialog = root.querySelector('.tzr-cm-dialog');
129		const token = root.getAttribute('data-tzr-token') || '';
130		if (!dialog || !token || seenAlready(token)) {
131			scrubSlot(root);
132			return false;
133		}
134
135		const nonce = dialog.getAttribute('data-tzr-cm-nonce') || '';
136		let deciding = false;
137
138		markSeen(token);
139		detachFromSlot(root);
140		// keepalive: a customer who navigates away the instant the dialog appears must still be
141		// recorded as having seen it, or the next login would ask again.
142		post('seen', nonce, true);
143
144		document.documentElement.classList.add('tzr-cm-open');
145		window.requestAnimationFrame(function () {
146			root.classList.add('is-visible');
147		});
148
149		/**
150		 * `quiet` is a dismissal: it still posts `keep`, but the dialog just goes — the choice
151		 * animation and the check mark are for a decision the customer actually made.
152		 */
153		function decide(choice, quiet) {
154			if (deciding) {
155				return;
156			}
157			deciding = true;
158
159			const answered = post(choice, nonce, true)
160				.then(function (response) {
161					return response.json();
162				})
163				.catch(function () {
164					return null;
165				});
166
167			let played = answered;
168			if (!quiet) {
169				// The animation is CSS, keyed on these attributes — the script only has to outwait it.
170				// The check mark waits for the server's answer, so it never claims a decision that
171				// has not been applied yet.
172				root.setAttribute('data-tzr-cm-deciding', choice);
173				played = pause(DECIDE_MS)
174					.then(function () {
175						return answered;
176					})
177					.then(function () {
178						root.setAttribute('data-tzr-cm-done', '1');
179						return pause(DONE_MS);
180					});
181			}
182
183			Promise.all([answered, played]).then(function () {
184				close(root);
185				refreshCartBlocks();
186			});
187		}
188
189		root.addEventListener('click', function (event) {
190			const target = event.target.closest ? event.target.closest('[data-tzr-cm-choice], [data-tzr-cm-dismiss]') : null;
191			if (!target) {
192				return;
193			}
194			event.preventDefault();
195			// Dismissing is not a separate outcome: it is the default one, said out loud.
196			const choice = target.getAttribute('data-tzr-cm-choice');
197			decide(choice || 'keep', !choice);
198		});
199
200		document.addEventListener('keydown', function (event) {
201			if (event.key === 'Escape' && root.parentNode) {
202				decide('keep', true);
203			}
204		});
205
206		return true;
207	}
208
209	document.addEventListener('DOMContentLoaded', function () {
210		const slot = document.querySelector('.tzr-cart-merge');
211		if (!slot || !slot.parentNode || !window.ezCartMerge) {
212			return;
213		}
214
215		// Already filled (a fragments response that beat DOMContentLoaded), or fill it later — the
216		// slot is replaced wholesale by jQuery's replaceWith, so its parent's child list is what
217		// changes. Nothing else on the page is watched. The parent is captured first: open() may
218		// replace the slot node itself when the markup is scrubbed.
219		const parent = slot.parentNode;
220		if (slot.hasAttribute('data-tzr-token') && open(slot)) {
221			return;
222		}
223
224		const observer = new MutationObserver(function () {
225			const filled = document.querySelector('.tzr-cart-merge[data-tzr-token]');
226			if (filled && open(filled)) {
227				observer.disconnect();
228			}
229		});
230		observer.observe(parent, { childList: true });
231	});
232})();

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.