From 9251da0be1e11b1d2711a1ab4f8a601ac1720425 Mon Sep 17 00:00:00 2001 From: matthewcsimpson Date: Mon, 13 Apr 2026 16:21:25 -0700 Subject: [PATCH 1/4] Adds a new filtering option, hybrid, leaving some fields multi-select but others not --- Dist/WebflowOnly/CMSFilter.js | 197 ++++++++++++++++++++-------------- __tests__/CMSFilter.test.js | 95 ++++++++++++++++ docs/WebflowOnly/CMSFilter.md | 4 +- 3 files changed, 213 insertions(+), 83 deletions(-) diff --git a/Dist/WebflowOnly/CMSFilter.js b/Dist/WebflowOnly/CMSFilter.js index fcb261e..1aee6fb 100644 --- a/Dist/WebflowOnly/CMSFilter.js +++ b/Dist/WebflowOnly/CMSFilter.js @@ -1,6 +1,13 @@ "use strict"; class CMSFilter { + /** + * For `wt-cmsfilter-filtering="hybrid"`, availability for these categories ignores + * that category's active filters (e.g. body type options stay available for multi-select + * while other facets narrow from the full filtered set). + */ + static HYBRID_SELF_EXCLUDE_CATEGORIES = ["bodytype"]; + constructor() { //CORE elements this.filterForm = document.querySelector( @@ -559,88 +566,100 @@ class CMSFilter { }); } - ApplyFilters() { - const filters = this.GetFilters(); - this.currentPage = 1; // Reset pagination to first page - this.filteredItems = this.allItems.filter((item) => { - return Object.keys(filters).every((category) => { - // Fix 1: Safari-compatible array handling - const categoryFilters = filters[category] || []; - const values = Array.isArray(categoryFilters) - ? categoryFilters.slice() - : []; - if (values.length === 0) return true; - - // Use cached search data instead of live DOM queries - const searchCache = item._wtSearchCache; - if (!searchCache) { - console.warn( - "Search cache missing for item, falling back to live query", - ); - // Fallback to original method if cache is missing - const categoryElement = item.querySelector( - `[wt-cmsfilter-category="${category}"]`, - ); - let matchingText = ""; - if (categoryElement && categoryElement.innerText) { - matchingText = categoryElement.innerText.toLowerCase(); - } - matchingText = matchingText.replace(/(?: |\s)+/gi, " "); + /** + * Whether a single list item matches the given filter map (same rules as ApplyFilters). + */ + itemMatchesFilters(item, filters) { + return Object.keys(filters).every((category) => { + const categoryFilters = filters[category] || []; + const values = Array.isArray(categoryFilters) + ? categoryFilters.slice() + : []; + if (values.length === 0) return true; + + const searchCache = item._wtSearchCache; + if (!searchCache) { + console.warn( + "Search cache missing for item, falling back to live query", + ); + const categoryElement = item.querySelector( + `[wt-cmsfilter-category="${category}"]`, + ); + let matchingText = ""; + if (categoryElement && categoryElement.innerText) { + matchingText = categoryElement.innerText.toLowerCase(); } + matchingText = matchingText.replace(/(?: |\s)+/gi, " "); + } - if (category === "*") { - // Global search using cached text - const globalText = searchCache ? searchCache.globalSearchText : ""; - return ( - values.some((value) => globalText.includes(value.toLowerCase())) || - Object.values(item.dataset || {}).some((dataValue) => - values.some((value) => { - if (dataValue && typeof dataValue.toLowerCase === "function") { - return dataValue.toLowerCase().includes(value.toLowerCase()); - } - return false; - }), - ) - ); - } else { - return values.some((value) => { - if (typeof value === "object" && value !== null) { - // Range filtering - use normalized dataset key - const datasetCategory = this.GetDataSet(category); - const datasetValue = - item.dataset && item.dataset[datasetCategory] - ? item.dataset[datasetCategory] - : ""; - const itemValue = parseFloat(datasetValue); - if (isNaN(itemValue)) return false; - if (value.from !== null && value.to !== null) { - return itemValue >= value.from && itemValue <= value.to; - } else if (value.from !== null && value.to == null) { - return itemValue >= value.from; - } else if (value.from == null && value.to !== null) { - return itemValue <= value.to; + if (category === "*") { + const globalText = searchCache ? searchCache.globalSearchText : ""; + return ( + values.some((value) => globalText.includes(value.toLowerCase())) || + Object.values(item.dataset || {}).some((dataValue) => + values.some((value) => { + if (dataValue && typeof dataValue.toLowerCase === "function") { + return dataValue.toLowerCase().includes(value.toLowerCase()); } return false; - } else { - // Text filtering using cached data - const datasetCategory = this.GetDataSet(category); - const cachedDatasetValue = searchCache - ? searchCache.datasetValues.get(datasetCategory) || "" - : ""; - const cachedCategoryText = searchCache - ? searchCache.categoryTexts.get(category) || "" - : ""; - const valueStr = value ? value.toString().toLowerCase() : ""; - - return ( - cachedDatasetValue.includes(valueStr) || - cachedCategoryText.includes(valueStr) - ); - } - }); + }), + ) + ); + } + return values.some((value) => { + if (typeof value === "object" && value !== null) { + const datasetCategory = this.GetDataSet(category); + const datasetValue = + item.dataset && item.dataset[datasetCategory] + ? item.dataset[datasetCategory] + : ""; + const itemValue = parseFloat(datasetValue); + if (isNaN(itemValue)) return false; + if (value.from !== null && value.to !== null) { + return itemValue >= value.from && itemValue <= value.to; + } else if (value.from !== null && value.to == null) { + return itemValue >= value.from; + } else if (value.from == null && value.to !== null) { + return itemValue <= value.to; + } + return false; + } else { + const datasetCategory = this.GetDataSet(category); + const cachedDatasetValue = searchCache + ? searchCache.datasetValues.get(datasetCategory) || "" + : ""; + const cachedCategoryText = searchCache + ? searchCache.categoryTexts.get(category) || "" + : ""; + const valueStr = value ? value.toString().toLowerCase() : ""; + + return ( + cachedDatasetValue.includes(valueStr) || + cachedCategoryText.includes(valueStr) + ); } }); }); + } + + /** + * Items matching current filters but ignoring one or more categories (for hybrid availability). + */ + getFilteredItemsIgnoringCategories(excludedCategoryAttrs) { + const filters = this.GetFilters(); + const f = { ...filters }; + excludedCategoryAttrs.forEach((cat) => { + f[cat] = []; + }); + return this.allItems.filter((item) => this.itemMatchesFilters(item, f)); + } + + ApplyFilters() { + const filters = this.GetFilters(); + this.currentPage = 1; // Reset pagination to first page + this.filteredItems = this.allItems.filter((item) => + this.itemMatchesFilters(item, filters), + ); this.activeFilters = filters; this.SortItems(); @@ -766,18 +785,29 @@ class CMSFilter { } UpdateAvailableFilters() { - if (this.filterForm.getAttribute("wt-cmsfilter-filtering") !== "advanced") - return; + const filteringMode = this.filterForm.getAttribute( + "wt-cmsfilter-filtering", + ); + if (filteringMode !== "advanced" && filteringMode !== "hybrid") return; + this.availableFilters = {}; this.filterElements.forEach((element) => { - const category = this.GetDataSet( - element.getAttribute("wt-cmsfilter-category"), - ); + const categoryAttr = element.getAttribute("wt-cmsfilter-category"); + const category = this.GetDataSet(categoryAttr); + + let sourceItems = this.filteredItems; + if ( + filteringMode === "hybrid" && + categoryAttr && + CMSFilter.HYBRID_SELF_EXCLUDE_CATEGORIES.includes(categoryAttr) + ) { + sourceItems = this.getFilteredItemsIgnoringCategories([categoryAttr]); + } // Safari-compatible dataset access const availableValues = new Set( - this.filteredItems + sourceItems .map((item) => item.dataset && item.dataset[category] ? item.dataset[category] @@ -919,7 +949,10 @@ class CMSFilter { input.value = ""; } } else if (input.type === "checkbox") { - if (advancedFiltering === "advanced") { + if ( + advancedFiltering === "advanced" || + advancedFiltering === "hybrid" + ) { input.checked = false; } else { if (categoryElement.innerText === value) { diff --git a/__tests__/CMSFilter.test.js b/__tests__/CMSFilter.test.js index 516ab00..93080a6 100644 --- a/__tests__/CMSFilter.test.js +++ b/__tests__/CMSFilter.test.js @@ -73,6 +73,33 @@ describe("CMSFilter", () => { `; } + /** make, bodytype, Category — hybrid (bodytype self-exclude) vs advanced */ + function buildHybridScenarioDOM(filteringMode = "hybrid") { + const modeAttr = filteringMode + ? `wt-cmsfilter-filtering="${filteringMode}"` + : ""; + document.body.innerHTML = ` +
+ + + + + + + + +
+
+
x
+
+
T1
+
T2
+
H1
+
+
+ `; + } + test("initializes and caches items, pushes instance", () => { buildBasicDOM(); InitializeCMSFilter(); @@ -172,6 +199,74 @@ describe("CMSFilter", () => { expect(ordered).toEqual(["gamma", "beta", "alpha"]); }); + test("hybrid mode narrows make like advanced but keeps all relevant body types visible for multi-select", () => { + buildHybridScenarioDOM("hybrid"); + InitializeCMSFilter(); + const form = document.querySelector('[wt-cmsfilter-element="filter-form"]'); + const toyota = Array.from( + form.querySelectorAll('label[wt-cmsfilter-category="make"]'), + ).find((l) => l.textContent.includes("Toyota")); + const suv = Array.from( + form.querySelectorAll('label[wt-cmsfilter-category="bodytype"]'), + ).find((l) => l.textContent.includes("SUV")); + toyota.querySelector("input").checked = true; + suv.querySelector("input").checked = true; + toyota + .querySelector("input") + .dispatchEvent(new Event("change", { bubbles: true })); + suv.querySelector("input").dispatchEvent(new Event("change", { bubbles: true })); + const instance = window.webtricks[0].CMSFilter; + instance.ApplyFilters(); + + expect(instance.filteredItems.length).toBe(1); + const makeLabels = Array.from( + form.querySelectorAll('label[wt-cmsfilter-category="make"]'), + ); + const visibleMake = makeLabels.filter((l) => l.style.display !== "none"); + expect(visibleMake.map((l) => l.textContent.trim())).toEqual(["Toyota"]); + + const bodyLabels = Array.from( + form.querySelectorAll('label[wt-cmsfilter-category="bodytype"]'), + ); + const visibleBody = bodyLabels.filter((l) => l.style.display !== "none"); + expect(visibleBody.map((l) => l.textContent.trim()).sort()).toEqual([ + "SUV", + "Sedan", + ]); + }); + + test("advanced mode hides sibling make and hides body types not in result set", () => { + buildHybridScenarioDOM("advanced"); + InitializeCMSFilter(); + const form = document.querySelector('[wt-cmsfilter-element="filter-form"]'); + const toyota = Array.from( + form.querySelectorAll('label[wt-cmsfilter-category="make"]'), + ).find((l) => l.textContent.includes("Toyota")); + const suv = Array.from( + form.querySelectorAll('label[wt-cmsfilter-category="bodytype"]'), + ).find((l) => l.textContent.includes("SUV")); + toyota.querySelector("input").checked = true; + suv.querySelector("input").checked = true; + toyota + .querySelector("input") + .dispatchEvent(new Event("change", { bubbles: true })); + suv.querySelector("input").dispatchEvent(new Event("change", { bubbles: true })); + const instance = window.webtricks[0].CMSFilter; + instance.ApplyFilters(); + + const makeLabels = Array.from( + form.querySelectorAll('label[wt-cmsfilter-category="make"]'), + ); + const visibleMake = makeLabels.filter((l) => l.style.display !== "none"); + expect(visibleMake.map((l) => l.textContent.trim())).toEqual(["Toyota"]); + + const bodyLabels = Array.from( + form.querySelectorAll('label[wt-cmsfilter-category="bodytype"]'), + ); + const visibleBody = bodyLabels.filter((l) => l.style.display !== "none"); + expect(visibleBody.map((l) => l.textContent.trim())).toEqual(["SUV"]); + }); + test("advanced filtering hides unavailable checkboxes then restores after clearing", () => { buildBasicDOM({ advanced: true }); InitializeCMSFilter(); diff --git a/docs/WebflowOnly/CMSFilter.md b/docs/WebflowOnly/CMSFilter.md index c2f4002..a59a613 100644 --- a/docs/WebflowOnly/CMSFilter.md +++ b/docs/WebflowOnly/CMSFilter.md @@ -12,6 +12,7 @@ CMSFilter is a powerful Webflow-specific script that provides advanced filtering - Multiple filter types (checkbox, radio, text, range) - Advanced filtering with dynamic availability updates +- Hybrid filtering mode (`hybrid`): advanced-style narrowing for **make** and other facets, while **body type** options stay available for multi-select - Pagination support with auto-loading across pages - Dynamic sorting (numeric, date, alphabetical) - Active filter tags with individual removal @@ -46,7 +47,8 @@ Add the script to your Webflow project and include the required attributes on yo #### Core Filter Attributes -- `wt-cmsfilter-filtering="advanced"` - Enables advanced filtering mode with dynamic availability updates +- `wt-cmsfilter-filtering="advanced"` - Enables advanced filtering mode with dynamic availability updates (every facet’s options narrow to the current result set; selecting one **make** hides other makes). +- `wt-cmsfilter-filtering="hybrid"` - Like advanced, but **`bodytype`** keeps **all** of its checkbox options that match the current filters **except** body type (so users can multi-select e.g. Minivan and SUV while other facets react). **`make`** and all other categories narrow from the current result set like **`advanced`**. Requires matching `data-bodytype` on list items (`wt-cmsfilter-category="bodytype"`). - `wt-cmsfilter-trigger="button"` - Changes filter trigger to button submit instead of real-time - `wt-cmsfilter-class="classname"` - CSS class applied to active filter elements - `wt-cmsfilter-resetix2="true"` - Reset IX2 interactions on filtered items From 5a1ad78a00052f8164b23dc0ed754b836442265b Mon Sep 17 00:00:00 2001 From: matthewcsimpson Date: Mon, 13 Apr 2026 16:27:06 -0700 Subject: [PATCH 2/4] hybrid filtering update --- Dist/WebflowOnly/CMSFilter.js | 28 ++++++++++++++++++-------- __tests__/CMSFilter.test.js | 37 +++++++++++++++++++++++++++++++++-- docs/WebflowOnly/CMSFilter.md | 3 ++- 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/Dist/WebflowOnly/CMSFilter.js b/Dist/WebflowOnly/CMSFilter.js index 1aee6fb..df640fd 100644 --- a/Dist/WebflowOnly/CMSFilter.js +++ b/Dist/WebflowOnly/CMSFilter.js @@ -1,13 +1,6 @@ "use strict"; class CMSFilter { - /** - * For `wt-cmsfilter-filtering="hybrid"`, availability for these categories ignores - * that category's active filters (e.g. body type options stay available for multi-select - * while other facets narrow from the full filtered set). - */ - static HYBRID_SELF_EXCLUDE_CATEGORIES = ["bodytype"]; - constructor() { //CORE elements this.filterForm = document.querySelector( @@ -784,6 +777,25 @@ class CMSFilter { }); } + /** + * Categories whose checkbox availability ignores that facet’s own selections (hybrid mode). + * Set on the form: wt-cmsfilter-hybrid-categories="bodytype" or "bodytype,colour" + * If the attribute is omitted, defaults to ["bodytype"]. If present but empty, no categories self-exclude. + */ + getHybridSelfExcludeCategories() { + const raw = this.filterForm.getAttribute( + "wt-cmsfilter-hybrid-categories", + ); + if (raw === null) { + return ["bodytype"]; + } + const parsed = raw + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + return parsed; + } + UpdateAvailableFilters() { const filteringMode = this.filterForm.getAttribute( "wt-cmsfilter-filtering", @@ -800,7 +812,7 @@ class CMSFilter { if ( filteringMode === "hybrid" && categoryAttr && - CMSFilter.HYBRID_SELF_EXCLUDE_CATEGORIES.includes(categoryAttr) + this.getHybridSelfExcludeCategories().includes(categoryAttr) ) { sourceItems = this.getFilteredItemsIgnoringCategories([categoryAttr]); } diff --git a/__tests__/CMSFilter.test.js b/__tests__/CMSFilter.test.js index 93080a6..d75927e 100644 --- a/__tests__/CMSFilter.test.js +++ b/__tests__/CMSFilter.test.js @@ -74,12 +74,19 @@ describe("CMSFilter", () => { } /** make, bodytype, Category — hybrid (bodytype self-exclude) vs advanced */ - function buildHybridScenarioDOM(filteringMode = "hybrid") { + function buildHybridScenarioDOM( + filteringMode = "hybrid", + hybridCategoriesAttr, + ) { const modeAttr = filteringMode ? `wt-cmsfilter-filtering="${filteringMode}"` : ""; + const hybridAttr = + hybridCategoriesAttr !== undefined + ? `wt-cmsfilter-hybrid-categories="${hybridCategoriesAttr}"` + : ""; document.body.innerHTML = ` -
+ @@ -235,6 +242,32 @@ describe("CMSFilter", () => { ]); }); + test("hybrid with empty wt-cmsfilter-hybrid-categories narrows all facets like advanced", () => { + buildHybridScenarioDOM("hybrid", ""); + InitializeCMSFilter(); + const form = document.querySelector('[wt-cmsfilter-element="filter-form"]'); + const toyota = Array.from( + form.querySelectorAll('label[wt-cmsfilter-category="make"]'), + ).find((l) => l.textContent.includes("Toyota")); + const suv = Array.from( + form.querySelectorAll('label[wt-cmsfilter-category="bodytype"]'), + ).find((l) => l.textContent.includes("SUV")); + toyota.querySelector("input").checked = true; + suv.querySelector("input").checked = true; + toyota + .querySelector("input") + .dispatchEvent(new Event("change", { bubbles: true })); + suv.querySelector("input").dispatchEvent(new Event("change", { bubbles: true })); + const instance = window.webtricks[0].CMSFilter; + instance.ApplyFilters(); + + const bodyLabels = Array.from( + form.querySelectorAll('label[wt-cmsfilter-category="bodytype"]'), + ); + const visibleBody = bodyLabels.filter((l) => l.style.display !== "none"); + expect(visibleBody.map((l) => l.textContent.trim())).toEqual(["SUV"]); + }); + test("advanced mode hides sibling make and hides body types not in result set", () => { buildHybridScenarioDOM("advanced"); InitializeCMSFilter(); diff --git a/docs/WebflowOnly/CMSFilter.md b/docs/WebflowOnly/CMSFilter.md index a59a613..9c577ae 100644 --- a/docs/WebflowOnly/CMSFilter.md +++ b/docs/WebflowOnly/CMSFilter.md @@ -48,7 +48,8 @@ Add the script to your Webflow project and include the required attributes on yo #### Core Filter Attributes - `wt-cmsfilter-filtering="advanced"` - Enables advanced filtering mode with dynamic availability updates (every facet’s options narrow to the current result set; selecting one **make** hides other makes). -- `wt-cmsfilter-filtering="hybrid"` - Like advanced, but **`bodytype`** keeps **all** of its checkbox options that match the current filters **except** body type (so users can multi-select e.g. Minivan and SUV while other facets react). **`make`** and all other categories narrow from the current result set like **`advanced`**. Requires matching `data-bodytype` on list items (`wt-cmsfilter-category="bodytype"`). +- `wt-cmsfilter-filtering="hybrid"` - Like advanced, but selected categories (see below) keep **all** of their checkbox options that match the current filters **except** that category’s own selections (multi-select friendly). All **other** categories narrow from the current result set like **`advanced`**. +- `wt-cmsfilter-hybrid-categories="bodytype"` - Comma-separated `wt-cmsfilter-category` names that use hybrid self-exclude behavior (e.g. `bodytype` or `bodytype,colour`). **If omitted**, defaults to **`bodytype`**. **If present but empty**, every facet narrows like **`advanced`** (no hybrid self-exclude). Requires matching `data-*` fields on list items for each listed category. - `wt-cmsfilter-trigger="button"` - Changes filter trigger to button submit instead of real-time - `wt-cmsfilter-class="classname"` - CSS class applied to active filter elements - `wt-cmsfilter-resetix2="true"` - Reset IX2 interactions on filtered items From 9eeb37d7c2aedd7477c53239d5d51bcee5f5c47d Mon Sep 17 00:00:00 2001 From: matthewcsimpson Date: Mon, 13 Apr 2026 16:30:00 -0700 Subject: [PATCH 3/4] hybrid filtering --- Dist/WebflowOnly/CMSFilter.js | 9 ++++----- __tests__/CMSFilter.test.js | 11 ++++------- docs/WebflowOnly/CMSFilter.md | 2 +- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/Dist/WebflowOnly/CMSFilter.js b/Dist/WebflowOnly/CMSFilter.js index df640fd..0dcb606 100644 --- a/Dist/WebflowOnly/CMSFilter.js +++ b/Dist/WebflowOnly/CMSFilter.js @@ -780,20 +780,19 @@ class CMSFilter { /** * Categories whose checkbox availability ignores that facet’s own selections (hybrid mode). * Set on the form: wt-cmsfilter-hybrid-categories="bodytype" or "bodytype,colour" - * If the attribute is omitted, defaults to ["bodytype"]. If present but empty, no categories self-exclude. + * Omit or leave empty for no self-exclude categories (hybrid availability matches advanced for every facet). */ getHybridSelfExcludeCategories() { const raw = this.filterForm.getAttribute( "wt-cmsfilter-hybrid-categories", ); - if (raw === null) { - return ["bodytype"]; + if (raw === null || raw.trim() === "") { + return []; } - const parsed = raw + return raw .split(",") .map((s) => s.trim()) .filter(Boolean); - return parsed; } UpdateAvailableFilters() { diff --git a/__tests__/CMSFilter.test.js b/__tests__/CMSFilter.test.js index d75927e..ef5d188 100644 --- a/__tests__/CMSFilter.test.js +++ b/__tests__/CMSFilter.test.js @@ -74,16 +74,13 @@ describe("CMSFilter", () => { } /** make, bodytype, Category — hybrid (bodytype self-exclude) vs advanced */ - function buildHybridScenarioDOM( - filteringMode = "hybrid", - hybridCategoriesAttr, - ) { + function buildHybridScenarioDOM(filteringMode = "hybrid", hybridCategoriesAttr) { const modeAttr = filteringMode ? `wt-cmsfilter-filtering="${filteringMode}"` : ""; const hybridAttr = - hybridCategoriesAttr !== undefined - ? `wt-cmsfilter-hybrid-categories="${hybridCategoriesAttr}"` + filteringMode === "hybrid" + ? ` wt-cmsfilter-hybrid-categories="${hybridCategoriesAttr ?? ""}"` : ""; document.body.innerHTML = ` @@ -207,7 +204,7 @@ describe("CMSFilter", () => { }); test("hybrid mode narrows make like advanced but keeps all relevant body types visible for multi-select", () => { - buildHybridScenarioDOM("hybrid"); + buildHybridScenarioDOM("hybrid", "bodytype"); InitializeCMSFilter(); const form = document.querySelector('[wt-cmsfilter-element="filter-form"]'); const toyota = Array.from( diff --git a/docs/WebflowOnly/CMSFilter.md b/docs/WebflowOnly/CMSFilter.md index 9c577ae..d40517d 100644 --- a/docs/WebflowOnly/CMSFilter.md +++ b/docs/WebflowOnly/CMSFilter.md @@ -49,7 +49,7 @@ Add the script to your Webflow project and include the required attributes on yo - `wt-cmsfilter-filtering="advanced"` - Enables advanced filtering mode with dynamic availability updates (every facet’s options narrow to the current result set; selecting one **make** hides other makes). - `wt-cmsfilter-filtering="hybrid"` - Like advanced, but selected categories (see below) keep **all** of their checkbox options that match the current filters **except** that category’s own selections (multi-select friendly). All **other** categories narrow from the current result set like **`advanced`**. -- `wt-cmsfilter-hybrid-categories="bodytype"` - Comma-separated `wt-cmsfilter-category` names that use hybrid self-exclude behavior (e.g. `bodytype` or `bodytype,colour`). **If omitted**, defaults to **`bodytype`**. **If present but empty**, every facet narrows like **`advanced`** (no hybrid self-exclude). Requires matching `data-*` fields on list items for each listed category. +- `wt-cmsfilter-hybrid-categories="bodytype"` - Comma-separated `wt-cmsfilter-category` names that use hybrid self-exclude behavior (e.g. `bodytype` or `bodytype,colour`). **Required** for any facet to use hybrid self-exclude; **omit or leave empty** if every facet should narrow like **`advanced`**. Requires matching `data-*` fields on list items for each listed category. - `wt-cmsfilter-trigger="button"` - Changes filter trigger to button submit instead of real-time - `wt-cmsfilter-class="classname"` - CSS class applied to active filter elements - `wt-cmsfilter-resetix2="true"` - Reset IX2 interactions on filtered items From 42276db18f0fcfd9b040b3879f4cf654030ef9b7 Mon Sep 17 00:00:00 2001 From: matthewcsimpson Date: Mon, 13 Apr 2026 16:30:41 -0700 Subject: [PATCH 4/4] hybrid fitering docs update --- docs/WebflowOnly/CMSFilter.md | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/docs/WebflowOnly/CMSFilter.md b/docs/WebflowOnly/CMSFilter.md index d40517d..0349446 100644 --- a/docs/WebflowOnly/CMSFilter.md +++ b/docs/WebflowOnly/CMSFilter.md @@ -12,7 +12,7 @@ CMSFilter is a powerful Webflow-specific script that provides advanced filtering - Multiple filter types (checkbox, radio, text, range) - Advanced filtering with dynamic availability updates -- Hybrid filtering mode (`hybrid`): advanced-style narrowing for **make** and other facets, while **body type** options stay available for multi-select +- Hybrid filtering mode (`hybrid` + `wt-cmsfilter-hybrid-categories`): like advanced, but listed categories keep sibling checkbox options for multi-select; other facets narrow from the current result set - Pagination support with auto-loading across pages - Dynamic sorting (numeric, date, alphabetical) - Active filter tags with individual removal @@ -213,6 +213,27 @@ Add the script to your Webflow project and include the required attributes on yo ``` +### Hybrid mode (advanced + multi-select on chosen facets) + +Use **`hybrid`** instead of **`advanced`** on the form, and set **`wt-cmsfilter-hybrid-categories`** to a comma-separated list of `wt-cmsfilter-category` values that should **not** hide sibling options when you select one value (e.g. body type so users can pick SUV and Minivan at once). **Omit** `wt-cmsfilter-hybrid-categories` or leave it **empty** if every facet should narrow like **`advanced`**. List items need matching `data-*` fields for each category you name. + +```html +
+ + + + +
+
+
+
+``` + +Multiple categories: `wt-cmsfilter-hybrid-categories="bodytype,colour"`. + ### Button-Triggered Filtering ```html @@ -250,7 +271,7 @@ Add the script to your Webflow project and include the required attributes on yo - Use debouncing for text inputs in large collections (adjust `wt-cmsfilter-debounce` value) - Enable pagination for collections with 50+ items -- Use advanced filtering mode only when needed for better performance +- Use advanced or hybrid filtering mode only when needed for better performance - Implement IX2 reset (`wt-cmsfilter-resetix2="true"`) sparingly as it impacts performance - Consider using button-triggered filtering for complex filter sets