advanced-filter-system 1.0.2 → 1.0.4
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/README.md +10 -0
- package/dist/AFS.esm.js +384 -14
- package/dist/AFS.esm.js.map +1 -1
- package/dist/AFS.js +761 -393
- package/dist/AFS.js.map +1 -1
- package/dist/AFS.min.js +1 -1
- package/dist/AFS.min.js.map +1 -1
- package/package.json +23 -4
package/README.md
CHANGED
|
@@ -105,6 +105,16 @@ filter.removeFilter('category', 'web');
|
|
|
105
105
|
|
|
106
106
|
// Get active filters by type
|
|
107
107
|
const activeCategories = filter.getActiveFiltersByType('category');
|
|
108
|
+
|
|
109
|
+
// Add filter groups
|
|
110
|
+
filter.addFilterGroup('categories', ['category:tech', 'category:fashion'], 'OR');
|
|
111
|
+
filter.addFilterGroup('price', ['price:low', 'price:medium'], 'AND');
|
|
112
|
+
|
|
113
|
+
// Set how groups combine
|
|
114
|
+
filter.setGroupMode('AND'); // Items must match both category AND price groups
|
|
115
|
+
|
|
116
|
+
// Remove a group
|
|
117
|
+
filter.removeFilterGroup('price');
|
|
108
118
|
```
|
|
109
119
|
|
|
110
120
|
### URL State Management
|
package/dist/AFS.esm.js
CHANGED
|
@@ -79,6 +79,9 @@ class AFS {
|
|
|
79
79
|
this.currentFilters = new Set(["*"]);
|
|
80
80
|
this.currentSearch = "";
|
|
81
81
|
this.visibleItems = new Set(this.items);
|
|
82
|
+
this.filterGroups = new Map();
|
|
83
|
+
this.groupMode = "OR"; // Default group mode
|
|
84
|
+
|
|
82
85
|
this.init();
|
|
83
86
|
}
|
|
84
87
|
|
|
@@ -202,29 +205,159 @@ class AFS {
|
|
|
202
205
|
* Apply current filters to items
|
|
203
206
|
* @public
|
|
204
207
|
*/
|
|
208
|
+
/**
|
|
209
|
+
* Apply current filters to items
|
|
210
|
+
* @public
|
|
211
|
+
*/
|
|
205
212
|
filter() {
|
|
206
|
-
|
|
213
|
+
// Store the original filter logic
|
|
214
|
+
const standardFilter = () => {
|
|
215
|
+
this.visibleItems.clear();
|
|
216
|
+
this.items.forEach(item => {
|
|
217
|
+
if (this.currentFilters.has("*")) {
|
|
218
|
+
this.showItem(item);
|
|
219
|
+
this.visibleItems.add(item);
|
|
220
|
+
} else {
|
|
221
|
+
const itemCategories = new Set(item.dataset.categories?.split(" ") || []);
|
|
222
|
+
const matchesFilter = this.options.filterMode === "OR" ? this.matchesAnyFilter(itemCategories) : this.matchesAllFilters(itemCategories);
|
|
223
|
+
if (matchesFilter) {
|
|
224
|
+
this.showItem(item);
|
|
225
|
+
this.visibleItems.add(item);
|
|
226
|
+
} else {
|
|
227
|
+
this.hideItem(item);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
};
|
|
207
232
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (matchesFilter) {
|
|
233
|
+
// Check if we should use group filtering or standard filtering
|
|
234
|
+
if (this.filterGroups.size === 0) {
|
|
235
|
+
standardFilter();
|
|
236
|
+
} else {
|
|
237
|
+
this.visibleItems.clear();
|
|
238
|
+
this.items.forEach(item => {
|
|
239
|
+
if (this.currentFilters.has("*")) {
|
|
216
240
|
this.showItem(item);
|
|
217
|
-
this.visibleItems.add(item);
|
|
241
|
+
this.visibleItems.add(item);
|
|
218
242
|
} else {
|
|
219
|
-
|
|
243
|
+
const itemCategories = new Set(item.dataset.categories?.split(" ") || []);
|
|
244
|
+
const matchesGroups = this.matchesFilterGroups(itemCategories);
|
|
245
|
+
if (matchesGroups) {
|
|
246
|
+
this.showItem(item);
|
|
247
|
+
this.visibleItems.add(item);
|
|
248
|
+
} else {
|
|
249
|
+
this.hideItem(item);
|
|
250
|
+
}
|
|
220
251
|
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
223
254
|
setTimeout(() => {
|
|
224
255
|
this.updateCounter();
|
|
225
256
|
}, this.options.animationDuration);
|
|
226
257
|
}
|
|
227
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Add or update a filter group
|
|
261
|
+
* @public
|
|
262
|
+
* @param {string} groupId - Group identifier
|
|
263
|
+
* @param {string[]} filters - Array of filter values
|
|
264
|
+
* @param {string} [operator='OR'] - Operator within group ('AND' or 'OR')
|
|
265
|
+
* @returns {boolean} Success status
|
|
266
|
+
*/
|
|
267
|
+
addFilterGroup(groupId, filters) {
|
|
268
|
+
let operator = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "OR";
|
|
269
|
+
try {
|
|
270
|
+
// Validate inputs
|
|
271
|
+
if (!groupId || !Array.isArray(filters)) {
|
|
272
|
+
console.warn("Invalid group parameters");
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
const validOperator = operator.toUpperCase();
|
|
276
|
+
if (!["AND", "OR"].includes(validOperator)) {
|
|
277
|
+
console.warn('Invalid operator. Using default "OR"');
|
|
278
|
+
operator = "OR";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Create or update group
|
|
282
|
+
this.filterGroups.set(groupId, {
|
|
283
|
+
filters: new Set(filters),
|
|
284
|
+
operator: validOperator
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Only update if we have active groups
|
|
288
|
+
if (this.filterGroups.size > 0) {
|
|
289
|
+
this.updateFiltersFromGroups();
|
|
290
|
+
this.filter();
|
|
291
|
+
this.updateURL();
|
|
292
|
+
}
|
|
293
|
+
return true;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error("Error adding filter group:", error);
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Set how groups combine with each other
|
|
302
|
+
* @public
|
|
303
|
+
* @param {string} mode - Mode for combining groups ('AND' or 'OR')
|
|
304
|
+
*/
|
|
305
|
+
setGroupMode(mode) {
|
|
306
|
+
const validMode = mode.toUpperCase();
|
|
307
|
+
if (["AND", "OR"].includes(validMode)) {
|
|
308
|
+
this.groupMode = validMode;
|
|
309
|
+
if (this.filterGroups.size > 0) {
|
|
310
|
+
this.filter();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Remove a filter group
|
|
317
|
+
* @public
|
|
318
|
+
* @param {string} groupId - Group identifier
|
|
319
|
+
* @returns {boolean} Success status
|
|
320
|
+
*/
|
|
321
|
+
removeFilterGroup(groupId) {
|
|
322
|
+
if (this.filterGroups.has(groupId)) {
|
|
323
|
+
this.filterGroups.delete(groupId);
|
|
324
|
+
|
|
325
|
+
// If no groups left, revert to normal filtering
|
|
326
|
+
if (this.filterGroups.size === 0) {
|
|
327
|
+
this.resetFilters();
|
|
328
|
+
} else {
|
|
329
|
+
this.updateFiltersFromGroups();
|
|
330
|
+
}
|
|
331
|
+
this.filter();
|
|
332
|
+
this.updateURL();
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Update filters based on groups
|
|
340
|
+
* @private
|
|
341
|
+
*/
|
|
342
|
+
updateFiltersFromGroups() {
|
|
343
|
+
// Only process if we have groups
|
|
344
|
+
if (this.filterGroups.size === 0) return;
|
|
345
|
+
|
|
346
|
+
// Clear current filters except '*'
|
|
347
|
+
if (!this.currentFilters.has("*")) {
|
|
348
|
+
this.currentFilters.clear();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Combine all group filters
|
|
352
|
+
for (const group of this.filterGroups.values()) {
|
|
353
|
+
group.filters.forEach(filter => {
|
|
354
|
+
if (filter !== "*") {
|
|
355
|
+
this.currentFilters.add(filter);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
228
361
|
/**
|
|
229
362
|
* Check if item matches any active filter (OR mode)
|
|
230
363
|
* @private
|
|
@@ -347,12 +480,36 @@ class AFS {
|
|
|
347
480
|
}, this.options.animationDuration);
|
|
348
481
|
}
|
|
349
482
|
|
|
483
|
+
/**
|
|
484
|
+
* Check if item matches filter groups
|
|
485
|
+
* @private
|
|
486
|
+
* @param {Set} itemCategories - Item's categories
|
|
487
|
+
* @returns {boolean} Whether item matches the group filters
|
|
488
|
+
*/
|
|
489
|
+
matchesFilterGroups(itemCategories) {
|
|
490
|
+
const groupMatches = [...this.filterGroups.values()].map(group => {
|
|
491
|
+
const groupFilters = [...group.filters];
|
|
492
|
+
if (groupFilters.length === 0) return true;
|
|
493
|
+
return group.operator === "OR" ? groupFilters.some(filter => itemCategories.has(filter)) : groupFilters.every(filter => itemCategories.has(filter));
|
|
494
|
+
});
|
|
495
|
+
return this.groupMode === "OR" ? groupMatches.some(matches => matches) : groupMatches.every(matches => matches);
|
|
496
|
+
}
|
|
497
|
+
|
|
350
498
|
/**
|
|
351
499
|
* Update URL with current filter state
|
|
352
500
|
* @private
|
|
353
501
|
*/
|
|
354
502
|
updateURL() {
|
|
355
|
-
const params = new URLSearchParams();
|
|
503
|
+
const params = new URLSearchParams(window.location.search);
|
|
504
|
+
|
|
505
|
+
// Add groups to URL if they exist
|
|
506
|
+
if (this.filterGroups.size > 0) {
|
|
507
|
+
for (const [groupId, group] of this.filterGroups.entries()) {
|
|
508
|
+
params.set(`group_${groupId}`, [...group.filters].join(","));
|
|
509
|
+
params.set(`groupOp_${groupId}`, group.operator.toLowerCase());
|
|
510
|
+
}
|
|
511
|
+
params.set("groupMode", this.groupMode.toLowerCase());
|
|
512
|
+
}
|
|
356
513
|
|
|
357
514
|
// Séparer les filtres par type
|
|
358
515
|
const filtersByType = {};
|
|
@@ -384,6 +541,23 @@ class AFS {
|
|
|
384
541
|
*/
|
|
385
542
|
loadFromURL() {
|
|
386
543
|
const params = new URLSearchParams(window.location.search);
|
|
544
|
+
|
|
545
|
+
// Load groups if they exist
|
|
546
|
+
this.filterGroups.clear();
|
|
547
|
+
for (const [key, value] of params.entries()) {
|
|
548
|
+
if (key.startsWith("group_")) {
|
|
549
|
+
const groupId = key.replace("group_", "");
|
|
550
|
+
const operator = params.get(`groupOp_${groupId}`)?.toUpperCase() || "OR";
|
|
551
|
+
const filters = value.split(",");
|
|
552
|
+
this.addFilterGroup(groupId, filters, operator);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Set group mode if present
|
|
557
|
+
const groupMode = params.get("groupMode")?.toUpperCase();
|
|
558
|
+
if (groupMode && ["AND", "OR"].includes(groupMode)) {
|
|
559
|
+
this.groupMode = groupMode;
|
|
560
|
+
}
|
|
387
561
|
this.currentFilters.clear();
|
|
388
562
|
|
|
389
563
|
// Si aucun filtre n'est présent, utiliser '*'
|
|
@@ -440,6 +614,202 @@ class AFS {
|
|
|
440
614
|
};
|
|
441
615
|
}
|
|
442
616
|
|
|
617
|
+
/**
|
|
618
|
+
* Set animation options
|
|
619
|
+
* @public
|
|
620
|
+
* @param {Object} options - Animation options
|
|
621
|
+
*/
|
|
622
|
+
setAnimationOptions() {
|
|
623
|
+
let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
624
|
+
this.options.animationDuration = options.duration || this.options.animationDuration;
|
|
625
|
+
this.options.animationType = options.type || "ease-out";
|
|
626
|
+
this.addStyles(); // Refresh styles with new options
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Event handling system
|
|
631
|
+
*/
|
|
632
|
+
addEventSystem() {
|
|
633
|
+
this.events = {};
|
|
634
|
+
this.on = (eventName, callback) => {
|
|
635
|
+
if (!this.events[eventName]) {
|
|
636
|
+
this.events[eventName] = [];
|
|
637
|
+
}
|
|
638
|
+
this.events[eventName].push(callback);
|
|
639
|
+
};
|
|
640
|
+
this.emit = (eventName, data) => {
|
|
641
|
+
if (this.events[eventName]) {
|
|
642
|
+
this.events[eventName].forEach(callback => callback(data));
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Add pagination
|
|
649
|
+
* @public
|
|
650
|
+
* @param {number} itemsPerPage - Number of items per page
|
|
651
|
+
*/
|
|
652
|
+
setPagination(itemsPerPage) {
|
|
653
|
+
this.pagination = {
|
|
654
|
+
currentPage: 1,
|
|
655
|
+
itemsPerPage: itemsPerPage,
|
|
656
|
+
totalPages: Math.ceil(this.visibleItems.size / itemsPerPage)
|
|
657
|
+
};
|
|
658
|
+
this.updatePagination();
|
|
659
|
+
}
|
|
660
|
+
updatePagination() {
|
|
661
|
+
const start = (this.pagination.currentPage - 1) * this.pagination.itemsPerPage;
|
|
662
|
+
const end = start + this.pagination.itemsPerPage;
|
|
663
|
+
[...this.visibleItems].forEach((item, index) => {
|
|
664
|
+
if (index >= start && index < end) {
|
|
665
|
+
this.showItem(item);
|
|
666
|
+
} else {
|
|
667
|
+
this.hideItem(item);
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Enable analytics tracking
|
|
674
|
+
* @public
|
|
675
|
+
* @param {Function} callback - Analytics callback function
|
|
676
|
+
*/
|
|
677
|
+
enableAnalytics(callback) {
|
|
678
|
+
this.analyticsCallback = callback;
|
|
679
|
+
this.on("filter", data => {
|
|
680
|
+
this.analyticsCallback({
|
|
681
|
+
type: "filter",
|
|
682
|
+
filters: [...this.currentFilters],
|
|
683
|
+
visibleItems: this.visibleItems.size,
|
|
684
|
+
timestamp: new Date().toISOString()
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Sort with custom comparator
|
|
691
|
+
* @public
|
|
692
|
+
* @param {string} key - Data attribute key
|
|
693
|
+
* @param {Function} comparator - Custom comparison function
|
|
694
|
+
*/
|
|
695
|
+
sortWithComparator(key, comparator) {
|
|
696
|
+
const items = [...this.items];
|
|
697
|
+
items.sort((a, b) => {
|
|
698
|
+
const valueA = a.dataset[key];
|
|
699
|
+
const valueB = b.dataset[key];
|
|
700
|
+
return comparator(valueA, valueB);
|
|
701
|
+
});
|
|
702
|
+
items.forEach(item => this.container.appendChild(item));
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Add responsive behavior
|
|
707
|
+
* @public
|
|
708
|
+
* @param {Object} breakpoints - Breakpoint configurations
|
|
709
|
+
*/
|
|
710
|
+
setResponsiveOptions(breakpoints) {
|
|
711
|
+
window.addEventListener("resize", debounce(() => {
|
|
712
|
+
const width = window.innerWidth;
|
|
713
|
+
for (const [breakpoint, options] of Object.entries(breakpoints)) {
|
|
714
|
+
if (width <= parseInt(breakpoint)) {
|
|
715
|
+
Object.assign(this.options, options);
|
|
716
|
+
this.filter();
|
|
717
|
+
break;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}, 250));
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Enable keyboard navigation
|
|
725
|
+
* @public
|
|
726
|
+
*/
|
|
727
|
+
enableKeyboardNavigation() {
|
|
728
|
+
document.addEventListener("keydown", e => {
|
|
729
|
+
if (e.key === "Enter" && document.activeElement.classList.contains(this.options.filterButtonSelector.slice(1))) {
|
|
730
|
+
document.activeElement.click();
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Export current filter state
|
|
737
|
+
* @public
|
|
738
|
+
* @returns {Object} Filter state
|
|
739
|
+
*/
|
|
740
|
+
exportState() {
|
|
741
|
+
return {
|
|
742
|
+
filters: [...this.currentFilters],
|
|
743
|
+
search: this.currentSearch,
|
|
744
|
+
mode: this.options.filterMode,
|
|
745
|
+
timestamp: new Date().toISOString()
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Import filter state
|
|
751
|
+
* @public
|
|
752
|
+
* @param {Object} state - Filter state to import
|
|
753
|
+
*/
|
|
754
|
+
importState(state) {
|
|
755
|
+
if (state.filters) {
|
|
756
|
+
this.currentFilters = new Set(state.filters);
|
|
757
|
+
this.currentSearch = state.search || "";
|
|
758
|
+
this.options.filterMode = state.mode || "OR";
|
|
759
|
+
this.filter();
|
|
760
|
+
this.updateURL();
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Save current filter state as preset
|
|
766
|
+
* @public
|
|
767
|
+
* @param {string} presetName - Name of the preset
|
|
768
|
+
*/
|
|
769
|
+
savePreset(presetName) {
|
|
770
|
+
const preset = {
|
|
771
|
+
filters: [...this.currentFilters],
|
|
772
|
+
search: this.currentSearch,
|
|
773
|
+
mode: this.options.filterMode
|
|
774
|
+
};
|
|
775
|
+
localStorage.setItem(`afs_preset_${presetName}`, JSON.stringify(preset));
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Load filter preset
|
|
780
|
+
* @public
|
|
781
|
+
* @param {string} presetName - Name of the preset to load
|
|
782
|
+
*/
|
|
783
|
+
loadPreset(presetName) {
|
|
784
|
+
const preset = JSON.parse(localStorage.getItem(`afs_preset_${presetName}`));
|
|
785
|
+
if (preset) {
|
|
786
|
+
this.currentFilters = new Set(preset.filters);
|
|
787
|
+
this.currentSearch = preset.search;
|
|
788
|
+
this.options.filterMode = preset.mode;
|
|
789
|
+
this.filter();
|
|
790
|
+
this.updateURL();
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Set filter logic mode (alias for setFilterMode)
|
|
796
|
+
* @public
|
|
797
|
+
* @param {string} logic - New filter logic ('AND' or 'OR')
|
|
798
|
+
*/
|
|
799
|
+
setLogic(logic) {
|
|
800
|
+
if (typeof logic === "boolean") {
|
|
801
|
+
// Handle boolean input (true = AND, false = OR)
|
|
802
|
+
this.options.filterMode = logic ? "AND" : "OR";
|
|
803
|
+
this.filter();
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const mode = logic.toUpperCase();
|
|
807
|
+
if (["OR", "AND"].includes(mode)) {
|
|
808
|
+
this.options.filterMode = mode;
|
|
809
|
+
this.filter();
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
443
813
|
/**
|
|
444
814
|
* Change filter mode
|
|
445
815
|
* @public
|