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/dist/AFS.js CHANGED
@@ -145,32 +145,28 @@
145
145
  */
146
146
  static defaults = {
147
147
  // Selectors
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',
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: 'active',
155
- hiddenClass: 'hidden',
156
- // Animation
157
- animationDuration: 300,
158
- animationType: 'fade',
159
- animationEasing: 'ease-out',
154
+ activeClass: "active",
155
+ hiddenClass: "hidden",
160
156
  // Filtering
161
- filterMode: 'OR',
162
- searchKeys: ['title'],
157
+ filterMode: "OR",
158
+ searchKeys: ["title"],
163
159
  debounceTime: 300,
164
160
  // Debug
165
161
  debug: false,
166
- logLevel: 'info',
162
+ logLevel: "info",
167
163
  // Date handling
168
- dateFormat: 'YYYY-MM-DD',
164
+ dateFormat: "YYYY-MM-DD",
169
165
  counter: {
170
- template: 'Showing {visible} of {total}',
166
+ template: "Showing {visible} of {total}",
171
167
  showFiltered: true,
172
- filteredTemplate: '({filtered} filtered)',
173
- noResultsTemplate: 'No items found',
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: '2px',
181
+ radius: "0",
186
182
  // Button radius
187
- background: '#e5e7eb' // Track color
183
+ background: "#e5e7eb" // Track color
188
184
  },
189
185
  thumb: {
190
- radius: '50%',
186
+ radius: "50%",
191
187
  // Button radius
192
- size: '16px',
188
+ size: "16px",
193
189
  // Button size
194
- background: '#000' // Button color
190
+ background: "#000" // Button color
195
191
  },
196
192
  histogram: {
197
- background: '#e5e7eb',
193
+ background: "#e5e7eb",
198
194
  // Histogram background
199
195
  bar: {
200
- background: '#000' // Bar color
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: 'transparent',
209
- border: '1px solid #000',
210
- borderRadius: '4px',
211
- padding: '8px 12px',
212
- color: '#000',
204
+ background: "transparent",
205
+ border: "1px solid #000",
206
+ borderRadius: "4px",
207
+ padding: "8px 12px",
208
+ color: "#000",
213
209
  active: {
214
- background: '#000',
215
- color: '#fff'
210
+ background: "#000",
211
+ color: "#fff"
216
212
  },
217
213
  hover: {
218
- background: '#000',
219
- color: '#fff'
214
+ background: "#000",
215
+ color: "#fff"
220
216
  }
221
217
  }
222
218
  }
223
219
  },
224
220
  colors: {
225
- primary: '#000',
226
- background: '#e5e7eb',
227
- text: '#000'
221
+ primary: "#000",
222
+ background: "#e5e7eb",
223
+ text: "#000"
228
224
  }
229
225
  },
230
226
  // Slider
231
227
  slider: {
232
- containerClass: 'afs-range-slider',
233
- trackClass: 'afs-range-track',
234
- thumbClass: 'afs-range-thumb',
235
- valueClass: 'afs-range-value',
236
- selectedClass: 'afs-range-selected'
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: '.afs-pagination-container',
243
- pageButtonClass: 'afs-page-button',
244
- activePageClass: 'afs-page-active',
245
- containerClass: 'afs-pagination',
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: 'smooth' // or 'auto' for instant scroll
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] === 'object' && !Array.isArray(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] !== 'object') {
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 = ['containerSelector', 'itemSelector'];
312
+ const requiredSelectors = ["containerSelector", "itemSelector"];
309
313
  for (const selector of requiredSelectors) {
310
- if (typeof this.options[selector] !== 'string') {
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.animationDuration !== 'number' || this.options.animationDuration < 0) {
317
- throw new Error('animationDuration must be a positive number');
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 (!['OR', 'AND'].includes(this.options.filterMode.toUpperCase())) {
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('searchKeys must be a non-empty array');
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 !== 'string') {
333
- throw new Error('counter.template must be a string');
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 !== 'boolean') {
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 !== 'function') {
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('.').reduce((obj, key) => obj?.[key], this.options);
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("animationDuration") || '300ms';
576
- const animationEasing = this.options.get("animationEasing") || 'ease-out';
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.thumb.background || colors.background};
640
- border-radius: ${sliderStyles.ui.track.radius || "2px"};
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('animationDuration') || '300ms';
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('animationDuration') || '300ms';
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('animationType', animationType);
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
- // Force all items to be visible first
1352
- const visibleItems = new Set(this.afs.items);
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
- item.classList.remove(this.afs.options.get('hiddenClass'));
1358
- requestAnimationFrame(() => {
1359
- this.animation.applyShowAnimation(item, this.afs.options.get("animationType"));
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 counter after reset
1364
- this.afs.updateCounter();
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
- // Update URL after reset
1367
- this.afs.urlManager.updateURL();
1428
+ // Wait for all animations to complete
1429
+ Promise.all(animationPromises).then(() => {
1430
+ // Update counter
1431
+ this.afs.updateCounter();
1368
1432
 
1369
- // Emit event
1370
- this.afs.emit("filtersReset");
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 pass: determine which items should be visible
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 with determined visible items
1521
+ // Update state before animations
1456
1522
  this.afs.state.setState("items.visible", visibleItems);
1457
1523
 
1458
- // Second pass: apply animations based on visibility changes
1524
+ // Track animation promises
1525
+ const animationPromises = [];
1526
+
1527
+ // Apply animations
1459
1528
  this.afs.items.forEach(item => {
1460
- if (visibleItems.has(item)) {
1461
- // Remove hidden class first
1462
- item.classList.remove(this.afs.options.get('hiddenClass'));
1463
- // Then apply show animation in next frame
1464
- requestAnimationFrame(() => {
1465
- this.animation.applyShowAnimation(item, this.afs.options.get("animationType"));
1466
- });
1467
- } else {
1468
- // Apply hide animation
1469
- requestAnimationFrame(() => {
1470
- this.animation.applyHideAnimation(item, this.afs.options.get("animationType"));
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
- // Emit events and update UI
1476
- this.emitFilterEvents(previouslyVisible, visibleItems);
1477
-
1478
- // Schedule UI updates
1479
- requestAnimationFrame(() => {
1480
- // Force reflow to ensure animations play
1481
- this.afs.container.offsetHeight;
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 counter - this needs to happen after filter changes
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.8';
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();