PageSourceSearch

https://yoursitehub.com/wp-content/plugins/sitehub-widgets/assets/js/sitehub-widgets.v2.js

js yoursitehub.com collected 2026-10-08 04:37:56 UTC 13,670 bytes, 369 lines download raw bytes

1/**
2 * Site Hub — Custom Elementor Widgets  |  v1.3.0
3 *
4 * Pinned Services Slider  — scroll-sticky crossfade
5 * Selected Work           — filter + modal
6 * Why Site Hub            — scroll-sticky step activator
7 *
8 * Works on the live frontend AND inside the Elementor preview iframe.
9 */
10(function () {
11  'use strict';
12
13  /* ── Zero-pad helper ──────────────────────────────────────── */
14  function pad2(n) {
15    return n < 10 ? '0' + n : '' + n;
16  }
17
18  /* ══════════════════════════════════════════════════════════
19     PINNED SERVICES SLIDER
20     ══════════════════════════════════════════════════════════ */
21
22  function initPinnedServices(section) {
23    var slidesWrap = section.querySelector('[data-sh-psw-slides]');
24    var ctr        = section.querySelector('[data-sh-psw-ctr]');
25
26    if (!slidesWrap) return;
27
28    var items = Array.prototype.slice.call(
29      slidesWrap.querySelectorAll('.sh-psw__sv')
30    );
31    var n = items.length;
32    if (!n) return;
33
34    if (
35      document.body.classList.contains('elementor-editor-active') ||
36      document.body.classList.contains('elementor-editor-preview')
37    ) {
38      return;
39    }
40
41    var lastIdx = -1;
42
43    function update() {
44      var r   = section.getBoundingClientRect();
45      var tot = r.height - window.innerHeight;
46      var p   = tot > 0 ? Math.max(0, Math.min(0.999, -r.top / tot)) : 0;
47      var idx = Math.min(n - 1, Math.floor(p * n));
48
49      if (idx === lastIdx) return;
50      lastIdx = idx;
51
52      items.forEach(function (slide, i) {
53        slide.classList.toggle('is-active', i === idx);
54      });
55
56      if (ctr) {
57        ctr.innerHTML =
58          '<em>' + pad2(idx + 1) + '</em>&thinsp;/&thinsp;' + pad2(n);
59      }
60    }
61
62    window.addEventListener('scroll', update, { passive: true });
63    window.addEventListener('resize', update);
64    update();
65  }
66
67  /* ══════════════════════════════════════════════════════════
68     SELECTED WORK  —  filter + modal
69     ══════════════════════════════════════════════════════════ */
70
71  function initSelectedWork(widgetRoot) {
72    var grid      = widgetRoot.querySelector('.sh-sw__grid');
73    var filterBar = widgetRoot.querySelector('.sh-sw__filter');
74    if (!grid || !filterBar) return;
75
76    // Derive widget UID from the grid's id attribute
77    var uid   = (grid.id || '').replace('sh-sw-grid-', '');
78    var modal = document.getElementById('sh-sw-modal-' + uid);
79
80    /* ── Filter ── */
81    var allCards = Array.prototype.slice.call(grid.querySelectorAll('.sh-sw__case'));
82
83    filterBar.querySelectorAll('button').forEach(function (btn) {
84      btn.addEventListener('click', function () {
85        filterBar.querySelectorAll('button').forEach(function (b) {
86          b.classList.remove('is-active');
87        });
88        btn.classList.add('is-active');
89
90        var active = (btn.dataset.f || 'All').trim();
91        allCards.forEach(function (card) {
92          var tags = (card.dataset.filters || 'All')
93            .split(',')
94            .map(function (t) { return t.trim(); });
95          card.classList.toggle(
96            'is-hidden',
97            active !== 'All' && tags.indexOf(active) === -1
98          );
99        });
100      });
101    });
102
103    /* ── Modal ── */
104    if (!modal) return;
105
106    var content  = modal.querySelector('.sh-sw-modal__content');
107    var closeBtn = modal.querySelector('.sh-sw-modal__close');
108
109    function openModal(data) {
110      if (!content) return;
111
112      var metricsHtml = (data.metrics || []).map(function (m) {
113        return '<div><dt>' + m.n + '</dt><dd>' + m.l + '</dd></div>';
114      }).join('');
115
116      var tagsHtml = (data.tags || []).map(function (t) {
117        return '<span>' + t + '</span>';
118      }).join('');
119
120      var ctaHtml = (data.link)
121        ? '<a href="' + data.link + '" class="sh-sw-modal__cta"'
122          + ' target="' + (data.link_target || '_self') + '"'
123          + ' rel="noopener noreferrer">'
124          + (data.cta || 'View case study') + ' →</a>'
125        : '';
126
127      content.innerHTML =
128        '<div class="sh-sw-modal__hero hue-' + data.hue + '">'
129          + '<span class="sh-sw__badge">' + data.industry + ' · ' + data.type + '</span>'
130          + '<h3>' + data.name + '</h3>'
131        + '</div>'
132        + '<div class="sh-sw-modal__body">'
133          + '<dl class="sh-sw-modal__metrics">' + metricsHtml + '</dl>'
134          + '<div class="sh-sw-modal__cols">'
135            + '<div><h4>The challenge</h4><p>' + data.challenge + '</p></div>'
136            + '<div><h4>What we shipped</h4><p>' + data.outcome + '</p></div>'
137          + '</div>'
138          + (tagsHtml ? '<div class="sh-sw-modal__tags">' + tagsHtml + '</div>' : '')
139          + ctaHtml
140        + '</div>';
141
142      modal.classList.add('is-open');
143      modal.setAttribute('aria-hidden', 'false');
144      document.body.style.overflow = 'hidden';
145    }
146
147    function closeModal() {
148      modal.classList.remove('is-open');
149      modal.setAttribute('aria-hidden', 'true');
150      document.body.style.overflow = '';
151    }
152
153    // Open on card click or Enter/Space key
154    allCards.forEach(function (card) {
155      card.addEventListener('click', function () {
156        try { openModal(JSON.parse(card.dataset.case || '{}')); } catch (e) {}
157      });
158      card.addEventListener('keydown', function (e) {
159        if (e.key === 'Enter' || e.key === ' ') {
160          e.preventDefault();
161          try { openModal(JSON.parse(card.dataset.case || '{}')); } catch (e2) {}
162        }
163      });
164    });
165
166    if (closeBtn) closeBtn.addEventListener('click', closeModal);
167
168    // Click backdrop to close
169    modal.addEventListener('click', function (e) {
170      if (e.target === modal) closeModal();
171    });
172
173    // Escape key to close
174    document.addEventListener('keydown', function (e) {
175      if (e.key === 'Escape' && modal.classList.contains('is-open')) closeModal();
176    });
177  }
178
179  /* ══════════════════════════════════════════════════════════
180     WHY SITE HUB  —  sticky step activator
181     ══════════════════════════════════════════════════════════ */
182
183  function initWhy(section) {
184    var stepsWrap = section.querySelector('[data-sh-why-steps]');
185    var ctr       = section.querySelector('[data-sh-why-ctr]');
186    if (!stepsWrap) return;
187
188    var steps = Array.prototype.slice.call(
189      stepsWrap.querySelectorAll('.sh-why__step')
190    );
191    var n = steps.length;
192    if (!n) return;
193
194    // Skip in editor — CSS already shows all steps at full opacity
195    if (
196      document.body.classList.contains('elementor-editor-active') ||
197      document.body.classList.contains('elementor-editor-preview')
198    ) {
199      return;
200    }
201
202    function pad2(x) { return x < 10 ? '0' + x : '' + x; }
203
204    var lastIdx = -1;
205
206    function update() {
207      // ── Position-based trigger ──────────────────────────────────
208      // Find the last step whose top edge has crossed 40% from the
209      // top of the viewport. This fires based on *actual visual
210      // position*, not synthetic section-height proportions, so it
211      // works regardless of content length or step padding.
212      //
213      // Edge cases:
214      //  • Before section scrolls in → no step qualifies → idx stays 0
215      //  • After section scrolls out → all qualify → idx = n-1
216      // Both are correct and handled by defaulting to 0.
217      var triggerY = window.innerHeight * 0.40;
218      var idx = 0;
219      steps.forEach(function (step, i) {
220        if (step.getBoundingClientRect().top <= triggerY) {
221          idx = i;
222        }
223      });
224
225      if (idx === lastIdx) return;
226      lastIdx = idx;
227
228      steps.forEach(function (step, i) {
229        step.classList.toggle('is-active', i === idx);
230      });
231
232      if (ctr) {
233        ctr.innerHTML = '<em>' + pad2(idx + 1) + '</em>&thinsp;/&thinsp;' + pad2(n);
234      }
235    }
236
237    window.addEventListener('scroll', update, { passive: true });
238    window.addEventListener('resize', update);
239    update();
240  }
241
242  /* ══════════════════════════════════════════════════════════
243     WIREFRAME → WEBSITE STAGE  —  scroll-sticky laptop animation
244     ══════════════════════════════════════════════════════════ */
245
246  function initStage(outer) {
247    var panel = outer.querySelector('.sh-stage__panel');
248    if (!panel) return;
249
250    // Skip scroll logic in Elementor editor
251    if (
252      document.body.classList.contains('elementor-editor-active') ||
253      document.body.classList.contains('elementor-editor-preview')
254    ) {
255      return;
256    }
257
258    var raf = 0;
259
260    function seg(p, a, b) {
261      return Math.max(0, Math.min(1, (p - a) / (b - a)));
262    }
263
264    function setVars(p) {
265      panel.style.setProperty('--p',         p.toFixed(4));
266      panel.style.setProperty('--p-flatten', seg(p, .05, .55).toFixed(4));
267      panel.style.setProperty('--p-fade',    seg(p, .30, .52).toFixed(4));
268      panel.style.setProperty('--p-zoom',    seg(p, .10, .92).toFixed(4));
269      panel.style.setProperty('--p-fill',    seg(p, .75, .95).toFixed(4));
270      panel.style.setProperty('--p-results', seg(p, .82, .93).toFixed(4));
271      panel.style.setProperty('--p-bg',      seg(p, .50, .82).toFixed(4));
272      panel.style.setProperty('--p-base',    seg(p, .45, .72).toFixed(4));
273      panel.style.setProperty('--p-cap',     Math.max(0, Math.min(1, 1 - p / .30)).toFixed(4));
274    }
275
276    function update() {
277      raf = 0;
278      var r   = outer.getBoundingClientRect();
279      var tot = outer.offsetHeight - window.innerHeight;
280      var p   = tot > 0 ? Math.max(0, Math.min(1, -r.top / tot)) : 0;
281      setVars(p);
282    }
283
284    function onSc() { if (!raf) raf = requestAnimationFrame(update); }
285
286    update();
287    window.addEventListener('scroll', onSc, { passive: true });
288    window.addEventListener('resize', update);
289  }
290
291  /* ══════════════════════════════════════════════════════════
292     BOOT  —  init everything present on the page
293     ══════════════════════════════════════════════════════════ */
294
295  function initAll() {
296    // Pinned Services
297    Array.prototype.forEach.call(
298      document.querySelectorAll('.sh-psw'),
299      initPinnedServices
300    );
301    // Selected Work — pass the Elementor widget wrapper
302    Array.prototype.forEach.call(
303      document.querySelectorAll('.sh-sw__grid'),
304      function (grid) {
305        var section = grid.closest('.sh-sw');
306        var root    = section ? section.parentElement : null;
307        if (root) initSelectedWork(root);
308      }
309    );
310    // Why Site Hub
311    Array.prototype.forEach.call(
312      document.querySelectorAll('.sh-why'),
313      initWhy
314    );
315    // Wireframe → Website Stage
316    Array.prototype.forEach.call(
317      document.querySelectorAll('[data-sh-stage]'),
318      initStage
319    );
320  }
321
322  if (document.readyState === 'loading') {
323    document.addEventListener('DOMContentLoaded', initAll);
324  } else {
325    initAll();
326  }
327
328  /* ── Elementor frontend hooks (re-init on widget re-render) ──
329     elementorFrontend exists before Elementor builds its .hooks object, so
330     registering immediately threw "Cannot read properties of undefined
331     (reading 'addAction')" on every page. Wait for elementor/frontend/init. */
332  function registerElementorHooks() {
333    if (!window.elementorFrontend || !window.elementorFrontend.hooks) { return; }
334    window.elementorFrontend.hooks.addAction(
335      'frontend/element_ready/sitehub-pinned-services.default',
336      function ($scope) {
337        var section = $scope[0];
338        if (section) initPinnedServices(section);
339      }
340    );
341    window.elementorFrontend.hooks.addAction(
342      'frontend/element_ready/sitehub-selected-work.default',
343      function ($scope) {
344        if ($scope[0]) initSelectedWork($scope[0]);
345      }
346    );
347    window.elementorFrontend.hooks.addAction(
348      'frontend/element_ready/sitehub-why.default',
349      function ($scope) {
350        var section = $scope[0] && $scope[0].querySelector('.sh-why');
351        if (section) initWhy(section);
352      }
353    );
354    window.elementorFrontend.hooks.addAction(
355      'frontend/element_ready/sitehub-stage.default',
356      function ($scope) {
357        var outer = $scope[0] && $scope[0].querySelector('[data-sh-stage]');
358        if (outer) initStage(outer);
359      }
360    );
361  }
362
363  if (window.elementorFrontend && window.elementorFrontend.hooks) {
364    registerElementorHooks();
365  } else if (window.jQuery) {
366    window.jQuery(window).on('elementor/frontend/init', registerElementorHooks);
367  }
368
369})();

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.