@widelab-nc/widelab 1.1.35 → 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.35",
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');
@@ -635,11 +668,11 @@ window.partnerSlider = function (e) {
635
668
  // Initialize Splide with drag restored
636
669
  const splide = new Splide(e, {
637
670
  type: 'slide',
638
- fixedWidth: '20%',
671
+ fixedWidth: '31.5%',
639
672
  padding: { left: '2.5rem', right: 0 },
640
673
  focus: 0,
641
674
  gap: 0,
642
- pagination: true,
675
+ pagination: false,
643
676
  omitEnd: true,
644
677
  arrows: false,
645
678
  drag: true, // restored dragging
@@ -1013,149 +1046,251 @@ window.badgeColorChange = function(e) {
1013
1046
  });
1014
1047
  }
1015
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
+ };
1016
1073
 
1017
- // Filtering logic on Client's page
1018
- window.clientFilters = function() {
1019
- 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
+ }
1020
1093
 
1021
- 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
+ }
1022
1110
 
1023
- 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) => {
1024
1114
  if (field === excludeField) return;
1025
1115
 
1026
- const checkboxes = document.querySelectorAll(`span[fs-cmsfilter-field="${field}"]`);
1027
-
1028
- checkboxes.forEach(function(checkbox) {
1029
- const originalText = checkbox.dataset.originalText || checkbox.textContent.trim();
1030
- checkbox.dataset.originalText = originalText;
1031
-
1032
- let matchedCount = 0;
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;
1033
1120
 
1034
- document.querySelectorAll(`div[fs-cmsfilter-field="${field}"]`).forEach(function(div) {
1035
- if (div.textContent.trim() === originalText) {
1036
- matchedCount++;
1037
- }
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++;
1038
1125
  });
1039
1126
 
1040
- let countSpan = checkbox.nextElementSibling;
1041
- if (!countSpan || !countSpan.classList.contains('count')) {
1042
- countSpan = document.createElement('span');
1043
- countSpan.className = 'count';
1044
- 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);
1045
1133
  }
1046
- countSpan.textContent = `(${matchedCount})`;
1134
+ countSpan.textContent = `(${matched})`;
1047
1135
 
1048
- const parentDiv = checkbox.closest('div.w-dyn-item');
1049
- if (matchedCount === 0) {
1050
- parentDiv.classList.add('empty-category');
1051
- } else {
1052
- parentDiv.classList.remove('empty-category');
1053
- }
1136
+ // Toggle empty-category on dyn item
1137
+ const dyn = span.closest(SEL.dynItem);
1138
+ if (dyn) dyn.classList.toggle("empty-category", matched === 0);
1054
1139
  });
1055
1140
 
1056
- 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
+ }
1057
1155
  });
1058
1156
  }
1059
1157
 
1060
- 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
+ }
1061
1175
 
1062
- const parentList = document.querySelector(`span[fs-cmsfilter-field="${field}"]`).closest('.collection-list');
1063
- const items = Array.from(parentList.children);
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
+ }
1064
1192
 
1065
- items.sort(function(a, b) {
1066
- const countA = parseInt((a.querySelector('.count')?.textContent || '').replace(/[()]/g, '')) || 0;
1067
- const countB = parseInt((b.querySelector('.count')?.textContent || '').replace(/[()]/g, '')) || 0;
1068
- return countB - countA;
1069
- });
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;
1070
1200
 
1071
- items.forEach(function(item) {
1072
- parentList.appendChild(item);
1073
- });
1201
+ holder.classList.toggle(SEL.activeDropdown, names.length > 0);
1202
+ toggle.classList.toggle(SEL.activeDropdown, names.length > 0);
1203
+
1204
+ const tagNum = $(SEL.tagNumber, holder);
1205
+ if (tagNum) {
1206
+ tagNum.textContent = names.length > 1 ? `+${names.length - 1}` : "";
1207
+ }
1074
1208
  }
1075
1209
 
1076
- updateCheckboxCounts();
1210
+ function updateAllDropdownTitles() {
1211
+ $all(SEL.dropdownHolder).forEach(updateDropdownTitle);
1212
+ }
1077
1213
 
1078
- function delayedUpdate(excludeField) {
1079
- setTimeout(function() {
1080
- updateCheckboxCounts(excludeField);
1081
- }, 200);
1214
+ function toggleClearButton() {
1215
+ const anyActive = !!$(`.${SEL.activeDropdown}`);
1216
+ const btn = $(SEL.clearButton);
1217
+ if (btn) btn.style.display = anyActive ? "block" : "none";
1082
1218
  }
1083
1219
 
1084
- document.querySelectorAll('.w-checkbox').forEach(function(checkbox) {
1085
- checkbox.addEventListener('click', function() {
1086
- const spanElement = checkbox.closest('label').querySelector('span[fs-cmsfilter-field]');
1087
- if (spanElement) {
1088
- const field = spanElement.getAttribute('fs-cmsfilter-field');
1089
- 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;
1090
1240
  }
1091
- });
1092
- });
1093
1241
 
1094
- document.querySelectorAll('.cross-icon').forEach(function(icon) {
1095
- icon.addEventListener('click', function() {
1096
- delayedUpdate(null); // Update all fields with 200ms delay
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;
1247
+ }
1248
+
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
+ }
1097
1260
  });
1098
- });
1099
1261
 
1100
- document.querySelectorAll('.filters-clients_dropdown-field').forEach(function(dropdownField) {
1101
- dropdownField.addEventListener('click', function() {
1102
- 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
+ }
1103
1272
  });
1104
- });
1105
1273
 
1106
- function toggleClearButton() {
1107
- if (document.querySelectorAll('.active-dropdown').length > 0) {
1108
- document.querySelector('.clear-button').style.display = 'block';
1109
- } else {
1110
- document.querySelector('.clear-button').style.display = 'none';
1111
- }
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"] });
1112
1282
  }
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
1283
 
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
-
1148
- if (checkedBoxes.length > 1) {
1149
- const numberText = `+${checkedBoxes.length - 1}`;
1150
- tagNumber.textContent = numberText;
1151
- } else {
1152
- tagNumber.textContent = '';
1153
- }
1154
- });
1155
- });
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);
1156
1292
  });
1157
- }
1158
-
1293
+ };
1159
1294
  // Filtering logic on Client's page
1160
1295
  window.formatCounter = function() {
1161
1296
  function format_number(x) {