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.esm.js CHANGED
@@ -139,68 +139,111 @@ class Options {
139
139
  */
140
140
  static defaults = {
141
141
  // Selectors
142
- containerSelector: '.filter-container',
143
- itemSelector: '.filter-item',
144
- filterButtonSelector: '.btn-filter',
145
- searchInputSelector: '.filter-search',
146
- counterSelector: '.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
171
167
  styles: {
172
168
  slider: {
173
- class: 'afs-range-slider',
174
- trackClass: 'afs-range-track',
175
- thumbClass: 'afs-range-thumb',
176
- valueClass: 'afs-range-value',
177
- selectedClass: 'afs-range-selected',
178
169
  // Add new UI options
179
170
  ui: {
180
171
  showHistogram: false,
181
- bins: 10 // Number of bins for histogram
172
+ bins: 10,
173
+ // Number of bins for histogram
174
+ track: {
175
+ radius: "0",
176
+ // Button radius
177
+ background: "#e5e7eb" // Track color
178
+ },
179
+ thumb: {
180
+ radius: "50%",
181
+ // Button radius
182
+ size: "16px",
183
+ // Button size
184
+ background: "#000" // Button color
185
+ },
186
+ histogram: {
187
+ background: "#e5e7eb",
188
+ // Histogram background
189
+ bar: {
190
+ background: "#000" // Bar color
191
+ }
192
+ }
193
+ }
194
+ },
195
+ pagination: {
196
+ ui: {
197
+ button: {
198
+ background: "transparent",
199
+ border: "1px solid #000",
200
+ borderRadius: "4px",
201
+ padding: "8px 12px",
202
+ color: "#000",
203
+ active: {
204
+ background: "#000",
205
+ color: "#fff"
206
+ },
207
+ hover: {
208
+ background: "#000",
209
+ color: "#fff"
210
+ }
211
+ }
182
212
  }
183
213
  },
184
214
  colors: {
185
- primary: '#000',
186
- background: '#e5e7eb',
187
- text: '#000',
188
- histogram: '#e5e7eb',
189
- // For histogram bars
190
- histogramActive: '#000' // For active histogram bars
215
+ primary: "#000",
216
+ background: "#e5e7eb",
217
+ text: "#000"
191
218
  }
192
219
  },
220
+ // Slider
221
+ slider: {
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"
227
+ },
193
228
  // Pagination
194
229
  pagination: {
195
230
  enabled: false,
196
231
  itemsPerPage: 10,
197
- container: '.afs-pagination-container',
198
- pageButtonClass: 'afs-page-button',
199
- activePageClass: 'afs-page-active',
200
- containerClass: 'afs-pagination',
232
+ container: ".afs-pagination-container",
233
+ pageButtonClass: "afs-page-button",
234
+ activePageClass: "afs-page-active",
235
+ containerClass: "afs-pagination",
201
236
  scrollToTop: false,
202
237
  scrollOffset: 50,
203
- 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"
204
247
  }
205
248
  };
206
249
  constructor() {
@@ -238,9 +281,9 @@ class Options {
238
281
  ...target
239
282
  };
240
283
  for (const key in source) {
241
- 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])) {
242
285
  // If the key doesn't exist in target or isn't an object, create/override it
243
- if (!target[key] || typeof target[key] !== 'object') {
286
+ if (!target[key] || typeof target[key] !== "object") {
244
287
  result[key] = {};
245
288
  }
246
289
  // Recursively merge nested objects
@@ -260,37 +303,37 @@ class Options {
260
303
  */
261
304
  validate() {
262
305
  // Required selectors
263
- const requiredSelectors = ['containerSelector', 'itemSelector'];
306
+ const requiredSelectors = ["containerSelector", "itemSelector"];
264
307
  for (const selector of requiredSelectors) {
265
- if (typeof this.options[selector] !== 'string') {
308
+ if (typeof this.options[selector] !== "string") {
266
309
  throw new Error(`${selector} must be a string`);
267
310
  }
268
311
  }
269
312
 
270
313
  // Animation duration
271
- if (typeof this.options.animationDuration !== 'number' || this.options.animationDuration < 0) {
272
- 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");
273
316
  }
274
317
 
275
318
  // Filter mode
276
- if (!['OR', 'AND'].includes(this.options.filterMode.toUpperCase())) {
319
+ if (!["OR", "AND"].includes(this.options.filterMode.toUpperCase())) {
277
320
  throw new Error('filterMode must be either "OR" or "AND"');
278
321
  }
279
322
 
280
323
  // Search keys
281
324
  if (!Array.isArray(this.options.searchKeys) || this.options.searchKeys.length === 0) {
282
- throw new Error('searchKeys must be a non-empty array');
325
+ throw new Error("searchKeys must be a non-empty array");
283
326
  }
284
327
 
285
328
  // Counter validation
286
329
  if (this.options.counter) {
287
- if (typeof this.options.counter.template !== 'string') {
288
- 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");
289
332
  }
290
- if (typeof this.options.counter.showFiltered !== 'boolean') {
333
+ if (typeof this.options.counter.showFiltered !== "boolean") {
291
334
  this.options.counter.showFiltered = true; // Set default
292
335
  }
293
- if (typeof this.options.counter.formatter !== 'function') {
336
+ if (typeof this.options.counter.formatter !== "function") {
294
337
  this.options.counter.formatter = num => num.toLocaleString(); // Set default
295
338
  }
296
339
  } else {
@@ -306,7 +349,7 @@ class Options {
306
349
  * @returns {any} Option value
307
350
  */
308
351
  get(path) {
309
- return path.split('.').reduce((obj, key) => obj?.[key], this.options);
352
+ return path.split(".").reduce((obj, key) => obj?.[key], this.options);
310
353
  }
311
354
 
312
355
  /**
@@ -315,7 +358,7 @@ class Options {
315
358
  * @param {any} value - New value
316
359
  */
317
360
  set(path, value) {
318
- const parts = path.split('.');
361
+ const parts = path.split(".");
319
362
  const last = parts.pop();
320
363
  const target = parts.reduce((obj, key) => {
321
364
  if (!(key in obj)) obj[key] = {};
@@ -515,24 +558,6 @@ class StyleManager {
515
558
  constructor(options) {
516
559
  this.options = options;
517
560
  this.styleElement = null;
518
- this.defaultStyles = {
519
- slider: {
520
- class: "afs-range-slider",
521
- trackClass: "afs-range-track",
522
- thumbClass: "afs-range-thumb",
523
- valueClass: "afs-range-value",
524
- selectedClass: "afs-range-selected"
525
- },
526
- colors: {
527
- primary: "#000",
528
- background: "#ddd",
529
- text: "#000"
530
- },
531
- animation: {
532
- duration: "300ms",
533
- type: "ease-out"
534
- }
535
- };
536
561
  }
537
562
 
538
563
  /**
@@ -542,11 +567,11 @@ class StyleManager {
542
567
  */
543
568
  createBaseStyles() {
544
569
  const hiddenClass = this.options.get("hiddenClass") || "hidden";
545
- const itemSelector = this.options.get("itemSelector") || ".filter-item";
546
- const filterButtonSelector = this.options.get("filterButtonSelector") || ".btn-filter";
570
+ const itemSelector = this.options.get("itemSelector") || ".afs-filter-item";
571
+ const filterButtonSelector = this.options.get("filterButtonSelector") || ".afs-btn-filter";
547
572
  const activeClass = this.options.get("activeClass") || "active";
548
- const animationDuration = this.options.get("animationDuration") || '300ms';
549
- 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';
550
575
  return `
551
576
  .${hiddenClass} {
552
577
  display: none !important;
@@ -584,12 +609,18 @@ class StyleManager {
584
609
  * @returns {string} CSS styles
585
610
  */
586
611
  createRangeStyles() {
587
- const styles = this.options.get("styles") || this.defaultStyles;
588
- const sliderStyles = styles.slider || this.defaultStyles.slider;
589
- const colors = styles.colors || this.defaultStyles.colors;
612
+ const styles = this.options.get("styles");
613
+ const sliderOptions = this.options.get("slider") || {};
614
+ const sliderStyles = styles.slider;
615
+ const colors = styles.colors;
616
+ const containerClass = sliderOptions.containerClass || "afs-range-slider";
617
+ const trackClass = sliderOptions.trackClass || "afs-range-track";
618
+ const thumbClass = sliderOptions.thumbClass || "afs-range-thumb";
619
+ const valueClass = sliderOptions.valueClass || "afs-range-value";
620
+ const selectedClass = sliderOptions.selectedClass || "afs-range-selected";
590
621
  return `
591
622
  /* Range Slider Styles */
592
- .${sliderStyles.class} {
623
+ .${containerClass} {
593
624
  position: relative;
594
625
  width: auto;
595
626
  height: 40px;
@@ -597,29 +628,29 @@ class StyleManager {
597
628
  padding: 0 8px;
598
629
  }
599
630
 
600
- .${sliderStyles.trackClass} {
631
+ .${trackClass} {
601
632
  position: absolute;
602
633
  top: 50%;
603
634
  transform: translateY(-50%);
604
635
  width: 100%;
605
636
  height: 4px;
606
- background: ${colors.background};
607
- border-radius: 2px;
637
+ background: ${sliderStyles.ui.track.background || colors.background};
638
+ border-radius: ${sliderStyles.ui.track.radius || "0"};
608
639
  }
609
640
 
610
- .${sliderStyles.thumbClass} {
641
+ .${thumbClass} {
611
642
  position: absolute;
612
643
  top: 50%;
613
- width: 16px;
614
- height: 16px;
615
- background: ${colors.primary};
616
- border-radius: 50%;
644
+ width: ${sliderStyles.ui.thumb.size || "16px"};
645
+ height: ${sliderStyles.ui.thumb.size || "16px"};
646
+ background: ${sliderStyles.ui.thumb.background || colors.primary};
647
+ border-radius: ${sliderStyles.ui.thumb.radius || "50%"};
617
648
  transform: translate(-50%, -50%);
618
649
  cursor: pointer;
619
650
  z-index: 2;
620
651
  }
621
652
 
622
- .${sliderStyles.valueClass} {
653
+ .${valueClass} {
623
654
  position: absolute;
624
655
  top: -20px;
625
656
  transform: translateX(-50%);
@@ -627,7 +658,7 @@ class StyleManager {
627
658
  color: ${colors.text};
628
659
  }
629
660
 
630
- .${sliderStyles.selectedClass} {
661
+ .${selectedClass} {
631
662
  position: absolute;
632
663
  height: 4px;
633
664
  background: ${colors.primary};
@@ -650,13 +681,13 @@ class StyleManager {
650
681
 
651
682
  .afs-histogram-bar {
652
683
  flex: 1;
653
- background-color: ${colors.background};
684
+ background-color: ${sliderStyles.ui.histogram.background || colors.background};
654
685
  min-height: 4px;
655
686
  transition: background-color 0.2s ease;
656
687
  }
657
688
 
658
689
  .afs-histogram-bar.active {
659
- background-color: ${colors.primary};
690
+ background-color: ${sliderStyles.ui.histogram.bar.background || colors.primary};
660
691
  }
661
692
  `;
662
693
  }
@@ -667,7 +698,7 @@ class StyleManager {
667
698
  * @returns {string} CSS styles
668
699
  */
669
700
  createDateStyles() {
670
- const colors = (this.options.get("styles") || this.defaultStyles).colors;
701
+ const colors = this.options.get("styles").colors;
671
702
  return `
672
703
  .afs-date-range-container {
673
704
  display: flex;
@@ -704,6 +735,49 @@ class StyleManager {
704
735
  `;
705
736
  }
706
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
+
707
781
  /**
708
782
  * Apply all styles
709
783
  * @public
@@ -725,6 +799,9 @@ class StyleManager {
725
799
 
726
800
  /* Search styles */
727
801
  ${this.createSearchStyles()}
802
+
803
+ /* Input range styles */
804
+ ${this.createInputRangeStyles()}
728
805
  `;
729
806
  if (this.styleElement) {
730
807
  this.styleElement.textContent = styles;
@@ -752,11 +829,13 @@ class StyleManager {
752
829
  * @returns {string} CSS styles
753
830
  */
754
831
  createPaginationStyles() {
832
+ const styles = this.options.get("styles");
755
833
  const paginationOptions = this.options.get("pagination") || {};
756
- const colors = (this.options.get("styles") || this.defaultStyles).colors;
834
+ const colors = this.options.get("styles").colors;
757
835
  const containerClass = paginationOptions.containerClass || "afs-pagination";
758
836
  const buttonClass = paginationOptions.pageButtonClass || "afs-page-button";
759
837
  const activeClass = paginationOptions.activePageClass || "afs-page-active";
838
+ const paginationStyles = styles.pagination;
760
839
  return `
761
840
  .${containerClass} {
762
841
  display: flex;
@@ -766,23 +845,23 @@ class StyleManager {
766
845
  }
767
846
 
768
847
  .${buttonClass} {
769
- padding: 8px 12px;
770
- border: 1px solid ${colors.primary};
771
- border-radius: 4px;
848
+ padding: ${paginationStyles.ui.button.padding || '8px 12px'};
849
+ border: ${paginationStyles.ui.button.border || '1px solid ' + colors.primary};
850
+ border-radius: ${paginationStyles.ui.button.borderRadius || '4px'};
772
851
  cursor: pointer;
773
852
  transition: all 200ms ease-out;
774
- background: transparent;
775
- color: ${colors.primary};
853
+ background: ${paginationStyles.ui.button.background || 'transparent'};
854
+ color: ${paginationStyles.ui.button.color || colors.primary};
776
855
  }
777
856
 
778
857
  .${buttonClass}:hover {
779
- background: ${colors.primary};
780
- color: white;
858
+ background: ${paginationStyles.ui.button.hover.background || colors.primary};
859
+ color: ${paginationStyles.ui.button.hover.color || 'white'};
781
860
  }
782
861
 
783
862
  .${buttonClass}.${activeClass} {
784
- background: ${colors.primary};
785
- color: white;
863
+ background: ${paginationStyles.ui.button.active.background || colors.primary};
864
+ color: ${paginationStyles.ui.button.active.color || 'white'};
786
865
  }
787
866
 
788
867
  .${buttonClass}:disabled {
@@ -799,7 +878,7 @@ class StyleManager {
799
878
  */
800
879
  createSearchStyles() {
801
880
  const searchClass = this.options.get("searchInputClass") || "afs-search";
802
- const colors = (this.options.get("styles") || this.defaultStyles).colors;
881
+ const colors = this.options.get("styles").colors;
803
882
  return `
804
883
  .${searchClass} {
805
884
  padding: 8px;
@@ -1143,9 +1222,14 @@ class Animation {
1143
1222
  }
1144
1223
  applyShowAnimation(item) {
1145
1224
  let animationType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'fade';
1146
- const duration = this.options.get('animationDuration') || '300ms';
1225
+ const duration = this.options.get('animation.duration') || '300ms';
1147
1226
  const animation = this.animations[animationType]?.in || this.animations.fade.in;
1227
+
1228
+ // Ensure display is set
1148
1229
  item.style.display = 'block';
1230
+ item.style.opacity = '0'; // Start with opacity 0
1231
+
1232
+ // Apply animation in next frame
1149
1233
  requestAnimationFrame(() => {
1150
1234
  Object.assign(item.style, animation, {
1151
1235
  transition: `opacity ${duration} ${animation.transitionTimingFunction}, transform ${duration} ${animation.transitionTimingFunction}`
@@ -1154,7 +1238,7 @@ class Animation {
1154
1238
  }
1155
1239
  applyHideAnimation(item) {
1156
1240
  let animationType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'fade';
1157
- const duration = this.options.get('animationDuration') || '300ms';
1241
+ const duration = this.options.get('animation.duration') || '300ms';
1158
1242
  const animation = this.animations[animationType]?.out || this.animations.fade.out;
1159
1243
  Object.assign(item.style, animation, {
1160
1244
  transition: `opacity ${duration} ${animation.transitionTimingFunction}, transform ${duration} ${animation.transitionTimingFunction}`
@@ -1166,7 +1250,7 @@ class Animation {
1166
1250
  item.addEventListener('transitionend', handleTransitionEnd);
1167
1251
  }
1168
1252
  setAnimation(animationType) {
1169
- this.afs.options.set('animationType', animationType);
1253
+ this.afs.options.set('animation.type', animationType);
1170
1254
  }
1171
1255
  }
1172
1256
 
@@ -1313,26 +1397,39 @@ class Filter {
1313
1397
  // Clear filter groups
1314
1398
  this.filterGroups.clear();
1315
1399
 
1316
- // Force all items to be visible first
1317
- const visibleItems = new Set(this.afs.items);
1318
- this.afs.state.setState("items.visible", visibleItems);
1400
+ // Create a promise to track animations
1401
+ const animationPromises = [];
1319
1402
 
1320
1403
  // Show all items with animation
1321
1404
  this.afs.items.forEach(item => {
1322
- item.classList.remove(this.afs.options.get('hiddenClass'));
1323
- requestAnimationFrame(() => {
1324
- 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
+ });
1325
1414
  });
1415
+ animationPromises.push(promise);
1326
1416
  });
1327
1417
 
1328
- // Update counter after reset
1329
- 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);
1330
1421
 
1331
- // Update URL after reset
1332
- this.afs.urlManager.updateURL();
1422
+ // Wait for all animations to complete
1423
+ Promise.all(animationPromises).then(() => {
1424
+ // Update counter
1425
+ this.afs.updateCounter();
1333
1426
 
1334
- // Emit event
1335
- this.afs.emit("filtersReset");
1427
+ // Update URL
1428
+ this.afs.urlManager.updateURL();
1429
+
1430
+ // Emit event
1431
+ this.afs.emit("filtersReset");
1432
+ });
1336
1433
  }
1337
1434
 
1338
1435
  /**
@@ -1403,60 +1500,64 @@ class Filter {
1403
1500
  * @public
1404
1501
  */
1405
1502
  applyFilters() {
1406
- // Log active filters
1407
1503
  const activeFilters = Array.from(this.activeFilters);
1408
1504
  this.afs.logger.debug("Active filters:", activeFilters);
1409
- this.afs.logger.debug("Applying filters");
1410
1505
  const previouslyVisible = new Set(this.afs.state.getState().items.visible);
1411
1506
  const visibleItems = new Set();
1412
1507
 
1413
- // First pass: determine which items should be visible
1508
+ // First determine visibility
1414
1509
  this.afs.items.forEach(item => {
1415
1510
  if (this.activeFilters.has("*") || this.itemMatchesFilters(item)) {
1416
1511
  visibleItems.add(item);
1417
1512
  }
1418
1513
  });
1419
1514
 
1420
- // Update state with determined visible items
1515
+ // Update state before animations
1421
1516
  this.afs.state.setState("items.visible", visibleItems);
1422
1517
 
1423
- // Second pass: apply animations based on visibility changes
1518
+ // Track animation promises
1519
+ const animationPromises = [];
1520
+
1521
+ // Apply animations
1424
1522
  this.afs.items.forEach(item => {
1425
- if (visibleItems.has(item)) {
1426
- // Remove hidden class first
1427
- item.classList.remove(this.afs.options.get('hiddenClass'));
1428
- // Then apply show animation in next frame
1429
- requestAnimationFrame(() => {
1430
- this.animation.applyShowAnimation(item, this.afs.options.get("animationType"));
1431
- });
1432
- } else {
1433
- // Apply hide animation
1434
- requestAnimationFrame(() => {
1435
- this.animation.applyHideAnimation(item, this.afs.options.get("animationType"));
1436
- });
1437
- }
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);
1438
1540
  });
1439
1541
 
1440
- // Emit events and update UI
1441
- this.emitFilterEvents(previouslyVisible, visibleItems);
1442
-
1443
- // Schedule UI updates
1444
- requestAnimationFrame(() => {
1445
- // Force reflow to ensure animations play
1446
- 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
+ });
1447
1549
 
1448
- // Update counter - this needs to happen after filter changes
1550
+ // Update UI
1449
1551
  this.afs.updateCounter();
1450
-
1451
- // Update URL
1452
1552
  this.afs.urlManager.updateURL();
1453
-
1454
- // Emit final event
1455
1553
  this.afs.emit("filtersApplied", {
1456
1554
  activeFilters,
1457
1555
  visibleItems: visibleItems.size
1458
1556
  });
1459
1557
  });
1558
+
1559
+ // Emit visibility change events
1560
+ this.emitFilterEvents(previouslyVisible, visibleItems);
1460
1561
  }
1461
1562
 
1462
1563
  /**
@@ -2004,6 +2105,310 @@ function debounce(func, wait) {
2004
2105
  };
2005
2106
  }
2006
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
+
2007
2412
  /**
2008
2413
  * @fileoverview Search functionality for AFS
2009
2414
  */
@@ -3172,6 +3577,9 @@ class URLManager {
3172
3577
  currentPage,
3173
3578
  itemsPerPage
3174
3579
  } = state.pagination;
3580
+ if (!this.afs.options.get('pagination.enabled')) {
3581
+ return;
3582
+ }
3175
3583
  if (currentPage > 1) {
3176
3584
  params.set('page', currentPage.toString());
3177
3585
  }
@@ -3464,6 +3872,7 @@ class RangeFilter {
3464
3872
  constructor(afs) {
3465
3873
  this.afs = afs;
3466
3874
  this.activeRanges = new Map();
3875
+ this.options = this.afs.options.get("slider");
3467
3876
  if (!this.afs.styleManager) {
3468
3877
  this.afs.styleManager = new StyleManager(this.afs.options);
3469
3878
  }
@@ -3578,14 +3987,14 @@ class RangeFilter {
3578
3987
  */
3579
3988
  createSliderElements(histogramData, sliderUiOptions) {
3580
3989
  const styles = this.afs.options.get("styles") || this.afs.styleManager.defaultStyles;
3581
- const sliderStyles = styles.slider || this.afs.styleManager.defaultStyles.slider;
3582
3990
  const colors = styles.colors || this.afs.styleManager.defaultStyles.colors;
3991
+ const sliderOptions = this.afs.options.get("slider") || {};
3583
3992
  const container = document.createElement("div");
3584
- container.className = "price-range-container";
3993
+ container.className = "afs-range-container";
3585
3994
  const slider = document.createElement("div");
3586
- slider.className = sliderStyles.class;
3995
+ slider.className = sliderOptions.containerClass;
3587
3996
  const track = document.createElement("div");
3588
- track.className = sliderStyles.trackClass;
3997
+ track.className = sliderOptions.trackClass;
3589
3998
 
3590
3999
  // Only add histogram if enabled in the slider-specific options
3591
4000
  if (sliderUiOptions?.showHistogram && histogramData?.counts?.length > 0) {
@@ -3593,15 +4002,15 @@ class RangeFilter {
3593
4002
  slider.appendChild(histogram);
3594
4003
  }
3595
4004
  const selectedRange = document.createElement("div");
3596
- selectedRange.className = sliderStyles.selectedClass;
4005
+ selectedRange.className = sliderOptions.selectedClass;
3597
4006
  const minThumb = document.createElement("div");
3598
- minThumb.className = sliderStyles.thumbClass;
4007
+ minThumb.className = sliderOptions.thumbClass;
3599
4008
  const maxThumb = document.createElement("div");
3600
- maxThumb.className = sliderStyles.thumbClass;
4009
+ maxThumb.className = sliderOptions.thumbClass;
3601
4010
  const minValue = document.createElement("div");
3602
- minValue.className = sliderStyles.valueClass;
4011
+ minValue.className = sliderOptions.valueClass;
3603
4012
  const maxValue = document.createElement("div");
3604
- maxValue.className = sliderStyles.valueClass;
4013
+ maxValue.className = sliderOptions.valueClass;
3605
4014
 
3606
4015
  // Build the slider
3607
4016
  slider.appendChild(track);
@@ -4321,7 +4730,7 @@ class DateFilter {
4321
4730
  */
4322
4731
 
4323
4732
  // Version
4324
- const VERSION = '1.0.7';
4733
+ const VERSION = '1.0.9';
4325
4734
  class AFS extends EventEmitter {
4326
4735
  /**
4327
4736
  * @param {Object} options - Configuration options
@@ -4384,6 +4793,7 @@ class AFS extends EventEmitter {
4384
4793
  this.urlManager = new URLManager(this);
4385
4794
  this.dateFilter = new DateFilter(this);
4386
4795
  this.pagination = new Pagination(this);
4796
+ this.inputRangeFilter = new InputRangeFilter(this);
4387
4797
 
4388
4798
  // Apply styles
4389
4799
  this.styleManager.applyStyles();