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/dist/AFS.esm.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Advanced Filter System for DOM elements
3
- * @version 1.0.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
- this.visibleItems.clear(); // Start with an empty set
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
- 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) {
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); // Add visible item to the set
245
+ this.visibleItems.add(item);
218
246
  } else {
219
- this.hideItem(item);
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
- const params = new URLSearchParams();
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
- // Séparer les filtres par type
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
- // Ajouter chaque type de filtre à l'URL
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 === 'boolean') {
809
+ if (typeof logic === "boolean") {
450
810
  // Handle boolean input (true = AND, false = OR)
451
- this.options.filterMode = logic ? 'AND' : 'OR';
811
+ this.options.filterMode = logic ? "AND" : "OR";
452
812
  this.filter();
453
813
  return;
454
814
  }
455
815
  const mode = logic.toUpperCase();
456
- if (['OR', 'AND'].includes(mode)) {
816
+ if (["OR", "AND"].includes(mode)) {
457
817
  this.options.filterMode = mode;
458
818
  this.filter();
459
819
  }