1/** 2 * csp-actions.js 3 * 4 * CSP hardening â see docs/csp-hardening-plan.md (Phase 2). Replaces inline on* handlers and 5 * href="javascript:..." attributes with delegated event listeners keyed by data-action (and, as 6 * more families migrate, data-change-action / data-submit-action), so the app no longer depends on 7 * 'unsafe-inline' in script-src. 8 * 9 * Document-level delegation is deliberate: many handlers live in partials injected via AJAX after 10 * page load, which per-element wiring would miss. Delegation at `document` catches those for free. 11 * 12 * The action maps are explicit (no `window[name](...)`) so every invoked function and its typed 13 * arguments are reviewable. The mapped functions (acceptCookies, openPanel_CLICK, ...) stay global, 14 * defined in their existing scripts; this file only changes how they're invoked, and resolves them 15 * lazily at event time (so async-loaded page scripts can define them after this file runs). 16 * 17 * Load once per page (it is idempotent). Loaded from the shared layouts / pilot partials. 18 */ 19(function () { 20 if (window.__cspActionsInit) { return; } 21 window.__cspActionsInit = true; 22 23 // data-action -> handler(el, event). Grows one entry per migrated function. 24 var clickActions = { 25 'cookies-accept': function () { acceptCookies(); }, 26 'cookies-deny': function () { denyCookies(); }, 27 // Product-detail panels (prodDetailGpRazor.js) 28 'panel-open': function (el) { openPanel_CLICK(el.getAttribute('data-panel')); }, 29 'panel-close': function (el) { closePanel_CLICK(el.getAttribute('data-panel')); }, 30 'scroll-top': function () { ScrollToTop_CLICK(); }, 31 'write-review': function () { WriteReview_CLICK(); }, 32 'show-reviews': function () { showReviews(); }, 33 'scroll-to-reviews': function () { scrollToReviews_CLICK(); }, 34 'close-promo-banner': function () { closePromoBannerPopup_CLICK(); }, 35 // Product-detail cart / wishlist / stock-notify (prodDetailGpRazor.js) 36 'add-to-cart-main': function (el) { AddToCartMain_CLICK(+el.dataset.productId, +el.dataset.os); }, 37 'wishlist-add': function () { AddProductToWishlist_CLICK(); }, 38 'wishlist-remove': function (el) { RemoveProductFromWishlist_CLICK(+el.dataset.productId); }, 39 'notify-in-stock': function (el) { NotfyWhenProdInStck_CLICK(+el.dataset.productId); }, 40 'promo-add-to-cart': function (el) { addPromoAndAddToCart_CLICK(+el.dataset.productId, el.dataset.promoCode); }, 41 'list-add-to-cart': function (el) { AddToCart_CLICK(+el.dataset.productId); }, 42 'show-auto-delivery': function () { ShowAutoDeliveryPopup_CLICK(); }, 43 // Account / order (MyAccount views) 44 'profile-cancel': function () { window.profileComponent.cancel(); }, 45 'reorder': function () { reorder(); }, 46 'order-reorder': function () { orderMainComponent.reorder(); }, 47 'logout': function () { window.$HeaderMiddleware.MyAccountMiddleware.logout_CLICK(); }, 48 'open-mobile-nav': function () { window.openMobileNav(); }, 49 'categories-dropdown': function () { TriggerCategoriesDropDown_CLICK(0); }, 50 // Modals / misc DOM one-liners 51 'close-modal': function () { closeModal(); }, 52 'clear-address-modal': function () { var m = document.querySelector('.address-modal'); if (m) { m.innerHTML = ''; } }, 53 'orders-pagination-show': function () { var d = document.querySelector('.orders-pagination'); if (d) { d.style.display = 'block'; } } 54 }; 55 56 // data-change-action -> handler(el) for 'change' events (el is the <select>/<input>). 57 var changeActions = { 58 'attributes-change': function (el) { AttributesDropDown_CHANGE(el); }, 59 'volume-discount-change': function (el) { VolumeDiscountDropDown_CHANGE(el); }, 60 'auto-delivery-toggle': function (el) { AutonDeliveryToggled(el); }, 61 'auto-delivery-change': function (el) { AutoDeliveryDropDown_CHANGE(el); }, 62 'language-select': function (el) { $HeaderMiddleware.LanguageSelector.OnChange(el.value); } 63 }; 64 65 // data-keyup-action -> handler(el) for 'keyup' events. 66 var keyupActions = { 67 'qty-keyup': function (el) { QtyInputField_KeyUP(el); }, 68 'address-keyup': function (el) { address.textKeyUp(el); } 69 }; 70 71 // data-submit-action -> handler(el, event) for 'submit' events. The mapped functions 72 // call event.preventDefault() themselves (matching the old onsubmit="return ..."). 73 var submitActions = { 74 'qty-submit': function (el, e) { QtyInputField_FormSubmit(e); }, 75 'check-captcha': function (el, e) { checkCaptcha(e); } 76 }; 77 78 // data-blur-action -> handler(el) for 'blur'. Delegated in the capture phase because blur doesn't bubble. 79 var blurActions = { 80 'address-blur': function (el) { address.textBlur(el); }, 81 'orders-pagination-hide': function () { var d = document.querySelector('.orders-pagination'); if (d) { d.style.display = 'none'; } } 82 }; 83 84 // Safe wrapper around e.target.closest: the event target isn't always an Element with a 85 // .closest method (blur/focus can target window/document; some events target text or legacy 86 // SVG nodes). Return null in those cases instead of throwing "closest is not a function". 87 function closestEl(e, selector) { 88 var t = e.target; 89 if (!t || typeof t.closest !== 'function') { return null; } 90 return t.closest(selector); 91 } 92 93 function dispatch(el, e) { 94 var fn = clickActions[el.getAttribute('data-action')]; 95 if (!fn) { return; } 96 // Anchors carrying a data-action stand in for the old href="javascript:void(0)". 97 if (el.tagName === 'A') { e.preventDefault(); } 98 fn(el, e); 99 } 100 101 document.addEventListener('click', function (e) { 102 var el = closestEl(e, '[data-action]'); 103 if (el) { dispatch(el, e); } 104 });
105 106 // Keyboard activation for non-native-button elements that took over a former anchor/handler 107 // (e.g. <a role="button"> that lost its href, or focusable divs). Native <button> and anchors 108 // that still have an href already fire click on Enter, so skip them to avoid double-invoking. 109 document.addEventListener('keydown', function (e) { 110 if (e.key !== 'Enter') { return; } 111 // Enter activates a target element (accessibility shim; replaces inline onkeydown Enter->click()). 112 var ec = closestEl(e, '[data-enter-click]'); 113 if (ec) { 114 var tid = ec.getAttribute('data-enter-click'); 115 var target = (tid && tid !== 'self') ? document.getElementById(tid) : ec; 116 if (target) { e.preventDefault(); target.click(); } 117 return; 118 } 119 // Enter activates a data-action element (skip native buttons / href anchors â they already do). 120 var el = closestEl(e, '[data-action]'); 121 if (!el) { return; } 122 if (el.tagName === 'BUTTON') { return; } 123 if (el.tagName === 'A' && el.hasAttribute('href')) { return; } 124 e.preventDefault(); 125 dispatch(el, e); 126 }); 127 128 // Generic delegation for events whose handlers just need the matched element (and event). 129 function delegate(eventType, attr, map) { 130 document.addEventListener(eventType, function (e) { 131 var el = closestEl(e, '[' + attr + ']'); 132 if (!el) { return; } 133 var fn = map[el.getAttribute(attr)]; 134 if (fn) { fn(el, e); } 135 }); 136 } 137 138 function delegateCapture(eventType, attr, map) { 139 document.addEventListener(eventType, function (e) { 140 var el = closestEl(e, '[' + attr + ']'); 141 if (!el) { return; } 142 var fn = map[el.getAttribute(attr)]; 143 if (fn) { fn(el, e); } 144 }, true); 145 } 146 147 delegate('change', 'data-change-action', changeActions); 148 delegate('keyup', 'data-keyup-action', keyupActions); 149 delegate('submit', 'data-submit-action', submitActions); 150 delegateCapture('blur', 'data-blur-action', blurActions); 151})();
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.