advanced-filter-system 1.0.3 → 1.0.5
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 +210 -100
- package/dist/AFS.esm.js +380 -20
- package/dist/AFS.esm.js.map +1 -1
- package/dist/AFS.js +768 -408
- 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 +1 -1
package/dist/AFS.esm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Advanced Filter System for DOM elements
|
|
3
|
-
* @version 1.0.
|
|
3
|
+
* @version 1.0.5
|
|
4
4
|
*
|
|
5
5
|
* A flexible and customizable filtering system that supports:
|
|
6
6
|
* - Multiple filtering modes (OR/AND)
|
|
@@ -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
|
|
|
@@ -189,8 +192,12 @@ class AFS {
|
|
|
189
192
|
if (button.classList.contains(this.options.activeClass)) {
|
|
190
193
|
button.classList.remove(this.options.activeClass);
|
|
191
194
|
this.currentFilters.delete(filterValue);
|
|
195
|
+
|
|
196
|
+
// If no filters are selected, reset to default state and clear URL
|
|
192
197
|
if (this.currentFilters.size === 0) {
|
|
193
198
|
this.resetFilters();
|
|
199
|
+
window.history.pushState({}, "", window.location.pathname);
|
|
200
|
+
return;
|
|
194
201
|
}
|
|
195
202
|
} else {
|
|
196
203
|
button.classList.add(this.options.activeClass);
|
|
@@ -198,33 +205,163 @@ class AFS {
|
|
|
198
205
|
}
|
|
199
206
|
}
|
|
200
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Apply current filters to items
|
|
210
|
+
* @public
|
|
211
|
+
*/
|
|
201
212
|
/**
|
|
202
213
|
* Apply current filters to items
|
|
203
214
|
* @public
|
|
204
215
|
*/
|
|
205
216
|
filter() {
|
|
206
|
-
|
|
217
|
+
// Store the original filter logic
|
|
218
|
+
const standardFilter = () => {
|
|
219
|
+
this.visibleItems.clear();
|
|
220
|
+
this.items.forEach(item => {
|
|
221
|
+
if (this.currentFilters.has("*")) {
|
|
222
|
+
this.showItem(item);
|
|
223
|
+
this.visibleItems.add(item);
|
|
224
|
+
} else {
|
|
225
|
+
const itemCategories = new Set(item.dataset.categories?.split(" ") || []);
|
|
226
|
+
const matchesFilter = this.options.filterMode === "OR" ? this.matchesAnyFilter(itemCategories) : this.matchesAllFilters(itemCategories);
|
|
227
|
+
if (matchesFilter) {
|
|
228
|
+
this.showItem(item);
|
|
229
|
+
this.visibleItems.add(item);
|
|
230
|
+
} else {
|
|
231
|
+
this.hideItem(item);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
};
|
|
207
236
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (matchesFilter) {
|
|
237
|
+
// Check if we should use group filtering or standard filtering
|
|
238
|
+
if (this.filterGroups.size === 0) {
|
|
239
|
+
standardFilter();
|
|
240
|
+
} else {
|
|
241
|
+
this.visibleItems.clear();
|
|
242
|
+
this.items.forEach(item => {
|
|
243
|
+
if (this.currentFilters.has("*")) {
|
|
216
244
|
this.showItem(item);
|
|
217
|
-
this.visibleItems.add(item);
|
|
245
|
+
this.visibleItems.add(item);
|
|
218
246
|
} else {
|
|
219
|
-
|
|
247
|
+
const itemCategories = new Set(item.dataset.categories?.split(" ") || []);
|
|
248
|
+
const matchesGroups = this.matchesFilterGroups(itemCategories);
|
|
249
|
+
if (matchesGroups) {
|
|
250
|
+
this.showItem(item);
|
|
251
|
+
this.visibleItems.add(item);
|
|
252
|
+
} else {
|
|
253
|
+
this.hideItem(item);
|
|
254
|
+
}
|
|
220
255
|
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
223
258
|
setTimeout(() => {
|
|
224
259
|
this.updateCounter();
|
|
225
260
|
}, this.options.animationDuration);
|
|
226
261
|
}
|
|
227
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Add or update a filter group
|
|
265
|
+
* @public
|
|
266
|
+
* @param {string} groupId - Group identifier
|
|
267
|
+
* @param {string[]} filters - Array of filter values
|
|
268
|
+
* @param {string} [operator='OR'] - Operator within group ('AND' or 'OR')
|
|
269
|
+
* @returns {boolean} Success status
|
|
270
|
+
*/
|
|
271
|
+
addFilterGroup(groupId, filters) {
|
|
272
|
+
let operator = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "OR";
|
|
273
|
+
try {
|
|
274
|
+
// Validate inputs
|
|
275
|
+
if (!groupId || !Array.isArray(filters)) {
|
|
276
|
+
console.warn("Invalid group parameters");
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
const validOperator = operator.toUpperCase();
|
|
280
|
+
if (!["AND", "OR"].includes(validOperator)) {
|
|
281
|
+
console.warn('Invalid operator. Using default "OR"');
|
|
282
|
+
operator = "OR";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Create or update group
|
|
286
|
+
this.filterGroups.set(groupId, {
|
|
287
|
+
filters: new Set(filters),
|
|
288
|
+
operator: validOperator
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Only update if we have active groups
|
|
292
|
+
if (this.filterGroups.size > 0) {
|
|
293
|
+
this.updateFiltersFromGroups();
|
|
294
|
+
this.filter();
|
|
295
|
+
this.updateURL();
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
} catch (error) {
|
|
299
|
+
console.error("Error adding filter group:", error);
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Set how groups combine with each other
|
|
306
|
+
* @public
|
|
307
|
+
* @param {string} mode - Mode for combining groups ('AND' or 'OR')
|
|
308
|
+
*/
|
|
309
|
+
setGroupMode(mode) {
|
|
310
|
+
const validMode = mode.toUpperCase();
|
|
311
|
+
if (["AND", "OR"].includes(validMode)) {
|
|
312
|
+
this.groupMode = validMode;
|
|
313
|
+
if (this.filterGroups.size > 0) {
|
|
314
|
+
this.filter();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Remove a filter group
|
|
321
|
+
* @public
|
|
322
|
+
* @param {string} groupId - Group identifier
|
|
323
|
+
* @returns {boolean} Success status
|
|
324
|
+
*/
|
|
325
|
+
removeFilterGroup(groupId) {
|
|
326
|
+
if (this.filterGroups.has(groupId)) {
|
|
327
|
+
this.filterGroups.delete(groupId);
|
|
328
|
+
|
|
329
|
+
// If no groups left, revert to normal filtering
|
|
330
|
+
if (this.filterGroups.size === 0) {
|
|
331
|
+
this.resetFilters();
|
|
332
|
+
} else {
|
|
333
|
+
this.updateFiltersFromGroups();
|
|
334
|
+
}
|
|
335
|
+
this.filter();
|
|
336
|
+
this.updateURL();
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Update filters based on groups
|
|
344
|
+
* @private
|
|
345
|
+
*/
|
|
346
|
+
updateFiltersFromGroups() {
|
|
347
|
+
// Only process if we have groups
|
|
348
|
+
if (this.filterGroups.size === 0) return;
|
|
349
|
+
|
|
350
|
+
// Clear current filters except '*'
|
|
351
|
+
if (!this.currentFilters.has("*")) {
|
|
352
|
+
this.currentFilters.clear();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Combine all group filters
|
|
356
|
+
for (const group of this.filterGroups.values()) {
|
|
357
|
+
group.filters.forEach(filter => {
|
|
358
|
+
if (filter !== "*") {
|
|
359
|
+
this.currentFilters.add(filter);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
228
365
|
/**
|
|
229
366
|
* Check if item matches any active filter (OR mode)
|
|
230
367
|
* @private
|
|
@@ -347,14 +484,43 @@ class AFS {
|
|
|
347
484
|
}, this.options.animationDuration);
|
|
348
485
|
}
|
|
349
486
|
|
|
487
|
+
/**
|
|
488
|
+
* Check if item matches filter groups
|
|
489
|
+
* @private
|
|
490
|
+
* @param {Set} itemCategories - Item's categories
|
|
491
|
+
* @returns {boolean} Whether item matches the group filters
|
|
492
|
+
*/
|
|
493
|
+
matchesFilterGroups(itemCategories) {
|
|
494
|
+
const groupMatches = [...this.filterGroups.values()].map(group => {
|
|
495
|
+
const groupFilters = [...group.filters];
|
|
496
|
+
if (groupFilters.length === 0) return true;
|
|
497
|
+
return group.operator === "OR" ? groupFilters.some(filter => itemCategories.has(filter)) : groupFilters.every(filter => itemCategories.has(filter));
|
|
498
|
+
});
|
|
499
|
+
return this.groupMode === "OR" ? groupMatches.some(matches => matches) : groupMatches.every(matches => matches);
|
|
500
|
+
}
|
|
501
|
+
|
|
350
502
|
/**
|
|
351
503
|
* Update URL with current filter state
|
|
352
504
|
* @private
|
|
353
505
|
*/
|
|
354
506
|
updateURL() {
|
|
355
|
-
|
|
507
|
+
// If only "*" filter is active or no filters are active, clear the URL
|
|
508
|
+
if (this.currentFilters.size === 0 || this.currentFilters.size === 1 && this.currentFilters.has("*")) {
|
|
509
|
+
window.history.pushState({}, "", window.location.pathname);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const params = new URLSearchParams(window.location.search);
|
|
356
513
|
|
|
357
|
-
//
|
|
514
|
+
// Add groups to URL if they exist
|
|
515
|
+
if (this.filterGroups.size > 0) {
|
|
516
|
+
for (const [groupId, group] of this.filterGroups.entries()) {
|
|
517
|
+
params.set(`group_${groupId}`, [...group.filters].join(","));
|
|
518
|
+
params.set(`groupOp_${groupId}`, group.operator.toLowerCase());
|
|
519
|
+
}
|
|
520
|
+
params.set("groupMode", this.groupMode.toLowerCase());
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Separate filters by type
|
|
358
524
|
const filtersByType = {};
|
|
359
525
|
for (const filter of this.currentFilters) {
|
|
360
526
|
if (filter !== "*") {
|
|
@@ -366,7 +532,7 @@ class AFS {
|
|
|
366
532
|
}
|
|
367
533
|
}
|
|
368
534
|
|
|
369
|
-
//
|
|
535
|
+
// Add each filter type to the URL
|
|
370
536
|
Object.entries(filtersByType).forEach(_ref => {
|
|
371
537
|
let [type, values] = _ref;
|
|
372
538
|
params.set(type, Array.from(values).join(","));
|
|
@@ -384,6 +550,23 @@ class AFS {
|
|
|
384
550
|
*/
|
|
385
551
|
loadFromURL() {
|
|
386
552
|
const params = new URLSearchParams(window.location.search);
|
|
553
|
+
|
|
554
|
+
// Load groups if they exist
|
|
555
|
+
this.filterGroups.clear();
|
|
556
|
+
for (const [key, value] of params.entries()) {
|
|
557
|
+
if (key.startsWith("group_")) {
|
|
558
|
+
const groupId = key.replace("group_", "");
|
|
559
|
+
const operator = params.get(`groupOp_${groupId}`)?.toUpperCase() || "OR";
|
|
560
|
+
const filters = value.split(",");
|
|
561
|
+
this.addFilterGroup(groupId, filters, operator);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Set group mode if present
|
|
566
|
+
const groupMode = params.get("groupMode")?.toUpperCase();
|
|
567
|
+
if (groupMode && ["AND", "OR"].includes(groupMode)) {
|
|
568
|
+
this.groupMode = groupMode;
|
|
569
|
+
}
|
|
387
570
|
this.currentFilters.clear();
|
|
388
571
|
|
|
389
572
|
// Si aucun filtre n'est présent, utiliser '*'
|
|
@@ -440,20 +623,197 @@ class AFS {
|
|
|
440
623
|
};
|
|
441
624
|
}
|
|
442
625
|
|
|
626
|
+
/**
|
|
627
|
+
* Set animation options
|
|
628
|
+
* @public
|
|
629
|
+
* @param {Object} options - Animation options
|
|
630
|
+
*/
|
|
631
|
+
setAnimationOptions() {
|
|
632
|
+
let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
633
|
+
this.options.animationDuration = options.duration || this.options.animationDuration;
|
|
634
|
+
this.options.animationType = options.type || "ease-out";
|
|
635
|
+
this.addStyles(); // Refresh styles with new options
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Event handling system
|
|
640
|
+
*/
|
|
641
|
+
addEventSystem() {
|
|
642
|
+
this.events = {};
|
|
643
|
+
this.on = (eventName, callback) => {
|
|
644
|
+
if (!this.events[eventName]) {
|
|
645
|
+
this.events[eventName] = [];
|
|
646
|
+
}
|
|
647
|
+
this.events[eventName].push(callback);
|
|
648
|
+
};
|
|
649
|
+
this.emit = (eventName, data) => {
|
|
650
|
+
if (this.events[eventName]) {
|
|
651
|
+
this.events[eventName].forEach(callback => callback(data));
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Add pagination
|
|
658
|
+
* @public
|
|
659
|
+
* @param {number} itemsPerPage - Number of items per page
|
|
660
|
+
*/
|
|
661
|
+
setPagination(itemsPerPage) {
|
|
662
|
+
this.pagination = {
|
|
663
|
+
currentPage: 1,
|
|
664
|
+
itemsPerPage: itemsPerPage,
|
|
665
|
+
totalPages: Math.ceil(this.visibleItems.size / itemsPerPage)
|
|
666
|
+
};
|
|
667
|
+
this.updatePagination();
|
|
668
|
+
}
|
|
669
|
+
updatePagination() {
|
|
670
|
+
const start = (this.pagination.currentPage - 1) * this.pagination.itemsPerPage;
|
|
671
|
+
const end = start + this.pagination.itemsPerPage;
|
|
672
|
+
[...this.visibleItems].forEach((item, index) => {
|
|
673
|
+
if (index >= start && index < end) {
|
|
674
|
+
this.showItem(item);
|
|
675
|
+
} else {
|
|
676
|
+
this.hideItem(item);
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Enable analytics tracking
|
|
683
|
+
* @public
|
|
684
|
+
* @param {Function} callback - Analytics callback function
|
|
685
|
+
*/
|
|
686
|
+
enableAnalytics(callback) {
|
|
687
|
+
this.analyticsCallback = callback;
|
|
688
|
+
this.on("filter", data => {
|
|
689
|
+
this.analyticsCallback({
|
|
690
|
+
type: "filter",
|
|
691
|
+
filters: [...this.currentFilters],
|
|
692
|
+
visibleItems: this.visibleItems.size,
|
|
693
|
+
timestamp: new Date().toISOString()
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Sort with custom comparator
|
|
700
|
+
* @public
|
|
701
|
+
* @param {string} key - Data attribute key
|
|
702
|
+
* @param {Function} comparator - Custom comparison function
|
|
703
|
+
*/
|
|
704
|
+
sortWithComparator(key, comparator) {
|
|
705
|
+
const items = [...this.items];
|
|
706
|
+
items.sort((a, b) => {
|
|
707
|
+
const valueA = a.dataset[key];
|
|
708
|
+
const valueB = b.dataset[key];
|
|
709
|
+
return comparator(valueA, valueB);
|
|
710
|
+
});
|
|
711
|
+
items.forEach(item => this.container.appendChild(item));
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Add responsive behavior
|
|
716
|
+
* @public
|
|
717
|
+
* @param {Object} breakpoints - Breakpoint configurations
|
|
718
|
+
*/
|
|
719
|
+
setResponsiveOptions(breakpoints) {
|
|
720
|
+
window.addEventListener("resize", debounce(() => {
|
|
721
|
+
const width = window.innerWidth;
|
|
722
|
+
for (const [breakpoint, options] of Object.entries(breakpoints)) {
|
|
723
|
+
if (width <= parseInt(breakpoint)) {
|
|
724
|
+
Object.assign(this.options, options);
|
|
725
|
+
this.filter();
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}, 250));
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Enable keyboard navigation
|
|
734
|
+
* @public
|
|
735
|
+
*/
|
|
736
|
+
enableKeyboardNavigation() {
|
|
737
|
+
document.addEventListener("keydown", e => {
|
|
738
|
+
if (e.key === "Enter" && document.activeElement.classList.contains(this.options.filterButtonSelector.slice(1))) {
|
|
739
|
+
document.activeElement.click();
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Export current filter state
|
|
746
|
+
* @public
|
|
747
|
+
* @returns {Object} Filter state
|
|
748
|
+
*/
|
|
749
|
+
exportState() {
|
|
750
|
+
return {
|
|
751
|
+
filters: [...this.currentFilters],
|
|
752
|
+
search: this.currentSearch,
|
|
753
|
+
mode: this.options.filterMode,
|
|
754
|
+
timestamp: new Date().toISOString()
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Import filter state
|
|
760
|
+
* @public
|
|
761
|
+
* @param {Object} state - Filter state to import
|
|
762
|
+
*/
|
|
763
|
+
importState(state) {
|
|
764
|
+
if (state.filters) {
|
|
765
|
+
this.currentFilters = new Set(state.filters);
|
|
766
|
+
this.currentSearch = state.search || "";
|
|
767
|
+
this.options.filterMode = state.mode || "OR";
|
|
768
|
+
this.filter();
|
|
769
|
+
this.updateURL();
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Save current filter state as preset
|
|
775
|
+
* @public
|
|
776
|
+
* @param {string} presetName - Name of the preset
|
|
777
|
+
*/
|
|
778
|
+
savePreset(presetName) {
|
|
779
|
+
const preset = {
|
|
780
|
+
filters: [...this.currentFilters],
|
|
781
|
+
search: this.currentSearch,
|
|
782
|
+
mode: this.options.filterMode
|
|
783
|
+
};
|
|
784
|
+
localStorage.setItem(`afs_preset_${presetName}`, JSON.stringify(preset));
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Load filter preset
|
|
789
|
+
* @public
|
|
790
|
+
* @param {string} presetName - Name of the preset to load
|
|
791
|
+
*/
|
|
792
|
+
loadPreset(presetName) {
|
|
793
|
+
const preset = JSON.parse(localStorage.getItem(`afs_preset_${presetName}`));
|
|
794
|
+
if (preset) {
|
|
795
|
+
this.currentFilters = new Set(preset.filters);
|
|
796
|
+
this.currentSearch = preset.search;
|
|
797
|
+
this.options.filterMode = preset.mode;
|
|
798
|
+
this.filter();
|
|
799
|
+
this.updateURL();
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
443
803
|
/**
|
|
444
804
|
* Set filter logic mode (alias for setFilterMode)
|
|
445
805
|
* @public
|
|
446
806
|
* @param {string} logic - New filter logic ('AND' or 'OR')
|
|
447
807
|
*/
|
|
448
808
|
setLogic(logic) {
|
|
449
|
-
if (typeof logic ===
|
|
809
|
+
if (typeof logic === "boolean") {
|
|
450
810
|
// Handle boolean input (true = AND, false = OR)
|
|
451
|
-
this.options.filterMode = logic ?
|
|
811
|
+
this.options.filterMode = logic ? "AND" : "OR";
|
|
452
812
|
this.filter();
|
|
453
813
|
return;
|
|
454
814
|
}
|
|
455
815
|
const mode = logic.toUpperCase();
|
|
456
|
-
if ([
|
|
816
|
+
if (["OR", "AND"].includes(mode)) {
|
|
457
817
|
this.options.filterMode = mode;
|
|
458
818
|
this.filter();
|
|
459
819
|
}
|