@widelab-nc/widelab 1.1.34 → 1.1.36

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@widelab-nc/widelab",
3
- "version": "1.1.34",
3
+ "version": "1.1.36",
4
4
  "description": "Widelab starter template based on Finsweet template + add-ons.",
5
5
  "homepage": "https://widelab.co",
6
6
  "license": "ISC",
package/src/index.js CHANGED
@@ -260,6 +260,39 @@ if (infoBar) {
260
260
  });
261
261
  }
262
262
 
263
+
264
+
265
+ // Safe new version that uses the same classes and structure
266
+ window.customCursorTextUpdate = function(el) {
267
+ const cursorEl = document.querySelector('.custom_cursor_text');
268
+ if (!cursorEl) return;
269
+
270
+ const defaultText = cursorEl.dataset.default || '';
271
+
272
+ const getText = (node) =>
273
+ node.getAttribute('widecursortext') || node.getAttribute('wideCursorText') || '';
274
+
275
+ const activate = () => {
276
+ cursorEl.textContent = getText(el);
277
+ document.documentElement.classList.add('has-custom-cursor');
278
+ };
279
+
280
+ const maybeDeactivate = (ev) => {
281
+ const to = ev.relatedTarget;
282
+ // Don't deactivate if moving between elements that also use the custom cursor
283
+ if (to && to.closest && to.closest('[wide="customCursor"]')) return;
284
+
285
+ cursorEl.textContent = defaultText;
286
+ document.documentElement.classList.remove('has-custom-cursor');
287
+ };
288
+
289
+ el.addEventListener('pointerenter', activate);
290
+ el.addEventListener('pointerleave', maybeDeactivate);
291
+ };
292
+
293
+
294
+
295
+
263
296
  // Hero animation on load
264
297
  window.heroAnimationOnLoad = function(e) {
265
298
  const videoMask = e.querySelector('.hero_headline_ract-mask');
@@ -627,6 +660,52 @@ window.reviewsSlider = function(e) {
627
660
  });
628
661
  }
629
662
 
663
+ // Partner slider
664
+ window.partnerSlider = function (e) {
665
+ // Keep the same querySelector for consistency with teamSlider
666
+ const teamList = e.querySelector('.team_collection_list');
667
+
668
+ // Initialize Splide with drag restored
669
+ const splide = new Splide(e, {
670
+ type: 'slide',
671
+ fixedWidth: '31.5%',
672
+ padding: { left: '2.5rem', right: 0 },
673
+ focus: 0,
674
+ gap: 0,
675
+ pagination: false,
676
+ omitEnd: true,
677
+ arrows: false,
678
+ drag: true, // restored dragging
679
+ easing: 'cubic-bezier(0.83, 0, 0.17, 1)',
680
+ snap: true,
681
+ classes: {
682
+ pagination: 'splide__pagination facts-pagination partner-bullets',
683
+ page: 'splide__pagination__page facts-page',
684
+ },
685
+ breakpoints: {
686
+ 992: { fixedWidth: '40%', padding: { left: '1.75rem' }, drag: 'free', snap: false },
687
+ 560: { fixedWidth: '75%', padding: { left: '1.375rem' }, drag: 'free', snap: false },
688
+ },
689
+ });
690
+
691
+ splide.mount();
692
+
693
+ // Removed: click-to-next handler
694
+
695
+ // GSAP in-view animation
696
+ gsap.from(e, {
697
+ scrollTrigger: {
698
+ trigger: e,
699
+ start: 'top bottom',
700
+ end: 'bottom bottom',
701
+ scrub: 1,
702
+ once: true,
703
+ },
704
+ x: '10%',
705
+ opacity: 0,
706
+ ease: 'linear',
707
+ });
708
+ };
630
709
  // Element padding change on scroll
