advanced-filter-system 1.0.7 → 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,68 +145,111 @@
145
145
  */
146
146
  static defaults = {
147
147
  // Selectors
148
- containerSelector: '.filter-container',
149
- itemSelector: '.filter-item',
150
- filterButtonSelector: '.btn-filter',
151
- searchInputSelector: '.filter-search',
152
- counterSelector: '.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
177
173
  styles: {
178
174
  slider: {
179
- class: 'afs-range-slider',
180
- trackClass: 'afs-range-track',
181
- thumbClass: 'afs-range-thumb',
182
- valueClass: 'afs-range-value',
183
- selectedClass: 'afs-range-selected',
184
175
  // Add new UI options
185
176
  ui: {
186
177
  showHistogram: false,
187
- bins: 10 // Number of bins for histogram
178
+ bins: 10,
179
+ // Number of bins for histogram
180
+ track: {
181
+ radius: "0",
182
+ // Button radius
183
+ background: "#e5e7eb" // Track color
184
+ },
185
+ thumb: {
186
+ radius: "50%",
187
+ // Button radius
188
+ size: "16px",
189
+ // Button size
190
+ background: "#000" // Button color
191
+ },
192
+ histogram: {
193
+ background: "#e5e7eb",
194
+ // Histogram background
195
+ bar: {
196
+ background: "#000" // Bar color
197
+ }
198
+ }
199
+ }
200
+ },
201
+ pagination: {
202
+ ui: {
203
+ button: {
204
+ background: "transparent",
205
+ border: "1px solid #000",
206
+ borderRadius: "4px",
207
+ padding: "8px 12px",
208
+ color: "#000",
209
+ active: {
210
+ background: "#000",
211
+ color: "#fff"
212
+ },
213
+ hover: {
214
+ background: "#000",
215
+ color: "#fff"
216
+ }
217
+ }
188
218
  }
189
219
  },
190
220
  colors: {
191
- primary: '#000',
192
- background: '#e5e7eb',
193
- text: '#000',
194
- histogram: '#e5e7eb',
195
- // For histogram bars
196
- histogramActive: '#000' // For active histogram bars
221
+ primary: "#000",
222
+ background: "#e5e7eb",
223
+ text: "#000"
197
224
  }
198
225
  },
226
+ // Slider
227
+ slider: {
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"
233
+ },
199
234
  // Pagination
200
235
  pagination: {
201
236
  enabled: false,
202
237
  itemsPerPage: 10,
203
- container: '.afs-pagination-container',
204
- pageButtonClass: 'afs-page-button',
205
- activePageClass: 'afs-page-active',
206
- containerClass: 'afs-pagination',
238
+ container: ".afs-pagination-container",
239
+ pageButtonClass: "afs-page-button",
240
+ activePageClass: "afs-page-active",
241
+ containerClass: "afs-pagination",
207
242
  scrollToTop: false,
208
243
  scrollOffset: 50,
209
- 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"
210
253
  }
211
254
  };
212
255
  constructor() {
@@ -244,9 +287,9 @@
244
287
  ...target
245
288
  };
