PageSourceSearch

https://www.medicon.co.il/template_inc/eshopstoresframework/js/esh-templates.js?v=4.7.13

js medicon.co.il collected 2026-09-29 20:26:01 UTC 18,281 bytes, 372 lines download raw bytes

1/* ============================================================================
2   esh-templates.js -- behavior for AI-generated article + product content.
3
4   Deployed platform-wide alongside esh-templates.css. It lives OUTSIDE every
5   editable field, so a seller cannot delete it. It attaches only to `esh-*`
6   hooks and does nothing when they are absent -- safe to load on every page.
7
8   No dependencies. ES5-safe for legacy storefronts.
9
10   Progressive enhancement: FAQ answers are visible until this script marks the
11   block ready, so a script failure degrades to plain expanded content rather
12   than permanently hidden answers.
13   ============================================================================ */
14(function () {
15  'use strict';
16
17  function ready(fn) {
18    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn);
19    else fn();
20  }
21
22  function scrollY() { return window.pageYOffset || document.documentElement.scrollTop || 0; }
23  function viewH()   { return window.innerHeight || document.documentElement.clientHeight; }
24  function docH()    {
25    var b = document.body, d = document.documentElement;
26    return Math.max(b.scrollHeight, b.offsetHeight, d.scrollHeight, d.offsetHeight);
27  }
28
29  /* == FAQ -- shared by articles and products =============================== */
30  function initFaq(block) {
31    var qs = [].slice.call(block.querySelectorAll('.esh-faq__q'));
32    if (!qs.length) return;
33
34    qs.forEach(function (q, i) {
35      var panel = q.nextElementSibling;
36      if (!panel || panel.className.indexOf('esh-faq__a') < 0) return;
37      if (!panel.id) panel.id = (block.id || 'esh-faq') + '-a' + (i + 1);
38      q.setAttribute('aria-controls', panel.id);
39      var open = i === 0;                       // first item open, as designed
40      q.setAttribute('aria-expanded', open ? 'true' : 'false');
41      panel.hidden = !open;
42
43      q.addEventListener('click', function () {
44        var isOpen = q.getAttribute('aria-expanded') === 'true';
45        qs.forEach(function (o) {                // single-open accordion
46          var p = document.getElementById(o.getAttribute('aria-controls'));
47          o.setAttribute('aria-expanded', 'false');
48          if (p) p.hidden = true;
49        });
50        if (!isOpen) {
51          q.setAttribute('aria-expanded', 'true');
52          panel.hidden = false;
53        }
54      });
55    });
56    block.setAttribute('data-ready', '');       // releases the no-JS CSS fallback
57  }
58
59  /* == ARTICLE ============================================================= */
60  function initArticle(root) {
61    var main    = root.querySelector('.esh-art__main');
62    var tocList = root.querySelector('.esh-toc__list');
63    var bar     = root.querySelector('.esh-art__progress-bar');
64    var pctEls  = [].slice.call(root.querySelectorAll('.esh-toc__pct'));
65    var side    = root.querySelector('.esh-art__side');
66    var OFFSET  = 140, JUMP = 80;
67
68    /* ---- site sticky header -------------------------------------------------
69       Platinum pins #sticky_header (and shops pin their own navs/promo bars).
70       It starts hidden and only gains height once you scroll, so a one-time
71       measurement at load reads 0 and our sidebar then slides underneath it.
72       Measure what is ACTUALLY covering the top of the viewport, on every
73       scroll/resize, and publish it as --esh-sticky-top. Selector-free, so it
74       works on any shop without knowing its markup.                            */
75    var stickyTop = 0;
76
77    // Does this computed style PAINT a fill down to its box bottom? A transparent WRAPPER (the
78    // common case: an off-canvas menu shell, a full-height nav container) paints nothing, so its
79    // box bottom must NOT count -- measuring the wrapper box is what overshot the real header on
80    // some tenants (e.g. a 175px transparent shell around a header that visibly ends ~156px).
81    function _paints(cs) {
82      var bg = cs.backgroundColor || '';
83      var transparent = bg === 'transparent' || /rgba\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0\s*\)/.test(bg);
84      if (!transparent) return true;
85      if ((cs.backgroundImage || 'none') !== 'none') return true;
86      var bw = parseFloat(cs.borderBottomWidth || 0);
87      return bw > 0 && cs.borderBottomStyle !== 'none';
88    }
89
90    // The site's top-pinned fixed/sticky headers, cached (recomputed on resize). Collected with
91    // querySelectorAll, NOT elementsFromPoint -- a real header bar often has pointer-events:none
92    // (so clicks pass through), which makes elementsFromPoint skip it entirely.
93    var stickyWraps = [];
94    function collectStickyWraps() {
95      stickyWraps = [];
96      var all = document.querySelectorAll('*'), i, cs;
97      for (i = 0; i < all.length; i++) {
98        if (root.contains(all[i])) continue;              // never our own chrome
99        cs = getComputedStyle(all[i]);
100        if (cs.position === 'fixed' || cs.position === 'sticky') stickyWraps.push(all[i]);
101      }
102    }
103
104    // Header height = the DEEPEST PAINTING element within any top-pinned fixed/sticky container
105    // (the container itself or a descendant). This measures the real painted bottom of the header
106    // per tenant, ignoring transparent wrapper padding, and works whatever the markup is.
107    // Horizontally inside the viewport? A CLOSED off-canvas panel (slide-out menu drawer, side
108    // cart) is fixed at top:0 and full height but parked off-screen left/right -- it covers
109    // nothing, and counting it read cell-tec's closed menu drawer as a 531px "header".
110    function _onScreenX(r, vw) { return r.right > 0 && r.left < vw; }
111
112    function measureStickyTop() {
113      var vh = window.innerHeight || 0, lim = vh * 0.6, best = 0, i, j, w, wcs, wr, nodes, e, cs, r;
114      var vw = window.innerWidth || document.documentElement.clientWidth || 0;
115      for (i = 0; i < stickyWraps.length; i++) {
116        w = stickyWraps[i]; wcs = getComputedStyle(w);
117        if (wcs.display === 'none' || wcs.visibility === 'hidden' || wcs.opacity === '0') continue;
118        wr = w.getBoundingClientRect();
119        if (wr.top > 1 || wr.height <= 0 || wr.top > lim) continue;   // must be pinned at the top
120        if (!_onScreenX(wr, vw)) continue;                            // closed off-canvas panel
121        nodes = w.querySelectorAll('*');
122        for (j = -1; j < nodes.length; j++) {
123          e = (j < 0) ? w : nodes[j];
124          cs = getComputedStyle(e);
125          if (cs.visibility === 'hidden' || cs.display === 'none') continue;
126          if (!_paints(cs)) continue;
127          r = e.getBoundingClientRect();
128          if (r.height <= 0 || r.width <= 0 || r.bottom <= 0 || r.bottom > lim) continue;
129          if (!_onScreenX(r, vw)) continue;                           // painted, but off-screen
130          if (r.bottom > best) best = r.bottom;
131        }
132      }
133      return Math.round(best);
134    }
135
136    var _lastMeasure = 0;
137    function syncStickyTop() {
138      // Per-tenant escape hatch: a shop can pin an exact offset in its own CSS with
139      //   .esh-art { --esh-sticky-top-manual: 156px }
140      // It wins over the auto scan (which reads live geometry but can't know a theme's intent).
141      var manual = getComputedStyle(root).getPropertyValue('--esh-sticky-top-manual').trim();
142      if (manual) {
143        if (manual !== root.style.getPropertyValue('--esh-sticky-top'))
144          root.style.setProperty('--esh-sticky-top', manual);
145        return;
146      }
147      // measureStickyTop() walks the header subtrees -- too heavy for every scroll frame. The
148      // header height is near-static, so throttle to ~200ms; onScroll still fires for progress.
149      var now = (Date.now ? Date.now() : +new Date());
150      if (now - _lastMeasure < 200) return;
151      _lastMeasure = now;
152      var v = measureStickyTop();
153      if (v === stickyTop) return;
154      stickyTop = v;
155      root.style.setProperty('--esh-sticky-top', v + 'px');
156    }
157
158    /* -- TOC built from the H2s inside PageContent ------------------------- */
159    var heads = main ? [].slice.call(main.querySelectorAll('h2')) : [];
160    var links = [];
161    if (tocList) {
162      heads.forEach(function (h, i) {
163        if (!h.id) h.id = 'esh-sec-' + (i + 1);
164        var li = document.createElement('li');
165        var a  = document.createElement('a');
166        a.className = 'esh-toc__link';
167        a.href = '#' + h.id;
168        a.textContent = (h.textContent || '').trim();
169        a.addEventListener('click', function (e) {
170          e.preventDefault();
171          var top = h.getBoundingClientRect().top + scrollY() - (JUMP + stickyTop);
172          try { window.scrollTo({ top: top, behavior: 'smooth' }); }
173          catch (err) { window.scrollTo(0, top); }
174        });
175        li.appendChild(a); tocList.appendChild(li); links.push(a);
176      });
177      if (!heads.length) {
178        var nav = tocList.closest ? tocList.closest('.esh-toc') : tocList.parentNode;
179        if (nav) nav.style.display = 'none';
180      }
181    }
182
183    /* -- active section -------------------------------------------------------
184       R1 normal scan  : last heading whose top passed OFFSET.
185       R2 bottom guard : at the very bottom the LAST item is always active.
186       R3 tail guard   : headings the document cannot scroll far enough to reach
187                         share the remaining range proportionally. Without it,
188                         short trailing sections are skipped entirely -- and when
189                         NOT EVEN THE FIRST heading is reachable (a short article
190                         on a tall screen) everything would stick on item 1.     */
191    function activeIndex() {
192      if (!heads.length) return -1;
193      var y = scrollY(), maxScroll = Math.max(1, docH() - viewH()), i;
194      var acts = heads.map(function (h) {
195        return h.getBoundingClientRect().top + y - (OFFSET + stickyTop);
196      });
197
198      var lastReachable = -1;
199      for (i = 0; i < acts.length; i++) if (acts[i] <= maxScroll) lastReachable = i;
200
201      if (lastReachable === acts.length - 1) {
202        var a = 0;
203        for (i = 0; i < acts.length; i++) if (y >= acts[i]) a = i;
204        if (y >= maxScroll - 2) a = acts.length - 1;
205        return a;
206      }
207      var base   = lastReachable < 0 ? 0 : lastReachable;
208      var startY = lastReachable < 0 ? 0 : acts[lastReachable];
209      if (y < startY) {
210        var b = 0;
211        for (i = 0; i <= base; i++) if (y >= acts[i]) b = i;
212        return b;
213      }
214      var tail = acts.length - base;
215      var frac = (y - startY) / Math.max(1, maxScroll - startY);
216      return Math.min(acts.length - 1, base + Math.floor(frac * tail));
217    }
218
219    function progress() {
220      if (!main) return 0;
221      var y = scrollY();
222      var top  = main.getBoundingClientRect().top + y;
223      var span = Math.max(1, main.offsetHeight - viewH() * 0.6);
224      return Math.max(0, Math.min(100, ((y - top + viewH() * 0.25) / span) * 100));
225    }
226
227    var lastIdx = -2, ticking = false;
228    function measure() {
229      ticking = false;
230      syncStickyTop();
231      var p = Math.round(progress());
232      if (bar)   bar.style.width = p + '%';
233      pctEls.forEach(function (el) { el.textContent = p + '% \u05e0\u05e7\u05e8\u05d0'; });
234
235      var idx = activeIndex();
236      if (idx === lastIdx) return;
237      lastIdx = idx;
238      links.forEach(function (a, i) {
239        var on = i === idx;
240        if (on) { a.className = 'esh-toc__link is-active'; a.setAttribute('aria-current', 'true'); }
241        else    { a.className = 'esh-toc__link'; a.removeAttribute('aria-current'); }
242      });
243      // keep the active item visible inside the internally-scrolling sidebar
244      if (idx >= 0 && links[idx] && side && side.scrollHeight > side.clientHeight) {
245        var l = links[idx].getBoundingClientRect(), s = side.getBoundingClientRect();
246        if (l.top < s.top || l.bottom > s.bottom) {
247          try { links[idx].scrollIntoView({ block: 'nearest' }); } catch (e) {}
248        }
249      }
250    }
251    function onScroll() {
252      if (ticking) return;
253      ticking = true;
254      (window.requestAnimationFrame || function (f) { setTimeout(f, 16); })(measure);
255    }
256    window.addEventListener('scroll', onScroll, { passive: true });
257    window.addEventListener('resize', function () { collectStickyWraps(); onScroll(); });
258    if (window.ResizeObserver && main) new ResizeObserver(onScroll).observe(main);
259    // Re-scan the sticky headers after late assets settle (webfonts, lazy header widgets)
260    // could change the header height after first paint.
261    setTimeout(function () { collectStickyWraps(); syncStickyTop(); }, 400);
262    window.addEventListener('load', function () { collectStickyWraps(); syncStickyTop(); });
263
264    /* -- mobile TOC burger -------------------------------------------------- */
265    var toc    = root.querySelector('.esh-toc');
266    var burger = root.querySelector('.esh-toc__burger');
267    if (toc && burger) {
268      burger.addEventListener('click', function () {
269        var open = burger.getAttribute('aria-expanded') === 'true';
270        burger.setAttribute('aria-expanded', open ? 'false' : 'true');
271        toc.className = open ? 'esh-card esh-toc' : 'esh-card esh-toc is-open';
272      });
273      // Jumping to a section should close the sheet, or the target sits behind it.
274      links.forEach(function (a) {
275        a.addEventListener('click', function () {
276          if (burger.offsetParent === null) return;   // desktop: button hidden
277          burger.setAttribute('aria-expanded', 'false');
278          toc.className = 'esh-card esh-toc';
279        });
280      });
281    }
282
283    /* -- share -------------------------------------------------------------- */
284    var url = location.href, title = document.title;
285    var wa = root.querySelector('.esh-share__btn--wa');
286    var fb = root.querySelector('.esh-share__btn--fb');
287    if (wa) wa.href = 'https://wa.me/?text=' + encodeURIComponent(title + ' ' + url);
288    if (fb) fb.href = 'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(url);
289
290    var copyBtn = root.querySelector('.esh-share__btn--copy');
291    var timer;
292    if (copyBtn) copyBtn.addEventListener('click', function () {
293      // Icon-only confirmation: swap the link glyph for a check, then revert.
294      // No text label, so the sidebar never reflows.
295      function done() {
296        copyBtn.className = 'esh-share__btn esh-share__btn--copy is-done';
297        copyBtn.setAttribute('aria-label', '\u05d4\u05e7\u05d9\u05e9\u05d5\u05e8 \u05d4\u05d5\u05e2\u05ea\u05e7');
298        clearTimeout(timer);
299        timer = setTimeout(function () {
300          copyBtn.className = 'esh-share__btn esh-share__btn--copy';
301          copyBtn.setAttribute('aria-label', '\u05d4\u05e2\u05ea\u05e7\u05ea \u05e7\u05d9\u05e9\u05d5\u05e8');
302        }, 2000);
303      }
304      function fallback() {             // legacy / non-HTTPS storefronts
305        var t = document.createElement('textarea');
306        t.value = location.href; t.setAttribute('readonly', '');
307        t.style.cssText = 'position:absolute;left:-9999px';
308        document.body.appendChild(t); t.select();
309        try { document.execCommand('copy'); done(); } catch (e) {}
310        document.body.removeChild(t);
311      }
312      if (navigator.clipboard && window.isSecureContext) {
313        navigator.clipboard.writeText(location.href).then(done, fallback);
314      } else fallback();
315    });
316
317    /* -- tables: horizontal-scroll wrapper ---------------------------------
318       The SEO transform emits generated tables already inside .esh-table-wrap,
319       but a seller can PASTE or edit a bare <table> after publish. Wrap any
320       article-body table not already wrapped, so wide tables scroll instead of
321       breaking the column. ES5-safe, null-safe, no deps. */
322    if (main) {
323      [].slice.call(main.querySelectorAll('table')).forEach(function (t) {
324        var p = t.parentNode;
325        if (!p) return;
326        var cn = '' + (p.className || '');
327        if (cn.indexOf('esh-table-wrap') >= 0) return;   // already wrapped
328        var wrap = document.createElement('div');
329        wrap.className = 'esh-table-wrap';
330        p.insertBefore(wrap, t);
331        wrap.appendChild(t);
332      });
333    }
334
335    collectStickyWraps();   // populate the header set before the first measure
336    measure();
337  }
338
339  /* == boot ================================================================ */
340  /* == brand contrast guard ================================================
341     The shop's --esh-brand is arbitrary. White text on a light brand (yellow,
342     lime, pale pink) is unreadable, so pick the on-brand foreground from the
343     colour's relative luminance. CSS defaults --esh-on-brand to #fff, so a JS
344     failure degrades to today's behaviour rather than to something broken.    */
345  function brandContrast() {
346    var probe = document.querySelector('.esh-art, .esh-pdx');
347    if (!probe) return;
348    var acc = getComputedStyle(probe).getPropertyValue('--esh-acc').trim();
349    if (!acc) return;
350    var d = document.createElement('span');
351    d.style.cssText = 'color:' + acc + ';position:absolute;left:-9999px';
352    document.body.appendChild(d);
353    var m = getComputedStyle(d).color.match(/[\d.]+/g);
354    document.body.removeChild(d);
355    if (!m) return;
356    var f = function (v) { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };
357    var L = 0.2126 * f(+m[0]) + 0.7152 * f(+m[1]) + 0.0722 * f(+m[2]);
358    // contrast of white-on-brand; below 4.5:1 switch to ink
359    var white = (1.05) / (L + 0.05);
360    probe.style.setProperty('--esh-on-brand', white < 4.5 ? '#0f172a' : '#fff');
361    document.querySelectorAll('.esh-art, .esh-pdx').forEach(function (el) {
362      el.style.setProperty('--esh-on-brand', white < 4.5 ? '#0f172a' : '#fff');
363    });
364  }
365
366  ready(function () {
367    brandContrast();
368    [].slice.call(document.querySelectorAll('.esh-faq')).forEach(initFaq);
369    var art = document.querySelector('.esh-art');
370    if (art) initArticle(art);
371  });
372})();

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.