1/* HES â inner-page behaviour: cart, filters, search, tabs, forms. */ 2(function(){ 3 var ROOT = document.body.getAttribute('data-root') || ''; 4 var LS_KEY = 'hes_cart_v1'; 5 function $(s, c){ return (c||document).querySelector(s); } 6 function $$(s, c){ return [].slice.call((c||document).querySelectorAll(s)); } 7 function money(n){ return '$' + (Math.round(n*100)/100).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','); } 8 function esc(s){ return String(s==null?'':s).replace(/[&<>"']/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]; }); } 9 10 /* ---------------- cart store (per-browser, works without a server) ---------------- */ 11 var mem = {}; 12 function load(){ try{ return JSON.parse(localStorage.getItem(LS_KEY)) || {}; }catch(e){ return mem; } } 13 function save(c){ mem = c; try{ localStorage.setItem(LS_KEY, JSON.stringify(c)); }catch(e){} paintCount(); } 14 function count(c){ c = c || load(); var n = 0; for(var k in c) n += c[k].q; return n; } 15 function subtotal(c){ c = c || load(); var t = 0; for(var k in c) t += c[k].q * c[k].p; return t; } 16 function paintCount(bump){ 17 var n = count(); 18 $$('.cartlink b').forEach(function(b){ b.textContent = n; }); 19 if(bump) $$('.cartlink').forEach(function(a){ a.classList.remove('bump'); void a.offsetWidth; a.classList.add('bump'); }); 20 } 21 function add(item, q){ 22 var c = load(); 23 if(c[item.s]) c[item.s].q += q; else { item.q = q; c[item.s] = item; } 24 save(c); paintCount(true); toast(item, q); 25 } 26 window.HESCart = {load:load, save:save, add:add, count:count, subtotal:subtotal}; 27 paintCount(); 28 29 /* ---------------- toast ---------------- */ 30 var toastEl, toastT; 31 function toast(item, q){ 32 if(!toastEl){ 33 toastEl = document.createElement('div'); toastEl.className = 'toast'; toastEl.setAttribute('role','status'); 34 document.body.appendChild(toastEl); 35 } 36 toastEl.innerHTML = '<span class="ti"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m4 12 5.5 5.5L20 7"/></svg></span>' + 37 '<span class="tt"><b>Added to cart' + (q > 1 ? ' Ã' + q : '') + '</b><span>' + esc(item.n) + '</span></span>' + 38 '<a href="' + ROOT + 'cart.html">View cart</a>'; 39 toastEl.classList.add('show'); 40 clearTimeout(toastT); toastT = setTimeout(function(){ toastEl.classList.remove('show'); }, 3600); 41 } 42 43 /* ---------------- add-to-cart buttons ---------------- */ 44 document.addEventListener('click', function(e){ 45 var b = e.target.closest('[data-add]'); if(!b) return; 46 e.preventDefault(); 47 var item = {s:b.dataset.slug, n:b.dataset.name, p:parseFloat(b.dataset.price)||0, i:b.dataset.img||'', u:b.dataset.url||'', t:b.dataset.type||'book', v:b.dataset.cv||'cv0', a:b.dataset.auth||''}; 48 var q = 1, qi = b.closest('[data-buy]'); qi = qi && qi.querySelector('.qty input'); 49 if(qi) q = Math.max(1, parseInt(qi.value, 10) || 1); 50 add(item, q); 51 b.classList.add('added'); 52 var old = b.innerHTML; 53 if(!b.dataset.keep){ b.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m4 12 5.5 5.5L20 7"/></svg>
53' + (b.classList.contains('btn') ? ' Added' : ''); } 54 setTimeout(function(){ b.classList.remove('added'); if(!b.dataset.keep) b.innerHTML = old; }, 1600); 55 if(b.dataset.go) location.href = ROOT + b.dataset.go; 56 }); 57 58 /* qty steppers */ 59 document.addEventListener('click', function(e){ 60 var b = e.target.closest('.qty button'); if(!b) return; 61 var inp = b.parentNode.querySelector('input'); 62 var v = (parseInt(inp.value, 10) || 1) + (b.dataset.d === '-' ? -1 : 1); 63 inp.value = Math.max(parseInt(inp.min || 1, 10), Math.min(99, v)); 64 inp.dispatchEvent(new Event('change', {bubbles:true})); 65 }); 66 67 /* ---------------- product thumbnails for cart/search (image or generated cover) ---------------- */ 68 function thumb(it){ 69 var t = it.t || 'book', v = it.v || 'cv0'; 70 if(it.i) return thumbCover(it, t, v) + '<img src="' + esc(it.i) + '" alt="" loading="lazy" onerror="this.remove()">'; 71 return thumbCover(it, t, v); 72 } 73 function thumbCover(it, t, v){ 74 if(t === 'med') return '<span class="cover med ' + v + '"><span class="cap"></span><span class="lab"><span class="ct">' + esc(it.n) + '</span></span></span>'; 75 if(t === 'av') return '<span class="cover av ' + v + '"><span class="play"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M7 4.5v15l13-7.5Z"/></svg></span><span class="ct">' + esc(it.n) + '</span></span>'; 76 return '<span class="cover ' + v + '"><span class="ct">' + esc(it.n) + '</span></span>'; 77 } 78 79 /* ---------------- category page: filter + sort ---------------- */ 80 var grid = $('#pgrid'); 81 if(grid){ 82 var cards = $$('.pcard', grid), qIn = $('#pfilter'), sortSel = $('#psort'), cnt = $('#pcount'), none = $('#pnone'); 83 function apply(){ 84 var q = (qIn && qIn.value || '').trim().toLowerCase(), shown = 0; 85 cards.forEach(function(c){ 86 var hit = !q || c.dataset.search.indexOf(q) > -1; 87 c.style.display = hit ? '' : 'none'; if(hit) shown++; 88 }); 89 var mode = sortSel ? sortSel.value : 'featured'; 90 var sorted = cards.slice().sort(function(a, b){ 91 if(mode === 'low') return (+a.dataset.price || 1e9) - (+b.dataset.price || 1e9); 92 if(mode === 'high') return (+b.dataset.price) - (+a.dataset.price); 93 if(mode === 'az') return a.dataset.name.localeCompare(b.dataset.name); 94 if(mode === 'sale') return (+b.dataset.save) - (+a.dataset.save); 95 return (+a.dataset.idx) - (+b.dataset.idx); 96 }); 97 sorted.forEach(function(c){ grid.appendChild(c); }); 98 if(none){ grid.appendChild(none); none.style.display = shown ? 'none' : ''; } 99 if(cnt) cnt.textContent = shown; 100 } 101 if(qIn) qIn.addEventListener('input', apply); 102 if(sortSel) sortSel.addEventListener('change', apply); 103 } 104 105 /* ---------------- tabs ---------------- */ 106 $$('.tabs').forEach(function(t){ 107 var btns = $$('.tabbtns button', t), panels = $$('.tabpanel', t); 108 btns.forEach(function(b, i){ 109 b.addEventListener('click', function(){ 110 btns.forEach(function(x, j){ x.setAttribute('aria-selected', j === i ? 'true' : 'false'); panels[j].hidden = j !== i; }); 111 }); 112 }); 113 }); 114 115 /* ---------------- segmented (account login/register) ---------------- */ 116 $$('.segs').forEach(function(s){ 117 var btns = $$('button', s); 118 btns.forEach(function(b){ 119 b.addEventListener('click', function(){ 120 btns.forEach(function(x){ x.setAttribute('aria-selected', x === b ? 'true' : 'false'); var p = document.getElementById(x.dataset.panel); if(p) p.hidden = x !== b; }); 121 }); 122 }); 123 if(/[?&]tab=register/.test(location.search)){ var r = btns.filter(function(b){ return b.dataset.panel === 'p-register'; })[0]; if(r) r.click(); } 124 }); 125 126 /* ---------------- article filters (chips + text) ---------------- */ 127 var ag = $('#agrid'); 128 if(ag){ 129 var acards = $$('[data-cat]', ag), chips = $$('.chip[data-filter]'), af = $('#artfilter'), acnt = $('#acount'), cur = 'all'; 130 function aapply(){ 131 var q = (af && af.value || '').trim().toLowerCase(), n = 0; 132 acards.forEach(function(c){ 133 var hit = (cur === 'all' || c.dataset.cat === cur) && (!q || c.dataset.title.indexOf(q) > -1); 134 c.style.display = hit ? '' : 'none'; if(hit) n++; 135 }); 136 if(acnt) acnt.textContent = n; 137 var none = $('#anone'); if(none) none.style.display = n ? 'none' : ''; 138 } 139 chips.forEach(function(ch){ 140 ch.addEventListener('click', function(){
141 cur = ch.dataset.filter; 142 chips.forEach(function(x){ x.setAttribute('aria-pressed', x === ch ? 'true' : 'false'); }); 143 aapply(); 144 }); 145 }); 146 if(af) af.addEventListener('input', aapply); 147 } 148 149 /* ---------------- links filter ---------------- */ 150 var lf = $('#linkfilter'); 151 if(lf){ 152 lf.addEventListener('input', function(){ 153 var q = lf.value.trim().toLowerCase(); 154 $$('.lsec').forEach(function(sec){ 155 var n = 0; 156 $$('.lk', sec).forEach(function(l){ var hit = !q || l.textContent.toLowerCase().indexOf(q) > -1; l.style.display = hit ? '' : 'none'; if(hit) n++; }); 157 sec.style.display = n ? '' : 'none'; 158 }); 159 }); 160 } 161 162 /* ---------------- info-page TOC highlight + progress ---------------- */ 163 (function(){ 164 var links = $$('.pg-toc a, .toc a'); 165 if(!links.length) return; 166 var secs = links.map(function(a){ return document.getElementById(a.getAttribute('href').slice(1)); }); 167 var bar = $('#pg-progress'), main = $('.pg-main') || $('.prose'); 168 function onScroll(){ 169 var y = window.scrollY + 150, cur = 0; 170 secs.forEach(function(s, i){ if(s && s.getBoundingClientRect().top + window.scrollY <= y) cur = i; }); 171 links.forEach(function(a, i){ a.classList.toggle('on', i === cur); }); 172 if(bar && main){ 173 var r = main.getBoundingClientRect(), total = r.height - window.innerHeight * .6; 174 bar.style.width = Math.max(0, Math.min(100, (-r.top + 120) / Math.max(1, total) * 100)) + '%'; 175 } 176 } 177 window.addEventListener('scroll', onScroll, {passive:true}); onScroll(); 178 })(); 179 180 /* ---------------- share (copy link) ---------------- */ 181 /* share links: filled with this page's own address */ 182 $$('[data-share]').forEach(function(a){ 183 a.href = a.getAttribute('data-share').replace('{u}', encodeURIComponent(location.href)).replace('{t}', encodeURIComponent(document.title)); 184 }); 185 /* product gallery: thumbnails swap the main photo */ 186 $$('.pd-gthumbs button').forEach(function(b){ 187 b.addEventListener('click', function(){ 188 var fr = document.querySelector('.pd-frame img'); if(fr) fr.src = b.getAttribute('data-full'); 189 $$('.pd-gthumbs button').forEach(function(x){ x.classList.toggle('on', x === b); }); 190 }); 191 }); 192 $$('[data-copy]').forEach(function(b){ 193 b.addEventListener('click', function(){ 194 var done = function(){ b.setAttribute('title', 'Link copied!'); b.style.background = 'var(--green)'; b.style.color = '#fff'; }; 195 if(navigator.clipboard) navigator.clipboard.writeText(location.href).then(done, done); else done(); 196 }); 197 }); 198 199 /* ---------------- demo forms (newsletter, contact, account) ---------------- */ 200 $$('form[data-demo]').forEach(function(f){ 201 f.addEventListener('submit', function(e){ 202 e.preventDefault(); 203 if(!f.checkValidity()){ f.reportValidity(); return; } 204 var ok = document.getElementById(f.dataset.demo); 205 f.style.display = 'none'; 206 if(ok){ ok.hidden = false; ok.scrollIntoView({behavior:'smooth', block:'center'}); } 207 }); 208 }); 209 210 /* ---------------- CART PAGE ---------------- */ 211 var cartRoot = $('#cart-root'); 212 function shipCost(dest, sub){ 213 if(!sub) return 0; 214 if(dest === 'ca') return 35; if(dest === 'intl') return 48; 215 return 8.5; 216 } 217 function renderCart(){ 218 if(!cartRoot) return; 219 var c = load(), keys = Object.keys(c); 220 if(!keys.length){ 221 cartRoot.innerHTML = '<div class="cartbox"><div class="cart-empty"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="20" r="1.3"/><circle cx="19" cy="20" r="1.3"/><path d="M2 3h3l2.4 11.6a1.8 1.8 0 0 0 1.8 1.4h8.6a1.8 1.8 0 0 0 1.8-1.4L21.5 7H6"/></svg><h2>Your cart is empty</h2><p>Browse our books, medicines, kits and e-courses â everything is hand-picked by Dana Ullman and our team.</p><a class="btn btn-green" href="' + ROOT + 'shop.html">Visit the store <em>→</em></a></div></div>'; 222 $('#cart-summary') && ($('#cart-summary').style.display = 'none'); 223 return; 224 }
225 $('#cart-summary') && ($('#cart-summary').style.display = ''); 226 var rows = keys.map(function(k){ 227 var it = c[k]; 228 return '<div class="citem" data-k="' + esc(k) + '"><a class="th" href="' + ROOT + esc(it.u) + '">' + thumb(it) + '</a>' + 229 '<div><a class="nm" href="' + ROOT + esc(it.u) + '">' + esc(it.n) + '</a><div class="row"><span class="qty sm"><button type="button" data-d="-" aria-label="Decrease">−</button><input type="number" min="1" max="99" value="' + it.q + '" aria-label="Quantity"><button type="button" data-d="+" aria-label="Increase">+</button></span><span class="unit">' + money(it.p) + ' each</span><button class="rm" type="button">Remove</button></div></div>' + 230 '<div class="lt">' + money(it.p * it.q) + '</div></div>'; 231 }).join(''); 232 cartRoot.innerHTML = '<div class="cartbox"><div class="hd"><h2>Your cart <span class="muted" style="font-size:16px">(' + count(c) + ')</span></h2><button type="button" id="cart-clear">Clear cart</button></div>' + rows + '</div>'; 233 paintSummary(); 234 } 235 function paintSummary(){ 236 var s = subtotal(), dest = ($('#ship-to') || {}).value || 'us', sh = shipCost(dest, s); 237 var set = function(id, v){ var el = document.getElementById(id); if(el) el.textContent = v; }; 238 set('sum-sub', money(s)); set('sum-ship', s ? money(sh) + (dest === 'us' ? '' : ' (est.)') : 'â'); set('sum-total', money(s + sh)); 239 set('sum-count', count()); 240 } 241 if(cartRoot){ 242 renderCart(); 243 cartRoot.addEventListener('change', function(e){ 244 var inp = e.target.closest('.citem input'); if(!inp) return; 245 var k = inp.closest('.citem').dataset.k, c = load(); 246 c[k].q = Math.max(1, Math.min(99, parseInt(inp.value, 10) || 1)); save(c); renderCart(); 247 }); 248 cartRoot.addEventListener('click', function(e){ 249 if(e.target.closest('.rm')){ var k = e.target.closest('.citem').dataset.k, c = load(); delete c[k]; save(c); renderCart(); } 250 if(e.target.id === 'cart-clear'){ save({}); renderCart(); } 251 }); 252 var st = $('#ship-to'); if(st) st.addEventListener('change', paintSummary); 253 } 254 255 /* ---------------- CHECKOUT PAGE ---------------- */ 256 var co = $('#checkout-lines'); 257 if(co){ 258 var c = load(), keys = Object.keys(c); 259 if(!keys.length){ co.innerHTML = '<p class="muted">Your cart is empty. <a href="' + ROOT + 'shop.html" style="color:var(--green);font-weight:700">Visit the store →</a></p>'; } 260 else co.innerHTML = keys.map(function(k){ var it = c[k]; return '<div class="ln"><span>' + esc(it.n) + ' <span class="muted">Ã' + it.q + '</span></span><b>' + money(it.p * it.q) + '</b></div>'; }).join(''); 261 var dsel = $('#co-country'); 262 function coTotals(){ 263 var s = subtotal(), d = dsel ? dsel.value : 'us', sh = shipCost(d, s); 264 $('#co-sub').textContent = money(s); $('#co-ship').textContent = s ? money(sh) : 'â'; $('#co-total').textContent = money(s + sh); 265 } 266 if(dsel) dsel.addEventListener('change', coTotals); coTotals(); 267 var cf = $('#checkout-form'); 268 if(cf) cf.addEventListener('submit', function(e){ 269 e.preventDefault(); if(!cf.checkValidity()){ cf.reportValidity(); return; } 270 var num = 'HES-' + String(Date.now()).slice(-6); 271 $('#co-num').textContent = num; 272 $('#co-wrap').hidden = true; $('#co-done').hidden = false; save({}); window.scrollTo({top:0, behavior:'smooth'}); 273 }); 274 } 275 276 /* ---------------- SEARCH PAGE ---------------- */ 277 var sr = $('#search-results'); 278 if(sr && window.HES_DATA){ 279 var params = new URLSearchParams(location.search), q = (params.get('s') || '').trim(); 280 var inp = $('#q-page'); if(inp) inp.value = q; 281 var P = HES_DATA.products, A = HES_DATA.articles; 282 var SYN = {'teething':'teeth|infant|baby|child|chamomilla','hay fever':'allerg|sinus','potency':'potenc|30c|200c|kit','family kit':'family|kit','flu':'flu|influenz|oscillo','cold':'cold|flu|sinus','sleep':'sleep|calm|stress','stress':'stress|calm|rescue|emotional','injury':'arnica|traumeel|injur|sprain|musculoskeletal|pain','pain':'pain|arnica|traumeel|relief','women':'women|woman|pregnan|menopause|cyclease','children':'child|infant|kids|baby','allergy':'allerg|sinus','digestion':'indigestion|acidil|digest|nux'}; 283 function terms(q){ 284 var ql = q.toLowerCase(), extra = []; 285 for(var k in SYN){ if(ql.indexOf(k) > -1) extra = extra.concat(SYN[k].split('|')); }
286 return {words: ql.split(/\s+/).filter(Boolean), extra: extra}; 287 } 288 function score(text, t){ 289 text = text.toLowerCase(); var s = 0; 290 t.words.forEach(function(w){ if(text.indexOf(w) > -1) s += 3; }); 291 if(t.words.length > 1 && text.indexOf(t.words.join(' ')) > -1) s += 4; 292 t.extra.forEach(function(w){ if(text.indexOf(w) > -1) s += 1; }); 293 return s; 294 } 295 var prodHits = [], artHits = []; 296 if(q){ 297 var t = terms(q); 298 prodHits = P.map(function(p){ return [score(p.n + ' ' + p.a + ' ' + p.k, t), p]; }).filter(function(x){ return x[0] > 0; }).sort(function(a, b){ return b[0] - a[0]; }).map(function(x){ return x[1]; }); 299 artHits = A.map(function(a){ return [score(a.t + ' ' + a.c, t), a]; }).filter(function(x){ return x[0] > 0; }).sort(function(a, b){ return b[0] - a[0]; }).map(function(x){ return x[1]; }); 300 } 301 var head = $('#search-head'); 302 if(head) head.innerHTML = q ? 'Results for <i>“' + esc(q) + '”</i>' : 'Search the <i>store & library</i>'; 303 var sub = $('#search-sub'); 304 if(sub) sub.textContent = q ? (prodHits.length + ' products and ' + artHits.length + ' articles found') : 'Search 1,000+ medicines, books, kits, e-courses and free articles.'; 305 function pcard(p){ 306 var price = p.p ? '<span class="kprice"><span class="now">' + money(p.p) + '</span>' + (p.r && p.r > p.p ? '<span class="was">' + money(p.r) + '</span>' : '') + '</span>' : '<span class="kprice"><span class="now" style="font-size:13px">Ask us</span></span>'; 307 return '<article class="pcard"><a class="pimg2" href="' + ROOT + p.u + '" tabindex="-1" aria-hidden="true">' + thumb(p) + '</a><div class="pbody"><a class="ptitle" href="' + ROOT + p.u + '">' + esc(p.n) + '</a><span class="pauth">' + esc(p.a || p.cl) + '</span><div class="pfoot">' + price + 308 (p.p ? '<button class="addbtn" type="button" data-add data-slug="' + esc(p.s) + '" data-name="' + esc(p.n) + '" data-price="' + p.p + '" data-img="' + esc(p.i) + '" data-url="' + esc(p.u) + '" data-type="' + p.t + '" data-cv="' + p.v + '" aria-label="Add to cart"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="20" r="1.3"/><circle cx="19" cy="20" r="1.3"/><path d="M2 3h3l2.4 11.6a1.8 1.8 0 0 0 1.8 1.4h8.6a1.8 1.8 0 0 0 1.8-1.4L21.5 7H6"/></svg></button>' : '') + '</div></div></article>'; 309 } 310 function acard(a){ 311 return '<a class="acard2" href="' + (a.x ? a.u : ROOT + a.u) + '"' + (a.x ? ' target="_blank" rel="noopener"' : '') + '><span class="ab"><span class="cat">' + esc(a.c) + '</span><h3>' + esc(a.t) + '</h3><span class="go">' + (a.x ? 'Read on homeopathic.com â' : 'Read article →') + '</span></span></a>'; 312 } 313 var tabs = $$('.srtabs .chip'), mode = 'all'; 314 function paint(){ 315 var html = ''; 316 if(!q){ html = '<div class="empty-res"><b>What are you looking for?</b>Try a remedy, a symptom, an author or a book title â for example <a href="?s=arnica" style="color:var(--green);font-weight:700">Arnica</a>, <a href="?s=teething" style="color:var(--green);font-weight:700">teething</a> or <a href="?s=Ullman" style="color:var(--green);font-weight:700">Ullman</a>.</div>'; sr.innerHTML = html; return; } 317 if(mode !== 'articles'){ 318 html += '<h2 style="font-size:28px;font-weight:400;margin:6px 0 18px">Products <span class="muted" style="font-size:16px">(' + prodHits.length + ')</span></h2>'; 319 html += prodHits.length ? '<div class="pgrid2">' + prodHits.slice(0, mode === 'all' ? 24 : 200).map(pcard).join('') + '</div>' : '<div class="empty-res"><b>No products matched.</b>Try a broader word, or browse the <a href="' + ROOT + 'shop.html" style="color:var(--green);font-weight:700">full store</a>.</div>'; 320 } 321 if(mode !== 'products'){ 322 html += '<h2 style="font-size:28px;font-weight:400;margin:40px 0 18px">Free articles <span class="muted" style="font-size:16px">(' + artHits.length + ')</span></h2>'; 323 html += artHits.length ? '<div class="agrid">' + artHits.slice(0, mode === 'all' ? 9 : 200).map(acard).join('') + '</div>' : '<div class="empty-res"><b>No articles matched.</b>Browse all <a href="' + ROOT + 'articles.html" style="color:var(--green);font-weight:700">150+ free articles</a>.</div>'; 324 } 325 sr.innerHTML = html; 326 }
327 tabs.forEach(function(ch){ ch.addEventListener('click', function(){ mode = ch.dataset.mode; tabs.forEach(function(x){ x.setAttribute('aria-pressed', x === ch ? 'true' : 'false'); }); paint(); }); }); 328 paint(); 329 } 330})();
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.