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> / ' + 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> / ' + 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.