advanced-filter-system 1.0.8 → 1.0.9
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 +17 -55
- package/dist/AFS.esm.js +488 -111
- package/dist/AFS.esm.js.map +1 -1
- package/dist/AFS.js +488 -111
- package/dist/AFS.js.map +1 -1
- package/dist/AFS.min.js +1 -1
- package/dist/AFS.min.js.map +1 -1
- package/package.json +1 -1
package/dist/AFS.esm.js
CHANGED
|
@@ -139,32 +139,28 @@ class Options {
|
|
|
139
139
|
*/
|
|
140
140
|
static defaults = {
|
|
141
141
|
// Selectors
|
|
142
|
-
containerSelector:
|
|
143
|
-
itemSelector:
|
|
144
|
-
filterButtonSelector:
|
|
145
|
-
searchInputSelector:
|
|
146
|
-
counterSelector:
|
|
142
|
+
containerSelector: ".afs-filter-container",
|
|
143
|
+
itemSelector: ".afs-filter-item",
|
|
144
|
+
filterButtonSelector: ".afs-btn-filter",
|
|
145
|
+
searchInputSelector: ".afs-filter-search",
|
|
146
|
+
counterSelector: ".afs-filter-counter",
|
|
147
147
|
// Classes
|
|
148
|
-
activeClass:
|
|
149
|
-
hiddenClass:
|
|
150
|
-
// Animation
|
|
151
|
-
animationDuration: 300,
|
|
152
|
-
animationType: 'fade',
|
|
153
|
-
animationEasing: 'ease-out',
|
|
148
|
+
activeClass: "active",
|
|
149
|
+
hiddenClass: "hidden",
|
|
154
150
|
// Filtering
|
|
155
|
-
filterMode:
|
|
156
|
-
searchKeys: [
|
|
151
|
+
filterMode: "OR",
|
|
152
|
+
searchKeys: ["title"],
|
|
157
153
|
debounceTime: 300,
|
|
158
154
|
// Debug
|
|
159
155
|
debug: false,
|
|
160
|
-
logLevel:
|
|
156
|
+
logLevel: "info",
|
|
161
157
|
// Date handling
|
|
162
|
-
dateFormat:
|
|
158
|
+
dateFormat: "YYYY-MM-DD",
|
|
163
159
|
counter: {
|
|
164
|
-
template:
|
|
160
|
+
template: "Showing {visible} of {total}",
|
|
165
161
|
showFiltered: true,
|
|
166
|
-
filteredTemplate:
|
|
167
|
-
noResultsTemplate:
|
|
162
|
+
filteredTemplate: "({filtered} filtered)",
|
|
163
|
+
noResultsTemplate: "No items found",
|
|
168
164
|
formatter: num => num.toLocaleString()
|
|
169
165
|
},
|
|
170
166
|
// Styles
|
|
@@ -176,22 +172,22 @@ class Options {
|
|
|
176
172
|
bins: 10,
|
|
177
173
|
// Number of bins for histogram
|
|
178
174
|
track: {
|
|
179
|
-
radius:
|
|
175
|
+
radius: "0",
|
|
180
176
|
// Button radius
|
|
181
|
-
background:
|
|
177
|
+
background: "#e5e7eb" // Track color
|
|
182
178
|
},
|
|
183
179
|
thumb: {
|
|
184
|
-
radius:
|
|
180
|
+
radius: "50%",
|
|
185
181
|
// Button radius
|
|
186
|
-
size:
|
|
182
|
+
size: "16px",
|
|
187
183
|
// Button size
|
|
188
|
-
background:
|
|
184
|
+
background: "#000" // Button color
|
|
189
185
|
},
|
|
190
186
|
histogram: {
|
|
191
|
-
background:
|
|
187
|
+
background: "#e5e7eb",
|
|
192
188
|
// Histogram background
|
|
193
189
|
bar: {
|
|
194
|
-
background:
|
|
190
|
+
background: "#000" // Bar color
|
|
195
191
|
}
|
|
196
192
|
}
|
|
197
193
|
}
|
|
@@ -199,47 +195,55 @@ class Options {
|
|
|
199
195
|
pagination: {
|
|
200
196
|
ui: {
|
|
201
197
|
button: {
|
|
202
|
-
background:
|
|
203
|
-
border:
|
|
204
|
-
borderRadius:
|
|
205
|
-
padding:
|
|
206
|
-
color:
|
|
198
|
+
background: "transparent",
|
|
199
|
+
border: "1px solid #000",
|
|
200
|
+
borderRadius: "4px",
|
|
201
|
+
padding: "8px 12px",
|
|
202
|
+
color: "#000",
|
|
207
203
|
active: {
|
|
208
|
-
background:
|
|
209
|
-
color:
|
|
204
|
+
background: "#000",
|
|
205
|
+
color: "#fff"
|
|
210
206
|
},
|
|
211
207
|
hover: {
|
|
212
|
-
background:
|
|
213
|
-
color:
|
|
208
|
+
background: "#000",
|
|
209
|
+
color: "#fff"
|
|
214
210
|
}
|
|
215
211
|
}
|
|
216
212
|
}
|
|
217
213
|
},
|
|
218
214
|
colors: {
|
|
219
|
-
primary:
|
|
220
|
-
background:
|
|
221
|
-
text:
|
|
215
|
+
primary: "#000",
|
|
216
|
+
background: "#e5e7eb",
|
|
217
|
+
text: "#000"
|
|
222
218
|
}
|
|
223
219
|
},
|
|
224
220
|
// Slider
|
|
225
221
|
slider: {
|
|
226
|
-
containerClass:
|
|
227
|
-
trackClass:
|
|
228
|
-
thumbClass:
|
|
229
|
-
valueClass:
|
|
230
|
-
selectedClass:
|
|
222
|
+
containerClass: "afs-range-slider",
|
|
223
|
+
trackClass: "afs-range-track",
|
|
224
|
+
thumbClass: "afs-range-thumb",
|
|
225
|
+
valueClass: "afs-range-value",
|
|
226
|
+
selectedClass: "afs-range-selected"
|
|
231
227
|
},
|
|
232
228
|
// Pagination
|
|
233
229
|
pagination: {
|
|
234
230
|
enabled: false,
|
|
235
231
|
itemsPerPage: 10,
|
|
236
|
-
container:
|
|
237
|
-
pageButtonClass:
|
|
238
|
-
activePageClass:
|
|
239
|
-
containerClass:
|
|
232
|
+
container: ".afs-pagination-container",
|
|
233
|
+
pageButtonClass: "afs-page-button",
|
|
234
|
+
activePageClass: "afs-page-active",
|
|
235
|
+
containerClass: "afs-pagination",
|
|
240
236
|
scrollToTop: false,
|
|
241
237
|
scrollOffset: 50,
|
|
242
|
-
scrollBehavior:
|
|
238
|
+
scrollBehavior: "smooth" // or 'auto' for instant scroll
|
|
239
|
+
},
|
|
240
|
+
// Animation
|
|
241
|
+
animation: {
|
|
242
|
+
type: "fade",
|
|
243
|
+
duration: 300,
|
|
244
|
+
easing: "ease-out",
|
|
245
|
+
inClass: "afs-animation-enter",
|
|
246
|
+
outClass: "afs-animation-leave"
|
|
243
247
|
}
|
|
244
248
|
};
|
|
245
249
|
constructor() {
|
|
@@ -277,9 +281,9 @@ class Options {
|
|
|
277
281
|
...target
|
|
278
282
|
};
|
|
279
283
|
for (const key in source) {
|
|
280
|
-
if (source[key] !== null && typeof source[key] ===
|
|
284
|
+
if (source[key] !== null && typeof source[key] === "object" && !Array.isArray(source[key])) {
|
|
281
285
|
// If the key doesn't exist in target or isn't an object, create/override it
|
|
282
|
-
if (!target[key] || typeof target[key] !==
|
|
286
|
+
if (!target[key] || typeof target[key] !== "object") {
|
|
283
287
|
result[key] = {};
|
|
284
288
|
}
|
|
285
289
|
// Recursively merge nested objects
|
|
@@ -299,37 +303,37 @@ class Options {
|
|
|
299
303
|
*/
|
|
300
304
|
validate() {
|
|
301
305
|
// Required selectors
|
|
302
|
-
const requiredSelectors = [
|
|
306
|
+
const requiredSelectors = ["containerSelector", "itemSelector"];
|
|
303
307
|
for (const selector of requiredSelectors) {
|
|
304
|
-
if (typeof this.options[selector] !==
|
|
308
|
+
if (typeof this.options[selector] !== "string") {
|
|
305
309
|
throw new Error(`${selector} must be a string`);
|
|
306
310
|
}
|
|
307
311
|
}
|
|
308
312
|
|
|
309
313
|
// Animation duration
|
|
310
|
-
if (typeof this.options.
|
|
311
|
-
throw new Error(
|
|
314
|
+
if (typeof this.options.animation.duration !== "number" || this.options.animation.duration < 0) {
|
|
315
|
+
throw new Error("animationDuration must be a positive number");
|
|
312
316
|
}
|
|
313
317
|
|
|
314
318
|
// Filter mode
|
|
315
|
-
if (![
|
|
319
|
+
if (!["OR", "AND"].includes(this.options.filterMode.toUpperCase())) {
|
|
316
320
|
throw new Error('filterMode must be either "OR" or "AND"');
|
|
317
321
|
}
|
|
318
322
|
|
|
319
323
|
// Search keys
|
|
320
324
|
if (!Array.isArray(this.options.searchKeys) || this.options.searchKeys.length === 0) {
|
|
321
|
-
throw new Error(
|
|
325
|
+
throw new Error("searchKeys must be a non-empty array");
|
|
322
326
|
}
|
|
323
327
|
|
|
324
328
|
// Counter validation
|
|
325
329
|
if (this.options.counter) {
|
|
326
|
-
if (typeof this.options.counter.template !==
|
|
327
|
-
throw new Error(
|
|
330
|
+
if (typeof this.options.counter.template !== "string") {
|
|
331
|
+
throw new Error("counter.template must be a string");
|
|
328
332
|
}
|
|
329
|
-
if (typeof this.options.counter.showFiltered !==
|
|
333
|
+
if (typeof this.options.counter.showFiltered !== "boolean") {
|
|
330
334
|
this.options.counter.showFiltered = true; // Set default
|
|
331
335
|
}
|
|
332
|
-
if (typeof this.options.counter.formatter !==
|
|
336
|
+
if (typeof this.options.counter.formatter !== "function") {
|
|
333
337
|
this.options.counter.formatter = num => num.toLocaleString(); // Set default
|
|
334
338
|
}
|
|
335
339
|
} else {
|
|
@@ -345,7 +349,7 @@ class Options {
|
|
|
345
349
|
* @returns {any} Option value
|
|
346
350
|
*/
|
|
347
351
|
get(path) {
|
|
348
|
-
return path.split(
|
|
352
|
+
return path.split(".").reduce((obj, key) => obj?.[key], this.options);
|
|
349
353
|
}
|
|
350
354
|
|
|
351
355
|
/**
|
|
@@ -354,7 +358,7 @@ class Options {
|
|
|
354
358
|
* @param {any} value - New value
|
|
355
359
|
*/
|
|
356
360
|
set(path, value) {
|
|
357
|
-
const parts = path.split(
|
|
361
|
+
const parts = path.split(".");
|
|
358
362
|
const last = parts.pop();
|
|
359
363
|
const target = parts.reduce((obj, key) => {
|
|
360
364
|
if (!(key in obj)) obj[key] = {};
|
|
@@ -566,8 +570,8 @@ class StyleManager {
|
|
|
566
570
|
const itemSelector = this.options.get("itemSelector") || ".afs-filter-item";
|
|
567
571
|
const filterButtonSelector = this.options.get("filterButtonSelector") || ".afs-btn-filter";
|
|
568
572
|
const activeClass = this.options.get("activeClass") || "active";
|
|
569
|
-
const animationDuration = this.options.get("
|
|
570
|
-
const animationEasing = this.options.get("
|
|
573
|
+
const animationDuration = this.options.get("animation.duration") || '300ms';
|
|
574
|
+
const animationEasing = this.options.get("animation.easing") || 'ease-out';
|
|
571
575
|
return `
|
|
572
576
|
.${hiddenClass} {
|
|
573
577
|
display: none !important;
|
|
@@ -630,8 +634,8 @@ class StyleManager {
|
|
|
630
634
|
transform: translateY(-50%);
|
|
631
635
|
width: 100%;
|
|
632
636
|
height: 4px;
|
|
633
|
-
background: ${sliderStyles.ui.
|
|
634
|
-
border-radius: ${sliderStyles.ui.track.radius || "
|
|
637
|
+
background: ${sliderStyles.ui.track.background || colors.background};
|
|
638
|
+
border-radius: ${sliderStyles.ui.track.radius || "0"};
|
|
635
639
|
}
|
|
636
640
|
|
|
637
641
|
.${thumbClass} {
|
|
@@ -731,6 +735,49 @@ class StyleManager {
|
|
|
731
735
|
`;
|
|
732
736
|
}
|
|
733
737
|
|
|
738
|
+
/**
|
|
739
|
+
* Create date filter styles
|
|
740
|
+
* @private
|
|
741
|
+
* @returns {string} CSS styles
|
|
742
|
+
*/
|
|
743
|
+
createInputRangeStyles() {
|
|
744
|
+
const colors = this.options.get("styles").colors;
|
|
745
|
+
return `
|
|
746
|
+
.afs-input-range-container {
|
|
747
|
+
display: flex;
|
|
748
|
+
flex-wrap: wrap;
|
|
749
|
+
gap: 1rem;
|
|
750
|
+
margin: 10px 0;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
.afs-input-wrapper {
|
|
754
|
+
flex: 1;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
.afs-input-label {
|
|
758
|
+
display: block;
|
|
759
|
+
font-size: 0.875rem;
|
|
760
|
+
color: ${colors.text};
|
|
761
|
+
margin-bottom: 0.5rem;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
.afs-input {
|
|
765
|
+
width: 100%;
|
|
766
|
+
padding: 0.5rem;
|
|
767
|
+
border: 1px solid ${colors.background};
|
|
768
|
+
border-radius: 0.25rem;
|
|
769
|
+
font-size: 0.875rem;
|
|
770
|
+
color: ${colors.text};
|
|
771
|
+
transition: border-color 0.2s ease;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.afs-input:focus {
|
|
775
|
+
outline: none;
|
|
776
|
+
border-color: ${colors.primary};
|
|
777
|
+
}
|
|
778
|
+
`;
|
|
779
|
+
}
|
|
780
|
+
|
|
734
781
|
/**
|
|
735
782
|
* Apply all styles
|
|
736
783
|
* @public
|
|
@@ -752,6 +799,9 @@ class StyleManager {
|
|
|
752
799
|
|
|
753
800
|
/* Search styles */
|
|
754
801
|
${this.createSearchStyles()}
|
|
802
|
+
|
|
803
|
+
/* Input range styles */
|
|
804
|
+
${this.createInputRangeStyles()}
|
|
755
805
|
`;
|
|
756
806
|
if (this.styleElement) {
|
|
757
807
|
this.styleElement.textContent = styles;
|
|
@@ -1172,9 +1222,14 @@ class Animation {
|
|
|
1172
1222
|
}
|
|
1173
1223
|
applyShowAnimation(item) {
|
|
1174
1224
|
let animationType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'fade';
|
|
1175
|
-
const duration = this.options.get('
|
|
1225
|
+
const duration = this.options.get('animation.duration') || '300ms';
|
|
1176
1226
|
const animation = this.animations[animationType]?.in || this.animations.fade.in;
|
|
1227
|
+
|
|
1228
|
+
// Ensure display is set
|
|
1177
1229
|
item.style.display = 'block';
|
|
1230
|
+
item.style.opacity = '0'; // Start with opacity 0
|
|
1231
|
+
|
|
1232
|
+
// Apply animation in next frame
|
|
1178
1233
|
requestAnimationFrame(() => {
|
|
1179
1234
|
Object.assign(item.style, animation, {
|
|
1180
1235
|
transition: `opacity ${duration} ${animation.transitionTimingFunction}, transform ${duration} ${animation.transitionTimingFunction}`
|
|
@@ -1183,7 +1238,7 @@ class Animation {
|
|
|
1183
1238
|
}
|
|
1184
1239
|
applyHideAnimation(item) {
|
|
1185
1240
|
let animationType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'fade';
|
|
1186
|
-
const duration = this.options.get('
|
|
1241
|
+
const duration = this.options.get('animation.duration') || '300ms';
|
|
1187
1242
|
const animation = this.animations[animationType]?.out || this.animations.fade.out;
|
|
1188
1243
|
Object.assign(item.style, animation, {
|
|
1189
1244
|
transition: `opacity ${duration} ${animation.transitionTimingFunction}, transform ${duration} ${animation.transitionTimingFunction}`
|
|
@@ -1195,7 +1250,7 @@ class Animation {
|
|
|
1195
1250
|
item.addEventListener('transitionend', handleTransitionEnd);
|
|
1196
1251
|
}
|
|
1197
1252
|
setAnimation(animationType) {
|
|
1198
|
-
this.afs.options.set('
|
|
1253
|
+
this.afs.options.set('animation.type', animationType);
|
|
1199
1254
|
}
|
|
1200
1255
|
}
|
|
1201
1256
|
|
|
@@ -1342,26 +1397,39 @@ class Filter {
|
|
|
1342
1397
|
// Clear filter groups
|
|
1343
1398
|
this.filterGroups.clear();
|
|
1344
1399
|
|
|
1345
|
-
//
|
|
1346
|
-
const
|
|
1347
|
-
this.afs.state.setState("items.visible", visibleItems);
|
|
1400
|
+
// Create a promise to track animations
|
|
1401
|
+
const animationPromises = [];
|
|
1348
1402
|
|
|
1349
1403
|
// Show all items with animation
|
|
1350
1404
|
this.afs.items.forEach(item => {
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1405
|
+
const promise = new Promise(resolve => {
|
|
1406
|
+
item.classList.remove(this.afs.options.get('hiddenClass'));
|
|
1407
|
+
item.style.display = 'block'; // Ensure item is visible
|
|
1408
|
+
|
|
1409
|
+
requestAnimationFrame(() => {
|
|
1410
|
+
this.animation.applyShowAnimation(item, this.afs.options.get("animation.type"));
|
|
1411
|
+
// Resolve after animation duration
|
|
1412
|
+
setTimeout(resolve, this.afs.options.get("animation.duration") || 300);
|
|
1413
|
+
});
|
|
1354
1414
|
});
|
|
1415
|
+
animationPromises.push(promise);
|
|
1355
1416
|
});
|
|
1356
1417
|
|
|
1357
|
-
// Update
|
|
1358
|
-
this.afs.
|
|
1418
|
+
// Update state after all items are visible
|
|
1419
|
+
const visibleItems = new Set(this.afs.items);
|
|
1420
|
+
this.afs.state.setState("items.visible", visibleItems);
|
|
1359
1421
|
|
|
1360
|
-
//
|
|
1361
|
-
|
|
1422
|
+
// Wait for all animations to complete
|
|
1423
|
+
Promise.all(animationPromises).then(() => {
|
|
1424
|
+
// Update counter
|
|
1425
|
+
this.afs.updateCounter();
|
|
1362
1426
|
|
|
1363
|
-
|
|
1364
|
-
|
|
1427
|
+
// Update URL
|
|
1428
|
+
this.afs.urlManager.updateURL();
|
|
1429
|
+
|
|
1430
|
+
// Emit event
|
|
1431
|
+
this.afs.emit("filtersReset");
|
|
1432
|
+
});
|
|
1365
1433
|
}
|
|
1366
1434
|
|
|
1367
1435
|
/**
|
|
@@ -1432,60 +1500,64 @@ class Filter {
|
|
|
1432
1500
|
* @public
|
|
1433
1501
|
*/
|
|
1434
1502
|
applyFilters() {
|
|
1435
|
-
// Log active filters
|
|
1436
1503
|
const activeFilters = Array.from(this.activeFilters);
|
|
1437
1504
|
this.afs.logger.debug("Active filters:", activeFilters);
|
|
1438
|
-
this.afs.logger.debug("Applying filters");
|
|
1439
1505
|
const previouslyVisible = new Set(this.afs.state.getState().items.visible);
|
|
1440
1506
|
const visibleItems = new Set();
|
|
1441
1507
|
|
|
1442
|
-
// First
|
|
1508
|
+
// First determine visibility
|
|
1443
1509
|
this.afs.items.forEach(item => {
|
|
1444
1510
|
if (this.activeFilters.has("*") || this.itemMatchesFilters(item)) {
|
|
1445
1511
|
visibleItems.add(item);
|
|
1446
1512
|
}
|
|
1447
1513
|
});
|
|
1448
1514
|
|
|
1449
|
-
// Update state
|
|
1515
|
+
// Update state before animations
|
|
1450
1516
|
this.afs.state.setState("items.visible", visibleItems);
|
|
1451
1517
|
|
|
1452
|
-
//
|
|
1518
|
+
// Track animation promises
|
|
1519
|
+
const animationPromises = [];
|
|
1520
|
+
|
|
1521
|
+
// Apply animations
|
|
1453
1522
|
this.afs.items.forEach(item => {
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1523
|
+
const promise = new Promise(resolve => {
|
|
1524
|
+
if (visibleItems.has(item)) {
|
|
1525
|
+
// Show item
|
|
1526
|
+
item.classList.remove(this.afs.options.get('hiddenClass'));
|
|
1527
|
+
requestAnimationFrame(() => {
|
|
1528
|
+
this.animation.applyShowAnimation(item, this.afs.options.get("animation.type"));
|
|
1529
|
+
setTimeout(resolve, parseFloat(this.afs.options.get("animation.duration")) || 300);
|
|
1530
|
+
});
|
|
1531
|
+
} else {
|
|
1532
|
+
// Hide item
|
|
1533
|
+
requestAnimationFrame(() => {
|
|
1534
|
+
this.animation.applyHideAnimation(item, this.afs.options.get("animation.type"));
|
|
1535
|
+
setTimeout(resolve, parseFloat(this.afs.options.get("animation.duration")) || 300);
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
});
|
|
1539
|
+
animationPromises.push(promise);
|
|
1467
1540
|
});
|
|
1468
1541
|
|
|
1469
|
-
//
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1542
|
+
// Handle completion
|
|
1543
|
+
Promise.all(animationPromises).then(() => {
|
|
1544
|
+
// Ensure visible items remain visible
|
|
1545
|
+
visibleItems.forEach(item => {
|
|
1546
|
+
item.style.display = 'block';
|
|
1547
|
+
item.style.opacity = '1';
|
|
1548
|
+
});
|
|
1476
1549
|
|
|
1477
|
-
// Update
|
|
1550
|
+
// Update UI
|
|
1478
1551
|
this.afs.updateCounter();
|
|
1479
|
-
|
|
1480
|
-
// Update URL
|
|
1481
1552
|
this.afs.urlManager.updateURL();
|
|
1482
|
-
|
|
1483
|
-
// Emit final event
|
|
1484
1553
|
this.afs.emit("filtersApplied", {
|
|
1485
1554
|
activeFilters,
|
|
1486
1555
|
visibleItems: visibleItems.size
|
|
1487
1556
|
});
|
|
1488
1557
|
});
|
|
1558
|
+
|
|
1559
|
+
// Emit visibility change events
|
|
1560
|
+
this.emitFilterEvents(previouslyVisible, visibleItems);
|
|
1489
1561
|
}
|
|
1490
1562
|
|
|
1491
1563
|
/**
|
|
@@ -2033,6 +2105,310 @@ function debounce(func, wait) {
|
|
|
2033
2105
|
};
|
|
2034
2106
|
}
|
|
2035
2107
|
|
|
2108
|
+
/**
|
|
2109
|
+
* @fileoverview Input range filter implementation for AFS
|
|
2110
|
+
*/
|
|
2111
|
+
|
|
2112
|
+
class InputRangeFilter {
|
|
2113
|
+
constructor(afs) {
|
|
2114
|
+
this.afs = afs;
|
|
2115
|
+
this.activeRanges = new Map();
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
/**
|
|
2119
|
+
* @typedef {Object} InputRangeOptions
|
|
2120
|
+
* @property {string} key - Data attribute key
|
|
2121
|
+
* @property {HTMLElement} container - Container element
|
|
2122
|
+
* @property {number} [min] - Minimum value
|
|
2123
|
+
* @property {number} [max] - Maximum value
|
|
2124
|
+
* @property {number} [step] - Step value
|
|
2125
|
+
* @property {string} [label] - Label for the input range
|
|
2126
|
+
*/
|
|
2127
|
+
|
|
2128
|
+
/**
|
|
2129
|
+
* Add input range filter
|
|
2130
|
+
* @param {InputRangeOptions} options - Input range options
|
|
2131
|
+
*/
|
|
2132
|
+
addInputRange(_ref) {
|
|
2133
|
+
let {
|
|
2134
|
+
key,
|
|
2135
|
+
container,
|
|
2136
|
+
min,
|
|
2137
|
+
max,
|
|
2138
|
+
step = 1,
|
|
2139
|
+
label = ''
|
|
2140
|
+
} = _ref;
|
|
2141
|
+
this.afs.logger.debug(`Adding input range for ${key}`);
|
|
2142
|
+
if (!container) {
|
|
2143
|
+
this.afs.logger.error('Container element required for input range');
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
// Calculate min/max if not provided
|
|
2148
|
+
const values = this.calculateMinMax(key);
|
|
2149
|
+
min = min ?? values.min;
|
|
2150
|
+
max = max ?? values.max;
|
|
2151
|
+
|
|
2152
|
+
// Create input elements
|
|
2153
|
+
const elements = this.createInputElements(label);
|
|
2154
|
+
const state = this.initializeState(min, max, step);
|
|
2155
|
+
|
|
2156
|
+
// Add elements to container
|
|
2157
|
+
this.appendElements(container, elements);
|
|
2158
|
+
|
|
2159
|
+
// Setup event handlers
|
|
2160
|
+
this.setupEventHandlers(elements, state, key);
|
|
2161
|
+
|
|
2162
|
+
// Store state
|
|
2163
|
+
this.activeRanges.set(key, {
|
|
2164
|
+
state,
|
|
2165
|
+
elements
|
|
2166
|
+
});
|
|
2167
|
+
|
|
2168
|
+
// Initial update
|
|
2169
|
+
this.updateInputUI(key);
|
|
2170
|
+
this.afs.logger.info(`Input range added for ${key}`);
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
/**
|
|
2174
|
+
* Calculate min and max values from items
|
|
2175
|
+
* @private
|
|
2176
|
+
*/
|
|
2177
|
+
calculateMinMax(key) {
|
|
2178
|
+
try {
|
|
2179
|
+
const values = Array.from(this.afs.items).map(item => {
|
|
2180
|
+
if (!item || !item.dataset || !item.dataset[key]) {
|
|
2181
|
+
return null;
|
|
2182
|
+
}
|
|
2183
|
+
const value = parseFloat(item.dataset[key]);
|
|
2184
|
+
return isNaN(value) ? null : value;
|
|
2185
|
+
}).filter(value => value !== null);
|
|
2186
|
+
if (values.length === 0) {
|
|
2187
|
+
return {
|
|
2188
|
+
min: 0,
|
|
2189
|
+
max: 100
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
return {
|
|
2193
|
+
min: Math.min(...values),
|
|
2194
|
+
max: Math.max(...values)
|
|
2195
|
+
};
|
|
2196
|
+
} catch (error) {
|
|
2197
|
+
this.afs.logger.error('Error calculating range:', error);
|
|
2198
|
+
return {
|
|
2199
|
+
min: 0,
|
|
2200
|
+
max: 100
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
/**
|
|
2206
|
+
* Create input elements
|
|
2207
|
+
* @private
|
|
2208
|
+
*/
|
|
2209
|
+
/**
|
|
2210
|
+
* Create input elements
|
|
2211
|
+
* @private
|
|
2212
|
+
*/
|
|
2213
|
+
createInputElements(label) {
|
|
2214
|
+
const container = document.createElement('div');
|
|
2215
|
+
container.className = 'afs-input-range-container';
|
|
2216
|
+
if (label) {
|
|
2217
|
+
const labelElement = document.createElement('div');
|
|
2218
|
+
labelElement.className = 'afs-input-range-label';
|
|
2219
|
+
labelElement.textContent = label;
|
|
2220
|
+
container.appendChild(labelElement);
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
// Min input wrapper
|
|
2224
|
+
const minWrapper = document.createElement('div');
|
|
2225
|
+
minWrapper.className = 'afs-input-wrapper';
|
|
2226
|
+
const minLabel = document.createElement('label');
|
|
2227
|
+
minLabel.textContent = 'Min';
|
|
2228
|
+
minLabel.className = 'afs-input-label';
|
|
2229
|
+
const minInput = document.createElement('input');
|
|
2230
|
+
minInput.type = 'number';
|
|
2231
|
+
minInput.className = 'afs-input min';
|
|
2232
|
+
minWrapper.appendChild(minLabel);
|
|
2233
|
+
minWrapper.appendChild(minInput);
|
|
2234
|
+
|
|
2235
|
+
// Max input wrapper
|
|
2236
|
+
const maxWrapper = document.createElement('div');
|
|
2237
|
+
maxWrapper.className = 'afs-input-wrapper';
|
|
2238
|
+
const maxLabel = document.createElement('label');
|
|
2239
|
+
maxLabel.textContent = 'Max';
|
|
2240
|
+
maxLabel.className = 'afs-input-label';
|
|
2241
|
+
const maxInput = document.createElement('input');
|
|
2242
|
+
maxInput.type = 'number';
|
|
2243
|
+
maxInput.className = 'afs-input max';
|
|
2244
|
+
maxWrapper.appendChild(maxLabel);
|
|
2245
|
+
maxWrapper.appendChild(maxInput);
|
|
2246
|
+
container.appendChild(minWrapper);
|
|
2247
|
+
container.appendChild(maxWrapper);
|
|
2248
|
+
return {
|
|
2249
|
+
container,
|
|
2250
|
+
minInput,
|
|
2251
|
+
maxInput
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
/**
|
|
2256
|
+
* Initialize input range state
|
|
2257
|
+
* @private
|
|
2258
|
+
*/
|
|
2259
|
+
initializeState(min, max, step) {
|
|
2260
|
+
return {
|
|
2261
|
+
min,
|
|
2262
|
+
max,
|
|
2263
|
+
step,
|
|
2264
|
+
currentMin: min,
|
|
2265
|
+
currentMax: max
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
/**
|
|
2270
|
+
* Append elements to container
|
|
2271
|
+
* @private
|
|
2272
|
+
*/
|
|
2273
|
+
appendElements(container, elements) {
|
|
2274
|
+
container.appendChild(elements.container);
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
/**
|
|
2278
|
+
* Setup event handlers
|
|
2279
|
+
* @private
|
|
2280
|
+
*/
|
|
2281
|
+
setupEventHandlers(elements, state, key) {
|
|
2282
|
+
const {
|
|
2283
|
+
minInput,
|
|
2284
|
+
maxInput
|
|
2285
|
+
} = elements;
|
|
2286
|
+
const handleInputChange = debounce(() => {
|
|
2287
|
+
const minValue = parseFloat(minInput.value);
|
|
2288
|
+
const maxValue = parseFloat(maxInput.value);
|
|
2289
|
+
if (!isNaN(minValue) && !isNaN(maxValue)) {
|
|
2290
|
+
state.currentMin = Math.max(state.min, Math.min(maxValue, minValue));
|
|
2291
|
+
state.currentMax = Math.min(state.max, Math.max(minValue, maxValue));
|
|
2292
|
+
this.updateInputUI(key);
|
|
2293
|
+
this.applyFilter(key);
|
|
2294
|
+
}
|
|
2295
|
+
}, 300);
|
|
2296
|
+
minInput.addEventListener('input', handleInputChange);
|
|
2297
|
+
maxInput.addEventListener('input', handleInputChange);
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
/**
|
|
2301
|
+
* Update input UI
|
|
2302
|
+
* @private
|
|
2303
|
+
*/
|
|
2304
|
+
updateInputUI(key) {
|
|
2305
|
+
try {
|
|
2306
|
+
const {
|
|
2307
|
+
state,
|
|
2308
|
+
elements
|
|
2309
|
+
} = this.activeRanges.get(key);
|
|
2310
|
+
const {
|
|
2311
|
+
minInput,
|
|
2312
|
+
maxInput
|
|
2313
|
+
} = elements;
|
|
2314
|
+
|
|
2315
|
+
// Set constraints
|
|
2316
|
+
minInput.min = state.min;
|
|
2317
|
+
minInput.max = state.max;
|
|
2318
|
+
minInput.step = state.step;
|
|
2319
|
+
maxInput.min = state.min;
|
|
2320
|
+
maxInput.max = state.max;
|
|
2321
|
+
maxInput.step = state.step;
|
|
2322
|
+
|
|
2323
|
+
// Set current values
|
|
2324
|
+
minInput.value = state.currentMin;
|
|
2325
|
+
maxInput.value = state.currentMax;
|
|
2326
|
+
} catch (error) {
|
|
2327
|
+
this.afs.logger.error('Error updating input UI:', error);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
/**
|
|
2332
|
+
* Apply filter
|
|
2333
|
+
* @private
|
|
2334
|
+
*/
|
|
2335
|
+
applyFilter(key) {
|
|
2336
|
+
this.afs.logger.info(`Applying input filter for ${key}`);
|
|
2337
|
+
const {
|
|
2338
|
+
state
|
|
2339
|
+
} = this.activeRanges.get(key);
|
|
2340
|
+
this.afs.items.forEach(item => {
|
|
2341
|
+
try {
|
|
2342
|
+
if (!item || !item.dataset || !item.dataset[key]) {
|
|
2343
|
+
this.afs.hideItem(item);
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
const itemValue = parseFloat(item.dataset[key]);
|
|
2347
|
+
if (isNaN(itemValue)) {
|
|
2348
|
+
this.afs.hideItem(item);
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
if (itemValue >= state.currentMin && itemValue <= state.currentMax) {
|
|
2352
|
+
this.afs.showItem(item);
|
|
2353
|
+
} else {
|
|
2354
|
+
this.afs.hideItem(item);
|
|
2355
|
+
}
|
|
2356
|
+
} catch (error) {
|
|
2357
|
+
this.afs.logger.error('Error filtering item:', error);
|
|
2358
|
+
this.afs.hideItem(item);
|
|
2359
|
+
}
|
|
2360
|
+
});
|
|
2361
|
+
this.afs.updateCounter();
|
|
2362
|
+
this.afs.urlManager.updateURL();
|
|
2363
|
+
this.afs.emit('inputRangeFilter', {
|
|
2364
|
+
key,
|
|
2365
|
+
min: state.currentMin,
|
|
2366
|
+
max: state.currentMax
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
/**
|
|
2371
|
+
* Get current range values
|
|
2372
|
+
* @param {string} key - Range key
|
|
2373
|
+
* @returns {Object} Current range values
|
|
2374
|
+
*/
|
|
2375
|
+
getRange(key) {
|
|
2376
|
+
const range = this.activeRanges.get(key);
|
|
2377
|
+
if (!range) return null;
|
|
2378
|
+
return {
|
|
2379
|
+
min: range.state.currentMin,
|
|
2380
|
+
max: range.state.currentMax
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
/**
|
|
2385
|
+
* Set range values
|
|
2386
|
+
* @param {string} key - Range key
|
|
2387
|
+
* @param {number} min - Minimum value
|
|
2388
|
+
* @param {number} max - Maximum value
|
|
2389
|
+
*/
|
|
2390
|
+
setRange(key, min, max) {
|
|
2391
|
+
const range = this.activeRanges.get(key);
|
|
2392
|
+
if (!range) return;
|
|
2393
|
+
range.state.currentMin = min;
|
|
2394
|
+
range.state.currentMax = max;
|
|
2395
|
+
this.updateInputUI(key);
|
|
2396
|
+
this.applyFilter(key);
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
/**
|
|
2400
|
+
* Remove input range
|
|
2401
|
+
* @param {string} key - Range key
|
|
2402
|
+
*/
|
|
2403
|
+
removeInputRange(key) {
|
|
2404
|
+
const range = this.activeRanges.get(key);
|
|
2405
|
+
if (!range) return;
|
|
2406
|
+
range.elements.container.remove();
|
|
2407
|
+
this.activeRanges.delete(key);
|
|
2408
|
+
this.afs.logger.info(`Input range removed for ${key}`);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2036
2412
|
/**
|
|
2037
2413
|
* @fileoverview Search functionality for AFS
|
|
2038
2414
|
*/
|
|
@@ -4354,7 +4730,7 @@ class DateFilter {
|
|
|
4354
4730
|
*/
|
|
4355
4731
|
|
|
4356
4732
|
// Version
|
|
4357
|
-
const VERSION = '1.0.
|
|
4733
|
+
const VERSION = '1.0.9';
|
|
4358
4734
|
class AFS extends EventEmitter {
|
|
4359
4735
|
/**
|
|
4360
4736
|
* @param {Object} options - Configuration options
|
|
@@ -4417,6 +4793,7 @@ class AFS extends EventEmitter {
|
|
|
4417
4793
|
this.urlManager = new URLManager(this);
|
|
4418
4794
|
this.dateFilter = new DateFilter(this);
|
|
4419
4795
|
this.pagination = new Pagination(this);
|
|
4796
|
+
this.inputRangeFilter = new InputRangeFilter(this);
|
|
4420
4797
|
|
|
4421
4798
|
// Apply styles
|
|
4422
4799
|
this.styleManager.applyStyles();
|