631
710
  window.elementPaddingChange = function(e) {
632
711
  gsap.to(e, {
@@ -967,149 +1046,251 @@ window.badgeColorChange = function(e) {
967
1046
  });
968
1047
  }
969
1048
 
1049
+ // Single entry you call from x-init
1050
+ window.clientFilters = function () {
1051
+ // Prevent double initialization if Alpine re-mounts
1052
+ if (window.__clientFiltersInitialized) return;
1053
+ window.__clientFiltersInitialized = true;
1054
+
1055
+ // --- Config ---
1056
+ const FIELDS = ["industry", "country", "services"];
1057
+ const SEL = {
1058
+ dropdown: '[fs-cmsfilter-element="dropdown"]',
1059
+ dropdownHolder: ".dropdown-holder",
1060
+ dropdownTitle: '[filters-clients_dropdown-field="tag-name-title"], .filters-clients_dropdown-field',
1061
+ tagNumber: '[fs-cmsfilter-element="tag-number"]',
1062
+ dynItem: ".w-dyn-item",
1063
+ checkboxWrap: ".w-checkbox",
1064
+ checkboxInput: 'input[type="checkbox"]',
1065
+ webflowChecked: "w--redirected-checked",
1066
+ fsActive: "fs-cmsfilter_active",
1067
+ listContainer: ".collection-list",
1068
+ count: ".count",
1069
+ crossIcon: ".cross-icon",
1070
+ clearButton: ".clear-button",
1071
+ activeDropdown: "active-dropdown",
1072
+ };
970
1073
 
971
- // Filtering logic on Client's page
972
- window.clientFilters = function() {
973
- function updateCheckboxCounts(excludeField) {
1074
+ // --- Utils ---
1075
+ function $(s, ctx = document) { return ctx.querySelector(s); }
1076
+ function $all(s, ctx = document) { return Array.from(ctx.querySelectorAll(s)); }
1077
+ function debounce(fn, ms) { let t; return (...a)=>{clearTimeout(t); t=setTimeout(()=>fn(...a), ms);} }
1078
+ function textClean(el){ return (el?.textContent || "").trim(); }
1079
+
1080
+ // Wait until Finsweet cmsfilter is ready (query params applied, classes toggled)
1081
+ function waitForCmsFilter() {
1082
+ return new Promise((resolve) => {
1083
+ const w = window;
1084
+ w.fsAttributes = w.fsAttributes || [];
1085
+ // already initialized?
1086
+ try {
1087
+ const exists = (w.fsAttributes.instances || []).some(i => i.name === "cmsfilter");
1088
+ if (exists) return resolve();
1089
+ } catch(e){}
1090
+ w.fsAttributes.push(["cmsfilter", function(){ resolve(); }]);
1091
+ });
1092
+ }
974
1093
 
975
- const fields = ['industry', 'country', 'services'];
1094
+ // Remember original texts for labels and toggles
1095
+ function hydrateOriginalTexts() {
1096
+ FIELDS.forEach((field) => {
1097
+ $all(`span[fs-cmsfilter-field="${field}"]`).forEach((span) => {
1098
+ if (!span.dataset.originalText) {
1099
+ span.dataset.originalText = textClean(span);
1100
+ }
1101
+ });
1102
+ });
1103
+ $all(SEL.dropdownHolder).forEach((holder) => {
1104
+ const toggle = $(SEL.dropdownTitle, holder);
1105
+ if (toggle && !toggle.dataset.defaultText) {
1106
+ toggle.dataset.defaultText = textClean(toggle);
1107
+ }
1108
+ });
1109
+ }
976
1110
 
977
- fields.forEach(function(field) {
1111
+ // Count matching results per value and sort UI items by count
1112
+ function updateCheckboxCounts(excludeField = null) {
1113
+ FIELDS.forEach((field) => {
978
1114
  if (field === excludeField) return;
979
1115
 
980
- const checkboxes = document.querySelectorAll(`span[fs-cmsfilter-field="${field}"]`);
981
-
982
- checkboxes.forEach(function(checkbox) {
983
- const originalText = checkbox.dataset.originalText || checkbox.textContent.trim();
984
- checkbox.dataset.originalText = originalText;
1116
+ const spans = $all(`span[fs-cmsfilter-field="${field}"]`);
1117
+ spans.forEach((span) => {
1118
+ const originalText = span.dataset.originalText || textClean(span);
1119
+ span.dataset.originalText = originalText;
985
1120
 
986
- let matchedCount = 0;
987
-
988
- document.querySelectorAll(`div[fs-cmsfilter-field="${field}"]`).forEach(function(div) {
989
- if (div.textContent.trim() === originalText) {
990
- matchedCount++;
991
- }
1121
+ // Count visible matches in results
1122
+ let matched = 0;
1123
+ $all(`div[fs-cmsfilter-field="${field}"]`).forEach((div) => {
1124
+ if (textClean(div) === originalText) matched++;
992
1125
  });
993
1126
 
994
- let countSpan = checkbox.nextElementSibling;
995
- if (!countSpan || !countSpan.classList.contains('count')) {
996
- countSpan = document.createElement('span');
997
- countSpan.className = 'count';
998
- checkbox.insertAdjacentElement('afterend', countSpan);
1127
+ // Ensure "(n)" bubble
1128
+ let countSpan = span.nextElementSibling;
1129
+ if (!countSpan || !countSpan.classList.contains("count")) {
1130
+ countSpan = document.createElement("span");
1131
+ countSpan.className = "count";
1132
+ span.insertAdjacentElement("afterend", countSpan);
999
1133
  }
1000
- countSpan.textContent = `(${matchedCount})`;
1134
+ countSpan.textContent = `(${matched})`;
1001
1135
 
1002
- const parentDiv = checkbox.closest('div.w-dyn-item');
1003
- if (matchedCount === 0) {
1004
- parentDiv.classList.add('empty-category');
1005
- } else {
1006
- parentDiv.classList.remove('empty-category');
1007
- }
1136
+ // Toggle empty-category on dyn item
1137
+ const dyn = span.closest(SEL.dynItem);
1138
+ if (dyn) dyn.classList.toggle("empty-category", matched === 0);
1008
1139
  });
1009
1140
 
1010
- sortCheckboxes(field);
1141
+ // Sort items in this field's collection-list by count desc
1142
+ const anySpan = document.querySelector(`span[fs-cmsfilter-field="${field}"]`);
1143
+ if (anySpan) {
1144
+ const list = anySpan.closest(SEL.listContainer);
1145
+ if (list) {
1146
+ const items = Array.from(list.children);
1147
+ items.sort((a, b) => {
1148
+ const ca = parseInt((a.querySelector(SEL.count)?.textContent || "").replace(/[()]/g, "")) || 0;
1149
+ const cb = parseInt((b.querySelector(SEL.count)?.textContent || "").replace(/[()]/g, "")) || 0;
1150
+ return cb - ca;
1151
+ });
1152
+ items.forEach((it) => list.appendChild(it));
1153
+ }
1154
+ }
1011
1155
  });
1012
1156
  }
1013
1157
 
1014
- function sortCheckboxes(field) {
1158
+ // Collect selected label names within a dropdown holder
1159
+ function getSelectedNames(holder) {
1160
+ const labels = $all(`${SEL.dynItem} ${SEL.checkboxWrap}, ${SEL.checkboxWrap}`, holder);
1161
+ return labels
1162
+ .filter((label) => {
1163
+ if (label.classList.contains(SEL.fsActive)) return true; // Finsweet
1164
+ const wbox = label.querySelector(".w-checkbox-input");
1165
+ if (wbox && wbox.classList.contains(SEL.webflowChecked)) return true; // Webflow styled
1166
+ const input = label.querySelector(SEL.checkboxInput);
1167
+ return !!(input && input.checked); // Native
1168
+ })
1169
+ .map((label) => {
1170
+ const span = label.querySelector('[fs-cmsfilter-field]');
1171
+ return (span && (span.getAttribute('data-original-text') || textClean(span))) || "";
1172
+ })
1173
+ .filter(Boolean);
1174
+ }
1175
+
1176
+ // Update dropdown button label (first selected + +N), class states and numeric tag
1177
+ function updateDropdownTitle(holder) {
1178
+ const toggle = $(SEL.dropdownTitle, holder);
1179
+ if (!toggle) return;
1180
+
1181
+ const defaults = toggle.dataset.defaultText || textClean(toggle);
1182
+ const names = getSelectedNames(holder);
1183
+
1184
+ // Persist "first selected" per dropdown (dataset)
1185
+ if (!holder.dataset.firstSelected) {
1186
+ holder.dataset.firstSelected = names[0] || "";
1187
+ } else if (names.length && !names.includes(holder.dataset.firstSelected)) {
1188
+ holder.dataset.firstSelected = names[0];
1189
+ } else if (!names.length) {
1190
+ holder.dataset.firstSelected = "";
1191
+ }
1015
1192
 
1016
- const parentList = document.querySelector(`span[fs-cmsfilter-field="${field}"]`).closest('.collection-list');
1017
- const items = Array.from(parentList.children);
1193
+ let display = defaults;
1194
+ if (names.length === 1) {
1195
+ display = names[0];
1196
+ } else if (names.length > 1) {
1197
+ display = `${holder.dataset.firstSelected || names[0]} +${names.length - 1}`;
1198
+ }
1199
+ toggle.textContent = display;
1018
1200
 
1019
- items.sort(function(a, b) {
1020
- const countA = parseInt((a.querySelector('.count')?.textContent || '').replace(/[()]/g, '')) || 0;
1021
- const countB = parseInt((b.querySelector('.count')?.textContent || '').replace(/[()]/g, '')) || 0;
1022
- return countB - countA;
1023
- });
1201
+ holder.classList.toggle(SEL.activeDropdown, names.length > 0);
1202
+ toggle.classList.toggle(SEL.activeDropdown, names.length > 0);
1024
1203
 
1025
- items.forEach(function(item) {
1026
- parentList.appendChild(item);
1027
- });
1204
+ const tagNum = $(SEL.tagNumber, holder);
1205
+ if (tagNum) {
1206
+ tagNum.textContent = names.length > 1 ? `+${names.length - 1}` : "";
1207
+ }
1028
1208
  }
1029
1209
 
1030
- updateCheckboxCounts();
1210
+ function updateAllDropdownTitles() {
1211
+ $all(SEL.dropdownHolder).forEach(updateDropdownTitle);
1212
+ }
1031
1213
 
1032
- function delayedUpdate(excludeField) {
1033
- setTimeout(function() {
1034
- updateCheckboxCounts(excludeField);
1035
- }, 200);
1214
+ function toggleClearButton() {
1215
+ const anyActive = !!$(`.${SEL.activeDropdown}`);
1216
+ const btn = $(SEL.clearButton);
1217
+ if (btn) btn.style.display = anyActive ? "block" : "none";
1036
1218
  }
1037
1219
 
1038
- document.querySelectorAll('.w-checkbox').forEach(function(checkbox) {
1039
- checkbox.addEventListener('click', function() {
1040
- const spanElement = checkbox.closest('label').querySelector('span[fs-cmsfilter-field]');
1041
- if (spanElement) {
1042
- const field = spanElement.getAttribute('fs-cmsfilter-field');
1043
- delayedUpdate(field);
1220
+ // Unified UI refresh
1221
+ const refreshUI = debounce(function (excludeField = null) {
1222
+ updateCheckboxCounts(excludeField);
1223
+ updateAllDropdownTitles();
1224
+ toggleClearButton();
1225
+ }, 60);
1226
+
1227
+ // --- Event wiring (delegated & once) ---
1228
+ function bindEventsOnce() {
1229
+ // Clicks: checkbox labels, dropdown toggles, cross icons, clear button
1230
+ document.addEventListener("click", (e) => {
1231
+ const label = e.target.closest(SEL.checkboxWrap);
1232
+ if (label) {
1233
+ // Let Finsweet/Webflow toggle classes first
1234
+ setTimeout(() => {
1235
+ const span = label.querySelector('[fs-cmsfilter-field]');
1236
+ const field = span?.getAttribute('fs-cmsfilter-field') || null;
1237
+ refreshUI(field);
1238
+ }, 0);
1239
+ return;
1240
+ }
1241
+
1242
+ const toggle = e.target.closest(SEL.dropdownTitle);
1243
+ if (toggle) {
1244
+ // After opening/closing or instant selections
1245
+ setTimeout(() => refreshUI(null), 120);
1246
+ return;
1044
1247
  }
1045
- });
1046
- });
1047
1248
 
1048
- document.querySelectorAll('.cross-icon').forEach(function(icon) {
1049
- icon.addEventListener('click', function() {
1050
- delayedUpdate(null); // Update all fields with 200ms delay
1249
+ const cross = e.target.closest(SEL.crossIcon);
1250
+ if (cross) {
1251
+ setTimeout(() => refreshUI(null), 80);
1252
+ return;
1253
+ }
1254
+
1255
+ const clear = e.target.closest(SEL.clearButton);
1256
+ if (clear) {
1257
+ setTimeout(() => refreshUI(null), 120);
1258
+ return;
1259
+ }
1051
1260
  });
1052
- });
1053
1261
 
1054
- document.querySelectorAll('.filters-clients_dropdown-field').forEach(function(dropdownField) {
1055
- dropdownField.addEventListener('click', function() {
1056
- delayedUpdate(null); // Update all fields with 200ms delay
1262
+ // Any checkbox state change (keyboard/tap)
1263
+ document.addEventListener("change", (e) => {
1264
+ if (e.target && e.target.matches(SEL.checkboxInput)) {
1265
+ const span = e.target.closest('label')?.querySelector('[fs-cmsfilter-field]') ||
1266
+ e.target.closest(SEL.checkboxWrap)?.querySelector('[fs-cmsfilter-field]');
1267
+ const field = span?.getAttribute('fs-cmsfilter-field') || null;
1268
+ const holder = e.target.closest(SEL.dropdownHolder);
1269
+ if (holder) updateDropdownTitle(holder);
1270
+ refreshUI(field);
1271
+ }
1057
1272
  });
1058
- });
1059
1273
 
1060
- function toggleClearButton() {
1061
- if (document.querySelectorAll('.active-dropdown').length > 0) {
1062
- document.querySelector('.clear-button').style.display = 'block';
1063
- } else {
1064
- document.querySelector('.clear-button').style.display = 'none';
1065
- }
1274
+ // Observe class mutations that might come from Finsweet
1275
+ const mo = new MutationObserver((muts) => {
1276
+ // If any class changed, softly refresh
1277
+ if (muts.some(m => m.type === "attributes" && m.attributeName === "class")) {
1278
+ refreshUI(null);
1279
+ }
1280
+ });
1281
+ mo.observe(document.body, { subtree: true, attributes: true, attributeFilter: ["class"] });
1066
1282
  }
1067
- toggleClearButton();
1068
- const observer = new MutationObserver(toggleClearButton);
1069
-
1070
- observer.observe(document.body, {
1071
- childList: true,
1072
- subtree: true,
1073
- attributes: true,
1074
- attributeFilter: ['class']
1075
- });
1076
1283
 
1077
- document.querySelectorAll('[fs-cmsfilter-element="dropdown"]').forEach(function(dropdown) {
1078
- const originalText = dropdown.querySelector('[filters-clients_dropdown-field="tag-name-title"]').textContent;
1079
- let firstCheckedText = null;
1080
-
1081
- dropdown.querySelectorAll('input[type="checkbox"]').forEach(function(checkbox) {
1082
- checkbox.addEventListener('change', function() {
1083
- const checkedBoxes = dropdown.querySelectorAll('input[type="checkbox"]:checked');
1084
- const titleField = dropdown.querySelector('[filters-clients_dropdown-field="tag-name-title"]');
1085
- const dropdownHolder = dropdown.closest('.dropdown-holder');
1086
- const tagNumber = dropdown.querySelector('[fs-cmsfilter-element="tag-number"]');
1087
-
1088
- if (checkedBoxes.length > 0) {
1089
- // Check if firstCheckedText is still checked
1090
- if (![...checkedBoxes].some(cb => cb.nextElementSibling.textContent.split('(')[0].trim() === firstCheckedText)) {
1091
- // If not, set it to the current checkbox text without the count
1092
- firstCheckedText = checkedBoxes[0].nextElementSibling.textContent.split('(')[0].trim();
1093
- }
1094
- titleField.textContent = firstCheckedText;
1095
- dropdownHolder.classList.add('active-dropdown');
1096
- } else {
1097
- titleField.textContent = originalText;
1098
- dropdownHolder.classList.remove('active-dropdown');
1099
- firstCheckedText = null;
1100
- }
1101
-
1102
- if (checkedBoxes.length > 1) {
1103
- const numberText = `+${checkedBoxes.length - 1}`;
1104
- tagNumber.textContent = numberText;
1105
- } else {
1106
- tagNumber.textContent = '';
1107
- }
1108
- });
1109
- });
1284
+ // --- Boot ---
1285
+ waitForCmsFilter().then(() => {
1286
+ // Allow Finsweet to finish applying query param checks/classes
1287
+ setTimeout(() => {
1288
+ hydrateOriginalTexts();
1289
+ bindEventsOnce();
1290
+ refreshUI(null);
1291
+ }, 120);
1110
1292
  });
1111
- }
1112
-
1293
+ };
1113
1294
  // Filtering logic on Client's page
1114
1295
  window.formatCounter = function() {
1115
1296
  function format_number(x) {
@@ -1503,20 +1684,35 @@ window.initBentoRevealScrub = function (container) {
1503
1684
  }
1504
1685
  );
1505
1686
  };
1506
-
1507
1687
  window.initPartnerVideos = function (container) {
1508
1688
  const items = Array.from(container.querySelectorAll('.partner_item'));
1509
1689
  const vids = items.map(i => i.querySelector('video')).filter(Boolean);
1510
1690
 
1691
+ // Helper: pause all other videos
1511
1692
  const pauseAll = (except) => {
1512
1693
  vids.forEach(v => {
1513
1694
  if (v !== except) {
1514
1695
  v.pause();
1515
1696
  v.muted = true;
1697
+ const parent = v.closest('.partner_item');
1698
+ if (parent) toggleIcons(parent, false);
1516
1699
  }
1517
1700
  });
1518
1701
  };
1519
1702
 
1703
+ // Helper: toggle play/pause icons
1704
+ function toggleIcons(item, isPlaying) {
1705
+ const playIcon = item.querySelector('.is-play');
1706
+ const pauseIcon = item.querySelector('.is-pause');
1707
+ if (isPlaying) {
1708
+ if (playIcon) playIcon.style.display = 'none';
1709
+ if (pauseIcon) pauseIcon.style.display = 'block';
1710
+ } else {
1711
+ if (playIcon) playIcon.style.display = 'block';
1712
+ if (pauseIcon) pauseIcon.style.display = 'none';
1713
+ }
1714
+ }
1715
+
1520
1716
  items.forEach((item) => {
1521
1717
  const v = item.querySelector('video');
1522
1718
  if (!v) return;
@@ -1527,17 +1723,29 @@ window.initPartnerVideos = function (container) {
1527
1723
  v.setAttribute('playsinline', '');
1528
1724
  v.playsInline = true;
1529
1725
 
1726
+ // Default state on init (show play icon)
1727
+ toggleIcons(item, false);
1728
+
1729
+ // Handle click (play/pause toggle)
1530
1730
  item.addEventListener('click', () => {
1531
1731
  if (v.paused) {
1532
1732
  pauseAll(v);
1533
1733
  v.muted = false;
1534
1734
  v.play();
1735
+ toggleIcons(item, true);
1535
1736
  } else {
1536
1737
  v.pause();
1738
+ toggleIcons(item, false);
1537
1739
  }
1538
1740
  });
1741
+
1742
+ // Handle pause and ended (ensure icons reset)
1743
+ v.addEventListener('pause', () => toggleIcons(item, false));
1744
+ v.addEventListener('ended', () => toggleIcons(item, false));
1745
+ v.addEventListener('play', () => toggleIcons(item, true));
1539
1746
  });
1540
1747
 
1748
+ // Pause videos when out of viewport
1541
1749
  const rootEl =
1542
1750
  document.querySelector('[data-scroll-root]') ||
1543
1751
  document.querySelector('[data-smooth-scroll-root]') ||
@@ -1548,7 +1756,10 @@ window.initPartnerVideos = function (container) {
1548
1756
  entries.forEach(({ isIntersecting, target }) => {
1549
1757
  const v = target.querySelector('video');
1550
1758
  if (!v) return;
1551
- if (!isIntersecting) v.pause();
1759
+ if (!isIntersecting) {
1760
+ v.pause();
1761
+ toggleIcons(target, false);
1762
+ }
1552
1763
  });
1553
1764
  },
1554
1765
  { root: rootEl, threshold: 0.25 }
@@ -1558,7 +1769,6 @@ window.initPartnerVideos = function (container) {
1558
1769
  };
1559
1770
 
1560
1771
 
1561
-
1562
1772
  // Case study - What's next interaction
1563
1773
  window.whatsNext = function() {
1564
1774
  const caseItem = document.querySelectorAll('.whats-next_links_item.is-case');