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.js CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  /**
8
8
  * @fileoverview Advanced Filter System for DOM elements
9
- * @version 1.0.0
9
+ * @version 1.0.6
10
10
  *
11
11
  * A flexible and customizable filtering system that supports:
12
12
  * - Multiple filtering modes (OR/AND)
@@ -71,12 +71,23 @@
71
71
  filterMode: "OR",
72
72
  searchKeys: ["title"],
73
73
  debounceTime: 300,
74
+ debug: false,
75
+ logLevel: 'info',
74
76
  ...options
75
77
  };
76
78
 
79
+ // Define logging levels hierarchy
80
+ this.logLevels = {
81
+ error: 0,
82
+ warn: 1,
83
+ info: 2,
84
+ debug: 3
85
+ };
86
+
77
87
  // Initialize elements
78
88
  this.container = document.querySelector(this.options.containerSelector);
79
89
  this.items = document.querySelectorAll(this.options.itemSelector);
90
+ this.sortOrders = {};
80
91
  this.filterButtons = document.querySelectorAll(this.options.filterButtonSelector);
81
92
  this.searchInput = document.querySelector(this.options.searchInputSelector);
82
93
  this.counter = document.querySelector(this.options.counterSelector);
@@ -88,18 +99,78 @@
88
99
  this.filterGroups = new Map();
89
100
  this.groupMode = "OR"; // Default group mode
90
101
 
102
+ this.log('debug', 'Initializing AFS with options:', this.options);
91
103
  this.init();
92
104
  }
93
105
 
106
+ /**
107
+ * Internal logging method
108
+ * Handles debug message output based on current log level
109
+ *
110
+ * @private
111
+ * @param {string} level - Log level ('error', 'warn', 'info', 'debug')
112
+ * @param {...any} args - Arguments to log
113
+ */
114
+ log(level) {
115
+ if (!this.options.debug) return;
116
+ const currentLevelValue = this.logLevels[this.options.logLevel];
117
+ const messageLevel = this.logLevels[level];
118
+ if (messageLevel <= currentLevelValue) {
119
+ const timestamp = new Date().toISOString();
120
+ const prefix = `[AFS ${level.toUpperCase()}] ${timestamp}`;
121
+ for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
122
+ args[_key2 - 1] = arguments[_key2];
123
+ }
124
+ switch (level) {
125
+ case 'error':
126
+ console.error(prefix, ...args);
127
+ break;
128
+ case 'warn':
129
+ console.warn(prefix, ...args);
130
+ break;
131
+ case 'info':
132
+ console.info(prefix, ...args);
133
+ break;
134
+ case 'debug':
135
+ console.debug(prefix, ...args);
136
+ break;
137
+ }
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Configure debug mode settings
143
+ *
144
+ * @public
145
+ * @param {boolean} enabled - Enable or disable debug mode
146
+ * @param {string} [level='info'] - Log level ('error', 'warn', 'info', 'debug')
147
+ */
148
+ setDebugMode(enabled) {
149
+ let level = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'info';
150
+ this.options.debug = enabled;
151
+ if (this.logLevels.hasOwnProperty(level)) {
152
+ this.options.logLevel = level;
153
+ }
154
+ this.log('info', `Debug mode ${enabled ? 'enabled' : 'disabled'} with level: ${level}`);
155
+ }
156
+
94
157
  /**
95
158
  * Initialize the filter system
159
+ * Sets up styles, events, and initial state
160
+ *
96
161
  * @private
97
162
  */
98
163
  init() {
164
+ this.log('debug', 'Starting initialization');
165
+ if (!this.container) {
166
+ this.log('error', `Container not found: ${this.options.containerSelector}`);
167
+ return;
168
+ }
99
169
  this.addStyles();
100
170
  this.bindEvents();
101
171
  this.loadFromURL();
102
172
  this.updateCounter();
173
+ this.log('info', 'Initialization complete');
103
174
  }
104
175
 
105
176
  /**
@@ -131,6 +202,7 @@
131
202
  const styleSheet = document.createElement("style");
132
203
  styleSheet.textContent = styles;
133
204
  document.head.appendChild(styleSheet);
205
+ this.log('debug', 'Styles added to document');
134
206
  }
135
207
 
136
208
  /**
@@ -138,6 +210,7 @@
138
210
  * @private
139
211
  */
140
212
  bindEvents() {
213
+ this.log('debug', 'Binding events');
141
214
  this.filterButtons.forEach(button => {
142
215
  button.addEventListener("click", () => this.handleFilterClick(button));
143
216
  });
@@ -147,6 +220,7 @@
147
220
  }, this.options.debounceTime));
