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.