advanced-filter-system 1.0.4 → 1.0.6

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.6
4
4
  *
5
5
  * A flexible and customizable filtering system that supports:
6
6
  * - Multiple filtering modes (OR/AND)
@@ -65,12 +65,23 @@ class AFS {
65
65
  filterMode: "OR",
66
66
  searchKeys: ["title"],
67
67
  debounceTime: 300,
68
+ debug: false,
69
+ logLevel: 'info',
68
70
  ...options
69
71
  };
70
72
 
73
+ // Define logging levels hierarchy
74
+ this.logLevels = {
75
+ error: 0,
76
+ warn: 1,
77
+ info: 2,
78
+ debug: 3
79
+ };
80
+
71
81
  // Initialize elements
72
82
  this.container = document.querySelector(this.options.containerSelector);
73
83
  this.items = document.querySelectorAll(this.options.itemSelector);
84
+ this.sortOrders = {};
74
85
  this.filterButtons = document.querySelectorAll(this.options.filterButtonSelector);
75
86
  this.searchInput = document.querySelector(this.options.searchInputSelector);
76
87
  this.counter = document.querySelector(this.options.counterSelector);
@@ -82,18 +93,78 @@ class AFS {
82
93
  this.filterGroups = new Map();
83
94
  this.groupMode = "OR"; // Default group mode
84
95
 
96
+ this.log('debug', 'Initializing AFS with options:', this.options);
85
97
  this.init();
86
98
  }
87
99
 
100
+ /**
101
+ * Internal logging method
102
+ * Handles debug message output based on current log level
103
+ *
104
+ * @private
105
+ * @param {string} level - Log level ('error', 'warn', 'info', 'debug')
106
+ * @param {...any} args - Arguments to log
107
+ */
108
+ log(level) {
109
+ if (!this.options.debug) return;
110
+ const currentLevelValue = this.logLevels[this.options.logLevel];
111
+ const messageLevel = this.logLevels[level];
112
+ if (messageLevel <= currentLevelValue) {
113
+ const timestamp = new Date().toISOString();
114
+ const prefix = `[AFS ${level.toUpperCase()}] ${timestamp}`;
115
+ for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
116
+ args[_key2 - 1] = arguments[_key2];
117
+ }
118
+ switch (level) {
119
+ case 'error':
120
+ console.error(prefix, ...args);
121
+ break;
122
+ case 'warn':
123
+ console.warn(prefix, ...args);
124
+ break;
125
+ case 'info':
126
+ console.info(prefix, ...args);
127
+ break;
128
+ case 'debug':
129
+ console.debug(prefix, ...args);
130
+ break;
131
+ }
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Configure debug mode settings
137
+ *
138
+ * @public
139
+ * @param {boolean} enabled - Enable or disable debug mode
140
+ * @param {string} [level='info'] - Log level ('error', 'warn', 'info', 'debug')
141
+ */
142
+ setDebugMode(enabled) {
143
+ let level = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'info';
144
+ this.options.debug = enabled;
145
+ if (this.logLevels.hasOwnProperty(level)) {
146
+ this.options.logLevel = level;
147
+ }
148
+ this.log('info', `Debug mode ${enabled ? 'enabled' : 'disabled'} with level: ${level}`);
149
+ }
150
+
88
151
  /**
89
152
  * Initialize the filter system
153
+ * Sets up styles, events, and initial state
154
+ *
90
155
  * @private
91
156
  */
92
157
  init() {
158
+ this.log('debug', 'Starting initialization');
159
+ if (!this.container) {
160
+ this.log('error', `Container not found: ${this.options.containerSelector}`);
161
+ return;
162
+ }
93
163
  this.addStyles();
94
164
  this.bindEvents();
95
165
  this.loadFromURL();
96
166
  this.updateCounter();
167
+ this.log('info', 'Initialization complete');
97
168
  }
98
169
 
99
170
  /**
@@ -125,6 +196,7 @@ class AFS {
125
196
  const styleSheet = document.createElement("style");
126
197
  styleSheet.textContent = styles;
127
198
  document.head.appendChild(styleSheet);
199
+ this.log('debug', 'Styles added to document');
128
200
  }
129
201
 
130
202
  /**
@@ -132,6 +204,7 @@ class AFS {
132
204
  * @private
133
205
  */
134
206
  bindEvents() {
207
+ this.log('debug', 'Binding events');
135
208
  this.filterButtons.forEach(button => {
136
209
  button.addEventListener("click", () => this.handleFilterClick(button));
137
210
  });
@@ -141,6 +214,7 @@ class AFS {
141
214
  }, this.options.debounceTime));