148
221
  }
149
222
  window.addEventListener("popstate", () => this.loadFromURL());
223
+ this.log('debug', 'Events bound successfully');
150
224
  }
151
225
 
152
226
  /**
@@ -156,6 +230,7 @@
156
230
  */
157
231
  handleFilterClick(button) {
158
232
  const filterValue = button.dataset.filter;
233
+ this.log('debug', 'Filter button clicked:', filterValue);
159
234
  if (filterValue === "*") {
160
235
  this.resetFilters();
161
236
  } else {
@@ -170,6 +245,7 @@
170
245
  * @private
171
246
  */
172
247
  resetFilters() {
248
+ this.log('debug', 'Resetting filters');
173
249
  this.filterButtons.forEach(btn => btn.classList.remove(this.options.activeClass));
174
250
  this.currentFilters.clear();
175
251
  this.currentFilters.add("*");
@@ -193,13 +269,18 @@
193
269
  * @param {HTMLElement} button - Filter button element
194
270
  */
195
271
  toggleFilter(filterValue, button) {
272
+ this.log('debug', `Toggling filter: ${filterValue}`);
196
273
  this.currentFilters.delete("*");
197
274
  this.filterButtons[0].classList.remove(this.options.activeClass);
198
275
  if (button.classList.contains(this.options.activeClass)) {
199
276
  button.classList.remove(this.options.activeClass);
200
277
  this.currentFilters.delete(filterValue);
278
+
279
+ // If no filters are selected, reset to default state and clear URL
201
280
  if (this.currentFilters.size === 0) {
202
281
  this.resetFilters();
282
+ window.history.pushState({}, "", window.location.pathname);
283
+ return;
203
284
  }
204
285
  } else {
205
286
  button.classList.add(this.options.activeClass);
@@ -207,17 +288,74 @@
207
288
  }
208
289
  }
209
290
 
291
+ /**
292
+ * Sort items based on auto-detecting ASC or DESC for each key
293
+ * @public
294
+ * @param {string} key - The data attribute key to sort by (e.g., 'title', 'price', 'date')
295
+ */
296
+ sortWithOrder(key) {
297
+ const items = [...this.items];
298
+
299
+ // Check if the current sort order is ASC, default to ASC if undefined
300
+ const currentOrder = this.sortOrders[key] || "asc";
301
+ const newOrder = currentOrder === "asc" ? "desc" : "asc"; // Toggle order
302
+
303
+ // Sort items based on the new order
304
+ items.sort((a, b) => {
305
+ let valueA = a.dataset[key];
306
+ let valueB = b.dataset[key];
307
+
308
+ // Check if values are numeric and convert them to numbers if they are
309
+ const isNumeric = !isNaN(valueA) && !isNaN(valueB);
310
+ if (isNumeric) {
311
+ valueA = parseFloat(valueA);
312
+ valueB = parseFloat(valueB);
313
+ }
314
+ if (newOrder === "asc") {
315
+ return isNumeric ? valueA - valueB : valueA.localeCompare(valueB);
316
+ } else {
317
+ return isNumeric ? valueB - valueA : valueB.localeCompare(valueA);
318
+ }
319
+ });
320
+
321
+ // Update the current sort order for the key
322
+ this.sortOrders[key] = newOrder;
323
+
324
+ // Reorder items in the DOM
325
+ items.forEach(item => this.container.appendChild(item));
326
+ this.log('debug', `Sorting items by ${key} in ${newOrder} order`);
327
+ }
328
+
329
+ /**
330
+ * Shuffle items randomly and display in the container
331
+ * @public
332
+ */
333
+ shuffle() {
334
+ const itemsArray = [...this.items];
335
+
336
+ // Shuffle the itemsArray using Fisher-Yates algorithm
337
+ for (let i = itemsArray.length - 1; i > 0; i--) {
338
+ const j = Math.floor(Math.random() * (i + 1));
339
+ [itemsArray[i], itemsArray[j]] = [itemsArray[j], itemsArray[i]];
340
+ }
341
+
342
+ // Re-append the shuffled items to the container
343
+ itemsArray.forEach(item => this.container.appendChild(item));
344
+ this.log('debug', 'Shuffling items');
345
+ }
346
+
210
347
  /**
211
348
  * Apply current filters to items
212
349
  * @public
213
350
  */
214
351
  /**
215
- * Apply current filters to items
216
- * @public
217
- */
352
+ * Apply current filters to items
353
+ * @public
354
+ */
218
355
  filter() {
219
356
  // Store the original filter logic
220
357
  const standardFilter = () => {
358
+ this.log('debug', 'Applying filters');
221
359
  this.visibleItems.clear();
222
360
  this.items.forEach(item => {
223
361
  if (this.currentFilters.has("*")) {
@@ -260,6 +398,7 @@
260
398
  setTimeout(() => {
261
399
  this.updateCounter();
262
400
  }, this.options.animationDuration);
401
+ this.log('info', `Filter applied. Visible items: ${this.visibleItems.size}`);
263
402
  }
264
403
 
265
404
  /**
@@ -275,12 +414,12 @@
275
414
  try {
276
415
  // Validate inputs
277
416
  if (!groupId || !Array.isArray(filters)) {
278
- console.warn("Invalid group parameters");
417
+ this.log('error', 'Invalid group ID or filters');
279
418
  return false;
280
419
  }
281
420
  const validOperator = operator.toUpperCase();
282
421
  if (!["AND", "OR"].includes(validOperator)) {
283
- console.warn('Invalid operator. Using default "OR"');
422
+ this.log('error', 'Invalid operator:', operator);
284
423
  operator = "OR";
285
424
  }
286
425
 
@@ -298,7 +437,7 @@
298
437
  }
299
438
  return true;
300
439
  } catch (error) {
301
- console.error("Error adding filter group:", error);
440
+ this.log('error', 'Error adding filter group:', error);
302
441
  return false;
303
442
  }
304
443
  }
@@ -429,17 +568,21 @@
429
568
  * @param {string} query - Search query
430
569
  */
431
570
  search(query) {
571
+ this.log('debug', 'Performing search with query:', query);
432
572
  this.currentSearch = query.toLowerCase().trim();
573
+ let matches = 0;
433
574
  this.items.forEach(item => {
434
575
  const searchText = this.options.searchKeys.map(key => item.dataset[key] || "").join(" ").toLowerCase();
435
576
  const matchesSearch = this.currentSearch === "" || searchText.includes(this.currentSearch);
436
577
  if (matchesSearch) {
437
578
  this.showItem(item);
579
+ matches++;
438
580
  } else {
439
581
  this.hideItem(item);
440
582
  }
441
583
  });
442
584
  this.updateURL();
585
+ this.log('info', `Search complete. Found ${matches} matches`);
443
586
  setTimeout(() => {
444
587
  this.updateCounter();
445
588
  }, this.options.animationDuration);
@@ -462,6 +605,7 @@
462
605
  return 0;
463
606
  });
464
607
  items.forEach(item => this.container.appendChild(item));
608
+ this.log('debug', 'Sorting items by multiple criteria:', criteria);
465
609
  }
466
610
 
467
611
  /**
@@ -484,6 +628,7 @@
484
628
  setTimeout(() => {
485
629
  this.updateCounter();
486
630
  }, this.options.animationDuration);
631
+ this.log('debug', `Filtering items by range: ${key} between ${min} and ${max}`);
487
632
  }
488
633
 
489
634
  /**
@@ -506,6 +651,13 @@
506
651
  * @private
507
652
  */
508
653
  updateURL() {
654
+ this.log('debug', 'Updating URL');
655
+
656
+ // If only "*" filter is active or no filters are active, clear the URL
657
+ if (this.currentFilters.size === 0 || this.currentFilters.size === 1 && this.currentFilters.has("*")) {
658
+ window.history.pushState({}, "", window.location.pathname);
659
+ return;
660
+ }
509
661
  const params = new URLSearchParams(window.location.search);
510
662
 
511
663
  // Add groups to URL if they exist
@@ -517,7 +669,7 @@
517
669
  params.set("groupMode", this.groupMode.toLowerCase());
518
670
  }
519
671
 
520
- // Séparer les filtres par type
672
+ // Separate filters by type
521
673
  const filtersByType = {};
522
674
  for (const filter of this.currentFilters) {
523
675
  if (filter !== "*") {
@@ -529,7 +681,7 @@
529
681
  }
530
682
  }
531
683
 
532
- // Ajouter chaque type de filtre à l'URL
684
+ // Add each filter type to the URL
533
685
  Object.entries(filtersByType).forEach(_ref => {
534
686
  let [type, values] = _ref;
535
687
  params.set(type, Array.from(values).join(","));
@@ -539,6 +691,7 @@
539
691
  }
540
692
  const newURL = `${window.location.pathname}${params.toString() ? "?" + params.toString() : ""}`;
541
693
  window.history.pushState({}, "", newURL);
694
+ this.log('debug', 'URL updated:', newURL);
542
695
  }
543
696
 
544
697
  /**
@@ -546,6 +699,7 @@
546
699
  * @private
547
700
  */
548
701
  loadFromURL() {
702
+ this.log('debug', 'Loading state from URL');
549
703
  const params = new URLSearchParams(window.location.search);
550
704
 
551
705
  // Load groups if they exist
@@ -601,6 +755,7 @@
601
755
  if (search) {
602
756
  this.search(search);
603
757
  }
758
+ this.log('info', 'State loaded from URL');
604
759
  }
605
760
 
606
761
  /**
@@ -614,6 +769,7 @@
614
769
  if (this.counter) {
615
770
  this.counter.textContent = `Showing ${visible} of ${total}`;
616
771
  }
772
+ this.log('debug', `Counter updated: ${visible}/${total}`);
617
773
  return {
618
774
  total,
619
775
  visible
@@ -779,6 +935,7 @@
779
935
  mode: this.options.filterMode
780
936
  };
781
937
  localStorage.setItem(`afs_preset_${presetName}`, JSON.stringify(preset));
938
+ this.log('info', `Preset saved: ${presetName}`);
782
939
  }
783
940
 
784
941
  /**
@@ -862,6 +1019,38 @@
862
1019
  getActiveFiltersByType(type) {
863
1020
  return [...this.currentFilters].filter(filter => filter.startsWith(`${type}:`)).map(filter => filter.split(":")[1]);
864
1021
  }
1022
+
1023
+ /**
1024
+ * Clear all filters, url and search
1025
+ *
1026
+ * @public
1027
+ */
1028
+ clearAllFilters() {
1029
+ this.currentFilters.clear();
1030
+ this.currentFilters.add("*");
1031
+ this.filter();
1032
+ this.updateURL();
1033
+
1034
+ // Uncheck all checkboxes if any with activeClass
1035
+ const checkboxes = document.querySelectorAll('input[type="checkbox"]');
1036
+ checkboxes.forEach(checkbox => {
1037
+ if (checkbox.classList.contains(this.options.activeClass)) {
1038
+ checkbox.checked = false;
1039
+ checkbox.classList.remove(this.options.activeClass);
1040
+ }
1041
+ });
1042
+
1043
+ // Clear active on buttons
1044
+ this.filterButtons.forEach(btn => {
1045
+ btn.classList.remove(this.options.activeClass);
1046
+ });
1047
+
1048
+ // Clear search input
1049
+ if (this.searchInput) {
1050
+ this.searchInput.value = "";
1051
+ }
1052
+ this.log('info', 'All filters cleared');
1053
+ }
865
1054
  }
866
1055
 
867
1056
  exports.AFS = AFS;