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/README.md CHANGED
@@ -224,6 +224,22 @@ filter.on('filter', (data) => {
224
224
  filter.enableKeyboardNavigation();
225
225
  ```
226
226
 
227
+ ### 13. Sort asc/desc
228
+
229
+ ```javascript
230
+ filter.sortWithComparator('price', (a, b) => parseFloat(a) - parseFloat(b), 'asc');
231
+
232
+ // or
233
+
234
+ filter.sortWithOrder('price');
235
+ ```
236
+
237
+ ### 13. Shuffle
238
+
239
+ ```javascript
240
+ filter.shuffle();
241
+ ```
242
+
227
243
  ## Browser Compatibility
228
244
 
229
245
  - ✅ Chrome
@@ -249,7 +265,9 @@ const filter = new AFS({
249
265
  animationDuration: 300,
250
266
  filterMode: 'OR',
251
267
  searchKeys: ['title'],
252
- debounceTime: 300
268
+ debounceTime: 300,
269
+ debug: false, // Enable console logs
270
+ logLevel: 'info', // 'info', 'warn', 'error', 'debug'
253
271
  });
254
272
  ```
255
273
 
@@ -275,6 +293,8 @@ const filter = new AFS({
275
293
  - `sortMultiple(criteria)`
276
294
  - `sortWithComparator(key, comparator)`
277
295
  - `addRangeFilter(key, min, max)`
296
+ - `shuffle()`
297
+ - `sortWithOrder(key)`
278
298
 
279
299
  ### State Management
280
300
 
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.5
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,6 +263,7 @@ 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)) {
@@ -205,6 +282,62 @@ class AFS {
205
282
  }
206
283
  }
207
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
+
208
341
  /**
209
342
  * Apply current filters to items
210
343
  * @public
@@ -216,6 +349,7 @@ class AFS {
216
349
  filter() {
217
350
  // Store the original filter logic
218
351
  const standardFilter = () => {
352
+ this.log('debug', 'Applying filters');
219
353
  this.visibleItems.clear();
220
354
  this.items.forEach(item => {
221
355
  if (this.currentFilters.has("*")) {
@@ -258,6 +392,7 @@ class AFS {
258
392
  setTimeout(() => {
259
393
  this.updateCounter();
260
394
  }, this.options.animationDuration);
395
+ this.log('info', `Filter applied. Visible items: ${this.visibleItems.size}`);
261
396
  }
262
397
 
263
398
  /**
@@ -273,12 +408,12 @@ class AFS {
273
408
  try {
274
409
  // Validate inputs
275
410
  if (!groupId || !Array.isArray(filters)) {
276
- console.warn("Invalid group parameters");
411
+ this.log('error', 'Invalid group ID or filters');
277
412
  return false;
278
413
  }
279
414
  const validOperator = operator.toUpperCase();
280
415
  if (!["AND", "OR"].includes(validOperator)) {
281
- console.warn('Invalid operator. Using default "OR"');
416
+ this.log('error', 'Invalid operator:', operator);
282
417
  operator = "OR";
283
418
  }
284
419
 
@@ -296,7 +431,7 @@ class AFS {
296
431
  }
297
432
  return true;
298
433
  } catch (error) {
299
- console.error("Error adding filter group:", error);
434
+ this.log('error', 'Error adding filter group:', error);
300
435
  return false;
301
436
  }
302
437
  }
@@ -427,17 +562,21 @@ class AFS {
427
562
  * @param {string} query - Search query
428
563
  */
429
564
  search(query) {
565
+ this.log('debug', 'Performing search with query:', query);
430
566
  this.currentSearch = query.toLowerCase().trim();
567
+ let matches = 0;
431
568
  this.items.forEach(item => {
432
569
  const searchText = this.options.searchKeys.map(key => item.dataset[key] || "").join(" ").toLowerCase();
433
570
  const matchesSearch = this.currentSearch === "" || searchText.includes(this.currentSearch);
434
571
  if (matchesSearch) {
435
572
  this.showItem(item);
573
+ matches++;
436
574
  } else {
437
575
  this.hideItem(item);
438
576
  }
439
577
  });
440
578
  this.updateURL();
579
+ this.log('info', `Search complete. Found ${matches} matches`);
441
580
  setTimeout(() => {
442
581
  this.updateCounter();
443
582
  }, this.options.animationDuration);
@@ -460,6 +599,7 @@ class AFS {
460
599
  return 0;
461
600
  });
462
601
  items.forEach(item => this.container.appendChild(item));
602
+ this.log('debug', 'Sorting items by multiple criteria:', criteria);
463
603
  }
464
604
 
465
605
  /**
@@ -482,6 +622,7 @@ class AFS {
482
622
  setTimeout(() => {
483
623
  this.updateCounter();
484
624
  }, this.options.animationDuration);
625
+ this.log('debug', `Filtering items by range: ${key} between ${min} and ${max}`);
485
626
  }
486
627
 
487
628
  /**
@@ -504,6 +645,8 @@ class AFS {
504
645
  * @private
505
646
  */
506
647
  updateURL() {
648
+ this.log('debug', 'Updating URL');
649
+
507
650
  // If only "*" filter is active or no filters are active, clear the URL
508
651
  if (this.currentFilters.size === 0 || this.currentFilters.size === 1 && this.currentFilters.has("*")) {
509
652
  window.history.pushState({}, "", window.location.pathname);
@@ -542,6 +685,7 @@ class AFS {
542
685
  }
543
686
  const newURL = `${window.location.pathname}${params.toString() ? "?" + params.toString() : ""}`;
544
687
  window.history.pushState({}, "", newURL);
688
+ this.log('debug', 'URL updated:', newURL);
545
689
  }
546
690
 
547
691
  /**
@@ -549,6 +693,7 @@ class AFS {
549
693
  * @private
550
694
  */
551
695
  loadFromURL() {
696
+ this.log('debug', 'Loading state from URL');
552
697
  const params = new URLSearchParams(window.location.search);
553
698
 
554
699
  // Load groups if they exist
@@ -604,6 +749,7 @@ class AFS {
604
749
  if (search) {
605
750
  this.search(search);
606
751
  }
752
+ this.log('info', 'State loaded from URL');
607
753
  }
608
754
 
609
755
  /**
@@ -617,6 +763,7 @@ class AFS {
617
763
  if (this.counter) {
618
764
  this.counter.textContent = `Showing ${visible} of ${total}`;
619
765
  }
766
+ this.log('debug', `Counter updated: ${visible}/${total}`);
620
767
  return {
621
768
  total,
622
769
  visible
@@ -782,6 +929,7 @@ class AFS {
782
929
  mode: this.options.filterMode
783
930
  };
784
931
  localStorage.setItem(`afs_preset_${presetName}`, JSON.stringify(preset));
932
+ this.log('info', `Preset saved: ${presetName}`);
785
933
  }
786
934
 
787
935
  /**
@@ -865,6 +1013,38 @@ class AFS {
865
1013
  getActiveFiltersByType(type) {
866
1014
  return [...this.currentFilters].filter(filter => filter.startsWith(`${type}:`)).map(filter => filter.split(":")[1]);
867
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
+ }
868
1048
  }
869
1049
 
870
1050
  export { AFS };