advanced-filter-system 1.0.2 → 1.0.4
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 +10 -0
- package/dist/AFS.esm.js +384 -14
- package/dist/AFS.esm.js.map +1 -1
- package/dist/AFS.js +761 -393
- 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 +23 -4
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.0
|
|
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,378 +128,743 @@
|
|
|
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
|
-
} else {
|
|
202
|
-
button.classList.add(this.options.activeClass);
|
|
203
|
-
this.currentFilters.add(filterValue);
|
|
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
|
+
if (this.currentFilters.size === 0) {
|
|
202
|
+
this.resetFilters();
|
|
204
203
|
}
|
|
204
|
+
} else {
|
|
205
|
+
button.classList.add(this.options.activeClass);
|
|
206
|
+
this.currentFilters.add(filterValue);
|
|
205
207
|
}
|
|
208
|
+
}
|
|
206
209
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Apply current filters to items
|
|
212
|
+
* @public
|
|
213
|
+
*/
|
|
214
|
+
/**
|
|
215
|
+
* Apply current filters to items
|
|
216
|
+
* @public
|
|
217
|
+
*/
|
|
218
|
+
filter() {
|
|
219
|
+
// Store the original filter logic
|
|
220
|
+
const standardFilter = () => {
|
|
221
|
+
this.visibleItems.clear();
|
|
214
222
|
this.items.forEach(item => {
|
|
215
223
|
if (this.currentFilters.has("*")) {
|
|
216
224
|
this.showItem(item);
|
|
217
|
-
this.visibleItems.add(item);
|
|
225
|
+
this.visibleItems.add(item);
|
|
218
226
|
} else {
|
|
219
227
|
const itemCategories = new Set(item.dataset.categories?.split(" ") || []);
|
|
220
228
|
const matchesFilter = this.options.filterMode === "OR" ? this.matchesAnyFilter(itemCategories) : this.matchesAllFilters(itemCategories);
|
|
221
229
|
if (matchesFilter) {
|
|
222
230
|
this.showItem(item);
|
|
223
|
-
this.visibleItems.add(item);
|
|
231
|
+
this.visibleItems.add(item);
|
|
224
232
|
} else {
|
|
225
233
|
this.hideItem(item);
|
|
226
234
|
}
|
|
227
235
|
}
|
|
228
236
|
});
|
|
229
|
-
|
|
230
|
-
this.updateCounter();
|
|
231
|
-
}, this.options.animationDuration);
|
|
232
|
-
}
|
|
237
|
+
};
|
|
233
238
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
239
|
+
// Check if we should use group filtering or standard filtering
|
|
240
|
+
if (this.filterGroups.size === 0) {
|
|
241
|
+
standardFilter();
|
|
242
|
+
} else {
|
|
243
|
+
this.visibleItems.clear();
|
|
244
|
+
this.items.forEach(item => {
|
|
245
|
+
if (this.currentFilters.has("*")) {
|
|
246
|
+
this.showItem(item);
|
|
247
|
+
this.visibleItems.add(item);
|
|
248
|
+
} else {
|
|
249
|
+
const itemCategories = new Set(item.dataset.categories?.split(" ") || []);
|
|
250
|
+
const matchesGroups = this.matchesFilterGroups(itemCategories);
|
|
251
|
+
if (matchesGroups) {
|
|
252
|
+
this.showItem(item);
|
|
253
|
+
this.visibleItems.add(item);
|
|
254
|
+
} else {
|
|
255
|
+
this.hideItem(item);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
244
258
|
});
|
|
245
259
|
}
|
|
260
|
+
setTimeout(() => {
|
|
261
|
+
this.updateCounter();
|
|
262
|
+
}, this.options.animationDuration);
|
|
263
|
+
}
|
|
246
264
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
265
|
+
/**
|
|
266
|
+
* Add or update a filter group
|
|
267
|
+
* @public
|
|
268
|
+
* @param {string} groupId - Group identifier
|
|
269
|
+
* @param {string[]} filters - Array of filter values
|
|
270
|
+
* @param {string} [operator='OR'] - Operator within group ('AND' or 'OR')
|
|
271
|
+
* @returns {boolean} Success status
|
|
272
|
+
*/
|
|
273
|
+
addFilterGroup(groupId, filters) {
|
|
274
|
+
let operator = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "OR";
|
|
275
|
+
try {
|
|
276
|
+
// Validate inputs
|
|
277
|
+
if (!groupId || !Array.isArray(filters)) {
|
|
278
|
+
console.warn("Invalid group parameters");
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
const validOperator = operator.toUpperCase();
|
|
282
|
+
if (!["AND", "OR"].includes(validOperator)) {
|
|
283
|
+
console.warn('Invalid operator. Using default "OR"');
|
|
284
|
+
operator = "OR";
|
|
285
|
+
}
|
|
259
286
|
|
|
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)";
|
|
287
|
+
// Create or update group
|
|
288
|
+
this.filterGroups.set(groupId, {
|
|
289
|
+
filters: new Set(filters),
|
|
290
|
+
operator: validOperator
|
|
274
291
|
});
|
|
292
|
+
|
|
293
|
+
// Only update if we have active groups
|
|
294
|
+
if (this.filterGroups.size > 0) {
|
|
295
|
+
this.updateFiltersFromGroups();
|
|
296
|
+
this.filter();
|
|
297
|
+
this.updateURL();
|
|
298
|
+
}
|
|
299
|
+
return true;
|
|
300
|
+
} catch (error) {
|
|
301
|
+
console.error("Error adding filter group:", error);
|
|
302
|
+
return false;
|
|
275
303
|
}
|
|
304
|
+
}
|
|
276
305
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
}, this.options.animationDuration);
|
|
306
|
+
/**
|
|
307
|
+
* Set how groups combine with each other
|
|
308
|
+
* @public
|
|
309
|
+
* @param {string} mode - Mode for combining groups ('AND' or 'OR')
|
|
310
|
+
*/
|
|
311
|
+
setGroupMode(mode) {
|
|
312
|
+
const validMode = mode.toUpperCase();
|
|
313
|
+
if (["AND", "OR"].includes(validMode)) {
|
|
314
|
+
this.groupMode = validMode;
|
|
315
|
+
if (this.filterGroups.size > 0) {
|
|
316
|
+
this.filter();
|
|
317
|
+
}
|
|
291
318
|
}
|
|
319
|
+
}
|
|
292
320
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
321
|
+
/**
|
|
322
|
+
* Remove a filter group
|
|
323
|
+
* @public
|
|
324
|
+
* @param {string} groupId - Group identifier
|
|
325
|
+
* @returns {boolean} Success status
|
|
326
|
+
*/
|
|
327
|
+
removeFilterGroup(groupId) {
|
|
328
|
+
if (this.filterGroups.has(groupId)) {
|
|
329
|
+
this.filterGroups.delete(groupId);
|
|
330
|
+
|
|
331
|
+
// If no groups left, revert to normal filtering
|
|
332
|
+
if (this.filterGroups.size === 0) {
|
|
333
|
+
this.resetFilters();
|
|
334
|
+
} else {
|
|
335
|
+
this.updateFiltersFromGroups();
|
|
336
|
+
}
|
|
337
|
+
this.filter();
|
|
309
338
|
this.updateURL();
|
|
310
|
-
|
|
311
|
-
this.updateCounter();
|
|
312
|
-
}, this.options.animationDuration);
|
|
339
|
+
return true;
|
|
313
340
|
}
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
314
343
|
|
|
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));
|
|
344
|
+
/**
|
|
345
|
+
* Update filters based on groups
|
|
346
|
+
* @private
|
|
347
|
+
*/
|
|
348
|
+
updateFiltersFromGroups() {
|
|
349
|
+
// Only process if we have groups
|
|
350
|
+
if (this.filterGroups.size === 0) return;
|
|
351
|
+
|
|
352
|
+
// Clear current filters except '*'
|
|
353
|
+
if (!this.currentFilters.has("*")) {
|
|
354
|
+
this.currentFilters.clear();
|
|
332
355
|
}
|
|
333
356
|
|
|
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);
|
|
357
|
+
// Combine all group filters
|
|
358
|
+
for (const group of this.filterGroups.values()) {
|
|
359
|
+
group.filters.forEach(filter => {
|
|
360
|
+
if (filter !== "*") {
|
|
361
|
+
this.currentFilters.add(filter);
|
|
349
362
|
}
|
|
350
363
|
});
|
|
351
|
-
setTimeout(() => {
|
|
352
|
-
this.updateCounter();
|
|
353
|
-
}, this.options.animationDuration);
|
|
354
364
|
}
|
|
365
|
+
}
|
|
355
366
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
367
|
+
/**
|
|
368
|
+
* Check if item matches any active filter (OR mode)
|
|
369
|
+
* @private
|
|
370
|
+
* @param {Set} itemCategories - Item's categories
|
|
371
|
+
* @returns {boolean} Whether item matches any filter
|
|
372
|
+
*/
|
|
373
|
+
matchesAnyFilter(itemCategories) {
|
|
374
|
+
return [...this.currentFilters].some(filter => {
|
|
375
|
+
const [type, value] = filter.split(":");
|
|
376
|
+
return itemCategories.has(`${type}:${value}`);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
362
379
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
380
|
+
/**
|
|
381
|
+
* Check if item matches all active filters (AND mode)
|
|
382
|
+
* @private
|
|
383
|
+
* @param {Set} itemCategories - Item's categories
|
|
384
|
+
* @returns {boolean} Whether item matches all filters
|
|
385
|
+
*/
|
|
386
|
+
matchesAllFilters(itemCategories) {
|
|
387
|
+
return [...this.currentFilters].every(filter => {
|
|
388
|
+
const [type, value] = filter.split(":");
|
|
389
|
+
return itemCategories.has(`${type}:${value}`);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
374
392
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
393
|
+
/**
|
|
394
|
+
* Show an item with animation
|
|
395
|
+
* @private
|
|
396
|
+
* @param {HTMLElement} item - Item to show
|
|
397
|
+
*/
|
|
398
|
+
showItem(item) {
|
|
399
|
+
this.visibleItems.add(item);
|
|
400
|
+
item.classList.remove(this.options.hiddenClass);
|
|
401
|
+
item.style.opacity = "0";
|
|
402
|
+
item.style.transform = "scale(0.95)";
|
|
403
|
+
item.offsetHeight;
|
|
404
|
+
requestAnimationFrame(() => {
|
|
405
|
+
item.style.opacity = "1";
|
|
406
|
+
item.style.transform = "scale(1)";
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Hide an item with animation
|
|
412
|
+
* @private
|
|
413
|
+
* @param {HTMLElement} item - Item to hide
|
|
414
|
+
*/
|
|
415
|
+
hideItem(item) {
|
|
416
|
+
item.style.opacity = "0";
|
|
417
|
+
item.style.transform = "scale(0.95)";
|
|
418
|
+
setTimeout(() => {
|
|
419
|
+
if (item.style.opacity === "0") {
|
|
420
|
+
item.classList.add(this.options.hiddenClass);
|
|
421
|
+
this.visibleItems.delete(item);
|
|
382
422
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
423
|
+
}, this.options.animationDuration);
|
|
424
|
+
}
|
|
386
425
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
426
|
+
/**
|
|
427
|
+
* Search items by text
|
|
428
|
+
* @public
|
|
429
|
+
* @param {string} query - Search query
|
|
430
|
+
*/
|
|
431
|
+
search(query) {
|
|
432
|
+
this.currentSearch = query.toLowerCase().trim();
|
|
433
|
+
this.items.forEach(item => {
|
|
434
|
+
const searchText = this.options.searchKeys.map(key => item.dataset[key] || "").join(" ").toLowerCase();
|
|
435
|
+
const matchesSearch = this.currentSearch === "" || searchText.includes(this.currentSearch);
|
|
436
|
+
if (matchesSearch) {
|
|
437
|
+
this.showItem(item);
|
|
438
|
+
} else {
|
|
439
|
+
this.hideItem(item);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
this.updateURL();
|
|
443
|
+
setTimeout(() => {
|
|
444
|
+
this.updateCounter();
|
|
445
|
+
}, this.options.animationDuration);
|
|
446
|
+
}
|
|
394
447
|
|
|
395
|
-
|
|
396
|
-
|
|
448
|
+
/**
|
|
449
|
+
* Sort items by multiple criteria
|
|
450
|
+
* @public
|
|
451
|
+
* @param {Array<{key: string, direction: string}>} criteria - Sort criteria
|
|
452
|
+
*/
|
|
453
|
+
sortMultiple(criteria) {
|
|
454
|
+
const items = [...this.items];
|
|
455
|
+
items.sort((a, b) => {
|
|
456
|
+
for (const criterion of criteria) {
|
|
457
|
+
const valueA = a.dataset[criterion.key];
|
|
458
|
+
const valueB = b.dataset[criterion.key];
|
|
459
|
+
const comparison = criterion.direction === "asc" ? valueA.localeCompare(valueB) : valueB.localeCompare(valueA);
|
|
460
|
+
if (comparison !== 0) return comparison;
|
|
461
|
+
}
|
|
462
|
+
return 0;
|
|
463
|
+
});
|
|
464
|
+
items.forEach(item => this.container.appendChild(item));
|
|
465
|
+
}
|
|
397
466
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
467
|
+
/**
|
|
468
|
+
* Filter items by numeric range
|
|
469
|
+
* @public
|
|
470
|
+
* @param {string} key - Data attribute key
|
|
471
|
+
* @param {number} min - Minimum value
|
|
472
|
+
* @param {number} max - Maximum value
|
|
473
|
+
*/
|
|
474
|
+
addRangeFilter(key, min, max) {
|
|
475
|
+
this.items.forEach(item => {
|
|
476
|
+
const value = parseFloat(item.dataset[key]);
|
|
477
|
+
const inRange = value >= min && value <= max;
|
|
478
|
+
if (inRange) {
|
|
479
|
+
this.showItem(item);
|
|
480
|
+
} else {
|
|
481
|
+
this.hideItem(item);
|
|
406
482
|
}
|
|
407
|
-
|
|
408
|
-
|
|
483
|
+
});
|
|
484
|
+
setTimeout(() => {
|
|
485
|
+
this.updateCounter();
|
|
486
|
+
}, this.options.animationDuration);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Check if item matches filter groups
|
|
491
|
+
* @private
|
|
492
|
+
* @param {Set} itemCategories - Item's categories
|
|
493
|
+
* @returns {boolean} Whether item matches the group filters
|
|
494
|
+
*/
|
|
495
|
+
matchesFilterGroups(itemCategories) {
|
|
496
|
+
const groupMatches = [...this.filterGroups.values()].map(group => {
|
|
497
|
+
const groupFilters = [...group.filters];
|
|
498
|
+
if (groupFilters.length === 0) return true;
|
|
499
|
+
return group.operator === "OR" ? groupFilters.some(filter => itemCategories.has(filter)) : groupFilters.every(filter => itemCategories.has(filter));
|
|
500
|
+
});
|
|
501
|
+
return this.groupMode === "OR" ? groupMatches.some(matches => matches) : groupMatches.every(matches => matches);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Update URL with current filter state
|
|
506
|
+
* @private
|
|
507
|
+
*/
|
|
508
|
+
updateURL() {
|
|
509
|
+
const params = new URLSearchParams(window.location.search);
|
|
510
|
+
|
|
511
|
+
// Add groups to URL if they exist
|
|
512
|
+
if (this.filterGroups.size > 0) {
|
|
513
|
+
for (const [groupId, group] of this.filterGroups.entries()) {
|
|
514
|
+
params.set(`group_${groupId}`, [...group.filters].join(","));
|
|
515
|
+
params.set(`groupOp_${groupId}`, group.operator.toLowerCase());
|
|
409
516
|
}
|
|
517
|
+
params.set("groupMode", this.groupMode.toLowerCase());
|
|
518
|
+
}
|
|
410
519
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
520
|
+
// Séparer les filtres par type
|
|
521
|
+
const filtersByType = {};
|
|
522
|
+
for (const filter of this.currentFilters) {
|
|
523
|
+
if (filter !== "*") {
|
|
524
|
+
const [type, value] = filter.split(":");
|
|
525
|
+
if (!filtersByType[type]) {
|
|
526
|
+
filtersByType[type] = new Set();
|
|
418
527
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
// Load search
|
|
422
|
-
const search = params.get("search") || "";
|
|
423
|
-
if (this.searchInput) {
|
|
424
|
-
this.searchInput.value = search;
|
|
528
|
+
filtersByType[type].add(value);
|
|
425
529
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Ajouter chaque type de filtre à l'URL
|
|
533
|
+
Object.entries(filtersByType).forEach(_ref => {
|
|
534
|
+
let [type, values] = _ref;
|
|
535
|
+
params.set(type, Array.from(values).join(","));
|
|
536
|
+
});
|
|
537
|
+
if (this.currentSearch) {
|
|
538
|
+
params.set("search", this.currentSearch);
|
|
539
|
+
}
|
|
540
|
+
const newURL = `${window.location.pathname}${params.toString() ? "?" + params.toString() : ""}`;
|
|
541
|
+
window.history.pushState({}, "", newURL);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Load filter state from URL
|
|
546
|
+
* @private
|
|
547
|
+
*/
|
|
548
|
+
loadFromURL() {
|
|
549
|
+
const params = new URLSearchParams(window.location.search);
|
|
550
|
+
|
|
551
|
+
// Load groups if they exist
|
|
552
|
+
this.filterGroups.clear();
|
|
553
|
+
for (const [key, value] of params.entries()) {
|
|
554
|
+
if (key.startsWith("group_")) {
|
|
555
|
+
const groupId = key.replace("group_", "");
|
|
556
|
+
const operator = params.get(`groupOp_${groupId}`)?.toUpperCase() || "OR";
|
|
557
|
+
const filters = value.split(",");
|
|
558
|
+
this.addFilterGroup(groupId, filters, operator);
|
|
429
559
|
}
|
|
430
560
|
}
|
|
431
561
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
562
|
+
// Set group mode if present
|
|
563
|
+
const groupMode = params.get("groupMode")?.toUpperCase();
|
|
564
|
+
if (groupMode && ["AND", "OR"].includes(groupMode)) {
|
|
565
|
+
this.groupMode = groupMode;
|
|
566
|
+
}
|
|
567
|
+
this.currentFilters.clear();
|
|
568
|
+
|
|
569
|
+
// Si aucun filtre n'est présent, utiliser '*'
|
|
570
|
+
let hasFilters = false;
|
|
571
|
+
|
|
572
|
+
// Parcourir tous les paramètres
|
|
573
|
+
for (const [type, values] of params.entries()) {
|
|
574
|
+
if (type !== "search") {
|
|
575
|
+
hasFilters = true;
|
|
576
|
+
values.split(",").forEach(value => {
|
|
577
|
+
this.currentFilters.add(`${type}:${value}`);
|
|
578
|
+
});
|
|
442
579
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
};
|
|
580
|
+
}
|
|
581
|
+
if (!hasFilters) {
|
|
582
|
+
this.currentFilters.add("*");
|
|
447
583
|
}
|
|
448
584
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
this.options.filterMode = mode.toUpperCase();
|
|
457
|
-
this.filter();
|
|
585
|
+
// Update active buttons
|
|
586
|
+
this.filterButtons.forEach(button => {
|
|
587
|
+
const filterValue = button.dataset.filter;
|
|
588
|
+
if (this.currentFilters.has(filterValue) || filterValue === "*" && this.currentFilters.has("*")) {
|
|
589
|
+
button.classList.add(this.options.activeClass);
|
|
590
|
+
} else {
|
|
591
|
+
button.classList.remove(this.options.activeClass);
|
|
458
592
|
}
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// Load search
|
|
596
|
+
const search = params.get("search") || "";
|
|
597
|
+
if (this.searchInput) {
|
|
598
|
+
this.searchInput.value = search;
|
|
459
599
|
}
|
|
600
|
+
this.filter();
|
|
601
|
+
if (search) {
|
|
602
|
+
this.search(search);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Update results counter
|
|
608
|
+
* @private
|
|
609
|
+
* @returns {{total: number, visible: number}}
|
|
610
|
+
*/
|
|
611
|
+
updateCounter() {
|
|
612
|
+
const total = this.items.length;
|
|
613
|
+
const visible = this.visibleItems.size;
|
|
614
|
+
if (this.counter) {
|
|
615
|
+
this.counter.textContent = `Showing ${visible} of ${total}`;
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
total,
|
|
619
|
+
visible
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Set animation options
|
|
625
|
+
* @public
|
|
626
|
+
* @param {Object} options - Animation options
|
|
627
|
+
*/
|
|
628
|
+
setAnimationOptions() {
|
|
629
|
+
let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
630
|
+
this.options.animationDuration = options.duration || this.options.animationDuration;
|
|
631
|
+
this.options.animationType = options.type || "ease-out";
|
|
632
|
+
this.addStyles(); // Refresh styles with new options
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Event handling system
|
|
637
|
+
*/
|
|
638
|
+
addEventSystem() {
|
|
639
|
+
this.events = {};
|
|
640
|
+
this.on = (eventName, callback) => {
|
|
641
|
+
if (!this.events[eventName]) {
|
|
642
|
+
this.events[eventName] = [];
|
|
643
|
+
}
|
|
644
|
+
this.events[eventName].push(callback);
|
|
645
|
+
};
|
|
646
|
+
this.emit = (eventName, data) => {
|
|
647
|
+
if (this.events[eventName]) {
|
|
648
|
+
this.events[eventName].forEach(callback => callback(data));
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Add pagination
|
|
655
|
+
* @public
|
|
656
|
+
* @param {number} itemsPerPage - Number of items per page
|
|
657
|
+
*/
|
|
658
|
+
setPagination(itemsPerPage) {
|
|
659
|
+
this.pagination = {
|
|
660
|
+
currentPage: 1,
|
|
661
|
+
itemsPerPage: itemsPerPage,
|
|
662
|
+
totalPages: Math.ceil(this.visibleItems.size / itemsPerPage)
|
|
663
|
+
};
|
|
664
|
+
this.updatePagination();
|
|
665
|
+
}
|
|
666
|
+
updatePagination() {
|
|
667
|
+
const start = (this.pagination.currentPage - 1) * this.pagination.itemsPerPage;
|
|
668
|
+
const end = start + this.pagination.itemsPerPage;
|
|
669
|
+
[...this.visibleItems].forEach((item, index) => {
|
|
670
|
+
if (index >= start && index < end) {
|
|
671
|
+
this.showItem(item);
|
|
672
|
+
} else {
|
|
673
|
+
this.hideItem(item);
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Enable analytics tracking
|
|
680
|
+
* @public
|
|
681
|
+
* @param {Function} callback - Analytics callback function
|
|
682
|
+
*/
|
|
683
|
+
enableAnalytics(callback) {
|
|
684
|
+
this.analyticsCallback = callback;
|
|
685
|
+
this.on("filter", data => {
|
|
686
|
+
this.analyticsCallback({
|
|
687
|
+
type: "filter",
|
|
688
|
+
filters: [...this.currentFilters],
|
|
689
|
+
visibleItems: this.visibleItems.size,
|
|
690
|
+
timestamp: new Date().toISOString()
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
}
|
|
460
694
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
695
|
+
/**
|
|
696
|
+
* Sort with custom comparator
|
|
697
|
+
* @public
|
|
698
|
+
* @param {string} key - Data attribute key
|
|
699
|
+
* @param {Function} comparator - Custom comparison function
|
|
700
|
+
*/
|
|
701
|
+
sortWithComparator(key, comparator) {
|
|
702
|
+
const items = [...this.items];
|
|
703
|
+
items.sort((a, b) => {
|
|
704
|
+
const valueA = a.dataset[key];
|
|
705
|
+
const valueB = b.dataset[key];
|
|
706
|
+
return comparator(valueA, valueB);
|
|
707
|
+
});
|
|
708
|
+
items.forEach(item => this.container.appendChild(item));
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Add responsive behavior
|
|
713
|
+
* @public
|
|
714
|
+
* @param {Object} breakpoints - Breakpoint configurations
|
|
715
|
+
*/
|
|
716
|
+
setResponsiveOptions(breakpoints) {
|
|
717
|
+
window.addEventListener("resize", debounce(() => {
|
|
718
|
+
const width = window.innerWidth;
|
|
719
|
+
for (const [breakpoint, options] of Object.entries(breakpoints)) {
|
|
720
|
+
if (width <= parseInt(breakpoint)) {
|
|
721
|
+
Object.assign(this.options, options);
|
|
722
|
+
this.filter();
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}, 250));
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Enable keyboard navigation
|
|
731
|
+
* @public
|
|
732
|
+
*/
|
|
733
|
+
enableKeyboardNavigation() {
|
|
734
|
+
document.addEventListener("keydown", e => {
|
|
735
|
+
if (e.key === "Enter" && document.activeElement.classList.contains(this.options.filterButtonSelector.slice(1))) {
|
|
736
|
+
document.activeElement.click();
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Export current filter state
|
|
743
|
+
* @public
|
|
744
|
+
* @returns {Object} Filter state
|
|
745
|
+
*/
|
|
746
|
+
exportState() {
|
|
747
|
+
return {
|
|
748
|
+
filters: [...this.currentFilters],
|
|
749
|
+
search: this.currentSearch,
|
|
750
|
+
mode: this.options.filterMode,
|
|
751
|
+
timestamp: new Date().toISOString()
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Import filter state
|
|
757
|
+
* @public
|
|
758
|
+
* @param {Object} state - Filter state to import
|
|
759
|
+
*/
|
|
760
|
+
importState(state) {
|
|
761
|
+
if (state.filters) {
|
|
762
|
+
this.currentFilters = new Set(state.filters);
|
|
763
|
+
this.currentSearch = state.search || "";
|
|
764
|
+
this.options.filterMode = state.mode || "OR";
|
|
469
765
|
this.filter();
|
|
470
766
|
this.updateURL();
|
|
471
767
|
}
|
|
768
|
+
}
|
|
472
769
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
770
|
+
/**
|
|
771
|
+
* Save current filter state as preset
|
|
772
|
+
* @public
|
|
773
|
+
* @param {string} presetName - Name of the preset
|
|
774
|
+
*/
|
|
775
|
+
savePreset(presetName) {
|
|
776
|
+
const preset = {
|
|
777
|
+
filters: [...this.currentFilters],
|
|
778
|
+
search: this.currentSearch,
|
|
779
|
+
mode: this.options.filterMode
|
|
780
|
+
};
|
|
781
|
+
localStorage.setItem(`afs_preset_${presetName}`, JSON.stringify(preset));
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Load filter preset
|
|
786
|
+
* @public
|
|
787
|
+
* @param {string} presetName - Name of the preset to load
|
|
788
|
+
*/
|
|
789
|
+
loadPreset(presetName) {
|
|
790
|
+
const preset = JSON.parse(localStorage.getItem(`afs_preset_${presetName}`));
|
|
791
|
+
if (preset) {
|
|
792
|
+
this.currentFilters = new Set(preset.filters);
|
|
793
|
+
this.currentSearch = preset.search;
|
|
794
|
+
this.options.filterMode = preset.mode;
|
|
483
795
|
this.filter();
|
|
484
796
|
this.updateURL();
|
|
485
797
|
}
|
|
798
|
+
}
|
|
486
799
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
800
|
+
/**
|
|
801
|
+
* Set filter logic mode (alias for setFilterMode)
|
|
802
|
+
* @public
|
|
803
|
+
* @param {string} logic - New filter logic ('AND' or 'OR')
|
|
804
|
+
*/
|
|
805
|
+
setLogic(logic) {
|
|
806
|
+
if (typeof logic === "boolean") {
|
|
807
|
+
// Handle boolean input (true = AND, false = OR)
|
|
808
|
+
this.options.filterMode = logic ? "AND" : "OR";
|
|
809
|
+
this.filter();
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
const mode = logic.toUpperCase();
|
|
813
|
+
if (["OR", "AND"].includes(mode)) {
|
|
814
|
+
this.options.filterMode = mode;
|
|
815
|
+
this.filter();
|
|
494
816
|
}
|
|
495
817
|
}
|
|
496
818
|
|
|
497
|
-
|
|
819
|
+
/**
|
|
820
|
+
* Change filter mode
|
|
821
|
+
* @public
|
|
822
|
+
* @param {string} mode - New filter mode ('OR' or 'AND')
|
|
823
|
+
*/
|
|
824
|
+
setFilterMode(mode) {
|
|
825
|
+
if (["OR", "AND"].includes(mode.toUpperCase())) {
|
|
826
|
+
this.options.filterMode = mode.toUpperCase();
|
|
827
|
+
this.filter();
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Add filter by type and value
|
|
833
|
+
* @public
|
|
834
|
+
* @param {string} type - Filter type
|
|
835
|
+
* @param {string} value - Filter value
|
|
836
|
+
*/
|
|
837
|
+
addFilter(type, value) {
|
|
838
|
+
this.currentFilters.add(`${type}:${value}`);
|
|
839
|
+
this.filter();
|
|
840
|
+
this.updateURL();
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Remove filter by type and value
|
|
845
|
+
* @public
|
|
846
|
+
* @param {string} type - Filter type
|
|
847
|
+
*/
|
|
848
|
+
removeFilter(type, value) {
|
|
849
|
+
this.currentFilters.delete(`${type}:${value}`);
|
|
850
|
+
if (this.currentFilters.size === 0) {
|
|
851
|
+
this.currentFilters.add("*");
|
|
852
|
+
}
|
|
853
|
+
this.filter();
|
|
854
|
+
this.updateURL();
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Get active filters by type
|
|
859
|
+
* @public
|
|
860
|
+
* @param {string} type - Filter type
|
|
861
|
+
*/
|
|
862
|
+
getActiveFiltersByType(type) {
|
|
863
|
+
return [...this.currentFilters].filter(filter => filter.startsWith(`${type}:`)).map(filter => filter.split(":")[1]);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
498
866
|
|
|
499
|
-
|
|
867
|
+
exports.AFS = AFS;
|
|
500
868
|
|
|
501
869
|
}));
|
|
502
870
|
//# sourceMappingURL=AFS.js.map
|