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.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @fileoverview Advanced Filter System for DOM elements
|
|
9
|
-
* @version 1.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
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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;
|