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.esm.js CHANGED
@@ -139,32 +139,28 @@ class Options {
139
139
  */
140
140
  static defaults = {
141
141
  // Selectors
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',
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: 'active',
149
- hiddenClass: 'hidden',
150
- // Animation
151
- animationDuration: 300,
152
- animationType: 'fade',
153
- animationEasing: 'ease-out',
148
+ activeClass: "active",
149
+ hiddenClass: "hidden",
154
150
  // Filtering
155
- filterMode: 'OR',
156
- searchKeys: ['title'],
151
+ filterMode: "OR",
152
+ searchKeys: ["title"],
157
153
  debounceTime: 300,
158
154
  // Debug
159
155
  debug: false,
160
- logLevel: 'info',
156
+ logLevel: "info",
161
157
  // Date handling
162
- dateFormat: 'YYYY-MM-DD',
158
+ dateFormat: "YYYY-MM-DD",
163
159
  counter: {
164
- template: 'Showing {visible} of {total}',
160
+ template: "Showing {visible} of {total}",
165
161
  showFiltered: true,
166
- filteredTemplate: '({filtered} filtered)',
167
- noResultsTemplate: 'No items found',
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: '2px',
175
+ radius: "0",
180
176
  // Button radius
181
- background: '#e5e7eb' // Track color
177
+ background: "#e5e7eb" // Track color
182
178
  },
183
179
  thumb: {
184
- radius: '50%',
180
+ radius: "50%",
185
181
  // Button radius
186
- size: '16px',
182
+ size: "16px",
187
183
  // Button size
188
- background: '#000' // Button color
184
+ background: "#000" // Button color
189
185
  },
190
186
  histogram: {
191
- background: '#e5e7eb',
187
+ background: "#e5e7eb",
192
188
  // Histogram background
193
189
  bar: {
194
- background: '#000' // Bar color
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: 'transparent',
203
- border: '1px solid #000',
204
- borderRadius: '4px',
205
- padding: '8px 12px',
206
- color: '#000',
198
+ background: "transparent",
199
+ border: "1px solid #000",
200
+ borderRadius: "4px",
201
+ padding: "8px 12px",
202
+ color: "#000",
207
203
  active: {
208
- background: '#000',
209
- color: '#fff'
204
+ background: "#000",
205
+ color: "#fff"
210
206
  },
211
207
  hover: {
212
- background: '#000',
213
- color: '#fff'
208
+ background: "#000",
209
+ color: "#fff"
214
210
  }
215
211
  }
216
212
  }
217
213
  },
218
214
  colors: {
219
- primary: '#000',
220
- background: '#e5e7eb',
221
- text: '#000'
215
+ primary: "#000",
216
+ background: "#e5e7eb",
217
+ text: "#000"
222
218
  }
223
219
  },
224
220
  // Slider
225
221
  slider: {
226
- containerClass: 'afs-range-slider',
227
- trackClass: 'afs-range-track',
228
- thumbClass: 'afs-range-thumb',
229
- valueClass: 'afs-range-value',
230
- selectedClass: 'afs-range-selected'
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: '.afs-pagination-container',
237
- pageButtonClass: 'afs-page-button',
238
- activePageClass: 'afs-page-active',
239
- containerClass: 'afs-pagination',
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: 'smooth' // or 'auto' for instant scroll
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] === 'object' && !Array.isArray(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] !== 'object') {
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 = ['containerSelector', 'itemSelector'];
306
+ const requiredSelectors = ["containerSelector", "itemSelector"];
303
307
  for (const selector of requiredSelectors) {
304
- if (typeof this.options[selector] !== 'string') {
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.animationDuration !== 'number' || this.options.animationDuration < 0) {
311
- throw new Error('animationDuration must be a positive number');
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 (!['OR', 'AND'].includes(this.options.filterMode.toUpperCase())) {
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('searchKeys must be a non-empty array');
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 !== 'string') {
327
- throw new Error('counter.template must be a string');
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 !== 'boolean') {
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 !== 'function') {
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('.').reduce((obj, key) => obj?.[key], this.options);
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("animationDuration") || '300ms';
570
- const animationEasing = this.options.get("animationEasing") || 'ease-out';
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.thumb.background || colors.background};
634
- border-radius: ${sliderStyles.ui.track.radius || "2px"};
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('animationDuration') || '300ms';
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('animationDuration') || '300ms';
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('animationType', animationType);
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
- // Force all items to be visible first
1346
- const visibleItems = new Set(this.afs.items);
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
- item.classList.remove(this.afs.options.get('hiddenClass'));
1352
- requestAnimationFrame(() => {
1353
- this.animation.applyShowAnimation(item, this.afs.options.get("animationType"));
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 counter after reset
1358
- this.afs.updateCounter();
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
- // Update URL after reset
1361
- this.afs.urlManager.updateURL();
1422
+ // Wait for all animations to complete
1423
+ Promise.all(animationPromises).then(() => {
1424
+ // Update counter
1425
+ this.afs.updateCounter();
1362
1426
 
1363
- // Emit event
1364
- this.afs.emit("filtersReset");
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 pass: determine which items should be visible
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 with determined visible items
1515
+ // Update state before animations
1450
1516
  this.afs.state.setState("items.visible", visibleItems);
1451
1517
 
1452
- // Second pass: apply animations based on visibility changes
1518
+ // Track animation promises
1519
+ const animationPromises = [];
1520
+
1521
+ // Apply animations
1453
1522
  this.afs.items.forEach(item => {
1454
- if (visibleItems.has(item)) {
1455
- // Remove hidden class first
1456
- item.classList.remove(this.afs.options.get('hiddenClass'));
1457
- // Then apply show animation in next frame
1458
- requestAnimationFrame(() => {
1459
- this.animation.applyShowAnimation(item, this.afs.options.get("animationType"));
1460
- });
1461
- } else {
1462
- // Apply hide animation
1463
- requestAnimationFrame(() => {
1464
- this.animation.applyHideAnimation(item, this.afs.options.get("animationType"));
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
- // Emit events and update UI
1470
- this.emitFilterEvents(previouslyVisible, visibleItems);
1471
-
1472
- // Schedule UI updates
1473
- requestAnimationFrame(() => {
1474
- // Force reflow to ensure animations play
1475
- this.afs.container.offsetHeight;
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 counter - this needs to happen after filter changes
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.8';
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();