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/README.md +228 -108
- package/dist/AFS.esm.js +198 -9
- package/dist/AFS.esm.js.map +1 -1
- package/dist/AFS.js +198 -9
- package/dist/AFS.js.map +1 -1
- package/dist/AFS.min.js +1 -1
- package/dist/AFS.min.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 };
|