PageSourceSearch

https://biovea.com/us/scripts/csp-actions.js

js biovea.com collected 2026-10-01 14:08:05 UTC 8,182 bytes, 151 lines download raw bytes

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.