@widelab-nc/widelab 1.1.35 → 1.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2 -2
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
- package/src/index.js +330 -209
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -16,6 +16,23 @@ function customGsapEasing() {
|
|
|
16
16
|
CustomEase.create('loader2', '0.77, 0, 0.175, 1');
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// Smoothly scrolls to section if current URL contains a hash
|
|
20
|
+
function scrollToHashOnLoad(offsetY = 0) {
|
|
21
|
+
window.addEventListener('load', () => {
|
|
22
|
+
const hash = window.location.hash;
|
|
23
|
+
if (!hash) return;
|
|
24
|
+
|
|
25
|
+
const target = document.querySelector(hash);
|
|
26
|
+
if (!target) return;
|
|
27
|
+
|
|
28
|
+
gsap.to(window, {
|
|
29
|
+
scrollTo: { y: target, offsetY: offsetY },
|
|
30
|
+
duration: 1,
|
|
31
|
+
ease: 'power2.out'
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
19
36
|
// Functions definitions
|
|
20
37
|
|
|
21
38
|
// Contact form open animation
|
|
@@ -260,6 +277,39 @@ if (infoBar) {
|
|
|
260
277
|
});
|
|
261
278
|
}
|
|
262
279
|
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
// Safe new version that uses the same classes and structure
|
|
283
|
+
window.customCursorTextUpdate = function(el) {
|
|
284
|
+
const cursorEl = document.querySelector('.custom_cursor_text');
|
|
285
|
+
if (!cursorEl) return;
|
|
286
|
+
|
|
287
|
+
const defaultText = cursorEl.dataset.default || '';
|
|
288
|
+
|
|
289
|
+
const getText = (node) =>
|
|
290
|
+
node.getAttribute('widecursortext') || node.getAttribute('wideCursorText') || '';
|
|
291
|
+
|
|
292
|
+
const activate = () => {
|
|
293
|
+
cursorEl.textContent = getText(el);
|
|
294
|
+
document.documentElement.classList.add('has-custom-cursor');
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const maybeDeactivate = (ev) => {
|
|
298
|
+
const to = ev.relatedTarget;
|
|
299
|
+
// Don't deactivate if moving between elements that also use the custom cursor
|
|
300
|
+
if (to && to.closest && to.closest('[wide="customCursor"]')) return;
|
|
301
|
+
|
|
302
|
+
cursorEl.textContent = defaultText;
|
|
303
|
+
document.documentElement.classList.remove('has-custom-cursor');
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
el.addEventListener('pointerenter', activate);
|
|
307
|
+
el.addEventListener('pointerleave', maybeDeactivate);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
|
|
263
313
|
// Hero animation on load
|
|
264
314
|
window.heroAnimationOnLoad = function(e) {
|
|
265
315
|
const videoMask = e.querySelector('.hero_headline_ract-mask');
|
|
@@ -310,53 +360,75 @@ if (infoBar) {
|
|
|
310
360
|
);
|
|
311
361
|
}
|
|
312
362
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
width: '100%',
|
|
319
|
-
duration: 0.35,
|
|
320
|
-
ease: 'loader2',
|
|
321
|
-
paused: true,
|
|
322
|
-
});
|
|
323
|
-
ScrollTrigger.create({
|
|
324
|
-
animation: maskAnimation,
|
|
325
|
-
trigger: mask,
|
|
326
|
-
start: 'top 80%',
|
|
327
|
-
onEnter: () => maskAnimation.play(),
|
|
328
|
-
});
|
|
363
|
+
// Case study mask animation + pill fade-in (last step)
|
|
364
|
+
window.caseStudyInteraction = function (e) {
|
|
365
|
+
// Video mask + pill timeline
|
|
366
|
+
const mask = e.querySelector('.portfolio_item_image-mask');
|
|
367
|
+
const pill = e.querySelector('.cs_pill');
|
|
329
368
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
369
|
+
// Build a single timeline so the pill fades in AFTER the mask animation
|
|
370
|
+
const tl = gsap.timeline({
|
|
371
|
+
paused: true,
|
|
372
|
+
defaults: { ease: 'loader2' }
|
|
373
|
+
});
|
|
335
374
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
});
|
|
339
|
-
e.addEventListener('mouseenter', hoverIN, false);
|
|
340
|
-
e.addEventListener('mouseleave', hoverOUT, false);
|
|
375
|
+
// Ensure pill starts hidden
|
|
376
|
+
if (pill) gsap.set(pill, { autoAlpha: 0 }); // autoAlpha controls visibility + opacity
|
|
341
377
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
378
|
+
// Mask reveals first...
|
|
379
|
+
tl.from(mask, {
|
|
380
|
+
width: '100%',
|
|
381
|
+
duration: 0.35
|
|
382
|
+
});
|
|
345
383
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
384
|
+
// ...then pill fades in as the last step
|
|
385
|
+
if (pill) {
|
|
386
|
+
tl.to(pill, {
|
|
387
|
+
autoAlpha: 1,
|
|
388
|
+
duration: 0.25
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Trigger on scroll
|
|
393
|
+
ScrollTrigger.create({
|
|
394
|
+
animation: tl,
|
|
395
|
+
trigger: mask,
|
|
396
|
+
start: 'top 80%',
|
|
397
|
+
onEnter: () => tl.play()
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// Video on hover actions
|
|
401
|
+
const videoElements = e.getElementsByTagName('video');
|
|
402
|
+
if (window.innerWidth > 992) {
|
|
403
|
+
const videoElement = videoElements.length > 0 ? videoElements[0] : null;
|
|
404
|
+
if (!videoElement) return;
|
|
405
|
+
|
|
406
|
+
videoElement.addEventListener('loadeddata', () => {
|
|
407
|
+
videoElement.pause();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
e.addEventListener('mouseenter', hoverIN, false);
|
|
411
|
+
e.addEventListener('mouseleave', hoverOUT, false);
|
|
412
|
+
|
|
413
|
+
function hoverIN() {
|
|
414
|
+
videoElement.play();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function hoverOUT() {
|
|
418
|
+
videoElement.pause();
|
|
419
|
+
// videoElement.currentTime = 0;
|
|
358
420
|
}
|
|
421
|
+
} else {
|
|
422
|
+
// Mobile: keep poster only
|
|
423
|
+
Array.from(videoElements).forEach((videoElem) => {
|
|
424
|
+
const poster = videoElem.getAttribute('poster');
|
|
425
|
+
videoElem.innerHTML = '';
|
|
426
|
+
videoElem.setAttribute('poster', poster);
|
|
427
|
+
videoElem.load();
|
|
428
|
+
videoElem.pause();
|
|
429
|
+
});
|
|
359
430
|
}
|
|
431
|
+
};
|
|
360
432
|
|
|
361
433
|
// Reviews slider
|
|
362
434
|
window.reviewsSlider = function(e) {
|
|
@@ -413,61 +485,6 @@ if (infoBar) {
|
|
|
413
485
|
});
|
|
414
486
|
}
|
|
415
487
|
|
|
416
|
-
// Reviews slider
|
|
417
|
-
window.reviewsSlider = function(e) {
|
|
418
|
-
// Put content of static slide into list
|
|
419
|
-
const staticContent = e.querySelector(
|
|
420
|
-
'.splide__slide.reviews_collection_item.is-static'
|
|
421
|
-
).innerHTML;
|
|
422
|
-
const dynamicElement = e.querySelector('.reviews_collection_item');
|
|
423
|
-
dynamicElement.innerHTML = staticContent;
|
|
424
|
-
dynamicElement.classList.add('is-static');
|
|
425
|
-
|
|
426
|
-
// Init reviews splide
|
|
427
|
-
const reviewsSplide = new Splide(e, {
|
|
428
|
-
type: 'slide',
|
|
429
|
-
fixedWidth: '24.5rem',
|
|
430
|
-
rewind: true,
|
|
431
|
-
focus: 0,
|
|
432
|
-
padding: { left: '2.5rem', right: 0 },
|
|
433
|
-
gap: 0,
|
|
434
|
-
omitEnd: true,
|
|
435
|
-
arrows: false,
|
|
436
|
-
drag: false,
|
|
437
|
-
easing: 'cubic-bezier(0.83, 0, 0.17, 1)',
|
|
438
|
-
snap: true,
|
|
439
|
-
classes: {
|
|
440
|
-
pagination: 'splide__pagination reviews-pagination',
|
|
441
|
-
page: 'splide__pagination__page reviews-page',
|
|
442
|
-
},
|
|
443
|
-
breakpoints: {
|
|
444
|
-
992: { fixedWidth: '17.438rem', padding: { left: '1.75rem' }, drag: 'free', snap: false },
|
|
445
|
-
480: { fixedWidth: '16.25rem', padding: { left: '1.375rem' }},
|
|
446
|
-
},
|
|
447
|
-
}).mount();
|
|
448
|
-
|
|
449
|
-
// Next slide on click
|
|
450
|
-
if (window.innerWidth > 992) {
|
|
451
|
-
reviewsSplide.on('click', function () {
|
|
452
|
-
reviewsSplide.go('+1');
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// Animation in
|
|
457
|
-
gsap.from(e, {
|
|
458
|
-
scrollTrigger: {
|
|
459
|
-
trigger: e,
|
|
460
|
-
start: 'top bottom',
|
|
461
|
-
end: 'bottom bottom-=15%',
|
|
462
|
-
scrub: 1,
|
|
463
|
-
once: true,
|
|
464
|
-
},
|
|
465
|
-
x: '10%',
|
|
466
|
-
opacity: 0,
|
|
467
|
-
ease: 'linear',
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
|
|
471
488
|
// Facts slider
|
|
472
489
|
window.factsSlider = function(e) {
|
|
473
490
|
// Facts slider splide initialize
|
|
@@ -635,11 +652,11 @@ window.partnerSlider = function (e) {
|
|
|
635
652
|
// Initialize Splide with drag restored
|
|
636
653
|
const splide = new Splide(e, {
|
|
637
654
|
type: 'slide',
|
|
638
|
-
fixedWidth: '
|
|
655
|
+
fixedWidth: '31.5%',
|
|
639
656
|
padding: { left: '2.5rem', right: 0 },
|
|
640
657
|
focus: 0,
|
|
641
658
|
gap: 0,
|
|
642
|
-
pagination:
|
|
659
|
+
pagination: false,
|
|
643
660
|
omitEnd: true,
|
|
644
661
|
arrows: false,
|
|
645
662
|
drag: true, // restored dragging
|
|
@@ -1013,149 +1030,251 @@ window.badgeColorChange = function(e) {
|
|
|
1013
1030
|
});
|
|
1014
1031
|
}
|
|
1015
1032
|
|
|
1033
|
+
// Single entry you call from x-init
|
|
1034
|
+
window.clientFilters = function () {
|
|
1035
|
+
// Prevent double initialization if Alpine re-mounts
|
|
1036
|
+
if (window.__clientFiltersInitialized) return;
|
|
1037
|
+
window.__clientFiltersInitialized = true;
|
|
1038
|
+
|
|
1039
|
+
// --- Config ---
|
|
1040
|
+
const FIELDS = ["industry", "country", "services"];
|
|
1041
|
+
const SEL = {
|
|
1042
|
+
dropdown: '[fs-cmsfilter-element="dropdown"]',
|
|
1043
|
+
dropdownHolder: ".dropdown-holder",
|
|
1044
|
+
dropdownTitle: '[filters-clients_dropdown-field="tag-name-title"], .filters-clients_dropdown-field',
|
|
1045
|
+
tagNumber: '[fs-cmsfilter-element="tag-number"]',
|
|
1046
|
+
dynItem: ".w-dyn-item",
|
|
1047
|
+
checkboxWrap: ".w-checkbox",
|
|
1048
|
+
checkboxInput: 'input[type="checkbox"]',
|
|
1049
|
+
webflowChecked: "w--redirected-checked",
|
|
1050
|
+
fsActive: "fs-cmsfilter_active",
|
|
1051
|
+
listContainer: ".collection-list",
|
|
1052
|
+
count: ".count",
|
|
1053
|
+
crossIcon: ".cross-icon",
|
|
1054
|
+
clearButton: ".clear-button",
|
|
1055
|
+
activeDropdown: "active-dropdown",
|
|
1056
|
+
};
|
|
1016
1057
|
|
|
1017
|
-
//
|
|
1018
|
-
|
|
1019
|
-
function
|
|
1058
|
+
// --- Utils ---
|
|
1059
|
+
function $(s, ctx = document) { return ctx.querySelector(s); }
|
|
1060
|
+
function $all(s, ctx = document) { return Array.from(ctx.querySelectorAll(s)); }
|
|
1061
|
+
function debounce(fn, ms) { let t; return (...a)=>{clearTimeout(t); t=setTimeout(()=>fn(...a), ms);} }
|
|
1062
|
+
function textClean(el){ return (el?.textContent || "").trim(); }
|
|
1063
|
+
|
|
1064
|
+
// Wait until Finsweet cmsfilter is ready (query params applied, classes toggled)
|
|
1065
|
+
function waitForCmsFilter() {
|
|
1066
|
+
return new Promise((resolve) => {
|
|
1067
|
+
const w = window;
|
|
1068
|
+
w.fsAttributes = w.fsAttributes || [];
|
|
1069
|
+
// already initialized?
|
|
1070
|
+
try {
|
|
1071
|
+
const exists = (w.fsAttributes.instances || []).some(i => i.name === "cmsfilter");
|
|
1072
|
+
if (exists) return resolve();
|
|
1073
|
+
} catch(e){}
|
|
1074
|
+
w.fsAttributes.push(["cmsfilter", function(){ resolve(); }]);
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1020
1077
|
|
|
1021
|
-
|
|
1078
|
+
// Remember original texts for labels and toggles
|
|
1079
|
+
function hydrateOriginalTexts() {
|
|
1080
|
+
FIELDS.forEach((field) => {
|
|
1081
|
+
$all(`span[fs-cmsfilter-field="${field}"]`).forEach((span) => {
|
|
1082
|
+
if (!span.dataset.originalText) {
|
|
1083
|
+
span.dataset.originalText = textClean(span);
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
});
|
|
1087
|
+
$all(SEL.dropdownHolder).forEach((holder) => {
|
|
1088
|
+
const toggle = $(SEL.dropdownTitle, holder);
|
|
1089
|
+
if (toggle && !toggle.dataset.defaultText) {
|
|
1090
|
+
toggle.dataset.defaultText = textClean(toggle);
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1022
1094
|
|
|
1023
|
-
|
|
1095
|
+
// Count matching results per value and sort UI items by count
|
|
1096
|
+
function updateCheckboxCounts(excludeField = null) {
|
|
1097
|
+
FIELDS.forEach((field) => {
|
|
1024
1098
|
if (field === excludeField) return;
|
|
1025
1099
|
|
|
1026
|
-
const
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
checkbox.dataset.originalText = originalText;
|
|
1100
|
+
const spans = $all(`span[fs-cmsfilter-field="${field}"]`);
|
|
1101
|
+
spans.forEach((span) => {
|
|
1102
|
+
const originalText = span.dataset.originalText || textClean(span);
|
|
1103
|
+
span.dataset.originalText = originalText;
|
|
1031
1104
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
if (div
|
|
1036
|
-
matchedCount++;
|
|
1037
|
-
}
|
|
1105
|
+
// Count visible matches in results
|
|
1106
|
+
let matched = 0;
|
|
1107
|
+
$all(`div[fs-cmsfilter-field="${field}"]`).forEach((div) => {
|
|
1108
|
+
if (textClean(div) === originalText) matched++;
|
|
1038
1109
|
});
|
|
1039
1110
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
countSpan
|
|
1044
|
-
|
|
1111
|
+
// Ensure "(n)" bubble
|
|
1112
|
+
let countSpan = span.nextElementSibling;
|
|
1113
|
+
if (!countSpan || !countSpan.classList.contains("count")) {
|
|
1114
|
+
countSpan = document.createElement("span");
|
|
1115
|
+
countSpan.className = "count";
|
|
1116
|
+
span.insertAdjacentElement("afterend", countSpan);
|
|
1045
1117
|
}
|
|
1046
|
-
countSpan.textContent = `(${
|
|
1118
|
+
countSpan.textContent = `(${matched})`;
|
|
1047
1119
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
} else {
|
|
1052
|
-
parentDiv.classList.remove('empty-category');
|
|
1053
|
-
}
|
|
1120
|
+
// Toggle empty-category on dyn item
|
|
1121
|
+
const dyn = span.closest(SEL.dynItem);
|
|
1122
|
+
if (dyn) dyn.classList.toggle("empty-category", matched === 0);
|
|
1054
1123
|
});
|
|
1055
1124
|
|
|
1056
|
-
|
|
1125
|
+
// Sort items in this field's collection-list by count desc
|
|
1126
|
+
const anySpan = document.querySelector(`span[fs-cmsfilter-field="${field}"]`);
|
|
1127
|
+
if (anySpan) {
|
|
1128
|
+
const list = anySpan.closest(SEL.listContainer);
|
|
1129
|
+
if (list) {
|
|
1130
|
+
const items = Array.from(list.children);
|
|
1131
|
+
items.sort((a, b) => {
|
|
1132
|
+
const ca = parseInt((a.querySelector(SEL.count)?.textContent || "").replace(/[()]/g, "")) || 0;
|
|
1133
|
+
const cb = parseInt((b.querySelector(SEL.count)?.textContent || "").replace(/[()]/g, "")) || 0;
|
|
1134
|
+
return cb - ca;
|
|
1135
|
+
});
|
|
1136
|
+
items.forEach((it) => list.appendChild(it));
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1057
1139
|
});
|
|
1058
1140
|
}
|
|
1059
1141
|
|
|
1060
|
-
|
|
1142
|
+
// Collect selected label names within a dropdown holder
|
|
1143
|
+
function getSelectedNames(holder) {
|
|
1144
|
+
const labels = $all(`${SEL.dynItem} ${SEL.checkboxWrap}, ${SEL.checkboxWrap}`, holder);
|
|
1145
|
+
return labels
|
|
1146
|
+
.filter((label) => {
|
|
1147
|
+
if (label.classList.contains(SEL.fsActive)) return true; // Finsweet
|
|
1148
|
+
const wbox = label.querySelector(".w-checkbox-input");
|
|
1149
|
+
if (wbox && wbox.classList.contains(SEL.webflowChecked)) return true; // Webflow styled
|
|
1150
|
+
const input = label.querySelector(SEL.checkboxInput);
|
|
1151
|
+
return !!(input && input.checked); // Native
|
|
1152
|
+
})
|
|
1153
|
+
.map((label) => {
|
|
1154
|
+
const span = label.querySelector('[fs-cmsfilter-field]');
|
|
1155
|
+
return (span && (span.getAttribute('data-original-text') || textClean(span))) || "";
|
|
1156
|
+
})
|
|
1157
|
+
.filter(Boolean);
|
|
1158
|
+
}
|
|
1061
1159
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1160
|
+
// Update dropdown button label (first selected + +N), class states and numeric tag
|
|
1161
|
+
function updateDropdownTitle(holder) {
|
|
1162
|
+
const toggle = $(SEL.dropdownTitle, holder);
|
|
1163
|
+
if (!toggle) return;
|
|
1164
|
+
|
|
1165
|
+
const defaults = toggle.dataset.defaultText || textClean(toggle);
|
|
1166
|
+
const names = getSelectedNames(holder);
|
|
1167
|
+
|
|
1168
|
+
// Persist "first selected" per dropdown (dataset)
|
|
1169
|
+
if (!holder.dataset.firstSelected) {
|
|
1170
|
+
holder.dataset.firstSelected = names[0] || "";
|
|
1171
|
+
} else if (names.length && !names.includes(holder.dataset.firstSelected)) {
|
|
1172
|
+
holder.dataset.firstSelected = names[0];
|
|
1173
|
+
} else if (!names.length) {
|
|
1174
|
+
holder.dataset.firstSelected = "";
|
|
1175
|
+
}
|
|
1064
1176
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1177
|
+
let display = defaults;
|
|
1178
|
+
if (names.length === 1) {
|
|
1179
|
+
display = names[0];
|
|
1180
|
+
} else if (names.length > 1) {
|
|
1181
|
+
display = `${holder.dataset.firstSelected || names[0]} +${names.length - 1}`;
|
|
1182
|
+
}
|
|
1183
|
+
toggle.textContent = display;
|
|
1070
1184
|
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1185
|
+
holder.classList.toggle(SEL.activeDropdown, names.length > 0);
|
|
1186
|
+
toggle.classList.toggle(SEL.activeDropdown, names.length > 0);
|
|
1187
|
+
|
|
1188
|
+
const tagNum = $(SEL.tagNumber, holder);
|
|
1189
|
+
if (tagNum) {
|
|
1190
|
+
tagNum.textContent = names.length > 1 ? `+${names.length - 1}` : "";
|
|
1191
|
+
}
|
|
1074
1192
|
}
|
|
1075
1193
|
|
|
1076
|
-
|
|
1194
|
+
function updateAllDropdownTitles() {
|
|
1195
|
+
$all(SEL.dropdownHolder).forEach(updateDropdownTitle);
|
|
1196
|
+
}
|
|
1077
1197
|
|
|
1078
|
-
function
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1198
|
+
function toggleClearButton() {
|
|
1199
|
+
const anyActive = !!$(`.${SEL.activeDropdown}`);
|
|
1200
|
+
const btn = $(SEL.clearButton);
|
|
1201
|
+
if (btn) btn.style.display = anyActive ? "block" : "none";
|
|
1082
1202
|
}
|
|
1083
1203
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1204
|
+
// Unified UI refresh
|
|
1205
|
+
const refreshUI = debounce(function (excludeField = null) {
|
|
1206
|
+
updateCheckboxCounts(excludeField);
|
|
1207
|
+
updateAllDropdownTitles();
|
|
1208
|
+
toggleClearButton();
|
|
1209
|
+
}, 60);
|
|
1210
|
+
|
|
1211
|
+
// --- Event wiring (delegated & once) ---
|
|
1212
|
+
function bindEventsOnce() {
|
|
1213
|
+
// Clicks: checkbox labels, dropdown toggles, cross icons, clear button
|
|
1214
|
+
document.addEventListener("click", (e) => {
|
|
1215
|
+
const label = e.target.closest(SEL.checkboxWrap);
|
|
1216
|
+
if (label) {
|
|
1217
|
+
// Let Finsweet/Webflow toggle classes first
|
|
1218
|
+
setTimeout(() => {
|
|
1219
|
+
const span = label.querySelector('[fs-cmsfilter-field]');
|
|
1220
|
+
const field = span?.getAttribute('fs-cmsfilter-field') || null;
|
|
1221
|
+
refreshUI(field);
|
|
1222
|
+
}, 0);
|
|
1223
|
+
return;
|
|
1090
1224
|
}
|
|
1091
|
-
});
|
|
1092
|
-
});
|
|
1093
1225
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1226
|
+
const toggle = e.target.closest(SEL.dropdownTitle);
|
|
1227
|
+
if (toggle) {
|
|
1228
|
+
// After opening/closing or instant selections
|
|
1229
|
+
setTimeout(() => refreshUI(null), 120);
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
const cross = e.target.closest(SEL.crossIcon);
|
|
1234
|
+
if (cross) {
|
|
1235
|
+
setTimeout(() => refreshUI(null), 80);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
const clear = e.target.closest(SEL.clearButton);
|
|
1240
|
+
if (clear) {
|
|
1241
|
+
setTimeout(() => refreshUI(null), 120);
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1097
1244
|
});
|
|
1098
|
-
});
|
|
1099
1245
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1246
|
+
// Any checkbox state change (keyboard/tap)
|
|
1247
|
+
document.addEventListener("change", (e) => {
|
|
1248
|
+
if (e.target && e.target.matches(SEL.checkboxInput)) {
|
|
1249
|
+
const span = e.target.closest('label')?.querySelector('[fs-cmsfilter-field]') ||
|
|
1250
|
+
e.target.closest(SEL.checkboxWrap)?.querySelector('[fs-cmsfilter-field]');
|
|
1251
|
+
const field = span?.getAttribute('fs-cmsfilter-field') || null;
|
|
1252
|
+
const holder = e.target.closest(SEL.dropdownHolder);
|
|
1253
|
+
if (holder) updateDropdownTitle(holder);
|
|
1254
|
+
refreshUI(field);
|
|
1255
|
+
}
|
|
1103
1256
|
});
|
|
1104
|
-
});
|
|
1105
1257
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1258
|
+
// Observe class mutations that might come from Finsweet
|
|
1259
|
+
const mo = new MutationObserver((muts) => {
|
|
1260
|
+
// If any class changed, softly refresh
|
|
1261
|
+
if (muts.some(m => m.type === "attributes" && m.attributeName === "class")) {
|
|
1262
|
+
refreshUI(null);
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
mo.observe(document.body, { subtree: true, attributes: true, attributeFilter: ["class"] });
|
|
1112
1266
|
}
|
|
1113
|
-
toggleClearButton();
|
|
1114
|
-
const observer = new MutationObserver(toggleClearButton);
|
|
1115
|
-
|
|
1116
|
-
observer.observe(document.body, {
|
|
1117
|
-
childList: true,
|
|
1118
|
-
subtree: true,
|
|
1119
|
-
attributes: true,
|
|
1120
|
-
attributeFilter: ['class']
|
|
1121
|
-
});
|
|
1122
|
-
|
|
1123
|
-
document.querySelectorAll('[fs-cmsfilter-element="dropdown"]').forEach(function(dropdown) {
|
|
1124
|
-
const originalText = dropdown.querySelector('[filters-clients_dropdown-field="tag-name-title"]').textContent;
|
|
1125
|
-
let firstCheckedText = null;
|
|
1126
|
-
|
|
1127
|
-
dropdown.querySelectorAll('input[type="checkbox"]').forEach(function(checkbox) {
|
|
1128
|
-
checkbox.addEventListener('change', function() {
|
|
1129
|
-
const checkedBoxes = dropdown.querySelectorAll('input[type="checkbox"]:checked');
|
|
1130
|
-
const titleField = dropdown.querySelector('[filters-clients_dropdown-field="tag-name-title"]');
|
|
1131
|
-
const dropdownHolder = dropdown.closest('.dropdown-holder');
|
|
1132
|
-
const tagNumber = dropdown.querySelector('[fs-cmsfilter-element="tag-number"]');
|
|
1133
|
-
|
|
1134
|
-
if (checkedBoxes.length > 0) {
|
|
1135
|
-
// Check if firstCheckedText is still checked
|
|
1136
|
-
if (![...checkedBoxes].some(cb => cb.nextElementSibling.textContent.split('(')[0].trim() === firstCheckedText)) {
|
|
1137
|
-
// If not, set it to the current checkbox text without the count
|
|
1138
|
-
firstCheckedText = checkedBoxes[0].nextElementSibling.textContent.split('(')[0].trim();
|
|
1139
|
-
}
|
|
1140
|
-
titleField.textContent = firstCheckedText;
|
|
1141
|
-
dropdownHolder.classList.add('active-dropdown');
|
|
1142
|
-
} else {
|
|
1143
|
-
titleField.textContent = originalText;
|
|
1144
|
-
dropdownHolder.classList.remove('active-dropdown');
|
|
1145
|
-
firstCheckedText = null;
|
|
1146
|
-
}
|
|
1147
1267
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
});
|
|
1268
|
+
// --- Boot ---
|
|
1269
|
+
waitForCmsFilter().then(() => {
|
|
1270
|
+
// Allow Finsweet to finish applying query param checks/classes
|
|
1271
|
+
setTimeout(() => {
|
|
1272
|
+
hydrateOriginalTexts();
|
|
1273
|
+
bindEventsOnce();
|
|
1274
|
+
refreshUI(null);
|
|
1275
|
+
}, 120);
|
|
1156
1276
|
});
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1277
|
+
};
|
|
1159
1278
|
// Filtering logic on Client's page
|
|
1160
1279
|
window.formatCounter = function() {
|
|
1161
1280
|
function format_number(x) {
|
|
@@ -1549,6 +1668,7 @@ window.initBentoRevealScrub = function (container) {
|
|
|
1549
1668
|
}
|
|
1550
1669
|
);
|
|
1551
1670
|
};
|
|
1671
|
+
|
|
1552
1672
|
window.initPartnerVideos = function (container) {
|
|
1553
1673
|
const items = Array.from(container.querySelectorAll('.partner_item'));
|
|
1554
1674
|
const vids = items.map(i => i.querySelector('video')).filter(Boolean);
|
|
@@ -1910,4 +2030,5 @@ window.closeInfoBar = function() {
|
|
|
1910
2030
|
customCursorOnClick();
|
|
1911
2031
|
contactFormOpenAnimation();
|
|
1912
2032
|
disableScrollMenu();
|
|
2033
|
+
scrollToHashOnLoad();
|
|
1913
2034
|
});
|