advanced-filter-system 1.0.3 → 1.0.5
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 +210 -100
- package/dist/AFS.esm.js +380 -20
- package/dist/AFS.esm.js.map +1 -1
- package/dist/AFS.js +768 -408
- 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
|
@@ -1,110 +1,113 @@
|
|
|
1
1
|
(function (global, factory) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
3
|
+
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
4
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.AFS = {}));
|
|
5
5
|
})(this, (function (exports) { 'use strict';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* @fileoverview Advanced Filter System for DOM elements
|
|
9
|
+
* @version 1.0.5
|
|
10
|
+
*
|
|
11
|
+
* A flexible and customizable filtering system that supports:
|
|
12
|
+
* - Multiple filtering modes (OR/AND)
|
|
13
|
+
* - Text search with debouncing
|
|
14
|
+
* - Multiple sorting criteria
|
|
15
|
+
* - Range filtering
|
|
16
|
+
* - URL state management
|
|
17
|
+
* - Animation and transitions
|
|
18
|
+
* - Results counter
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Utility function for debouncing
|
|
23
|
+
* @param {Function} func - Function to debounce
|
|
24
|
+
* @param {number} wait - Delay in milliseconds
|
|
25
|
+
* @returns {Function} Debounced function
|
|
26
|
+
*/
|
|
27
|
+
function debounce(func, wait) {
|
|
28
|
+
let timeout;
|
|
29
|
+
return function executedFunction() {
|
|
30
|
+
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
31
|
+
args[_key] = arguments[_key];
|
|
32
|
+
}
|
|
33
|
+
const later = () => {
|
|
34
|
+
clearTimeout(timeout);
|
|
35
|
+
func(...args);
|
|
36
|
+
};
|
|
37
|
+
clearTimeout(timeout);
|
|
38
|
+
timeout = setTimeout(later, wait);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
class AFS {
|
|
7
42
|
/**
|
|
8
|
-
* @
|
|
9
|
-
* @
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
* -
|
|
16
|
-
* -
|
|
17
|
-
* -
|
|
18
|
-
* -
|
|
43
|
+
* @typedef {Object} FilterOptions
|
|
44
|
+
* @property {string} containerSelector - Main container selector
|
|
45
|
+
* @property {string} itemSelector - Items to filter selector
|
|
46
|
+
* @property {string} filterButtonSelector - Filter buttons selector
|
|
47
|
+
* @property {string} [searchInputSelector] - Search input selector
|
|
48
|
+
* @property {string} [counterSelector] - Results counter selector
|
|
49
|
+
* @property {string} [activeClass='active'] - Active state class
|
|
50
|
+
* @property {string} [hiddenClass='hidden'] - Hidden state class
|
|
51
|
+
* @property {number} [animationDuration=300] - Animation duration in ms
|
|
52
|
+
* @property {string} [filterMode='OR'] - Filter mode ('OR' or 'AND')
|
|
53
|
+
* @property {string[]} [searchKeys=['title']] - Data attributes to search in
|
|
54
|
+
* @property {number} [debounceTime=300] - Search debounce delay in ms
|
|
19
55
|
*/
|
|
20
56
|
|
|
21
57
|
/**
|
|
22
|
-
*
|
|
23
|
-
* @param {Function} func - Function to debounce
|
|
24
|
-
* @param {number} wait - Delay in milliseconds
|
|
25
|
-
* @returns {Function} Debounced function
|
|
58
|
+
* @param {FilterOptions} options - Filter configuration options
|
|
26
59
|
*/
|
|
27
|
-
|
|
28
|
-
let
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
60
|
+
constructor() {
|
|
61
|
+
let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
62
|
+
this.options = {
|
|
63
|
+
containerSelector: ".filter-container",
|
|
64
|
+
itemSelector: ".filter-item",
|
|
65
|
+
filterButtonSelector: ".btn-filter",
|
|
66
|
+
searchInputSelector: ".filter-search",
|
|
67
|
+
counterSelector: ".filter-counter",
|
|
68
|
+
activeClass: "active",
|
|
69
|
+
hiddenClass: "hidden",
|
|
70
|
+
animationDuration: 300,
|
|
71
|
+
filterMode: "OR",
|
|
72
|
+
searchKeys: ["title"],
|
|
73
|
+
debounceTime: 300,
|
|
74
|
+
...options
|
|
39
75
|
};
|
|
76
|
+
|
|
77
|
+
// Initialize elements
|
|
78
|
+
this.container = document.querySelector(this.options.containerSelector);
|
|
79
|
+
this.items = document.querySelectorAll(this.options.itemSelector);
|
|
80
|
+
this.filterButtons = document.querySelectorAll(this.options.filterButtonSelector);
|
|
81
|
+
this.searchInput = document.querySelector(this.options.searchInputSelector);
|
|
82
|
+
this.counter = document.querySelector(this.options.counterSelector);
|
|
83
|
+
|
|
84
|
+
// Initialize state
|
|
85
|
+
this.currentFilters = new Set(["*"]);
|
|
86
|
+
this.currentSearch = "";
|
|
87
|
+
this.visibleItems = new Set(this.items);
|
|
88
|
+
this.filterGroups = new Map();
|
|
89
|
+
this.groupMode = "OR"; // Default group mode
|
|
90
|
+
|
|
91
|
+
this.init();
|
|
40
92
|
}
|
|
41
|
-
class AFS {
|
|
42
|
-
/**
|
|
43
|
-
* @typedef {Object} FilterOptions
|
|
44
|
-
* @property {string} containerSelector - Main container selector
|
|
45
|
-
* @property {string} itemSelector - Items to filter selector
|
|
46
|
-
* @property {string} filterButtonSelector - Filter buttons selector
|
|
47
|
-
* @property {string} [searchInputSelector] - Search input selector
|
|
48
|
-
* @property {string} [counterSelector] - Results counter selector
|
|
49
|
-
* @property {string} [activeClass='active'] - Active state class
|
|
50
|
-
* @property {string} [hiddenClass='hidden'] - Hidden state class
|
|
51
|
-
* @property {number} [animationDuration=300] - Animation duration in ms
|
|
52
|
-
* @property {string} [filterMode='OR'] - Filter mode ('OR' or 'AND')
|
|
53
|
-
* @property {string[]} [searchKeys=['title']] - Data attributes to search in
|
|
54
|
-
* @property {number} [debounceTime=300] - Search debounce delay in ms
|
|
55
|
-
*/
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* @param {FilterOptions} options - Filter configuration options
|
|
59
|
-
*/
|
|
60
|
-
constructor() {
|
|
61
|
-
let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
62
|
-
this.options = {
|
|
63
|
-
containerSelector: ".filter-container",
|
|
64
|
-
itemSelector: ".filter-item",
|
|
65
|
-
filterButtonSelector: ".btn-filter",
|
|
66
|
-
searchInputSelector: ".filter-search",
|
|
67
|
-
counterSelector: ".filter-counter",
|
|
68
|
-
activeClass: "active",
|
|
69
|
-
hiddenClass: "hidden",
|
|
70
|
-
animationDuration: 300,
|
|
71
|
-
filterMode: "OR",
|
|
72
|
-
searchKeys: ["title"],
|
|
73
|
-
debounceTime: 300,
|
|
74
|
-
...options
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// Initialize elements
|
|
78
|
-
this.container = document.querySelector(this.options.containerSelector);
|
|
79
|
-
this.items = document.querySelectorAll(this.options.itemSelector);
|
|
80
|
-
this.filterButtons = document.querySelectorAll(this.options.filterButtonSelector);
|
|
81
|
-
this.searchInput = document.querySelector(this.options.searchInputSelector);
|
|
82
|
-
this.counter = document.querySelector(this.options.counterSelector);
|
|
83
|
-
|
|
84
|
-
// Initialize state
|
|
85
|
-
this.currentFilters = new Set(["*"]);
|
|
86
|
-
this.currentSearch = "";
|
|
87
|
-
this.visibleItems = new Set(this.items);
|
|
88
|
-
this.init();
|
|
89
|
-
}
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Initialize the filter system
|
|
96
|
+
* @private
|
|
97
|
+
*/
|
|
98
|
+
init() {
|
|
99
|
+
this.addStyles();
|
|
100
|
+
this.bindEvents();
|
|
101
|
+
this.loadFromURL();
|
|
102
|
+
this.updateCounter();
|
|
103
|
+
}
|
|
101
104
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Add required styles to document
|
|
107
|
+
* @private
|
|
108
|
+
*/
|
|
109
|
+
addStyles() {
|
|
110
|
+
const styles = `
|
|
108
111
|
.${this.options.hiddenClass} {
|
|
109
112
|
display: none !important;
|
|
110
113
|
}
|
|
@@ -125,395 +128,752 @@
|
|
|
125
128
|
opacity: 1;
|
|
126
129
|
}
|
|
127
130
|
`;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
const styleSheet = document.createElement("style");
|
|
132
|
+
styleSheet.textContent = styles;
|
|
133
|
+
document.head.appendChild(styleSheet);
|
|
134
|
+
}
|
|
132
135
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
window.addEventListener("popstate", () => this.loadFromURL());
|
|
136
|
+
/**
|
|
137
|
+
* Bind all event listeners
|
|
138
|
+
* @private
|
|
139
|
+
*/
|
|
140
|
+
bindEvents() {
|
|
141
|
+
this.filterButtons.forEach(button => {
|
|
142
|
+
button.addEventListener("click", () => this.handleFilterClick(button));
|
|
143
|
+
});
|
|
144
|
+
if (this.searchInput) {
|
|
145
|
+
this.searchInput.addEventListener("input", debounce(e => {
|
|
146
|
+
this.search(e.target.value);
|
|
147
|
+
}, this.options.debounceTime));
|
|
147
148
|
}
|
|
149
|
+
window.addEventListener("popstate", () => this.loadFromURL());
|
|
150
|
+
}
|
|
148
151
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
this.filter();
|
|
162
|
-
this.updateURL();
|
|
152
|
+
/**
|
|
153
|
+
* Handle filter button clicks
|
|
154
|
+
* @private
|
|
155
|
+
* @param {HTMLElement} button - Clicked filter button
|
|
156
|
+
*/
|
|
157
|
+
handleFilterClick(button) {
|
|
158
|
+
const filterValue = button.dataset.filter;
|
|
159
|
+
if (filterValue === "*") {
|
|
160
|
+
this.resetFilters();
|
|
161
|
+
} else {
|
|
162
|
+
this.toggleFilter(filterValue, button);
|
|
163
163
|
}
|
|
164
|
+
this.filter();
|
|
165
|
+
this.updateURL();
|
|
166
|
+
}
|
|
164
167
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Reset all filters to default state
|
|
170
|
+
* @private
|
|
171
|
+
*/
|
|
172
|
+
resetFilters() {
|
|
173
|
+
this.filterButtons.forEach(btn => btn.classList.remove(this.options.activeClass));
|
|
174
|
+
this.currentFilters.clear();
|
|
175
|
+
this.currentFilters.add("*");
|
|
176
|
+
this.filterButtons[0].classList.add(this.options.activeClass);
|
|
177
|
+
this.resetCounter();
|
|
178
|
+
}
|
|
176
179
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Reset visible items counter
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
184
|
+
resetCounter() {
|
|
185
|
+
this.visibleItems = new Set(this.items);
|
|
186
|
+
this.updateCounter();
|
|
187
|
+
}
|
|
185
188
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Toggle individual filter state
|
|
191
|
+
* @private
|
|
192
|
+
* @param {string} filterValue - Filter value to toggle
|
|
193
|
+
* @param {HTMLElement} button - Filter button element
|
|
194
|
+
*/
|
|
195
|
+
toggleFilter(filterValue, button) {
|
|
196
|
+
this.currentFilters.delete("*");
|
|
197
|
+
this.filterButtons[0].classList.remove(this.options.activeClass);
|
|
198
|
+
if (button.classList.contains(this.options.activeClass)) {
|
|
199
|
+
button.classList.remove(this.options.activeClass);
|
|
200
|
+
this.currentFilters.delete(filterValue);
|
|
201
|
+
|
|
202
|
+
// If no filters are selected, reset to default state and clear URL
|
|
203
|
+
if (this.currentFilters.size === 0) {
|
|
204
|
+
this.resetFilters();
|
|
205
|
+
window.history.pushState({}, "", window.location.pathname);
|
|
206
|
+
return;
|
|
204
207
|
}
|
|
208
|
+
} else {
|
|
209
|
+
button.classList.add(this.options.activeClass);
|
|
210
|
+
this.currentFilters.add(filterValue);
|
|
205
211
|
}
|
|
212
|
+
}
|
|
206
213
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
+
/**
|
|
215
|
+
* Apply current filters to items
|
|
216
|
+
* @public
|
|
217
|
+
*/
|
|
218
|
+
/**
|
|
219
|
+
* Apply current filters to items
|
|
220
|
+
* @public
|
|
221
|
+
*/
|
|
222
|
+
filter() {
|
|
223
|
+
// Store the original filter logic
|
|
224
|
+
const standardFilter = () => {
|
|
225
|
+
this.visibleItems.clear();
|
|
214
226
|
this.items.forEach(item => {
|
|
215
227
|
if (this.currentFilters.has("*")) {
|
|
216
228
|
this.showItem(item);
|
|
217
|
-
this.visibleItems.add(item);
|
|
229
|
+
this.visibleItems.add(item);
|
|
218
230
|
} else {
|
|
219
231
|
const itemCategories = new Set(item.dataset.categories?.split(" ") || []);
|
|
220
232
|
const matchesFilter = this.options.filterMode === "OR" ? this.matchesAnyFilter(itemCategories) : this.matchesAllFilters(itemCategories);
|
|
221
233
|
if (matchesFilter) {
|
|
222
234
|
this.showItem(item);
|
|
223
|
-
this.visibleItems.add(item);
|
|
235
|
+
this.visibleItems.add(item);
|
|
224
236
|
} else {
|
|
225
237
|
this.hideItem(item);
|
|
226
238
|
}
|
|
227
239
|
}
|
|
228
240
|
});
|
|
229
|
-
|
|
230
|
-
this.updateCounter();
|
|
231
|
-
}, this.options.animationDuration);
|
|
232
|
-
}
|
|
241
|
+
};
|
|
233
242
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
243
|
+
// Check if we should use group filtering or standard filtering
|
|
244
|
+
if (this.filterGroups.size === 0) {
|
|
245
|
+
standardFilter();
|
|
246
|
+
} else {
|
|
247
|
+
this.visibleItems.clear();
|
|
248
|
+
this.items.forEach(item => {
|
|
249
|
+
if (this.currentFilters.has("*")) {
|
|
250
|
+
this.showItem(item);
|
|
251
|
+
this.visibleItems.add(item);
|
|
252
|
+
} else {
|
|
253
|
+
const itemCategories = new Set(item.dataset.categories?.split(" ") || []);
|
|
254
|
+
const matchesGroups = this.matchesFilterGroups(itemCategories);
|
|
255
|
+
if (matchesGroups) {
|
|
256
|
+
this.showItem(item);
|
|
257
|
+
this.visibleItems.add(item);
|
|
258
|
+
} else {
|
|
259
|
+
this.hideItem(item);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
244
262
|
});
|
|
245
263
|
}
|
|
264
|
+
setTimeout(() => {
|
|
265
|
+
this.updateCounter();
|
|
266
|
+
}, this.options.animationDuration);
|
|
267
|
+
}
|
|
246
268
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
269
|
+
/**
|
|
270
|
+
* Add or update a filter group
|
|
271
|
+
* @public
|
|
272
|
+
* @param {string} groupId - Group identifier
|
|
273
|
+
* @param {string[]} filters - Array of filter values
|
|
274
|
+
* @param {string} [operator='OR'] - Operator within group ('AND' or 'OR')
|
|
275
|
+
* @returns {boolean} Success status
|
|
276
|
+
*/
|
|
277
|
+
addFilterGroup(groupId, filters) {
|
|
278
|
+
let operator = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "OR";
|
|
279
|
+
try {
|
|
280
|
+
// Validate inputs
|
|
281
|
+
if (!groupId || !Array.isArray(filters)) {
|
|
282
|
+
console.warn("Invalid group parameters");
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
const validOperator = operator.toUpperCase();
|
|
286
|
+
if (!["AND", "OR"].includes(validOperator)) {
|
|
287
|
+
console.warn('Invalid operator. Using default "OR"');
|
|
288
|
+
operator = "OR";
|
|
289
|
+
}
|
|
259
290
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
*/
|
|
265
|
-
showItem(item) {
|
|
266
|
-
this.visibleItems.add(item);
|
|
267
|
-
item.classList.remove(this.options.hiddenClass);
|
|
268
|
-
item.style.opacity = "0";
|
|
269
|
-
item.style.transform = "scale(0.95)";
|
|
270
|
-
item.offsetHeight;
|
|
271
|
-
requestAnimationFrame(() => {
|
|
272
|
-
item.style.opacity = "1";
|
|
273
|
-
item.style.transform = "scale(1)";
|
|
291
|
+
// Create or update group
|
|
292
|
+
this.filterGroups.set(groupId, {
|
|
293
|
+
filters: new Set(filters),
|
|
294
|
+
operator: validOperator
|
|
274
295
|
});
|
|
296
|
+
|
|
297
|
+
// Only update if we have active groups
|
|
298
|
+
if (this.filterGroups.size > 0) {
|
|
299
|
+
this.updateFiltersFromGroups();
|
|
300
|
+
this.filter();
|
|
301
|
+
this.updateURL();
|
|
302
|
+
}
|
|
303
|
+
return true;
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.error("Error adding filter group:", error);
|
|
306
|
+
return false;
|
|
275
307
|
}
|
|
308
|
+
}
|
|
276
309
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
}, this.options.animationDuration);
|
|
310
|
+
/**
|
|
311
|
+
* Set how groups combine with each other
|
|
312
|
+
* @public
|
|
313
|
+
* @param {string} mode - Mode for combining groups ('AND' or 'OR')
|
|
314
|
+
*/
|
|
315
|
+
setGroupMode(mode) {
|
|
316
|
+
const validMode = mode.toUpperCase();
|
|
317
|
+
if (["AND", "OR"].includes(validMode)) {
|
|
318
|
+
this.groupMode = validMode;
|
|
319
|
+
if (this.filterGroups.size > 0) {
|
|
320
|
+
this.filter();
|
|
321
|
+
}
|
|
291
322
|
}
|
|
323
|
+
}
|
|
292
324
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
325
|
+
/**
|
|
326
|
+
* Remove a filter group
|
|
327
|
+
* @public
|
|
328
|
+
* @param {string} groupId - Group identifier
|
|
329
|
+
* @returns {boolean} Success status
|
|
330
|
+
*/
|
|
331
|
+
removeFilterGroup(groupId) {
|
|
332
|
+
if (this.filterGroups.has(groupId)) {
|
|
333
|
+
this.filterGroups.delete(groupId);
|
|
334
|
+
|
|
335
|
+
// If no groups left, revert to normal filtering
|
|
336
|
+
if (this.filterGroups.size === 0) {
|
|
337
|
+
this.resetFilters();
|
|
338
|
+
} else {
|
|
339
|
+
this.updateFiltersFromGroups();
|
|
340
|
+
}
|
|
341
|
+
this.filter();
|
|
309
342
|
this.updateURL();
|
|
310
|
-
|
|
311
|
-
this.updateCounter();
|
|
312
|
-
}, this.options.animationDuration);
|
|
343
|
+
return true;
|
|
313
344
|
}
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
314
347
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const comparison = criterion.direction === "asc" ? valueA.localeCompare(valueB) : valueB.localeCompare(valueA);
|
|
327
|
-
if (comparison !== 0) return comparison;
|
|
328
|
-
}
|
|
329
|
-
return 0;
|
|
330
|
-
});
|
|
331
|
-
items.forEach(item => this.container.appendChild(item));
|
|
348
|
+
/**
|
|
349
|
+
* Update filters based on groups
|
|
350
|
+
* @private
|
|
351
|
+
*/
|
|
352
|
+
updateFiltersFromGroups() {
|
|
353
|
+
// Only process if we have groups
|
|
354
|
+
if (this.filterGroups.size === 0) return;
|
|
355
|
+
|
|
356
|
+
// Clear current filters except '*'
|
|
357
|
+
if (!this.currentFilters.has("*")) {
|
|
358
|
+
this.currentFilters.clear();
|
|
332
359
|
}
|
|
333
360
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
* @param {number} max - Maximum value
|
|
340
|
-
*/
|
|
341
|
-
addRangeFilter(key, min, max) {
|
|
342
|
-
this.items.forEach(item => {
|
|
343
|
-
const value = parseFloat(item.dataset[key]);
|
|
344
|
-
const inRange = value >= min && value <= max;
|
|
345
|
-
if (inRange) {
|
|
346
|
-
this.showItem(item);
|
|
347
|
-
} else {
|
|
348
|
-
this.hideItem(item);
|
|
361
|
+
// Combine all group filters
|
|
362
|
+
for (const group of this.filterGroups.values()) {
|
|
363
|
+
group.filters.forEach(filter => {
|
|
364
|
+
if (filter !== "*") {
|
|
365
|
+
this.currentFilters.add(filter);
|
|
349
366
|
}
|
|
350
367
|
});
|
|
351
|
-
setTimeout(() => {
|
|
352
|
-
this.updateCounter();
|
|
353
|
-
}, this.options.animationDuration);
|
|
354
368
|
}
|
|
369
|
+
}
|
|
355
370
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
371
|
+
/**
|
|
372
|
+
* Check if item matches any active filter (OR mode)
|
|
373
|
+
* @private
|
|
374
|
+
* @param {Set} itemCategories - Item's categories
|
|
375
|
+
* @returns {boolean} Whether item matches any filter
|
|
376
|
+
*/
|
|
377
|
+
matchesAnyFilter(itemCategories) {
|
|
378
|
+
return [...this.currentFilters].some(filter => {
|
|
379
|
+
const [type, value] = filter.split(":");
|
|
380
|
+
return itemCategories.has(`${type}:${value}`);
|
|
381
|
+
});
|
|
382
|
+
}
|
|
362
383
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
384
|
+
/**
|
|
385
|
+
* Check if item matches all active filters (AND mode)
|
|
386
|
+
* @private
|
|
387
|
+
* @param {Set} itemCategories - Item's categories
|
|
388
|
+
* @returns {boolean} Whether item matches all filters
|
|
389
|
+
*/
|
|
390
|
+
matchesAllFilters(itemCategories) {
|
|
391
|
+
return [...this.currentFilters].every(filter => {
|
|
392
|
+
const [type, value] = filter.split(":");
|
|
393
|
+
return itemCategories.has(`${type}:${value}`);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
374
396
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
397
|
+
/**
|
|
398
|
+
* Show an item with animation
|
|
399
|
+
* @private
|
|
400
|
+
* @param {HTMLElement} item - Item to show
|
|
401
|
+
*/
|
|
402
|
+
showItem(item) {
|
|
403
|
+
this.visibleItems.add(item);
|
|
404
|
+
item.classList.remove(this.options.hiddenClass);
|
|
405
|
+
item.style.opacity = "0";
|
|
406
|
+
item.style.transform = "scale(0.95)";
|
|
407
|
+
item.offsetHeight;
|
|
408
|
+
requestAnimationFrame(() => {
|
|
409
|
+
item.style.opacity = "1";
|
|
410
|
+
item.style.transform = "scale(1)";
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Hide an item with animation
|
|
416
|
+
* @private
|
|
417
|
+
* @param {HTMLElement} item - Item to hide
|
|
418
|
+
*/
|
|
419
|
+
hideItem(item) {
|
|
420
|
+
item.style.opacity = "0";
|
|
421
|
+
item.style.transform = "scale(0.95)";
|
|
422
|
+
setTimeout(() => {
|
|
423
|
+
if (item.style.opacity === "0") {
|
|
424
|
+
item.classList.add(this.options.hiddenClass);
|
|
425
|
+
this.visibleItems.delete(item);
|
|
382
426
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
427
|
+
}, this.options.animationDuration);
|
|
428
|
+
}
|
|
386
429
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
430
|
+
/**
|
|
431
|
+
* Search items by text
|
|
432
|
+
* @public
|
|
433
|
+
* @param {string} query - Search query
|
|
434
|
+
*/
|
|
435
|
+
search(query) {
|
|
436
|
+
this.currentSearch = query.toLowerCase().trim();
|
|
437
|
+
this.items.forEach(item => {
|
|
438
|
+
const searchText = this.options.searchKeys.map(key => item.dataset[key] || "").join(" ").toLowerCase();
|
|
439
|
+
const matchesSearch = this.currentSearch === "" || searchText.includes(this.currentSearch);
|
|
440
|
+
if (matchesSearch) {
|
|
441
|
+
this.showItem(item);
|
|
442
|
+
} else {
|
|
443
|
+
this.hideItem(item);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
this.updateURL();
|
|
447
|
+
setTimeout(() => {
|
|
448
|
+
this.updateCounter();
|
|
449
|
+
}, this.options.animationDuration);
|
|
450
|
+
}
|
|
394
451
|
|
|
395
|
-
|
|
396
|
-
|
|
452
|
+
/**
|
|
453
|
+
* Sort items by multiple criteria
|
|
454
|
+
* @public
|
|
455
|
+
* @param {Array<{key: string, direction: string}>} criteria - Sort criteria
|
|
456
|
+
*/
|
|
457
|
+
sortMultiple(criteria) {
|
|
458
|
+
const items = [...this.items];
|
|
459
|
+
items.sort((a, b) => {
|
|
460
|
+
for (const criterion of criteria) {
|
|
461
|
+
const valueA = a.dataset[criterion.key];
|
|
462
|
+
const valueB = b.dataset[criterion.key];
|
|
463
|
+
const comparison = criterion.direction === "asc" ? valueA.localeCompare(valueB) : valueB.localeCompare(valueA);
|
|
464
|
+
if (comparison !== 0) return comparison;
|
|
465
|
+
}
|
|
466
|
+
return 0;
|
|
467
|
+
});
|
|
468
|
+
items.forEach(item => this.container.appendChild(item));
|
|
469
|
+
}
|
|
397
470
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
471
|
+
/**
|
|
472
|
+
* Filter items by numeric range
|
|
473
|
+
* @public
|
|
474
|
+
* @param {string} key - Data attribute key
|
|
475
|
+
* @param {number} min - Minimum value
|
|
476
|
+
* @param {number} max - Maximum value
|
|
477
|
+
*/
|
|
478
|
+
addRangeFilter(key, min, max) {
|
|
479
|
+
this.items.forEach(item => {
|
|
480
|
+
const value = parseFloat(item.dataset[key]);
|
|
481
|
+
const inRange = value >= min && value <= max;
|
|
482
|
+
if (inRange) {
|
|
483
|
+
this.showItem(item);
|
|
484
|
+
} else {
|
|
485
|
+
this.hideItem(item);
|
|
406
486
|
}
|
|
407
|
-
|
|
408
|
-
|
|
487
|
+
});
|
|
488
|
+
setTimeout(() => {
|
|
489
|
+
this.updateCounter();
|
|
490
|
+
}, this.options.animationDuration);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Check if item matches filter groups
|
|
495
|
+
* @private
|
|
496
|
+
* @param {Set} itemCategories - Item's categories
|
|
497
|
+
* @returns {boolean} Whether item matches the group filters
|
|
498
|
+
*/
|
|
499
|
+
matchesFilterGroups(itemCategories) {
|
|
500
|
+
const groupMatches = [...this.filterGroups.values()].map(group => {
|
|
501
|
+
const groupFilters = [...group.filters];
|
|
502
|
+
if (groupFilters.length === 0) return true;
|
|
503
|
+
return group.operator === "OR" ? groupFilters.some(filter => itemCategories.has(filter)) : groupFilters.every(filter => itemCategories.has(filter));
|
|
504
|
+
});
|
|
505
|
+
return this.groupMode === "OR" ? groupMatches.some(matches => matches) : groupMatches.every(matches => matches);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Update URL with current filter state
|
|
510
|
+
* @private
|
|
511
|
+
*/
|
|
512
|
+
updateURL() {
|
|
513
|
+
// If only "*" filter is active or no filters are active, clear the URL
|
|
514
|
+
if (this.currentFilters.size === 0 || this.currentFilters.size === 1 && this.currentFilters.has("*")) {
|
|
515
|
+
window.history.pushState({}, "", window.location.pathname);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const params = new URLSearchParams(window.location.search);
|
|
519
|
+
|
|
520
|
+
// Add groups to URL if they exist
|
|
521
|
+
if (this.filterGroups.size > 0) {
|
|
522
|
+
for (const [groupId, group] of this.filterGroups.entries()) {
|
|
523
|
+
params.set(`group_${groupId}`, [...group.filters].join(","));
|
|
524
|
+
params.set(`groupOp_${groupId}`, group.operator.toLowerCase());
|
|
409
525
|
}
|
|
526
|
+
params.set("groupMode", this.groupMode.toLowerCase());
|
|
527
|
+
}
|
|
410
528
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
529
|
+
// Separate filters by type
|
|
530
|
+
const filtersByType = {};
|
|
531
|
+
for (const filter of this.currentFilters) {
|
|
532
|
+
if (filter !== "*") {
|
|
533
|
+
const [type, value] = filter.split(":");
|
|
534
|
+
if (!filtersByType[type]) {
|
|
535
|
+
filtersByType[type] = new Set();
|
|
418
536
|
}
|
|
419
|
-
|
|
537
|
+
filtersByType[type].add(value);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Add each filter type to the URL
|
|
542
|
+
Object.entries(filtersByType).forEach(_ref => {
|
|
543
|
+
let [type, values] = _ref;
|
|
544
|
+
params.set(type, Array.from(values).join(","));
|
|
545
|
+
});
|
|
546
|
+
if (this.currentSearch) {
|
|
547
|
+
params.set("search", this.currentSearch);
|
|
548
|
+
}
|
|
549
|
+
const newURL = `${window.location.pathname}${params.toString() ? "?" + params.toString() : ""}`;
|
|
550
|
+
window.history.pushState({}, "", newURL);
|
|
551
|
+
}
|
|
420
552
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
553
|
+
/**
|
|
554
|
+
* Load filter state from URL
|
|
555
|
+
* @private
|
|
556
|
+
*/
|
|
557
|
+
loadFromURL() {
|
|
558
|
+
const params = new URLSearchParams(window.location.search);
|
|
559
|
+
|
|
560
|
+
// Load groups if they exist
|
|
561
|
+
this.filterGroups.clear();
|
|
562
|
+
for (const [key, value] of params.entries()) {
|
|
563
|
+
if (key.startsWith("group_")) {
|
|
564
|
+
const groupId = key.replace("group_", "");
|
|
565
|
+
const operator = params.get(`groupOp_${groupId}`)?.toUpperCase() || "OR";
|
|
566
|
+
const filters = value.split(",");
|
|
567
|
+
this.addFilterGroup(groupId, filters, operator);
|
|
425
568
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Set group mode if present
|
|
572
|
+
const groupMode = params.get("groupMode")?.toUpperCase();
|
|
573
|
+
if (groupMode && ["AND", "OR"].includes(groupMode)) {
|
|
574
|
+
this.groupMode = groupMode;
|
|
575
|
+
}
|
|
576
|
+
this.currentFilters.clear();
|
|
577
|
+
|
|
578
|
+
// Si aucun filtre n'est présent, utiliser '*'
|
|
579
|
+
let hasFilters = false;
|
|
580
|
+
|
|
581
|
+
// Parcourir tous les paramètres
|
|
582
|
+
for (const [type, values] of params.entries()) {
|
|
583
|
+
if (type !== "search") {
|
|
584
|
+
hasFilters = true;
|
|
585
|
+
values.split(",").forEach(value => {
|
|
586
|
+
this.currentFilters.add(`${type}:${value}`);
|
|
587
|
+
});
|
|
429
588
|
}
|
|
430
589
|
}
|
|
590
|
+
if (!hasFilters) {
|
|
591
|
+
this.currentFilters.add("*");
|
|
592
|
+
}
|
|
431
593
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const visible = this.visibleItems.size;
|
|
440
|
-
if (this.counter) {
|
|
441
|
-
this.counter.textContent = `Showing ${visible} of ${total}`;
|
|
594
|
+
// Update active buttons
|
|
595
|
+
this.filterButtons.forEach(button => {
|
|
596
|
+
const filterValue = button.dataset.filter;
|
|
597
|
+
if (this.currentFilters.has(filterValue) || filterValue === "*" && this.currentFilters.has("*")) {
|
|
598
|
+
button.classList.add(this.options.activeClass);
|
|
599
|
+
} else {
|
|
600
|
+
button.classList.remove(this.options.activeClass);
|
|
442
601
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// Load search
|
|
605
|
+
const search = params.get("search") || "";
|
|
606
|
+
if (this.searchInput) {
|
|
607
|
+
this.searchInput.value = search;
|
|
447
608
|
}
|
|
609
|
+
this.filter();
|
|
610
|
+
if (search) {
|
|
611
|
+
this.search(search);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
448
614
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
615
|
+
/**
|
|
616
|
+
* Update results counter
|
|
617
|
+
* @private
|
|
618
|
+
* @returns {{total: number, visible: number}}
|
|
619
|
+
*/
|
|
620
|
+
updateCounter() {
|
|
621
|
+
const total = this.items.length;
|
|
622
|
+
const visible = this.visibleItems.size;
|
|
623
|
+
if (this.counter) {
|
|
624
|
+
this.counter.textContent = `Showing ${visible} of ${total}`;
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
total,
|
|
628
|
+
visible
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Set animation options
|
|
634
|
+
* @public
|
|
635
|
+
* @param {Object} options - Animation options
|
|
636
|
+
*/
|
|
637
|
+
setAnimationOptions() {
|
|
638
|
+
let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
639
|
+
this.options.animationDuration = options.duration || this.options.animationDuration;
|
|
640
|
+
this.options.animationType = options.type || "ease-out";
|
|
641
|
+
this.addStyles(); // Refresh styles with new options
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Event handling system
|
|
646
|
+
*/
|
|
647
|
+
addEventSystem() {
|
|
648
|
+
this.events = {};
|
|
649
|
+
this.on = (eventName, callback) => {
|
|
650
|
+
if (!this.events[eventName]) {
|
|
651
|
+
this.events[eventName] = [];
|
|
460
652
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
653
|
+
this.events[eventName].push(callback);
|
|
654
|
+
};
|
|
655
|
+
this.emit = (eventName, data) => {
|
|
656
|
+
if (this.events[eventName]) {
|
|
657
|
+
this.events[eventName].forEach(callback => callback(data));
|
|
465
658
|
}
|
|
466
|
-
}
|
|
659
|
+
};
|
|
660
|
+
}
|
|
467
661
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
662
|
+
/**
|
|
663
|
+
* Add pagination
|
|
664
|
+
* @public
|
|
665
|
+
* @param {number} itemsPerPage - Number of items per page
|
|
666
|
+
*/
|
|
667
|
+
setPagination(itemsPerPage) {
|
|
668
|
+
this.pagination = {
|
|
669
|
+
currentPage: 1,
|
|
670
|
+
itemsPerPage: itemsPerPage,
|
|
671
|
+
totalPages: Math.ceil(this.visibleItems.size / itemsPerPage)
|
|
672
|
+
};
|
|
673
|
+
this.updatePagination();
|
|
674
|
+
}
|
|
675
|
+
updatePagination() {
|
|
676
|
+
const start = (this.pagination.currentPage - 1) * this.pagination.itemsPerPage;
|
|
677
|
+
const end = start + this.pagination.itemsPerPage;
|
|
678
|
+
[...this.visibleItems].forEach((item, index) => {
|
|
679
|
+
if (index >= start && index < end) {
|
|
680
|
+
this.showItem(item);
|
|
681
|
+
} else {
|
|
682
|
+
this.hideItem(item);
|
|
477
683
|
}
|
|
478
|
-
}
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Enable analytics tracking
|
|
689
|
+
* @public
|
|
690
|
+
* @param {Function} callback - Analytics callback function
|
|
691
|
+
*/
|
|
692
|
+
enableAnalytics(callback) {
|
|
693
|
+
this.analyticsCallback = callback;
|
|
694
|
+
this.on("filter", data => {
|
|
695
|
+
this.analyticsCallback({
|
|
696
|
+
type: "filter",
|
|
697
|
+
filters: [...this.currentFilters],
|
|
698
|
+
visibleItems: this.visibleItems.size,
|
|
699
|
+
timestamp: new Date().toISOString()
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Sort with custom comparator
|
|
706
|
+
* @public
|
|
707
|
+
* @param {string} key - Data attribute key
|
|
708
|
+
* @param {Function} comparator - Custom comparison function
|
|
709
|
+
*/
|
|
710
|
+
sortWithComparator(key, comparator) {
|
|
711
|
+
const items = [...this.items];
|
|
712
|
+
items.sort((a, b) => {
|
|
713
|
+
const valueA = a.dataset[key];
|
|
714
|
+
const valueB = b.dataset[key];
|
|
715
|
+
return comparator(valueA, valueB);
|
|
716
|
+
});
|
|
717
|
+
items.forEach(item => this.container.appendChild(item));
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Add responsive behavior
|
|
722
|
+
* @public
|
|
723
|
+
* @param {Object} breakpoints - Breakpoint configurations
|
|
724
|
+
*/
|
|
725
|
+
setResponsiveOptions(breakpoints) {
|
|
726
|
+
window.addEventListener("resize", debounce(() => {
|
|
727
|
+
const width = window.innerWidth;
|
|
728
|
+
for (const [breakpoint, options] of Object.entries(breakpoints)) {
|
|
729
|
+
if (width <= parseInt(breakpoint)) {
|
|
730
|
+
Object.assign(this.options, options);
|
|
731
|
+
this.filter();
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}, 250));
|
|
736
|
+
}
|
|
479
737
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
738
|
+
/**
|
|
739
|
+
* Enable keyboard navigation
|
|
740
|
+
* @public
|
|
741
|
+
*/
|
|
742
|
+
enableKeyboardNavigation() {
|
|
743
|
+
document.addEventListener("keydown", e => {
|
|
744
|
+
if (e.key === "Enter" && document.activeElement.classList.contains(this.options.filterButtonSelector.slice(1))) {
|
|
745
|
+
document.activeElement.click();
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Export current filter state
|
|
752
|
+
* @public
|
|
753
|
+
* @returns {Object} Filter state
|
|
754
|
+
*/
|
|
755
|
+
exportState() {
|
|
756
|
+
return {
|
|
757
|
+
filters: [...this.currentFilters],
|
|
758
|
+
search: this.currentSearch,
|
|
759
|
+
mode: this.options.filterMode,
|
|
760
|
+
timestamp: new Date().toISOString()
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Import filter state
|
|
766
|
+
* @public
|
|
767
|
+
* @param {Object} state - Filter state to import
|
|
768
|
+
*/
|
|
769
|
+
importState(state) {
|
|
770
|
+
if (state.filters) {
|
|
771
|
+
this.currentFilters = new Set(state.filters);
|
|
772
|
+
this.currentSearch = state.search || "";
|
|
773
|
+
this.options.filterMode = state.mode || "OR";
|
|
488
774
|
this.filter();
|
|
489
775
|
this.updateURL();
|
|
490
776
|
}
|
|
777
|
+
}
|
|
491
778
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
779
|
+
/**
|
|
780
|
+
* Save current filter state as preset
|
|
781
|
+
* @public
|
|
782
|
+
* @param {string} presetName - Name of the preset
|
|
783
|
+
*/
|
|
784
|
+
savePreset(presetName) {
|
|
785
|
+
const preset = {
|
|
786
|
+
filters: [...this.currentFilters],
|
|
787
|
+
search: this.currentSearch,
|
|
788
|
+
mode: this.options.filterMode
|
|
789
|
+
};
|
|
790
|
+
localStorage.setItem(`afs_preset_${presetName}`, JSON.stringify(preset));
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Load filter preset
|
|
795
|
+
* @public
|
|
796
|
+
* @param {string} presetName - Name of the preset to load
|
|
797
|
+
*/
|
|
798
|
+
loadPreset(presetName) {
|
|
799
|
+
const preset = JSON.parse(localStorage.getItem(`afs_preset_${presetName}`));
|
|
800
|
+
if (preset) {
|
|
801
|
+
this.currentFilters = new Set(preset.filters);
|
|
802
|
+
this.currentSearch = preset.search;
|
|
803
|
+
this.options.filterMode = preset.mode;
|
|
502
804
|
this.filter();
|
|
503
805
|
this.updateURL();
|
|
504
806
|
}
|
|
807
|
+
}
|
|
505
808
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
809
|
+
/**
|
|
810
|
+
* Set filter logic mode (alias for setFilterMode)
|
|
811
|
+
* @public
|
|
812
|
+
* @param {string} logic - New filter logic ('AND' or 'OR')
|
|
813
|
+
*/
|
|
814
|
+
setLogic(logic) {
|
|
815
|
+
if (typeof logic === "boolean") {
|
|
816
|
+
// Handle boolean input (true = AND, false = OR)
|
|
817
|
+
this.options.filterMode = logic ? "AND" : "OR";
|
|
818
|
+
this.filter();
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
const mode = logic.toUpperCase();
|
|
822
|
+
if (["OR", "AND"].includes(mode)) {
|
|
823
|
+
this.options.filterMode = mode;
|
|
824
|
+
this.filter();
|
|
513
825
|
}
|
|
514
826
|
}
|
|
515
827
|
|
|
516
|
-
|
|
828
|
+
/**
|
|
829
|
+
* Change filter mode
|
|
830
|
+
* @public
|
|
831
|
+
* @param {string} mode - New filter mode ('OR' or 'AND')
|
|
832
|
+
*/
|
|
833
|
+
setFilterMode(mode) {
|
|
834
|
+
if (["OR", "AND"].includes(mode.toUpperCase())) {
|
|
835
|
+
this.options.filterMode = mode.toUpperCase();
|
|
836
|
+
this.filter();
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Add filter by type and value
|
|
842
|
+
* @public
|
|
843
|
+
* @param {string} type - Filter type
|
|
844
|
+
* @param {string} value - Filter value
|
|
845
|
+
*/
|
|
846
|
+
addFilter(type, value) {
|
|
847
|
+
this.currentFilters.add(`${type}:${value}`);
|
|
848
|
+
this.filter();
|
|
849
|
+
this.updateURL();
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Remove filter by type and value
|
|
854
|
+
* @public
|
|
855
|
+
* @param {string} type - Filter type
|
|
856
|
+
*/
|
|
857
|
+
removeFilter(type, value) {
|
|
858
|
+
this.currentFilters.delete(`${type}:${value}`);
|
|
859
|
+
if (this.currentFilters.size === 0) {
|
|
860
|
+
this.currentFilters.add("*");
|
|
861
|
+
}
|
|
862
|
+
this.filter();
|
|
863
|
+
this.updateURL();
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Get active filters by type
|
|
868
|
+
* @public
|
|
869
|
+
* @param {string} type - Filter type
|
|
870
|
+
*/
|
|
871
|
+
getActiveFiltersByType(type) {
|
|
872
|
+
return [...this.currentFilters].filter(filter => filter.startsWith(`${type}:`)).map(filter => filter.split(":")[1]);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
exports.AFS = AFS;
|
|
517
877
|
|
|
518
878
|
}));
|
|
519
879
|
//# sourceMappingURL=AFS.js.map
|