1(function () { 2 'use strict'; 3 4 // AB#24083: the pre-payment webhook remains authoritative for existing carts. 5 // This guard gives immediate feedback on product pages and listing cards. 6 if (window.mhfStockGuard) window.mhfStockGuard.dispose(); 7 var states = new Map(); 8 var pending = new Map(); 9 var disposed = false; 10 var store = (window.FC && window.FC.settings && window.FC.settings.storedomain) || 'mhf.foxycart.com'; 11 12 function lookup(sku) { 13 if (!pending.has(sku)) { 14 var controller = new AbortController(); 15 var timer = setTimeout(function () { controller.abort(); }, 7000); 16 var request = fetch('/umbraco/api/inventorystock?sku=' + encodeURIComponent(sku), { 17 cache: 'no-store', signal: controller.signal 18 }).then(function (response) { 19 if (!response.ok) throw new Error('Stock lookup failed'); 20 return response.json(); 21 }).finally(function () { clearTimeout(timer); pending.delete(sku); }); 22 pending.set(sku, request); 23 } 24 return pending.get(sku); 25 } 26 27 function disable(state, message) { 28 state.available = false; 29 state.link.removeAttribute('href'); 30 state.link.setAttribute('aria-disabled', 'true'); 31 state.link.setAttribute('role', 'link'); 32 state.link.setAttribute('tabindex', '0'); 33 state.message.textContent = message; 34 if (state.selector) state.selector.style.display = 'none'; 35 } 36 37 function quantitySelector(state, max) { 38 var container = state.selector; 39 if (!container) return; 40 var select = container.querySelector('.mhf-qty-select'); 41 var note = container.querySelector('.mhf-quantity-max-note'); 42 if (!select) return; 43 var minimum = Math.max(1, Number(state.url.searchParams.get('quantity_min')) || 1); 44 var configured = state.url.searchParams.get('quantity_options'); 45 var previous = Number(new URL(state.link.href).searchParams.get('quantity')) || Number(select.value); 46 var choices = configured ? configured.split('|').map(Number) 47 : Array.from(select.options).map(function (option) { return Number(option.value); }); 48 if (!configured && !choices.includes(previous)) choices.push(previous); 49 var options = choices.filter(function (n) { 50 return Number.isInteger(n) && n >= minimum && (max === null || n <= max); 51 }); 52 if (!options.length) return; 53 select.replaceChildren(); 54 options.forEach(function (quantity) { 55 var option = document.createElement('option'); 56 option.value = quantity; 57 option.textContent = quantity; 58 select.appendChild(option); 59 }); 60 select.value = options.includes(previous) ? previous : options[0]; 61 select.onchange = function () { 62 if (!state.available) return; 63 var url = new URL(state.link.href); 64 url.searchParams.set('quantity', select.value); 65 state.link.href = url.toString(); 66 }; 67 select.onchange(); 68 if (note) { 69 note.textContent = max === null ? '' : 'Maximum order quantity: ' + max + '. For larger orders, please contact us.'; 70 note.style.display = max === null ? 'none' : ''; 71 } 72 container.style.display = ''; 73 } 74 75 function check(state) { 76 disable(state, 'Checking availability...'); 77 return lookup(state.sku).then(function (data) { 78 if (disposed || !state.link.isConnected) return; 79 if (!data || typeof data.isAvailable !== 'boolean' 80 || String(data.sku).toLowerCase() !== state.sku.toLowerCase() 81 || !(data.quantityMax === null || Number.isInteger(data.quantityMax) && data.quantityMax >= 0)) { 82 throw new Error('Invalid stock response'); 83 } 84 if (!data.isAvailable || data.quantityMax === 0) { 85 disable(state, data.outOfStockMessage || 'Currently unavailable'); 86 return; 87 } 88 var url = new URL(state.url); 89 var originalMax = Number(url.searchParams.get('quantity_max')); 90 var max = data.quantityMax; 91 if (originalMax > 0) max = max === null ? originalMax : Math.min(originalMax, max); 92 var minimum = Math.max(1, Number(url.searchParams.get('quantity_min')) || 1); 93 if (max !== null && max < minimum) { disable(state, 'Currently unavailable'); return; } 94 if (max !== null) url.searchParams.set('quantity_max', max); 95 var quantity = Math.max(minimum, Number(url.searchParams.get('quantity')) || minimum); 96 url.searchParams.set('quantity', max === null ? quantity : Math.min(quantity, max)); 97 state.link.href = url.toString(); 98 state.link.removeAttribute('aria-disabled'); 99 ['role', 'tabindex'].forEach(function (attribute) { 100 if (state.attributes[attribute] === null) state.link.removeAttribute(attribute); 101 else state.link.setAttribute(attribute, state.attributes[attribute]); 102 }); 103 state.available = true; 104 state.message.textContent = ''; 105 quantitySelector(state, max); 106 }).catch(function () { 107 if (!disposed) disable(state, 'Stock could not be checked. Please refresh to try again.'); 108 }); 109 } 110 111 function scan() { 112 if (disposed) return; 113 document.querySelectorAll('a[href]').forEach(function (link) { 114 if (states.has(link)) return; 115 var url; 116 try { url = new URL(link.href); } catch (_) { return; } 117 if (url.hostname !== store || !/^\/cart\/?$/.test(url.pathname) 118 || !url.searchParams.has('name')) return; 119 var sku = (url.searchParams.get('code') || '').trim(); 120 var info = link.closest('.shop_product-info'); 121 var message = document.createElement('span'); 122 message.className = 'mhf-stock-status'; 123 message.setAttribute('role', 'status'); 124 message.style.display = 'block'; 125 link.insertAdjacentElement('afterend', message); 126 var state = { link: link, url: url, sku: sku, message: message, available: false, 127 selector: info && info.querySelector('.mhf-quantity-selector'), 128 attributes: { role: link.getAttribute('role'), tabindex: link.getAttribute('tabindex') } }; 129 states.set(link, state); 130 if (sku) check(state); 131 else disable(state, 'Stock could not be checked. Please contact us.'); 132 }); 133 } 134 135 function guard(event) { 136 var link = event.target.closest && event.target.closest('a'); 137 var state = states.get(link); 138 if (state && !state.available) { 139 event.preventDefault(); 140 event.stopImmediatePropagation(); 141 } 142 } 143 function refresh() {
144 states.forEach(function (state, link) { 145 if (!link.isConnected) states.delete(link); 146 else if (state.sku) check(state); 147 }); 148 scan(); 149 } 150 document.addEventListener('click', guard, true); 151 window.addEventListener('pageshow', refresh); 152 var observer = new MutationObserver(scan); 153 observer.observe(document.body, { childList: true, subtree: true }); 154 window.mhfStockGuard = { dispose: function () { 155 disposed = true; 156 observer.disconnect(); 157 document.removeEventListener('click', guard, true); 158 window.removeEventListener('pageshow', refresh); 159 } }; 160 scan(); 161}());
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.