142
215
  }
143
216
  window.addEventListener("popstate", () => this.loadFromURL());
217
+ this.log('debug', 'Events bound successfully');
144
218
  }
145
219
 
146
220
  /**
@@ -150,6 +224,7 @@ class AFS {
150
224
  */
151
225
  handleFilterClick(button) {
152
226
  const filterValue = button.dataset.filter;
227
+ this.log('debug', 'Filter button clicked:', filterValue);
153
228
  if (filterValue === "*") {
154
229
  this.resetFilters();
155
230
  } else {
@@ -164,6 +239,7 @@ class AFS {
164
239
  * @private
165
240
  */
166
241
  resetFilters() {
242
+ this.log('debug', 'Resetting filters');
167
243
  this.filterButtons.forEach(btn => btn.classList.remove(this.options.activeClass));
168
244
  this.currentFilters.clear();
169
245
  this.currentFilters.add("*");
@@ -187,13 +263,18 @@ class AFS {
187
263
  * @param {HTMLElement} button - Filter button element
188
264
  */
189
265
  toggleFilter(filterValue, button) {
266
+ this.log('debug', `Toggling filter: ${filterValue}`);
190
267
  this.currentFilters.delete("*");
191
268
  this.filterButtons[0].classList.remove(this.options.activeClass);
192
269
  if (button.classList.contains(this.options.activeClass)) {
193
270
  button.classList.remove(this.options.activeClass);
194
271
  this.currentFilters.delete(filterValue);
272
+
273
+ // If no filters are selected, reset to default state and clear URL
195
274
  if (this.currentFilters.size === 0) {
196
275
  this.resetFilters();
276
+ window.history.pushState({}, "", window.location.pathname);
277
+ return;
197
278
  }
198
279
  } else {
199
280
  button.classList.add(this.options.activeClass);
@@ -201,17 +282,74 @@ class AFS {
201
282
  }
202
283
  }
203
284
 
285
+ /**
286
+ * Sort items based on auto-detecting ASC or DESC for each key
287
+ * @public
288
+ * @param {string} key - The data attribute key to sort by (e.g., 'title', 'price', 'date')
289
+ */
290
+ sortWithOrder(key) {
291
+ const items = [...this.items];
292
+
293
+ // Check if the current sort order is ASC, default to ASC if undefined
294
+ const currentOrder = this.sortOrders[key] || "asc";
295
+ const newOrder = currentOrder === "asc" ? "desc" : "asc"; // Toggle order
296
+
297
+ // Sort items based on the new order
298
+ items.sort((a, b) => {
299
+ let valueA = a.dataset[key];
300
+ let valueB = b.dataset[key];
301
+
302
+ // Check if values are numeric and convert them to numbers if they are
303
+ const isNumeric = !isNaN(valueA) && !isNaN(valueB);
304
+ if (isNumeric) {
305
+ valueA = parseFloat(valueA);
306
+ valueB = parseFloat(valueB);
307
+ }
308
+ if (newOrder === "asc") {
309
+ return isNumeric ? valueA - valueB : valueA.localeCompare(valueB);
310
+ } else {
311
+ return isNumeric ? valueB - valueA : valueB.localeCompare(valueA);
312
+ }
313
+ });
314
+
315
+ // Update the current sort order for the key
316
+ this.sortOrders[key] = newOrder;
317
+
318
+ // Reorder items in the DOM
319
+ items.forEach(item => this.container.appendChild(item));
320
+ this.log('debug', `Sorting items by ${key} in ${newOrder} order`);
321
+ }
322
+
323
+ /**
324
+ * Shuffle items randomly and display in the container
325
+ * @public
326
+ */
327
+ shuffle() {
328
+ const itemsArray = [...this.items];
329
+
330
+ // Shuffle the itemsArray using Fisher-Yates algorithm
331
+ for (let i = itemsArray.length - 1; i > 0; i--) {
332
+ const j = Math.floor(Math.random() * (i + 1));
333
+ [itemsArray[i], itemsArray[j]] = [itemsArray[j], itemsArray[i]];
334
+ }
335
+
336
+ // Re-append the shuffled items to the container
337
+ itemsArray.forEach(item => this.container.appendChild(item));
338
+ this.log('debug', 'Shuffling items');
339
+ }
340
+
204
341
  /**
205
342
  * Apply current filters to items
206
343
  * @public
207
344
  */
208
345
  /**
209
- * Apply current filters to items
210
- * @public
211
- */
346
+ * Apply current filters to items
347
+ * @public
348
+ */
212
349
  filter() {
213
350
  // Store the original filter logic
214
351
  const standardFilter = () => {
352
+ this.log('debug', 'Applying filters');
215
353
  this.visibleItems.clear();
216
354
  this.items.forEach(item => {
217
355
  if (this.currentFilters.has("*")) {
@@ -254,6 +392,7 @@ class AFS {
254
392
  setTimeout(() => {
255
393
  this.updateCounter();
256
394
  }, this.options.animationDuration);
395
+ this.log('info', `Filter applied. Visible items: ${this.visibleItems.size}`);
257
396
  }
258
397
 
259
398
  /**
@@ -269,12 +408,12 @@ class AFS {
269
408
  try {
270
409
  // Validate inputs
271
410
  if (!groupId || !Array.isArray(filters)) {
272
- console.warn("Invalid group parameters");
411
+ this.log('error', 'Invalid group ID or filters');
273
412
  return false;
274
413
  }
275
414
  const validOperator = operator.toUpperCase();
276
415
  if (!["AND", "OR"].includes(validOperator)) {
277
- console.warn('Invalid operator. Using default "OR"');
416
+ this.log('error', 'Invalid operator:', operator);
278
417
  operator = "OR";
279
418
  }
280
419
 
@@ -292,7 +431,7 @@ class AFS {
292
431
  }
293
432
  return true;
294
433
  } catch (error) {
295
- console.error("Error adding filter group:", error);
434
+ this.log('error', 'Error adding filter group:', error);
296
435
  return false;
297
436
  }
298
437
  }
@@ -423,17 +562,21 @@ class AFS {
423
562
  * @param {string} query - Search query
424
563
  */
425
564
  search(query) {
565
+ this.log('debug', 'Performing search with query:', query);
426
566
  this.currentSearch = query.toLowerCase().trim();
567
+ let matches = 0;
427
568
  this.items.forEach(item => {
428
569
  const searchText = this.options.searchKeys.map(key => item.dataset[key] || "").join(" ").toLowerCase();
429
570
  const matchesSearch = this.currentSearch === "" || searchText.includes(this.currentSearch);
430
571
  if (matchesSearch) {
431
572
  this.showItem(item);
573
+ matches++;
432
574
  } else {
433
575
  this.hideItem(item);
434
576
  }
435
577
  });
436
578
  this.updateURL();
579
+ this.log('info', `Search complete. Found ${matches} matches`);
437
580
  setTimeout(() => {
438
581
  this.updateCounter();
439
582
  }, this.options.animationDuration);
@@ -456,6 +599,7 @@ class AFS {
456
599
  return 0;
457
600
  });
458
601
  items.forEach(item => this.container.appendChild(item));
602
+ this.log('debug', 'Sorting items by multiple criteria:', criteria);
459
603
  }
460
604
 
461
605
  /**
@@ -478,6 +622,7 @@ class AFS {
478
622
  setTimeout(() => {
479
623
  this.updateCounter();
480
624
  }, this.options.animationDuration);
625
+ this.log('debug', `Filtering items by range: ${key} between ${min} and ${max}`);
481
626
  }
482
627
 
483
628
  /**
@@ -500,6 +645,13 @@ class AFS {
500
645
  * @private
501
646
  */
502
647
  updateURL() {
648
+ this.log('debug', 'Updating URL');
649
+
650
+ // If only "*" filter is active or no filters are active, clear the URL
651
+ if (this.currentFilters.size === 0 || this.currentFilters.size === 1 && this.currentFilters.has("*")) {
652
+ window.history.pushState({}, "", window.location.pathname);
653
+ return;
654
+ }
503
655
  const params = new URLSearchParams(window.location.search);
504
656
 
505
657
  // Add groups to URL if they exist
@@ -511,7 +663,7 @@ class AFS {
511
663
  params.set("groupMode", this.groupMode.toLowerCase());
512
664
  }
513
665
 
514
- // Séparer les filtres par type
666
+ // Separate filters by type
515
667
  const filtersByType = {};
516
668
  for (const filter of this.currentFilters) {
517
669
  if (filter !== "*") {
@@ -523,7 +675,7 @@ class AFS {
523
675
  }
524
676
  }
525
677
 
526
- // Ajouter chaque type de filtre à l'URL
678
+ // Add each filter type to the URL
527
679
  Object.entries(filtersByType).forEach(_ref => {
528
680
  let [type, values] = _ref;
529
681
  params.set(type, Array.from(values).join(","));
@@ -533,6 +685,7 @@ class AFS {
533
685
  }
534
686
  const newURL = `${window.location.pathname}${params.toString() ? "?" + params.toString() : ""}`;
535
687
  window.history.pushState({}, "", newURL);
688
+ this.log('debug', 'URL updated:', newURL);
536
689
  }
537
690
 
538
691
  /**
@@ -540,6 +693,7 @@ class AFS {
540
693
  * @private
541
694
  */
542
695
  loadFromURL() {
696
+ this.log('debug', 'Loading state from URL');
543
697
  const params = new URLSearchParams(window.location.search);
544
698
 
545
699
  // Load groups if they exist
@@ -595,6 +749,7 @@ class AFS {
595
749
  if (search) {
596
750
  this.search(search);
597
751
  }
752
+ this.log('info', 'State loaded from URL');
598
753
  }
599
754
 
600
755
  /**
@@ -608,6 +763,7 @@ class AFS {
608
763
  if (this.counter) {
609
764
  this.counter.textContent = `Showing ${visible} of ${total}`;
610
765
  }
766
+ this.log('debug', `Counter updated: ${visible}/${total}`);
611
767
  return {
612
768
  total,
613
769
  visible
@@ -773,6 +929,7 @@ class AFS {
773
929
  mode: this.options.filterMode
774
930
  };
775
931
  localStorage.setItem(`afs_preset_${presetName}`, JSON.stringify(preset));
932
+ this.log('info', `Preset saved: ${presetName}`);
776
933
  }
777
934
 
778
935
  /**
@@ -856,6 +1013,38 @@ class AFS {
856
1013
  getActiveFiltersByType(type) {
857
1014
  return [...this.currentFilters].filter(filter => filter.startsWith(`${type}:`)).map(filter => filter.split(":")[1]);
858
1015
  }
1016
+
1017
+ /**
1018
+ * Clear all filters, url and search
1019
+ *
1020
+ * @public
1021
+ */
1022
+ clearAllFilters() {
1023
+ this.currentFilters.clear();
1024
+ this.currentFilters.add("*");
1025
+ this.filter();
1026
+ this.updateURL();
1027
+
1028
+ // Uncheck all checkboxes if any with activeClass
1029
+ const checkboxes = document.querySelectorAll('input[type="checkbox"]');
1030
+ checkboxes.forEach(checkbox => {
1031
+ if (checkbox.classList.contains(this.options.activeClass)) {
1032
+ checkbox.checked = false;
1033
+ checkbox.classList.remove(this.options.activeClass);
1034
+ }
1035
+ });
1036
+
1037
+ // Clear active on buttons
1038
+ this.filterButtons.forEach(btn => {
1039
+ btn.classList.remove(this.options.activeClass);
1040
+ });
1041
+
1042
+ // Clear search input
1043
+ if (this.searchInput) {
1044
+ this.searchInput.value = "";
1045
+ }
1046
+ this.log('info', 'All filters cleared');
1047
+ }
859
1048
  }
860
1049
 
861
1050
  export { AFS };