246
289
  for (const key in source) {
247
- 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])) {
248
291
  // If the key doesn't exist in target or isn't an object, create/override it
249
- if (!target[key] || typeof target[key] !== 'object') {
292
+ if (!target[key] || typeof target[key] !== "object") {
250
293
  result[key] = {};
251
294
  }
252
295
  // Recursively merge nested objects
@@ -266,37 +309,37 @@
266
309
  */
267
310
  validate() {
268
311
  // Required selectors
269
- const requiredSelectors = ['containerSelector', 'itemSelector'];
312
+ const requiredSelectors = ["containerSelector", "itemSelector"];
270
313
  for (const selector of requiredSelectors) {
271
- if (typeof this.options[selector] !== 'string') {
314
+ if (typeof this.options[selector] !== "string") {
272
315
  throw new Error(`${selector} must be a string`);
273
316
  }
274
317
  }
275
318
 
276
319
  // Animation duration
277
- if (typeof this.options.animationDuration !== 'number' || this.options.animationDuration < 0) {
278
- 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");
279
322
  }
280
323
 
281
324
  // Filter mode
282
- if (!['OR', 'AND'].includes(this.options.filterMode.toUpperCase())) {
325
+ if (!["OR", "AND"].includes(this.options.filterMode.toUpperCase())) {
283
326
  throw new Error('filterMode must be either "OR" or "AND"');
284
327
  }
285
328
 
286
329
  // Search keys
287
330
  if (!Array.isArray(this.options.searchKeys) || this.options.searchKeys.length === 0) {
288
- throw new Error('searchKeys must be a non-empty array');
331
+ throw new Error("searchKeys must be a non-empty array");
289
332
  }
290
333
 
291
334
  // Counter validation
292
335
  if (this.options.counter) {
293
- if (typeof this.options.counter.template !== 'string') {
294
- 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");
295
338
  }
296
- if (typeof this.options.counter.showFiltered !== 'boolean') {
339
+ if (typeof this.options.counter.showFiltered !== "boolean") {
297
340
  this.options.counter.showFiltered = true; // Set default
298
341
  }
299
- if (typeof this.options.counter.formatter !== 'function') {
342
+ if (typeof this.options.counter.formatter !== "function") {
300
343
  this.options.counter.formatter = num => num.toLocaleString(); // Set default
301
344
  }
302
345
  } else {
@@ -312,7 +355,7 @@
312
355
  * @returns {any} Option value
313
356
  */
314
357
  get(path) {
315
- return path.split('.').reduce((obj, key) => obj?.[key], this.options);
358
+ return path.split(".").reduce((obj, key) => obj?.[key], this.options);
316
359
  }
317
360
 
318
361
  /**
@@ -321,7 +364,7 @@
321
364
  * @param {any} value - New value
322
365
  */
323
366
  set(path, value) {
324
- const parts = path.split('.');
367
+ const parts = path.split(".");
325
368
  const last = parts.pop();
326
369
  const target = parts.reduce((obj, key) => {
327
370
  if (!(key in obj)) obj[key] = {};
@@ -521,24 +564,6 @@
521
564
  constructor(options) {
522
565
  this.options = options;
523
566
  this.styleElement = null;
524
- this.defaultStyles = {
525
- slider: {
526
- class: "afs-range-slider",
527
- trackClass: "afs-range-track",
528
- thumbClass: "afs-range-thumb",
529
- valueClass: "afs-range-value",
530
- selectedClass: "afs-range-selected"
531
- },
532
- colors: {
533
- primary: "#000",
534
- background: "#ddd",
535
- text: "#000"
536
- },
537
- animation: {
538
- duration: "300ms",
539
- type: "ease-out"
540
- }
541
- };
542
567
  }
543
568
 
544
569
  /**
@@ -548,11 +573,11 @@
548
573
  */
549
574
  createBaseStyles() {
550
575
  const hiddenClass = this.options.get("hiddenClass") || "hidden";
551
- const itemSelector = this.options.get("itemSelector") || ".filter-item";
552
- const filterButtonSelector = this.options.get("filterButtonSelector") || ".btn-filter";
576
+ const itemSelector = this.options.get("itemSelector") || ".afs-filter-item";
577
+ const filterButtonSelector = this.options.get("filterButtonSelector") || ".afs-btn-filter";
553
578
  const activeClass = this.options.get("activeClass") || "active";
554
- const animationDuration = this.options.get("animationDuration") || '300ms';
555
- 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';
556
581
  return `
557
582
  .${hiddenClass} {
558
583
  display: none !important;
@@ -590,12 +615,18 @@
590
615
  * @returns {string} CSS styles
591
616
  */
592
617
  createRangeStyles() {
593
- const styles = this.options.get("styles") || this.defaultStyles;
594
- const sliderStyles = styles.slider || this.defaultStyles.slider;
595
- const colors = styles.colors || this.defaultStyles.colors;
618
+ const styles = this.options.get("styles");
619
+ const sliderOptions = this.options.get("slider") || {};
620
+ const sliderStyles = styles.slider;
621
+ const colors = styles.colors;
622
+ const containerClass = sliderOptions.containerClass || "afs-range-slider";
623
+ const trackClass = sliderOptions.trackClass || "afs-range-track";
624
+ const thumbClass = sliderOptions.thumbClass || "afs-range-thumb";
625
+ const valueClass = sliderOptions.valueClass || "afs-range-value";
626
+ const selectedClass = sliderOptions.selectedClass || "afs-range-selected";
596
627
  return `
597
628
  /* Range Slider Styles */
598
- .${sliderStyles.class} {
629
+ .${containerClass} {
599
630
  position: relative;
600
631
  width: auto;
601
632
  height: 40px;
@@ -603,29 +634,29 @@
603
634
  padding: 0 8px;
604
635
  }
605
636
 
606
- .${sliderStyles.trackClass} {
637
+ .${trackClass} {
607
638
  position: absolute;
608
639
  top: 50%;
609
640
  transform: translateY(-50%);
610
641
  width: 100%;
611
642
  height: 4px;
612
- background: ${colors.background};
613
- border-radius: 2px;
643
+ background: ${sliderStyles.ui.track.background || colors.background};
644
+ border-radius: ${sliderStyles.ui.track.radius || "0"};
614
645
  }
615
646
 
616
- .${sliderStyles.thumbClass} {
647
+ .${thumbClass} {
617
648
  position: absolute;
618
649
  top: 50%;
619
- width: 16px;
620
- height: 16px;
621
- background: ${colors.primary};
622
- border-radius: 50%;
650
+ width: ${sliderStyles.ui.thumb.size || "16px"};
651
+ height: ${sliderStyles.ui.thumb.size || "16px"};
652
+ background: ${sliderStyles.ui.thumb.background || colors.primary};
653
+ border-radius: ${sliderStyles.ui.thumb.radius || "50%"};
623
654
  transform: translate(-50%, -50%);
624
655
  cursor: pointer;
625
656
  z-index: 2;
626
657
  }
627
658
 
628
- .${sliderStyles.valueClass} {
659
+ .${valueClass} {
629
660
  position: absolute;
630
661
  top: -20px;
631
662
  transform: translateX(-50%);
@@ -633,7 +664,7 @@
633
664
  color: ${colors.text};
634
665
  }
635
666
 
636
- .${sliderStyles.selectedClass} {
667
+ .${selectedClass} {
637
668
  position: absolute;
638
669
  height: 4px;
639
670
  background: ${colors.primary};
@@ -656,13 +687,13 @@
656
687
 
657
688
  .afs-histogram-bar {
658
689
  flex: 1;
659
- background-color: ${colors.background};
690
+ background-color: ${sliderStyles.ui.histogram.background || colors.background};
660
691
  min-height: 4px;
661
692
  transition: background-color 0.2s ease;
662
693
  }
663
694
 
664
695
  .afs-histogram-bar.active {
665
- background-color: ${colors.primary};
696
+ background-color: ${sliderStyles.ui.histogram.bar.background || colors.primary};
666
697
  }
667
698
  `;
668
699
  }
@@ -673,7 +704,7 @@
673
704
  * @returns {string} CSS styles
674
705
  */
675
706
  createDateStyles() {
676
- const colors = (this.options.get("styles") || this.defaultStyles).colors;
707
+ const colors = this.options.get("styles").colors;
677
708
  return `
678
709
  .afs-date-range-container {
679
710
  display: flex;
@@ -710,6 +741,49 @@
710
741
  `;
711
742
  }
712
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
+
713
787
  /**
714
788
  * Apply all styles
715
789
  * @public
@@ -731,6 +805,9 @@
731
805
 
732
806
  /* Search styles */
733
807
  ${this.createSearchStyles()}
808
+
809
+ /* Input range styles */
810
+ ${this.createInputRangeStyles()}
734
811
  `;
735
812
  if (this.styleElement) {
736
813
  this.styleElement.textContent = styles;
@@ -758,11 +835,13 @@
758
835
  * @returns {string} CSS styles
759
836
  */
760
837
  createPaginationStyles() {
838
+ const styles = this.options.get("styles");
761
839
  const paginationOptions = this.options.get("pagination") || {};
762
- const colors = (this.options.get("styles") || this.defaultStyles).colors;
840
+ const colors = this.options.get("styles").colors;
763
841
  const containerClass = paginationOptions.containerClass || "afs-pagination";
764
842
  const buttonClass = paginationOptions.pageButtonClass || "afs-page-button";
765
843
  const activeClass = paginationOptions.activePageClass || "afs-page-active";
844
+ const paginationStyles = styles.pagination;
766
845
  return `
767
846
  .${containerClass} {
768
847
  display: flex;
@@ -772,23 +851,23 @@
772
851
  }
773
852
 
774
853
  .${buttonClass} {
775
- padding: 8px 12px;
776
- border: 1px solid ${colors.primary};
777
- border-radius: 4px;
854
+ padding: ${paginationStyles.ui.button.padding || '8px 12px'};
855
+ border: ${paginationStyles.ui.button.border || '1px solid ' + colors.primary};
856
+ border-radius: ${paginationStyles.ui.button.borderRadius || '4px'};
778
857
  cursor: pointer;
779
858
  transition: all 200ms ease-out;
780
- background: transparent;
781
- color: ${colors.primary};
859
+ background: ${paginationStyles.ui.button.background || 'transparent'};
860
+ color: ${paginationStyles.ui.button.color || colors.primary};
782
861
  }
783
862
 
784
863
  .${buttonClass}:hover {
785
- background: ${colors.primary};
786
- color: white;
864
+ background: ${paginationStyles.ui.button.hover.background || colors.primary};
865
+ color: ${paginationStyles.ui.button.hover.color || 'white'};
787
866
  }
788
867
 
789
868
  .${buttonClass}.${activeClass} {
790
- background: ${colors.primary};
791
- color: white;
869
+ background: ${paginationStyles.ui.button.active.background || colors.primary};
870
+ color: ${paginationStyles.ui.button.active.color || 'white'};
792
871
  }
793
872
 
794
873
  .${buttonClass}:disabled {
@@ -805,7 +884,7 @@
805
884
  */
806
885
  createSearchStyles() {
807
886
  const searchClass = this.options.get("searchInputClass") || "afs-search";
808
- const colors = (this.options.get("styles") || this.defaultStyles).colors;
887
+ const colors = this.options.get("styles").colors;
809
888
  return `
810
889
  .${searchClass} {
811
890
  padding: 8px;
@@ -1149,9 +1228,14 @@
1149
1228
  }
1150
1229
  applyShowAnimation(item) {
1151
1230
  let animationType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'fade';
1152
- const duration = this.options.get('animationDuration') || '300ms';
1231
+ const duration = this.options.get('animation.duration') || '300ms';
1153
1232
  const animation = this.animations[animationType]?.in || this.animations.fade.in;
1233
+
1234
+ // Ensure display is set
1154
1235
  item.style.display = 'block';
1236
+ item.style.opacity = '0'; // Start with opacity 0
1237
+
1238
+ // Apply animation in next frame
1155
1239
  requestAnimationFrame(() => {
1156
1240
  Object.assign(item.style, animation, {
1157
1241
  transition: `opacity ${duration} ${animation.transitionTimingFunction}, transform ${duration} ${animation.transitionTimingFunction}`
@@ -1160,7 +1244,7 @@
1160
1244
  }
1161
1245
  applyHideAnimation(item) {
1162
1246
  let animationType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'fade';
1163
- const duration = this.options.get('animationDuration') || '300ms';
1247
+ const duration = this.options.get('animation.duration') || '300ms';
1164
1248
  const animation = this.animations[animationType]?.out || this.animations.fade.out;
1165
1249
  Object.assign(item.style, animation, {
1166
1250
  transition: `opacity ${duration} ${animation.transitionTimingFunction}, transform ${duration} ${animation.transitionTimingFunction}`
@@ -1172,7 +1256,7 @@
1172
1256
  item.addEventListener('transitionend', handleTransitionEnd);
1173
1257
  }
1174
1258
  setAnimation(animationType) {
1175
- this.afs.options.set('animationType', animationType);
1259
+ this.afs.options.set('animation.type', animationType);
1176
1260
  }
1177
1261
  }
1178
1262
 
@@ -1319,26 +1403,39 @@
1319
1403
  // Clear filter groups
1320
1404
  this.filterGroups.clear();
1321
1405
 
1322
- // Force all items to be visible first
1323
- const visibleItems = new Set(this.afs.items);
1324
- this.afs.state.setState("items.visible", visibleItems);
1406
+ // Create a promise to track animations
1407
+ const animationPromises = [];
1325
1408
 
1326
1409
  // Show all items with animation
1327
1410
  this.afs.items.forEach(item => {
1328
- item.classList.remove(this.afs.options.get('hiddenClass'));
1329
- requestAnimationFrame(() => {
1330
- 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
+ });
1331
1420
  });
1421
+ animationPromises.push(promise);
1332
1422
  });
1333
1423
 
1334
- // Update counter after reset
1335
- 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);
1336
1427
 
1337
- // Update URL after reset
1338
- this.afs.urlManager.updateURL();
1428
+ // Wait for all animations to complete
1429
+ Promise.all(animationPromises).then(() => {
1430
+ // Update counter
1431
+ this.afs.updateCounter();
1339
1432
 
1340
- // Emit event
1341
- this.afs.emit("filtersReset");
1433
+ // Update URL
1434
+ this.afs.urlManager.updateURL();
1435
+
1436
+ // Emit event
1437
+ this.afs.emit("filtersReset");
1438
+ });
1342
1439
  }
1343
1440
 
1344
1441
  /**
@@ -1409,60 +1506,64 @@
1409
1506
  * @public
1410
1507
  */
1411
1508
  applyFilters() {
1412
- // Log active filters
1413
1509
  const activeFilters = Array.from(this.activeFilters);
1414
1510
  this.afs.logger.debug("Active filters:", activeFilters);
1415
- this.afs.logger.debug("Applying filters");
1416
1511
  const previouslyVisible = new Set(this.afs.state.getState().items.visible);
1417
1512
  const visibleItems = new Set();
1418
1513
 
1419
- // First pass: determine which items should be visible
1514
+ // First determine visibility
1420
1515
  this.afs.items.forEach(item => {
1421
1516
  if (this.activeFilters.has("*") || this.itemMatchesFilters(item)) {
1422
1517
  visibleItems.add(item);
1423
1518
  }
1424
1519
  });
1425
1520
 
1426
- // Update state with determined visible items
1521
+ // Update state before animations
1427
1522
  this.afs.state.setState("items.visible", visibleItems);
1428
1523
 
1429
- // Second pass: apply animations based on visibility changes
1524
+ // Track animation promises
1525
+ const animationPromises = [];
1526
+
1527
+ // Apply animations
1430
1528
  this.afs.items.forEach(item => {
1431
- if (visibleItems.has(item)) {
1432
- // Remove hidden class first
1433
- item.classList.remove(this.afs.options.get('hiddenClass'));
1434
- // Then apply show animation in next frame
1435
- requestAnimationFrame(() => {
1436
- this.animation.applyShowAnimation(item, this.afs.options.get("animationType"));
1437
- });
1438
- } else {
1439
- // Apply hide animation
1440
- requestAnimationFrame(() => {
1441
- this.animation.applyHideAnimation(item, this.afs.options.get("animationType"));
1442
- });
1443
- }
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);
1444
1546
  });
1445
1547
 
1446
- // Emit events and update UI
1447
- this.emitFilterEvents(previouslyVisible, visibleItems);
1448
-
1449
- // Schedule UI updates
1450
- requestAnimationFrame(() => {
1451
- // Force reflow to ensure animations play
1452
- 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
+ });
1453
1555
 
1454
- // Update counter - this needs to happen after filter changes
1556
+ // Update UI
1455
1557
  this.afs.updateCounter();
1456
-
1457
- // Update URL
1458
1558
  this.afs.urlManager.updateURL();
1459
-
1460
- // Emit final event
1461
1559
  this.afs.emit("filtersApplied", {
1462
1560
  activeFilters,
1463
1561
  visibleItems: visibleItems.size
1464
1562
  });
1465
1563
  });
1564
+
1565
+ // Emit visibility change events
1566
+ this.emitFilterEvents(previouslyVisible, visibleItems);
1466
1567
  }
1467
1568
 
1468
1569
  /**
@@ -2010,6 +2111,310 @@
2010
2111
  };
2011
2112
  }
2012
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
+
2013
2418
  /**
2014
2419
  * @fileoverview Search functionality for AFS
2015
2420
  */
@@ -3178,6 +3583,9 @@
3178
3583
  currentPage,
3179
3584
  itemsPerPage
3180
3585
  } = state.pagination;
3586
+ if (!this.afs.options.get('pagination.enabled')) {
3587
+ return;
3588
+ }
3181
3589
  if (currentPage > 1) {
3182
3590
  params.set('page', currentPage.toString());
3183
3591
  }
@@ -3470,6 +3878,7 @@
3470
3878
  constructor(afs) {
3471
3879
  this.afs = afs;
3472
3880
  this.activeRanges = new Map();
3881
+ this.options = this.afs.options.get("slider");
3473
3882
  if (!this.afs.styleManager) {
3474
3883
  this.afs.styleManager = new StyleManager(this.afs.options);
3475
3884
  }
@@ -3584,14 +3993,14 @@
3584
3993
  */
3585
3994
  createSliderElements(histogramData, sliderUiOptions) {
3586
3995
  const styles = this.afs.options.get("styles") || this.afs.styleManager.defaultStyles;
3587
- const sliderStyles = styles.slider || this.afs.styleManager.defaultStyles.slider;
3588
3996
  const colors = styles.colors || this.afs.styleManager.defaultStyles.colors;
3997
+ const sliderOptions = this.afs.options.get("slider") || {};
3589
3998
  const container = document.createElement("div");
3590
- container.className = "price-range-container";
3999
+ container.className = "afs-range-container";
3591
4000
  const slider = document.createElement("div");
3592
- slider.className = sliderStyles.class;
4001
+ slider.className = sliderOptions.containerClass;
3593
4002
  const track = document.createElement("div");
3594
- track.className = sliderStyles.trackClass;
4003
+ track.className = sliderOptions.trackClass;
3595
4004
 
3596
4005
  // Only add histogram if enabled in the slider-specific options
3597
4006
  if (sliderUiOptions?.showHistogram && histogramData?.counts?.length > 0) {
@@ -3599,15 +4008,15 @@
3599
4008
  slider.appendChild(histogram);
3600
4009
  }
3601
4010
  const selectedRange = document.createElement("div");
3602
- selectedRange.className = sliderStyles.selectedClass;
4011
+ selectedRange.className = sliderOptions.selectedClass;
3603
4012
  const minThumb = document.createElement("div");
3604
- minThumb.className = sliderStyles.thumbClass;
4013
+ minThumb.className = sliderOptions.thumbClass;
3605
4014
  const maxThumb = document.createElement("div");
3606
- maxThumb.className = sliderStyles.thumbClass;
4015
+ maxThumb.className = sliderOptions.thumbClass;
3607
4016
  const minValue = document.createElement("div");
3608
- minValue.className = sliderStyles.valueClass;
4017
+ minValue.className = sliderOptions.valueClass;
3609
4018
  const maxValue = document.createElement("div");
3610
- maxValue.className = sliderStyles.valueClass;
4019
+ maxValue.className = sliderOptions.valueClass;
3611
4020
 
3612
4021
  // Build the slider
3613
4022
  slider.appendChild(track);
@@ -4327,7 +4736,7 @@
4327
4736
  */
4328
4737
 
4329
4738
  // Version
4330
- const VERSION = '1.0.7';
4739
+ const VERSION = '1.0.9';
4331
4740
  class AFS extends EventEmitter {
4332
4741
  /**
4333
4742
  * @param {Object} options - Configuration options
@@ -4390,6 +4799,7 @@
4390
4799
  this.urlManager = new URLManager(this);
4391
4800
  this.dateFilter = new DateFilter(this);
4392
4801
  this.pagination = new Pagination(this);
4802
+ this.inputRangeFilter = new InputRangeFilter(this);
4393
4803
 
4394
4804
  // Apply styles
4395
4805
  this.styleManager.applyStyles();