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