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 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
- this.visibleItems.clear(); // Start with an empty set
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
- this.items.forEach(item => {
209
- if (this.currentFilters.has("*")) {
210
- this.showItem(item);
211
- this.visibleItems.add(item); // Add visible item to the set
212
- } else {
213
- const itemCategories = new Set(item.dataset.categories?.split(" ") || []);
214
- const matchesFilter = this.options.filterMode === "OR" ? this.matchesAnyFilter(itemCategories) : this.matchesAllFilters(itemCategories);
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); // Add visible item to the set
241
+ this.visibleItems.add(item);
218
242
  } else {
219
- this.hideItem(item);
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