advanced-filter-system 1.0.5 → 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.5
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,6 +269,7 @@
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)) {
@@ -211,6 +288,62 @@
211
288
  }
212
289
  }
213
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
+
214
347
  /**
215
348
  * Apply current filters to items
216
349
  * @public
@@ -222,6 +355,7 @@
222
355
  filter() {
223
356
  // Store the original filter logic
224
357
  const standardFilter = () => {
358
+ this.log('debug', 'Applying filters');
225
359
  this.visibleItems.clear();
226
360
  this.items.forEach(item => {
227
361
  if (this.currentFilters.has("*")) {
@@ -264,6 +398,7 @@
264
398
  setTimeout(() => {
265
399
  this.updateCounter();
266
400
  }, this.options.animationDuration);
401
+ this.log('info', `Filter applied. Visible items: ${this.visibleItems.size}`);
267
402
  }
268
403
 
269
404
  /**
@@ -279,12 +414,12 @@
279
414
  try {
280
415
  // Validate inputs
281
416
  if (!groupId || !Array.isArray(filters)) {
282
- console.warn("Invalid group parameters");
417
+ this.log('error', 'Invalid group ID or filters');
283
418
  return false;
284
419
  }
285
420
  const validOperator = operator.toUpperCase();
286
421
  if (!["AND", "OR"].includes(validOperator)) {
287
- console.warn('Invalid operator. Using default "OR"');
422
+ this.log('error', 'Invalid operator:', operator);
288
423
  operator = "OR";
289
424
  }
290
425
 
@@ -302,7 +437,7 @@
302
437
  }
303
438
  return true;
304
439
  } catch (error) {
305
- console.error("Error adding filter group:", error);
440
+ this.log('error', 'Error adding filter group:', error);
306
441
  return false;
307
442
  }
308
443
  }
@@ -433,17 +568,21 @@
433
568
  * @param {string} query - Search query
434
569
  */
435
570
  search(query) {
571
+ this.log('debug', 'Performing search with query:', query);
436
572
  this.currentSearch = query.toLowerCase().trim();
573
+ let matches = 0;
437
574
  this.items.forEach(item => {
438
575
  const searchText = this.options.searchKeys.map(key => item.dataset[key] || "").join(" ").toLowerCase();
439
576
  const matchesSearch = this.currentSearch === "" || searchText.includes(this.currentSearch);
440
577
  if (matchesSearch) {
441
578
  this.showItem(item);
579
+ matches++;
442
580
  } else {
443
581
  this.hideItem(item);
444
582
  }
445
583
  });
446
584
  this.updateURL();
585
+ this.log('info', `Search complete. Found ${matches} matches`);
447
586
  setTimeout(() => {
448
587
  this.updateCounter();
449
588
  }, this.options.animationDuration);
@@ -466,6 +605,7 @@
466
605
  return 0;
467
606
  });
468
607
  items.forEach(item => this.container.appendChild(item));
608
+ this.log('debug', 'Sorting items by multiple criteria:', criteria);
469
609
  }
470
610
 
471
611
  /**
@@ -488,6 +628,7 @@
488
628
  setTimeout(() => {
489
629
  this.updateCounter();
490
630
  }, this.options.animationDuration);
631
+ this.log('debug', `Filtering items by range: ${key} between ${min} and ${max}`);
491
632
  }
492
633
 
493
634
  /**
@@ -510,6 +651,8 @@
510
651
  * @private
511
652
  */
512
653
  updateURL() {
654
+ this.log('debug', 'Updating URL');
655
+
513
656
  // If only "*" filter is active or no filters are active, clear the URL
514
657
  if (this.currentFilters.size === 0 || this.currentFilters.size === 1 && this.currentFilters.has("*")) {
515
658
  window.history.pushState({}, "", window.location.pathname);
@@ -548,6 +691,7 @@
548
691
  }
549
692
  const newURL = `${window.location.pathname}${params.toString() ? "?" + params.toString() : ""}`;
550
693
  window.history.pushState({}, "", newURL);
694
+ this.log('debug', 'URL updated:', newURL);
551
695
  }
552
696
 
553
697
  /**
@@ -555,6 +699,7 @@
555
699
  * @private
556
700
  */
557
701
  loadFromURL() {
702
+ this.log('debug', 'Loading state from URL');
558
703
  const params = new URLSearchParams(window.location.search);
559
704
 
560
705
  // Load groups if they exist
@@ -610,6 +755,7 @@
610
755
  if (search) {
611
756
  this.search(search);
612
757
  }
758
+ this.log('info', 'State loaded from URL');
613
759
  }
614
760
 
615
761
  /**
@@ -623,6 +769,7 @@
623
769
  if (this.counter) {
624
770
  this.counter.textContent = `Showing ${visible} of ${total}`;
625
771
  }
772
+ this.log('debug', `Counter updated: ${visible}/${total}`);
626
773
  return {
627
774
  total,
628
775
  visible
@@ -788,6 +935,7 @@
788
935
  mode: this.options.filterMode
789
936
  };
790
937
  localStorage.setItem(`afs_preset_${presetName}`, JSON.stringify(preset));
938
+ this.log('info', `Preset saved: ${presetName}`);
791
939
  }
792
940
 
793
941
  /**
@@ -871,6 +1019,38 @@
871
1019
  getActiveFiltersByType(type) {
872
1020
  return [...this.currentFilters].filter(filter => filter.startsWith(`${type}:`)).map(filter => filter.split(":")[1]);
873
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
+ }
874
1054
  }
875
1055
 
876
1056
  exports.AFS = AFS;