advanced-filter-system 1.0.8 → 1.1.0

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 DELETED
@@ -1,4818 +0,0 @@
1
- (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
- typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.AFS = {}));
5
- })(this, (function (exports) { 'use strict';
6
-
7
- /**
8
- * @fileoverview Logging system for AFS
9
- */
10
-
11
- class Logger {
12
- /**
13
- * @param {boolean} [debug=false] - Enable debug mode
14
- * @param {string} [logLevel='info'] - Logging level
15
- */
16
- constructor() {
17
- let debug = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
18
- let logLevel = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'info';
19
- this.enabled = debug;
20
- this.level = logLevel;
21
- this.levels = {
22
- error: 0,
23
- warn: 1,
24
- info: 2,
25
- debug: 3
26
- };
27
- }
28
-
29
- /**
30
- * Internal log method
31
- * @private
32
- */
33
- _log(level) {
34
- if (!this.enabled) return;
35
- const currentLevelValue = this.levels[this.level];
36
- const messageLevel = this.levels[level];
37
- if (messageLevel <= currentLevelValue) {
38
- const timestamp = new Date().toISOString();
39
- const prefix = `[AFS ${level.toUpperCase()}] ${timestamp}`;
40
- for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
41
- args[_key - 1] = arguments[_key];
42
- }
43
- switch (level) {
44
- case 'error':
45
- console.error(prefix, ...args);
46
- break;
47
- case 'warn':
48
- console.warn(prefix, ...args);
49
- break;
50
- case 'info':
51
- console.info(prefix, ...args);
52
- break;
53
- case 'debug':
54
- console.debug(prefix, ...args);
55
- break;
56
- }
57
- }
58
- }
59
-
60
- /**
61
- * Log error message
62
- * @public
63
- */
64
- error() {
65
- for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
66
- args[_key2] = arguments[_key2];
67
- }
68
- this._log('error', ...args);
69
- }
70
-
71
- /**
72
- * Log warning message
73
- * @public
74
- */
75
- warn() {
76
- for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
77
- args[_key3] = arguments[_key3];
78
- }
79
- this._log('warn', ...args);
80
- }
81
-
82
- /**
83
- * Log info message
84
- * @public
85
- */
86
- info() {
87
- for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
88
- args[_key4] = arguments[_key4];
89
- }
90
- this._log('info', ...args);
91
- }
92
-
93
- /**
94
- * Log debug message
95
- * @public
96
- */
97
- debug() {
98
- for (var _len5 = arguments.length, args = new Array(_len5), _key5 = 0; _key5 < _len5; _key5++) {
99
- args[_key5] = arguments[_key5];
100
- }
101
- this._log('debug', ...args);
102
- }
103
-
104
- /**
105
- * Enable or disable debug mode
106
- * @public
107
- */
108
- setDebugMode(enabled) {
109
- let level = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'info';
110
- this.enabled = enabled;
111
- if (this.levels.hasOwnProperty(level)) {
112
- this.level = level;
113
- }
114
- this._log('info', `Debug mode ${enabled ? 'enabled' : 'disabled'} with level: ${level}`);
115
- }
116
- }
117
-
118
- /**
119
- * @fileoverview Configuration management for AFS
120
- */
121
-
122
- class Options {
123
- /**
124
- * @typedef {Object} AFSOptions
125
- * @property {string} containerSelector - Main container selector
126
- * @property {string} itemSelector - Items to filter selector
127
- * @property {string} filterButtonSelector - Filter buttons selector
128
- * @property {string} searchInputSelector - Search input selector
129
- * @property {string} counterSelector - Results counter selector
130
- * @property {string} activeClass - Active state class
131
- * @property {string} hiddenClass - Hidden state class
132
- * @property {number} animationDuration - Animation duration in ms
133
- * @property {string} filterMode - Filter mode ('OR' or 'AND')
134
- * @property {string[]} searchKeys - Data attributes to search in
135
- * @property {number} debounceTime - Search debounce delay in ms
136
- * @property {boolean} debug - Enable debug mode
137
- * @property {string} logLevel - Log level
138
- * @property {string} dateFormat - Date format
139
- * @property {Object} counter - Counter-related options
140
- * @property {Object} styles - Style-related options
141
- */
142
-
143
- /**
144
- * @type {AFSOptions}
145
- */
146
- static defaults = {
147
- // Selectors
148
- containerSelector: '.afs-filter-container',
149
- itemSelector: '.afs-filter-item',
150
- filterButtonSelector: '.afs-btn-filter',
151
- searchInputSelector: '.afs-filter-search',
152
- counterSelector: '.afs-filter-counter',
153
- // Classes
154
- activeClass: 'active',
155
- hiddenClass: 'hidden',
156
- // Animation
157
- animationDuration: 300,
158
- animationType: 'fade',
159
- animationEasing: 'ease-out',
160
- // Filtering
161
- filterMode: 'OR',
162
- searchKeys: ['title'],
163
- debounceTime: 300,
164
- // Debug
165
- debug: false,
166
- logLevel: 'info',
167
- // Date handling
168
- dateFormat: 'YYYY-MM-DD',
169
- counter: {
170
- template: 'Showing {visible} of {total}',
171
- showFiltered: true,
172
- filteredTemplate: '({filtered} filtered)',
173
- noResultsTemplate: 'No items found',
174
- formatter: num => num.toLocaleString()
175
- },
176
- // Styles
177
- styles: {
178
- slider: {
179
- // Add new UI options
180
- ui: {
181
- showHistogram: false,
182
- bins: 10,
183
- // Number of bins for histogram
184
- track: {
185
- radius: '2px',
186
- // Button radius
187
- background: '#e5e7eb' // Track color
188
- },
189
- thumb: {
190
- radius: '50%',
191
- // Button radius
192
- size: '16px',
193
- // Button size
194
- background: '#000' // Button color
195
- },
196
- histogram: {
197
- background: '#e5e7eb',
198
- // Histogram background
199
- bar: {
200
- background: '#000' // Bar color
201
- }
202
- }
203
- }
204
- },
205
- pagination: {
206
- ui: {
207
- button: {
208
- background: 'transparent',
209
- border: '1px solid #000',
210
- borderRadius: '4px',
211
- padding: '8px 12px',
212
- color: '#000',
213
- active: {
214
- background: '#000',
215
- color: '#fff'
216
- },
217
- hover: {
218
- background: '#000',
219
- color: '#fff'
220
- }
221
- }
222
- }
223
- },
224
- colors: {
225
- primary: '#000',
226
- background: '#e5e7eb',
227
- text: '#000'
228
- }
229
- },
230
- // Slider
231
- slider: {
232
- containerClass: 'afs-range-slider',
233
- trackClass: 'afs-range-track',
234
- thumbClass: 'afs-range-thumb',
235
- valueClass: 'afs-range-value',
236
- selectedClass: 'afs-range-selected'
237
- },
238
- // Pagination
239
- pagination: {
240
- enabled: false,
241
- itemsPerPage: 10,
242
- container: '.afs-pagination-container',
243
- pageButtonClass: 'afs-page-button',
244
- activePageClass: 'afs-page-active',
245
- containerClass: 'afs-pagination',
246
- scrollToTop: false,
247
- scrollOffset: 50,
248
- scrollBehavior: 'smooth' // or 'auto' for instant scroll
249
- }
250
- };
251
- constructor() {
252
- let userOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
253
- this.options = this.mergeOptions(Options.defaults, userOptions);
254
- this.initializeStyles();
255
- this.validate();
256
- }
257
-
258
- /**
259
- * Initialize styles with defaults
260
- * @private
261
- */
262
- initializeStyles() {
263
- const defaultStyles = Options.defaults.styles;
264
- const currentStyles = this.options.styles || {};
265
- this.options.styles = this.mergeOptions(defaultStyles, currentStyles);
266
-
267
- // Ensure colors object exists and has all required properties
268
- this.options.styles.colors = {
269
- ...defaultStyles.colors,
270
- ...(currentStyles.colors || {})
271
- };
272
- }
273
-
274
- /**
275
- * Deep merge options
276
- * @private
277
- * @param {Object} target - Target object
278
- * @param {Object} source - Source object
279
- * @returns {Object} Merged options
280
- */
281
- mergeOptions(target, source) {
282
- const result = {
283
- ...target
284
- };
285
- for (const key in source) {
286
- if (source[key] !== null && typeof source[key] === 'object' && !Array.isArray(source[key])) {
287
- // If the key doesn't exist in target or isn't an object, create/override it
288
- if (!target[key] || typeof target[key] !== 'object') {
289
- result[key] = {};
290
- }
291
- // Recursively merge nested objects
292
- result[key] = this.mergeOptions(result[key], source[key]);
293
- } else if (source[key] !== undefined) {
294
- // Only override if the source value is defined
295
- result[key] = source[key];
296
- }
297
- }
298
- return result;
299
- }
300
-
301
- /**
302
- * Validate options
303
- * @private
304
- * @throws {Error} If options are invalid
305
- */
306
- validate() {
307
- // Required selectors
308
- const requiredSelectors = ['containerSelector', 'itemSelector'];
309
- for (const selector of requiredSelectors) {
310
- if (typeof this.options[selector] !== 'string') {
311
- throw new Error(`${selector} must be a string`);
312
- }
313
- }
314
-
315
- // Animation duration
316
- if (typeof this.options.animationDuration !== 'number' || this.options.animationDuration < 0) {
317
- throw new Error('animationDuration must be a positive number');
318
- }
319
-
320
- // Filter mode
321
- if (!['OR', 'AND'].includes(this.options.filterMode.toUpperCase())) {
322
- throw new Error('filterMode must be either "OR" or "AND"');
323
- }
324
-
325
- // Search keys
326
- if (!Array.isArray(this.options.searchKeys) || this.options.searchKeys.length === 0) {
327
- throw new Error('searchKeys must be a non-empty array');
328
- }
329
-
330
- // Counter validation
331
- if (this.options.counter) {
332
- if (typeof this.options.counter.template !== 'string') {
333
- throw new Error('counter.template must be a string');
334
- }
335
- if (typeof this.options.counter.showFiltered !== 'boolean') {
336
- this.options.counter.showFiltered = true; // Set default
337
- }
338
- if (typeof this.options.counter.formatter !== 'function') {
339
- this.options.counter.formatter = num => num.toLocaleString(); // Set default
340
- }
341
- } else {
342
- this.options.counter = {
343
- ...Options.defaults.counter
344
- }; // Set defaults if missing
345
- }
346
- }
347
-
348
- /**
349
- * Get option value
350
- * @param {string} path - Dot notation path to option
351
- * @returns {any} Option value
352
- */
353
- get(path) {
354
- return path.split('.').reduce((obj, key) => obj?.[key], this.options);
355
- }
356
-
357
- /**
358
- * Set option value
359
- * @param {string} path - Dot notation path to option
360
- * @param {any} value - New value
361
- */
362
- set(path, value) {
363
- const parts = path.split('.');
364
- const last = parts.pop();
365
- const target = parts.reduce((obj, key) => {
366
- if (!(key in obj)) obj[key] = {};
367
- return obj[key];
368
- }, this.options);
369
- target[last] = value;
370
- this.validate();
371
- }
372
-
373
- /**
374
- * Update multiple options
375
- * @param {Object} updates - Options to update
376
- */
377
- update(updates) {
378
- this.options = this.mergeOptions(this.options, updates);
379
- this.validate();
380
- }
381
-
382
- /**
383
- * Reset options to defaults
384
- */
385
- reset() {
386
- this.options = {
387
- ...Options.defaults
388
- };
389
- }
390
-
391
- /**
392
- * Export options
393
- * @returns {Object} Current options
394
- */
395
- export() {
396
- return {
397
- ...this.options
398
- };
399
- }
400
- }
401
-
402
- /**
403
- * @fileoverview State management for AFS
404
- */
405
-
406
- class State {
407
- constructor() {
408
- this.state = {
409
- filters: {
410
- current: new Set(['*']),
411
- groups: new Map(),
412
- ranges: new Map(),
413
- dateRanges: new Map(),
414
- mode: 'OR',
415
- groupMode: 'OR'
416
- },
417
- search: {
418
- query: '',
419
- keys: ['title']
420
- },
421
- sort: {
422
- orders: {},
423
- current: null
424
- },
425
- items: {
426
- visible: new Set(),
427
- total: 0
428
- },
429
- pagination: {
430
- currentPage: 1,
431
- itemsPerPage: 10,
432
- totalPages: 0
433
- }
434
- };
435
- }
436
-
437
- /**
438
- * Get current state
439
- * @returns {Object} Current state
440
- */
441
- getState() {
442
- return this.state;
443
- }
444
-
445
- /**
446
- * Update state
447
- * @param {string} path - Dot notation path to update
448
- * @param {any} value - New value
449
- */
450
- setState(path, value) {
451
- const parts = path.split('.');
452
- let current = this.state;
453
- for (let i = 0; i < parts.length - 1; i++) {
454
- if (!(parts[i] in current)) {
455
- current[parts[i]] = {};
456
- }
457
- current = current[parts[i]];
458
- }
459
- current[parts[parts.length - 1]] = value;
460
- }
461
-
462
- /**
463
- * Export state
464
- * @returns {Object} Exportable state
465
- */
466
- export() {
467
- return {
468
- filters: {
469
- current: Array.from(this.state.filters.current),
470
- groups: Array.from(this.state.filters.groups.entries()),
471
- ranges: Array.from(this.state.filters.ranges.entries()),
472
- dateRanges: Array.from(this.state.filters.dateRanges.entries()),
473
- mode: this.state.filters.mode,
474
- groupMode: this.state.filters.groupMode
475
- },
476
- search: {
477
- ...this.state.search
478
- },
479
- sort: {
480
- ...this.state.sort
481
- },
482
- pagination: {
483
- ...this.state.pagination
484
- }
485
- };
486
- }
487
-
488
- /**
489
- * Import state
490
- * @param {Object} importedState - State to import
491
- */
492
- import(importedState) {
493
- if (importedState.filters) {
494
- this.state.filters.current = new Set(importedState.filters.current);
495
- this.state.filters.groups = new Map(importedState.filters.groups);
496
- this.state.filters.ranges = new Map(importedState.filters.ranges);
497
- this.state.filters.dateRanges = new Map(importedState.filters.dateRanges);
498
- this.state.filters.mode = importedState.filters.mode;
499
- this.state.filters.groupMode = importedState.filters.groupMode;
500
- }
501
- if (importedState.search) {
502
- this.state.search = {
503
- ...importedState.search
504
- };
505
- }
506
- if (importedState.sort) {
507
- this.state.sort = {
508
- ...importedState.sort
509
- };
510
- }
511
- if (importedState.pagination) {
512
- this.state.pagination = {
513
- ...importedState.pagination
514
- };
515
- }
516
- }
517
-
518
- /**
519
- * Reset state to initial values
520
- */
521
- reset() {
522
- this.state = {
523
- filters: {
524
- current: new Set(['*']),
525
- groups: new Map(),
526
- ranges: new Map(),
527
- dateRanges: new Map(),
528
- mode: 'OR',
529
- groupMode: 'OR'
530
- },
531
- search: {
532
- query: '',
533
- keys: ['title']
534
- },
535
- sort: {
536
- orders: {},
537
- current: null
538
- },
539
- items: {
540
- visible: new Set(),
541
- total: 0
542
- },
543
- pagination: {
544
- currentPage: 1,
545
- itemsPerPage: 10,
546
- totalPages: 0
547
- }
548
- };
549
- }
550
- }
551
-
552
- /**
553
- * @fileoverview Style management for AFS
554
- */
555
-
556
- class StyleManager {
557
- /**
558
- * @param {import('../core/Options').Options} options - Options instance
559
- */
560
- constructor(options) {
561
- this.options = options;
562
- this.styleElement = null;
563
- }
564
-
565
- /**
566
- * Create base styles
567
- * @private
568
- * @returns {string} CSS styles
569
- */
570
- createBaseStyles() {
571
- const hiddenClass = this.options.get("hiddenClass") || "hidden";
572
- const itemSelector = this.options.get("itemSelector") || ".afs-filter-item";
573
- const filterButtonSelector = this.options.get("filterButtonSelector") || ".afs-btn-filter";
574
- const activeClass = this.options.get("activeClass") || "active";
575
- const animationDuration = this.options.get("animationDuration") || '300ms';
576
- const animationEasing = this.options.get("animationEasing") || 'ease-out';
577
- return `
578
- .${hiddenClass} {
579
- display: none !important;
580
- }
581
-
582
- ${itemSelector} {
583
- opacity: 1;
584
- transform: scale(1);
585
- filter: blur(0);
586
- transition: opacity ${animationDuration} ${animationEasing},
587
- transform ${animationDuration} ${animationEasing},
588
- filter ${animationDuration} ${animationEasing};
589
- }
590
-
591
- ${itemSelector}.${hiddenClass} {
592
- opacity: 0;
593
- transform: scale(0.95);
594
- filter: blur(5px);
595
- }
596
-
597
- ${filterButtonSelector} {
598
- opacity: 0.5;
599
- transition: opacity ${animationDuration} ${animationEasing};
600
- }
601
-
602
- ${filterButtonSelector}.${activeClass} {
603
- opacity: 1;
604
- }
605
- `;
606
- }
607
-
608
- /**
609
- * Create range slider styles
610
- * @private
611
- * @returns {string} CSS styles
612
- */
613
- createRangeStyles() {
614
- const styles = this.options.get("styles");
615
- const sliderOptions = this.options.get("slider") || {};
616
- const sliderStyles = styles.slider;
617
- const colors = styles.colors;
618
- const containerClass = sliderOptions.containerClass || "afs-range-slider";
619
- const trackClass = sliderOptions.trackClass || "afs-range-track";
620
- const thumbClass = sliderOptions.thumbClass || "afs-range-thumb";
621
- const valueClass = sliderOptions.valueClass || "afs-range-value";
622
- const selectedClass = sliderOptions.selectedClass || "afs-range-selected";
623
- return `
624
- /* Range Slider Styles */
625
- .${containerClass} {
626
- position: relative;
627
- width: auto;
628
- height: 40px;
629
- margin: 10px 0;
630
- padding: 0 8px;
631
- }
632
-
633
- .${trackClass} {
634
- position: absolute;
635
- top: 50%;
636
- transform: translateY(-50%);
637
- width: 100%;
638
- height: 4px;
639
- background: ${sliderStyles.ui.thumb.background || colors.background};
640
- border-radius: ${sliderStyles.ui.track.radius || "2px"};
641
- }
642
-
643
- .${thumbClass} {
644
- position: absolute;
645
- top: 50%;
646
- width: ${sliderStyles.ui.thumb.size || "16px"};
647
- height: ${sliderStyles.ui.thumb.size || "16px"};
648
- background: ${sliderStyles.ui.thumb.background || colors.primary};
649
- border-radius: ${sliderStyles.ui.thumb.radius || "50%"};
650
- transform: translate(-50%, -50%);
651
- cursor: pointer;
652
- z-index: 2;
653
- }
654
-
655
- .${valueClass} {
656
- position: absolute;
657
- top: -20px;
658
- transform: translateX(-50%);
659
- font-size: 10px;
660
- color: ${colors.text};
661
- }
662
-
663
- .${selectedClass} {
664
- position: absolute;
665
- height: 4px;
666
- background: ${colors.primary};
667
- top: 50%;
668
- transform: translateY(-50%);
669
- }
670
-
671
- /* Histogram Styles */
672
- .afs-histogram {
673
- position: absolute;
674
- bottom: 22px;
675
- left: 8px;
676
- right: 8px;
677
- height: 20px;
678
- display: flex;
679
- align-items: flex-end;
680
- gap: 1px;
681
- opacity: 0.5;
682
- }
683
-
684
- .afs-histogram-bar {
685
- flex: 1;
686
- background-color: ${sliderStyles.ui.histogram.background || colors.background};
687
- min-height: 4px;
688
- transition: background-color 0.2s ease;
689
- }
690
-
691
- .afs-histogram-bar.active {
692
- background-color: ${sliderStyles.ui.histogram.bar.background || colors.primary};
693
- }
694
- `;
695
- }
696
-
697
- /**
698
- * Create date filter styles
699
- * @private
700
- * @returns {string} CSS styles
701
- */
702
- createDateStyles() {
703
- const colors = this.options.get("styles").colors;
704
- return `
705
- .afs-date-range-container {
706
- display: flex;
707
- flex-wrap: wrap;
708
- gap: 1rem;
709
- margin: 10px 0;
710
- }
711
-
712
- .afs-date-input-wrapper {
713
- flex: 1;
714
- }
715
-
716
- .afs-date-input-wrapper label {
717
- display: block;
718
- font-size: 0.875rem;
719
- color: ${colors.text};
720
- margin-bottom: 0.5rem;
721
- }
722
-
723
- .afs-date-input {
724
- width: 100%;
725
- padding: 0.5rem;
726
- border: 1px solid ${colors.background};
727
- border-radius: 0.25rem;
728
- font-size: 0.875rem;
729
- color: ${colors.text};
730
- transition: border-color 0.2s ease;
731
- }
732
-
733
- .afs-date-input:focus {
734
- outline: none;
735
- border-color: ${colors.primary};
736
- }
737
- `;
738
- }
739
-
740
- /**
741
- * Apply all styles
742
- * @public
743
- */
744
- applyStyles() {
745
- try {
746
- const styles = `
747
- /* Base styles */
748
- ${this.createBaseStyles()}
749
-
750
- /* Range slider styles */
751
- ${this.createRangeStyles()}
752
-
753
- /* Date filter styles */
754
- ${this.createDateStyles()}
755
-
756
- /* Pagination styles */
757
- ${this.createPaginationStyles()}
758
-
759
- /* Search styles */
760
- ${this.createSearchStyles()}
761
- `;
762
- if (this.styleElement) {
763
- this.styleElement.textContent = styles;
764
- } else {
765
- this.styleElement = document.createElement("style");
766
- this.styleElement.textContent = styles;
767
- document.head.appendChild(this.styleElement);
768
- }
769
- } catch (error) {
770
- console.error("Error applying styles:", error);
771
- const fallbackStyles = this.createBaseStyles();
772
- if (this.styleElement) {
773
- this.styleElement.textContent = fallbackStyles;
774
- } else {
775
- this.styleElement = document.createElement("style");
776
- this.styleElement.textContent = fallbackStyles;
777
- document.head.appendChild(this.styleElement);
778
- }
779
- }
780
- }
781
-
782
- /**
783
- * Create pagination styles
784
- * @private
785
- * @returns {string} CSS styles
786
- */
787
- createPaginationStyles() {
788
- const styles = this.options.get("styles");
789
- const paginationOptions = this.options.get("pagination") || {};
790
- const colors = this.options.get("styles").colors;
791
- const containerClass = paginationOptions.containerClass || "afs-pagination";
792
- const buttonClass = paginationOptions.pageButtonClass || "afs-page-button";
793
- const activeClass = paginationOptions.activePageClass || "afs-page-active";
794
- const paginationStyles = styles.pagination;
795
- return `
796
- .${containerClass} {
797
- display: flex;
798
- justify-content: center;
799
- gap: 8px;
800
- margin-top: 20px;
801
- }
802
-
803
- .${buttonClass} {
804
- padding: ${paginationStyles.ui.button.padding || '8px 12px'};
805
- border: ${paginationStyles.ui.button.border || '1px solid ' + colors.primary};
806
- border-radius: ${paginationStyles.ui.button.borderRadius || '4px'};
807
- cursor: pointer;
808
- transition: all 200ms ease-out;
809
- background: ${paginationStyles.ui.button.background || 'transparent'};
810
- color: ${paginationStyles.ui.button.color || colors.primary};
811
- }
812
-
813
- .${buttonClass}:hover {
814
- background: ${paginationStyles.ui.button.hover.background || colors.primary};
815
- color: ${paginationStyles.ui.button.hover.color || 'white'};
816
- }
817
-
818
- .${buttonClass}.${activeClass} {
819
- background: ${paginationStyles.ui.button.active.background || colors.primary};
820
- color: ${paginationStyles.ui.button.active.color || 'white'};
821
- }
822
-
823
- .${buttonClass}:disabled {
824
- opacity: 0.5;
825
- cursor: not-allowed;
826
- }
827
- `;
828
- }
829
-
830
- /**
831
- * Create search styles
832
- * @private
833
- * @returns {string} CSS styles
834
- */
835
- createSearchStyles() {
836
- const searchClass = this.options.get("searchInputClass") || "afs-search";
837
- const colors = this.options.get("styles").colors;
838
- return `
839
- .${searchClass} {
840
- padding: 8px;
841
- border: 1px solid ${colors.background};
842
- border-radius: 4px;
843
- width: 100%;
844
- max-width: 300px;
845
- transition: border-color 200ms ease-out;
846
- }
847
-
848
- .${searchClass}:focus {
849
- outline: none;
850
- border-color: ${colors.primary};
851
- }
852
- `;
853
- }
854
-
855
- /**
856
- * Update styles
857
- * @public
858
- * @param {Object} newOptions - New style options
859
- */
860
- updateStyles(newOptions) {
861
- this.options = newOptions;
862
- this.applyStyles();
863
- }
864
-
865
- /**
866
- * Remove styles
867
- * @public
868
- */
869
- removeStyles() {
870
- if (this.styleElement) {
871
- this.styleElement.remove();
872
- this.styleElement = null;
873
- }
874
- }
875
- }
876
-
877
- /**
878
- * @fileoverview Event handling system for AFS
879
- */
880
-
881
- class EventEmitter {
882
- constructor() {
883
- this.events = new Map();
884
- this.onceEvents = new Map();
885
- }
886
-
887
- /**
888
- * Subscribe to an event
889
- * @param {string} eventName - Name of the event
890
- * @param {Function} callback - Callback function
891
- * @returns {Function} Unsubscribe function
892
- */
893
- on(eventName, callback) {
894
- if (!this.events.has(eventName)) {
895
- this.events.set(eventName, new Set());
896
- }
897
- this.events.get(eventName).add(callback);
898
-
899
- // Return unsubscribe function
900
- return () => this.off(eventName, callback);
901
- }
902
-
903
- /**
904
- * Subscribe to an event once
905
- * @param {string} eventName - Name of the event
906
- * @param {Function} callback - Callback function
907
- * @returns {Function} Unsubscribe function
908
- */
909
- once(eventName, callback) {
910
- var _this = this;
911
- const onceWrapper = function () {
912
- _this.off(eventName, onceWrapper);
913
- for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
914
- args[_key] = arguments[_key];
915
- }
916
- callback.apply(_this, args);
917
- };
918
- if (!this.onceEvents.has(eventName)) {
919
- this.onceEvents.set(eventName, new Map());
920
- }
921
- this.onceEvents.get(eventName).set(callback, onceWrapper);
922
- return this.on(eventName, onceWrapper);
923
- }
924
-
925
- /**
926
- * Unsubscribe from an event
927
- * @param {string} eventName - Name of the event
928
- * @param {Function} callback - Callback function
929
- */
930
- off(eventName, callback) {
931
- // Remove from regular events
932
- if (this.events.has(eventName)) {
933
- this.events.get(eventName).delete(callback);
934
-
935
- // Cleanup if no more listeners
936
- if (this.events.get(eventName).size === 0) {
937
- this.events.delete(eventName);
938
- }
939
- }
940
-
941
- // Remove from once events
942
- if (this.onceEvents.has(eventName)) {
943
- const onceWrapper = this.onceEvents.get(eventName).get(callback);
944
- if (onceWrapper) {
945
- this.events.get(eventName)?.delete(onceWrapper);
946
- this.onceEvents.get(eventName).delete(callback);
947
- }
948
-
949
- // Cleanup if no more once listeners
950
- if (this.onceEvents.get(eventName).size === 0) {
951
- this.onceEvents.delete(eventName);
952
- }
953
- }
954
- }
955
-
956
- /**
957
- * Emit an event
958
- * @param {string} eventName - Name of the event
959
- * @param {...any} args - Arguments to pass to callbacks
960
- */
961
- emit(eventName) {
962
- for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
963
- args[_key2 - 1] = arguments[_key2];
964
- }
965
- if (this.events.has(eventName)) {
966
- this.events.get(eventName).forEach(callback => {
967
- try {
968
- callback.apply(this, args);
969
- } catch (error) {
970
- console.error(`Error in event handler for ${eventName}:`, error);
971
- }
972
- });
973
- }
974
- }
975
-
976
- /**
977
- * Remove all event listeners
978
- * @param {string} [eventName] - Optional event name to clear specific event
979
- */
980
- removeAllListeners(eventName) {
981
- if (eventName) {
982
- this.events.delete(eventName);
983
- this.onceEvents.delete(eventName);
984
- } else {
985
- this.events.clear();
986
- this.onceEvents.clear();
987
- }
988
- }
989
-
990
- /**
991
- * Get the number of listeners for an event
992
- * @param {string} eventName - Name of the event
993
- * @returns {number} Number of listeners
994
- */
995
- listenerCount(eventName) {
996
- return (this.events.get(eventName)?.size || 0) + (this.onceEvents.get(eventName)?.size || 0);
997
- }
998
- }
999
-
1000
- /**
1001
- * @fileoverview Animation management for AFS
1002
- */
1003
-
1004
- class Animation {
1005
- constructor(afs) {
1006
- this.afs = afs;
1007
- this.options = this.afs.options;
1008
- this.animations = {
1009
- fade: {
1010
- in: {
1011
- opacity: 1,
1012
- transform: 'scale(1)',
1013
- transitionTimingFunction: 'ease-in'
1014
- },
1015
- out: {
1016
- opacity: 0,
1017
- transform: 'scale(0.95)',
1018
- transitionTimingFunction: 'ease-out'
1019
- }
1020
- },
1021
- slide: {
1022
- in: {
1023
- opacity: 1,
1024
- transform: 'translateY(0)',
1025
- transitionTimingFunction: 'ease-in-out'
1026
- },
1027
- out: {
1028
- opacity: 0,
1029
- transform: 'translateY(20px)',
1030
- transitionTimingFunction: 'ease-in-out'
1031
- }
1032
- },
1033
- scale: {
1034
- in: {
1035
- opacity: 1,
1036
- transform: 'scale(1)',
1037
- transitionTimingFunction: 'ease-in'
1038
- },
1039
- out: {
1040
- opacity: 0,
1041
- transform: 'scale(0.8)',
1042
- transitionTimingFunction: 'ease-out'
1043
- }
1044
- },
1045
- rotate: {
1046
- in: {
1047
- opacity: 1,
1048
- transform: 'rotate(0deg) scale(1)',
1049
- transitionTimingFunction: 'ease-in'
1050
- },
1051
- out: {
1052
- opacity: 0,
1053
- transform: 'rotate(90deg) scale(0.9)',
1054
- transitionTimingFunction: 'ease-out'
1055
- }
1056
- },
1057
- flip: {
1058
- in: {
1059
- opacity: 1,
1060
- transform: 'rotateY(0)',
1061
- transitionTimingFunction: 'ease-in'
1062
- },
1063
- out: {
1064
- opacity: 0,
1065
- transform: 'rotateY(180deg)',
1066
- transitionTimingFunction: 'ease-out'
1067
- }
1068
- },
1069
- zoom: {
1070
- in: {
1071
- opacity: 1,
1072
- transform: 'scale(1.2)',
1073
- transitionTimingFunction: 'ease-in'
1074
- },
1075
- out: {
1076
- opacity: 0,
1077
- transform: 'scale(0.8)',
1078
- transitionTimingFunction: 'ease-out'
1079
- }
1080
- },
1081
- bounce: {
1082
- in: {
1083
- opacity: 1,
1084
- transform: 'translateY(0)',
1085
- animation: 'bounce 1s cubic-bezier(0.68, -0.55, 0.27, 1.55)'
1086
- },
1087
- out: {
1088
- opacity: 0,
1089
- transform: 'translateY(-20px)',
1090
- animation: 'bounceOut 1s ease-out'
1091
- }
1092
- },
1093
- blur: {
1094
- in: {
1095
- opacity: 1,
1096
- filter: 'blur(0)',
1097
- transitionTimingFunction: 'ease-in'
1098
- },
1099
- out: {
1100
- opacity: 0,
1101
- filter: 'blur(5px)',
1102
- transitionTimingFunction: 'ease-out'
1103
- }
1104
- },
1105
- skew: {
1106
- in: {
1107
- opacity: 1,
1108
- transform: 'skew(0deg)',
1109
- transitionTimingFunction: 'ease-in-out'
1110
- },
1111
- out: {
1112
- opacity: 0,
1113
- transform: 'skew(10deg)',
1114
- transitionTimingFunction: 'ease-in-out'
1115
- }
1116
- },
1117
- slideInLeft: {
1118
- in: {
1119
- opacity: 1,
1120
- transform: 'translateX(0)',
1121
- transitionTimingFunction: 'ease-in'
1122
- },
1123
- out: {
1124
- opacity: 0,
1125
- transform: 'translateX(-100%)',
1126
- transitionTimingFunction: 'ease-out'
1127
- }
1128
- },
1129
- slideInRight: {
1130
- in: {
1131
- opacity: 1,
1132
- transform: 'translateX(0)',
1133
- transitionTimingFunction: 'ease-in'
1134
- },
1135
- out: {
1136
- opacity: 0,
1137
- transform: 'translateX(100%)',
1138
- transitionTimingFunction: 'ease-out'
1139
- }
1140
- },
1141
- fadeInUp: {
1142
- in: {
1143
- opacity: 1,
1144
- transform: 'translateY(0)',
1145
- transitionTimingFunction: 'ease-in'
1146
- },
1147
- out: {
1148
- opacity: 0,
1149
- transform: 'translateY(10px)',
1150
- transitionTimingFunction: 'ease-out'
1151
- }
1152
- },
1153
- fadeInDown: {
1154
- in: {
1155
- opacity: 1,
1156
- transform: 'translateY(0)',
1157
- transitionTimingFunction: 'ease-in'
1158
- },
1159
- out: {
1160
- opacity: 0,
1161
- transform: 'translateY(-10px)',
1162
- transitionTimingFunction: 'ease-out'
1163
- }
1164
- },
1165
- bounceIn: {
1166
- in: {
1167
- opacity: 1,
1168
- transform: 'scale(1.05)',
1169
- transitionTimingFunction: 'cubic-bezier(0.68, -0.55, 0.27, 1.55)'
1170
- },
1171
- out: {
1172
- opacity: 0,
1173
- transform: 'scale(0.9)',
1174
- transitionTimingFunction: 'ease-out'
1175
- }
1176
- }
1177
- };
1178
- }
1179
- applyShowAnimation(item) {
1180
- let animationType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'fade';
1181
- const duration = this.options.get('animationDuration') || '300ms';
1182
- const animation = this.animations[animationType]?.in || this.animations.fade.in;
1183
- item.style.display = 'block';
1184
- requestAnimationFrame(() => {
1185
- Object.assign(item.style, animation, {
1186
- transition: `opacity ${duration} ${animation.transitionTimingFunction}, transform ${duration} ${animation.transitionTimingFunction}`
1187
- });
1188
- });
1189
- }
1190
- applyHideAnimation(item) {
1191
- let animationType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'fade';
1192
- const duration = this.options.get('animationDuration') || '300ms';
1193
- const animation = this.animations[animationType]?.out || this.animations.fade.out;
1194
- Object.assign(item.style, animation, {
1195
- transition: `opacity ${duration} ${animation.transitionTimingFunction}, transform ${duration} ${animation.transitionTimingFunction}`
1196
- });
1197
- const handleTransitionEnd = () => {
1198
- item.style.display = 'none';
1199
- item.removeEventListener('transitionend', handleTransitionEnd);
1200
- };
1201
- item.addEventListener('transitionend', handleTransitionEnd);
1202
- }
1203
- setAnimation(animationType) {
1204
- this.afs.options.set('animationType', animationType);
1205
- }
1206
- }
1207
-
1208
- /**
1209
- * @fileoverview Filter functionality for AFS
1210
- */
1211
-
1212
- class Filter {
1213
- /**
1214
- * @param {import('../AFS').AFS} afs - Main AFS instance
1215
- */
1216
- constructor(afs) {
1217
- this.afs = afs;
1218
- this.animation = new Animation(afs);
1219
- this.filterButtons = new Map();
1220
- this.activeFilters = new Set(["*"]);
1221
- this.currentFilters = new Set(["*"]);
1222
- this.filterGroups = new Map();
1223
- this.sortOrders = new Map();
1224
- this.setupFilters();
1225
- }
1226
-
1227
- /**
1228
- * Setup filters
1229
- * @private
1230
- */
1231
- setupFilters() {
1232
- this.afs.logger.debug("Setting up filters");
1233
- const filterSelector = this.afs.options.get("filterButtonSelector");
1234
- if (!filterSelector) return;
1235
-
1236
- // Initialize filter buttons
1237
- document.querySelectorAll(filterSelector).forEach(button => {
1238
- const filterValue = button.dataset.filter;
1239
- if (!filterValue) {
1240
- this.afs.logger.warn("Filter button missing data-filter attribute:", button);
1241
- return;
1242
- }
1243
- this.filterButtons.set(button, filterValue);
1244
- this.bindFilterEvent(button);
1245
- });
1246
- this.afs.logger.debug("Filters initialized");
1247
- }
1248
-
1249
- /**
1250
- * Set filter logic mode
1251
- * @public
1252
- * @param {string|boolean} logic - 'AND'/'OR' or true/false (true = AND, false = OR)
1253
- */
1254
- setLogic(logic) {
1255
- if (typeof logic === "boolean") {
1256
- // Handle boolean input (true = AND, false = OR)
1257
- this.afs.options.set("filterMode", logic ? "AND" : "OR");
1258
- } else {
1259
- const mode = logic.toUpperCase();
1260
- if (["OR", "AND"].includes(mode)) {
1261
- this.afs.options.set("filterMode", mode);
1262
- } else {
1263
- this.afs.logger.warn(`Invalid filter mode: ${logic}`);
1264
- return;
1265
- }
1266
- }
1267
- this.afs.logger.debug(`Filter logic set to: ${this.afs.options.get("filterMode")}`);
1268
- this.applyFilters();
1269
- }
1270
-
1271
- /**
1272
- * Clear all filters
1273
- * @public
1274
- */
1275
- clearAllFilters() {
1276
- this.afs.logger.debug("Clearing all filters");
1277
-
1278
- // Reset filters
1279
- this.activeFilters.clear();
1280
- this.activeFilters.add("*");
1281
-
1282
- // Reset filter buttons
1283
- this.filterButtons.forEach((_, button) => {
1284
- button.classList.remove(this.afs.options.get("activeClass"));
1285
- });
1286
-
1287
- // Reset filter groups
1288
- this.filterGroups.clear();
1289
-
1290
- // Find and activate "all" button if exists
1291
- const allButton = this.findAllButton();
1292
- if (allButton) {
1293
- allButton.classList.add(this.afs.options.get("activeClass"));
1294
- }
1295
-
1296
- // Clear sorting
1297
- this.sortOrders.clear();
1298
-
1299
- // Apply changes and update UI
1300
- this.applyFilters();
1301
- this.afs.urlManager.updateURL();
1302
- this.afs.emit("filtersCleared");
1303
- }
1304
-
1305
- /**
1306
- * Bind filter event to button
1307
- * @private
1308
- * @param {HTMLElement} button - Filter button
1309
- */
1310
- bindFilterEvent(button) {
1311
- this.afs.logger.debug("Binding filter event to button:", button);
1312
- button.addEventListener("click", () => {
1313
- const filterValue = this.filterButtons.get(button);
1314
- if (!filterValue) return;
1315
- if (filterValue === "*") {
1316
- this.resetFilters();
1317
- } else {
1318
- this.toggleFilter(filterValue, button);
1319
- }
1320
-
1321
- // Update the URL after filter change
1322
- this.afs.urlManager.updateURL();
1323
- });
1324
- }
1325
-
1326
- /**
1327
- * Reset filters to default state
1328
- * @public
1329
- */
1330
- resetFilters() {
1331
- this.afs.logger.debug("Resetting filters");
1332
-
1333
- // Clear existing filters
1334
- this.activeFilters.clear();
1335
-
1336
- // Reset button states
1337
- this.filterButtons.forEach((_, button) => {
1338
- button.classList.remove(this.afs.options.get("activeClass"));
1339
- });
1340
-
1341
- // Add "*" filter and activate "all" button
1342
- this.activeFilters.add("*");
1343
- const allButton = this.findAllButton();
1344
- if (allButton) {
1345
- allButton.classList.add(this.afs.options.get("activeClass"));
1346
- }
1347
-
1348
- // Clear filter groups
1349
- this.filterGroups.clear();
1350
-
1351
- // Force all items to be visible first
1352
- const visibleItems = new Set(this.afs.items);
1353
- this.afs.state.setState("items.visible", visibleItems);
1354
-
1355
- // Show all items with animation
1356
- this.afs.items.forEach(item => {
1357
- item.classList.remove(this.afs.options.get('hiddenClass'));
1358
- requestAnimationFrame(() => {
1359
- this.animation.applyShowAnimation(item, this.afs.options.get("animationType"));
1360
- });
1361
- });
1362
-
1363
- // Update counter after reset
1364
- this.afs.updateCounter();
1365
-
1366
- // Update URL after reset
1367
- this.afs.urlManager.updateURL();
1368
-
1369
- // Emit event
1370
- this.afs.emit("filtersReset");
1371
- }
1372
-
1373
- /**
1374
- * Find "all" filter button
1375
- * @private
1376
- * @returns {HTMLElement|null} All button
1377
- */
1378
- findAllButton() {
1379
- for (const [button, value] of this.filterButtons.entries()) {
1380
- if (value === "*") return button;
1381
- }
1382
- return null;
1383
- }
1384
- handleFilterClick(button) {
1385
- const filterValue = button.dataset.filter;
1386
- this.afs.logger.debug("Filter clicked:", filterValue);
1387
- if (filterValue === "*") {
1388
- this.resetFilters();
1389
- } else {
1390
- this.toggleFilter(filterValue, button);
1391
- }
1392
- this.filter();
1393
-
1394
- // Call updateURL to reflect the new filter state in the URL
1395
- this.updateURL();
1396
- }
1397
-
1398
- /**
1399
- * Toggle filter state
1400
- * @private
1401
- * @param {string} filterValue - Filter value
1402
- * @param {HTMLElement} button - Filter button
1403
- */
1404
- toggleFilter(filterValue, button) {
1405
- // Remove "all" filter
1406
- this.activeFilters.delete("*");
1407
- const allButton = this.findAllButton();
1408
- if (allButton) {
1409
- allButton.classList.remove(this.afs.options.get("activeClass"));
1410
- }
1411
-
1412
- // Toggle filter
1413
- if (button.classList.contains(this.afs.options.get("activeClass"))) {
1414
- button.classList.remove(this.afs.options.get("activeClass"));
1415
- this.activeFilters.delete(filterValue);
1416
-
1417
- // Reset to "all" if no filters active
1418
- if (this.activeFilters.size === 0) {
1419
- this.resetFilters();
1420
- return;
1421
- }
1422
- } else {
1423
- button.classList.add(this.afs.options.get("activeClass"));
1424
- this.activeFilters.add(filterValue);
1425
- }
1426
- this.applyFilters();
1427
-
1428
- // Emit event
1429
- this.afs.emit("filterToggled", {
1430
- filter: filterValue,
1431
- activeFilters: Array.from(this.activeFilters)
1432
- });
1433
- this.afs.logger.debug("Filter toggled:", filterValue);
1434
- }
1435
-
1436
- /**
1437
- * Apply current filters
1438
- * @public
1439
- */
1440
- applyFilters() {
1441
- // Log active filters
1442
- const activeFilters = Array.from(this.activeFilters);
1443
- this.afs.logger.debug("Active filters:", activeFilters);
1444
- this.afs.logger.debug("Applying filters");
1445
- const previouslyVisible = new Set(this.afs.state.getState().items.visible);
1446
- const visibleItems = new Set();
1447
-
1448
- // First pass: determine which items should be visible
1449
- this.afs.items.forEach(item => {
1450
- if (this.activeFilters.has("*") || this.itemMatchesFilters(item)) {
1451
- visibleItems.add(item);
1452
- }
1453
- });
1454
-
1455
- // Update state with determined visible items
1456
- this.afs.state.setState("items.visible", visibleItems);
1457
-
1458
- // Second pass: apply animations based on visibility changes
1459
- this.afs.items.forEach(item => {
1460
- if (visibleItems.has(item)) {
1461
- // Remove hidden class first
1462
- item.classList.remove(this.afs.options.get('hiddenClass'));
1463
- // Then apply show animation in next frame
1464
- requestAnimationFrame(() => {
1465
- this.animation.applyShowAnimation(item, this.afs.options.get("animationType"));
1466
- });
1467
- } else {
1468
- // Apply hide animation
1469
- requestAnimationFrame(() => {
1470
- this.animation.applyHideAnimation(item, this.afs.options.get("animationType"));
1471
- });
1472
- }
1473
- });
1474
-
1475
- // Emit events and update UI
1476
- this.emitFilterEvents(previouslyVisible, visibleItems);
1477
-
1478
- // Schedule UI updates
1479
- requestAnimationFrame(() => {
1480
- // Force reflow to ensure animations play
1481
- this.afs.container.offsetHeight;
1482
-
1483
- // Update counter - this needs to happen after filter changes
1484
- this.afs.updateCounter();
1485
-
1486
- // Update URL
1487
- this.afs.urlManager.updateURL();
1488
-
1489
- // Emit final event
1490
- this.afs.emit("filtersApplied", {
1491
- activeFilters,
1492
- visibleItems: visibleItems.size
1493
- });
1494
- });
1495
- }
1496
-
1497
- /**
1498
- * Check if item matches current filters
1499
- * @private
1500
- * @param {HTMLElement} item - DOM element
1501
- * @returns {boolean} Whether item matches filters
1502
- */
1503
- itemMatchesFilters(item) {
1504
- // Show all items if "*" is active
1505
- if (this.activeFilters.has("*")) {
1506
- return true;
1507
- }
1508
-
1509
- // Regular filter matching
1510
- const itemCategories = new Set(item.dataset.categories?.split(" ") || []);
1511
-
1512
- // If using filter groups
1513
- if (this.filterGroups.size > 0) {
1514
- return this.itemMatchesFilterGroups(itemCategories);
1515
- }
1516
-
1517
- // Regular filtering
1518
- return this.afs.options.get("filterMode") === "OR" ? this.itemMatchesAnyFilter(itemCategories) : this.itemMatchesAllFilters(itemCategories);
1519
- }
1520
-
1521
- /**
1522
- * Check if item matches any active filter (OR mode)
1523
- * @private
1524
- * @param {Set} itemCategories - Item's categories
1525
- * @returns {boolean} Whether item matches any filter
1526
- */
1527
- itemMatchesAnyFilter(itemCategories) {
1528
- return Array.from(this.activeFilters).some(filter => {
1529
- if (filter === "*") return true;
1530
- return itemCategories.has(filter);
1531
- });
1532
- }
1533
-
1534
- /**
1535
- * Check if item matches all active filters (AND mode)
1536
- * @private
1537
- * @param {Set} itemCategories - Item's categories
1538
- * @returns {boolean} Whether item matches all filters
1539
- */
1540
- itemMatchesAllFilters(itemCategories) {
1541
- return Array.from(this.activeFilters).every(filter => {
1542
- if (filter === "*") return true;
1543
- return itemCategories.has(filter);
1544
- });
1545
- }
1546
-
1547
- /**
1548
- * Check if item matches filter groups
1549
- * @private
1550
- * @param {Set} itemCategories - Item's categories
1551
- * @returns {boolean} Whether item matches groups
1552
- */
1553
- itemMatchesFilterGroups(itemCategories) {
1554
- const groupMatches = Array.from(this.filterGroups.values()).map(group => {
1555
- if (group.filters.size === 0) return true;
1556
- return group.operator === "OR" ? Array.from(group.filters).some(filter => itemCategories.has(filter)) : Array.from(group.filters).every(filter => itemCategories.has(filter));
1557
- });
1558
- return this.afs.options.get("groupMode") === "OR" ? groupMatches.some(matches => matches) : groupMatches.every(matches => matches);
1559
- }
1560
-
1561
- /**
1562
- * Emit filter-related events
1563
- * @private
1564
- * @param {Set} previouslyVisible - Previously visible items
1565
- * @param {Set} nowVisible - Currently visible items
1566
- */
1567
- emitFilterEvents(previouslyVisible, nowVisible) {
1568
- // Determine added and removed items
1569
- const added = new Set([...nowVisible].filter(item => !previouslyVisible.has(item)));
1570
- const removed = new Set([...previouslyVisible].filter(item => !nowVisible.has(item)));
1571
-
1572
- // Emit filter event
1573
- this.afs.emit("filter", {
1574
- activeFilters: Array.from(this.activeFilters),
1575
- visibleItems: nowVisible.size,
1576
- added: added.size,
1577
- removed: removed.size
1578
- });
1579
-
1580
- // Emit specific events for added/removed items
1581
- if (added.size > 0) {
1582
- this.afs.emit("itemsShown", {
1583
- items: added
1584
- });
1585
- }
1586
- if (removed.size > 0) {
1587
- this.afs.emit("itemsHidden", {
1588
- items: removed
1589
- });
1590
- }
1591
- }
1592
-
1593
- /**
1594
- * Add or update a filter group
1595
- * @public
1596
- * @param {string} groupId - Group identifier
1597
- * @param {string[]} filters - Array of filter values
1598
- * @param {string} [operator='OR'] - Operator within group
1599
- */
1600
- addFilterGroup(groupId, filters) {
1601
- let operator = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "OR";
1602
- this.afs.logger.debug(`Adding filter group: ${groupId}`);
1603
- if (!Array.isArray(filters)) {
1604
- this.afs.logger.error("Filters must be an array");
1605
- return;
1606
- }
1607
- const validOperator = operator.toUpperCase();
1608
- if (!["AND", "OR"].includes(validOperator)) {
1609
- this.afs.logger.warn(`Invalid operator: ${operator}, defaulting to OR`);
1610
- operator = "OR";
1611
- }
1612
- this.filterGroups.set(groupId, {
1613
- filters: new Set(filters),
1614
- operator: validOperator
1615
- });
1616
- this.applyFilters();
1617
- }
1618
-
1619
- /**
1620
- * Remove a filter group
1621
- * @public
1622
- * @param {string} groupId - Group identifier
1623
- */
1624
- removeFilterGroup(groupId) {
1625
- if (this.filterGroups.delete(groupId)) {
1626
- this.afs.logger.debug(`Removed filter group: ${groupId}`);
1627
- if (this.filterGroups.size === 0) {
1628
- this.resetFilters();
1629
- } else {
1630
- this.applyFilters();
1631
- }
1632
- }
1633
- }
1634
-
1635
- /**
1636
- * Set filter group mode
1637
- * @public
1638
- * @param {string} mode - Mode for combining groups
1639
- */
1640
- setGroupMode(mode) {
1641
- const validMode = mode.toUpperCase();
1642
- if (["AND", "OR"].includes(validMode)) {
1643
- this.afs.options.set("groupMode", validMode);
1644
- this.afs.logger.debug(`Set group mode to: ${validMode}`);
1645
- this.applyFilters();
1646
- } else {
1647
- this.afs.logger.warn(`Invalid group mode: ${mode}`);
1648
- }
1649
- }
1650
-
1651
- /**
1652
- * Add filter manually
1653
- * @public
1654
- * @param {string} filter - Filter value
1655
- */
1656
- addFilter(filter) {
1657
- this.afs.logger.debug(`Adding filter: ${filter}`);
1658
- if (filter === "*") {
1659
- this.resetFilters();
1660
- return;
1661
- }
1662
- this.activeFilters.delete("*");
1663
- this.activeFilters.add(filter);
1664
-
1665
- // Update button states
1666
- this.filterButtons.forEach((value, button) => {
1667
- if (value === filter) {
1668
- button.classList.add(this.afs.options.get("activeClass"));
1669
- } else if (value === "*") {
1670
- button.classList.remove(this.afs.options.get("activeClass"));
1671
- }
1672
- });
1673
- this.applyFilters();
1674
- }
1675
-
1676
- /**
1677
- * Remove filter manually
1678
- * @public
1679
- * @param {string} filter - Filter value
1680
- */
1681
- removeFilter(filter) {
1682
- this.afs.logger.debug(`Removing filter: ${filter}`);
1683
- this.activeFilters.delete(filter);
1684
-
1685
- // Update button states
1686
- this.filterButtons.forEach((value, button) => {
1687
- if (value === filter) {
1688
- button.classList.remove(this.afs.options.get("activeClass"));
1689
- }
1690
- });
1691
-
1692
- // Reset to all if no filters active
1693
- if (this.activeFilters.size === 0) {
1694
- this.resetFilters();
1695
- } else {
1696
- this.applyFilters();
1697
- }
1698
- }
1699
-
1700
- /**
1701
- * Set filter mode
1702
- * @public
1703
- * @param {string} mode - Filter mode ('AND' or 'OR')
1704
- */
1705
- setFilterMode(mode) {
1706
- this.afs.logger.debug(`Setting filter mode to: ${mode}`);
1707
- const validMode = mode.toUpperCase();
1708
- if (["AND", "OR"].includes(validMode)) {
1709
- this.afs.options.set("filterMode", validMode);
1710
- this.afs.logger.debug(`Set filter mode to: ${validMode}`);
1711
- this.applyFilters();
1712
- } else {
1713
- this.afs.logger.warn(`Invalid filter mode: ${mode}`);
1714
- }
1715
- }
1716
-
1717
- /**
1718
- * Get active filters
1719
- * @public
1720
- * @returns {Set} Active filters
1721
- */
1722
- getActiveFilters() {
1723
- return new Set(this.activeFilters);
1724
- }
1725
-
1726
- /**
1727
- * Get filter groups
1728
- * @public
1729
- * @returns {Map} Filter groups
1730
- */
1731
- getFilterGroups() {
1732
- return new Map(this.filterGroups);
1733
- }
1734
-
1735
- /**
1736
- * Add filter button dynamically
1737
- * @public
1738
- * @param {HTMLElement} button - Button element
1739
- * @param {string} filter - Filter value
1740
- */
1741
- addFilterButton(button, filter) {
1742
- if (!filter) {
1743
- this.afs.logger.warn("Filter value required for new filter button");
1744
- return;
1745
- }
1746
- this.filterButtons.set(button, filter);
1747
- this.bindFilterEvent(button);
1748
- this.afs.logger.debug(`Added filter button for: ${filter}`);
1749
- }
1750
-
1751
- /**
1752
- * Remove filter button
1753
- * @public
1754
- * @param {HTMLElement} button - Button to remove
1755
- */
1756
- removeFilter(filter) {
1757
- this.afs.logger.debug(`Removing filter: ${filter}`);
1758
- this.activeFilters.delete(filter);
1759
-
1760
- // Update button states
1761
- this.filterButtons.forEach((value, button) => {
1762
- if (value === filter) {
1763
- button.classList.remove(this.afs.options.get("activeClass"));
1764
- }
1765
- });
1766
-
1767
- // Reset to all if no filters active
1768
- if (this.activeFilters.size === 0) {
1769
- this.resetFilters();
1770
- } else {
1771
- this.applyFilters();
1772
- }
1773
- }
1774
-
1775
- /**
1776
- * Sort items with automatic order detection
1777
- * @public
1778
- * @param {string} key - The data attribute key to sort by
1779
- */
1780
- sortWithOrder(key) {
1781
- this.afs.logger.debug(`Sorting by ${key}`);
1782
- try {
1783
- const items = Array.from(this.afs.items);
1784
-
1785
- // Toggle sort order for the key
1786
- const currentOrder = this.sortOrders.get(key) || "asc";
1787
- const newOrder = currentOrder === "asc" ? "desc" : "asc";
1788
- this.sortOrders.set(key, newOrder);
1789
-
1790
- // Determine sort type from first item
1791
- const sortType = this.determineSortType(items[0], key);
1792
-
1793
- // Sort items
1794
- items.sort((a, b) => {
1795
- const valueA = this.getSortValue(a, key, sortType);
1796
- const valueB = this.getSortValue(b, key, sortType);
1797
- return this.compareValues(valueA, valueB, newOrder);
1798
- });
1799
-
1800
- // Reorder DOM elements
1801
- this.reorderItems(items);
1802
-
1803
- // Emit event
1804
- this.afs.emit("sort", {
1805
- key,
1806
- order: newOrder
1807
- });
1808
- this.afs.logger.info(`Sorted items by ${key} in ${newOrder} order`);
1809
- return newOrder; // Return the new order for UI updates
1810
- } catch (error) {
1811
- this.afs.logger.error("Sort error:", error);
1812
- return null;
1813
- }
1814
- }
1815
-
1816
- /**
1817
- * Shuffle items randomly
1818
- * @public
1819
- */
1820
- shuffle() {
1821
- this.afs.logger.debug("Shuffling items");
1822
- try {
1823
- const items = Array.from(this.afs.items);
1824
-
1825
- // Fisher-Yates shuffle algorithm
1826
- for (let i = items.length - 1; i > 0; i--) {
1827
- const j = Math.floor(Math.random() * (i + 1));
1828
- [items[i], items[j]] = [items[j], items[i]];
1829
- }
1830
-
1831
- // Use the existing reorderItems method to update the DOM
1832
- this.reorderItems(items);
1833
-
1834
- // Clear any existing sort orders as we've shuffled
1835
- this.sortOrders.clear();
1836
- this.afs.emit("shuffled", {
1837
- itemCount: items.length
1838
- });
1839
- this.afs.logger.debug("Items shuffled successfully");
1840
- } catch (error) {
1841
- this.afs.logger.error("Shuffle error:", error);
1842
- }
1843
- }
1844
-
1845
- /**
1846
- * Determine sort type from item value
1847
- * @private
1848
- * @param {HTMLElement} item - DOM element
1849
- * @param {string} key - Sort key
1850
- * @returns {string} Sort type ('number', 'date', or 'string')
1851
- */
1852
- determineSortType(item, key) {
1853
- this.afs.logger.debug(`Determining sort type for ${key}`);
1854
- const value = item.dataset[key];
1855
- if (!value) return "string";
1856
- if (!isNaN(value)) return "number";
1857
- if (/^\d{4}-\d{2}-\d{2}/.test(value)) return "date";
1858
- return "string";
1859
- }
1860
-
1861
- /**
1862
- * Get sort value from item
1863
- * @private
1864
- * @param {HTMLElement} item - DOM element
1865
- * @param {string} key - Sort key
1866
- * @param {string} type - Sort type
1867
- * @returns {any} Parsed value for sorting
1868
- */
1869
- getSortValue(item, key, type) {
1870
- const value = item.dataset[key];
1871
- switch (type) {
1872
- case "number":
1873
- return parseFloat(value) || 0;
1874
- case "date":
1875
- return new Date(value).getTime() || 0;
1876
- default:
1877
- return (value || "").toLowerCase();
1878
- }
1879
- }
1880
-
1881
- /**
1882
- * Compare two values for sorting
1883
- * @private
1884
- * @param {any} a - First value
1885
- * @param {any} b - Second value
1886
- * @param {string} order - Sort order ('asc' or 'desc')
1887
- * @returns {number} Comparison result
1888
- */
1889
- compareValues(a, b, order) {
1890
- // Handle null/undefined values
1891
- if (a === null || a === undefined) return order === "asc" ? 1 : -1;
1892
- if (b === null || b === undefined) return order === "asc" ? -1 : 1;
1893
-
1894
- // Compare values
1895
- const comparison = a < b ? -1 : a > b ? 1 : 0;
1896
- return order === "asc" ? comparison : -comparison;
1897
- }
1898
-
1899
- /**
1900
- * Reorder DOM elements
1901
- * @private
1902
- * @param {HTMLElement[]} items - Sorted items
1903
- */
1904
- reorderItems(items) {
1905
- const fragment = document.createDocumentFragment();
1906
- items.forEach(item => fragment.appendChild(item));
1907
- this.afs.container.appendChild(fragment);
1908
- }
1909
-
1910
- /**
1911
- * Get current sort order for key
1912
- * @public
1913
- * @param {string} key - Sort key
1914
- * @returns {string} Current sort order ('asc' or 'desc')
1915
- */
1916
- getCurrentSortOrder(key) {
1917
- return this.sortOrders.get(key) || "asc";
1918
- }
1919
-
1920
- /**
1921
- * Clear all sort orders
1922
- * @public
1923
- */
1924
- clearSortOrders() {
1925
- this.afs.logger.debug("Clearing all sort orders");
1926
- this.sortOrders.clear();
1927
- this.afs.emit("sortCleared");
1928
- }
1929
-
1930
- /**
1931
- * Clear all filters, url and search
1932
- * @public
1933
- */
1934
- clearAllFilters() {
1935
- this.afs.logger.debug("Clearing all filters and search");
1936
-
1937
- // Reset filters
1938
- this.activeFilters.clear();
1939
- this.activeFilters.add("*");
1940
-
1941
- // Update filter buttons
1942
- this.filterButtons.forEach((_, button) => {
1943
- button.classList.remove(this.afs.options.get("activeClass"));
1944
- });
1945
-
1946
- // Find and activate "all" button if exists
1947
- const allButton = this.findAllButton();
1948
- if (allButton) {
1949
- allButton.classList.add(this.afs.options.get("activeClass"));
1950
- }
1951
-
1952
- // Clear checkboxes
1953
- const checkboxes = document.querySelectorAll('input[type="checkbox"]');
1954
- checkboxes.forEach(checkbox => {
1955
- if (checkbox.classList.contains(this.afs.options.get("activeClass"))) {
1956
- checkbox.checked = false;
1957
- checkbox.classList.remove(this.afs.options.get("activeClass"));
1958
- }
1959
- });
1960
-
1961
- // Clear search
1962
- if (this.afs.search) {
1963
- this.afs.search.setValue("");
1964
- }
1965
-
1966
- // Clear filter groups and sort orders
1967
- this.filterGroups.clear();
1968
- this.sortOrders.clear();
1969
-
1970
- // Apply changes and ensure counter is updated
1971
- this.applyFilters();
1972
-
1973
- // Update URL if URLManager exists
1974
- if (this.afs.urlManager) {
1975
- this.afs.urlManager.updateURL();
1976
- }
1977
-
1978
- // Emit event
1979
- this.afs.emit("filtersCleared");
1980
- this.afs.logger.info("All filters cleared");
1981
- }
1982
-
1983
- /**
1984
- * Refresh the view
1985
- * @public
1986
- */
1987
- refresh() {
1988
- this.afs.logger.debug("Refreshing view");
1989
- this.applyFilters();
1990
- this.afs.updateCounter();
1991
- }
1992
- removeFilterButton(button) {
1993
- this.filterButtons.delete(button);
1994
- button.removeEventListener("click", this.handleFilterClick);
1995
- }
1996
-
1997
- /**
1998
- * Destroy instance
1999
- * @public
2000
- */
2001
- destroy() {
2002
- this.filterButtons.forEach((_, button) => {
2003
- this.removeFilterButton(button);
2004
- });
2005
- this.filterButtons.clear();
2006
- this.activeFilters.clear();
2007
- this.filterGroups.clear();
2008
- this.afs.logger.debug("Filter functionality destroyed");
2009
- }
2010
- }
2011
-
2012
- /**
2013
- * @fileoverview Utility functions for AFS
2014
- */
2015
-
2016
- /**
2017
- * Debounce function
2018
- * @param {Function} func - Function to debounce
2019
- * @param {number} wait - Delay in milliseconds
2020
- * @param {boolean} [immediate=false] - Execute immediately
2021
- * @returns {Function} Debounced function
2022
- */
2023
- function debounce(func, wait) {
2024
- let immediate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
2025
- let timeout;
2026
- return function executedFunction() {
2027
- for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
2028
- args[_key] = arguments[_key];
2029
- }
2030
- const context = this;
2031
- const later = () => {
2032
- timeout = null;
2033
- if (!immediate) func.apply(context, args);
2034
- };
2035
- const callNow = immediate && !timeout;
2036
- clearTimeout(timeout);
2037
- timeout = setTimeout(later, wait);
2038
- if (callNow) func.apply(context, args);
2039
- };
2040
- }
2041
-
2042
- /**
2043
- * @fileoverview Search functionality for AFS
2044
- */
2045
-
2046
- class Search {
2047
- /**
2048
- * @param {import('../AFS').AFS} afs - Main AFS instance
2049
- */
2050
- constructor(afs) {
2051
- this.afs = afs;
2052
- this.searchInput = null;
2053
- this.searchKeys = ['title']; // Default search keys
2054
- this.minSearchLength = 2;
2055
- this.highlightClass = 'afs-highlight';
2056
- this.setupSearch();
2057
- }
2058
-
2059
- /**
2060
- * Setup search functionality
2061
- * @private
2062
- */
2063
- setupSearch() {
2064
- const searchSelector = this.afs.options.get('searchInputSelector');
2065
- if (!searchSelector) return;
2066
- this.searchInput = document.querySelector(searchSelector);
2067
- if (!this.searchInput) {
2068
- this.afs.logger.warn(`Search input not found: ${searchSelector}`);
2069
- return;
2070
- }
2071
-
2072
- // Configure search
2073
- this.searchKeys = this.afs.options.get('searchKeys') || this.searchKeys;
2074
- this.minSearchLength = this.afs.options.get('minSearchLength') || this.minSearchLength;
2075
-
2076
- // Bind events
2077
- this.bindSearchEvents();
2078
- this.afs.logger.debug('Search functionality initialized');
2079
- }
2080
-
2081
- /**
2082
- * Bind search events
2083
- * @private
2084
- */
2085
- bindSearchEvents() {
2086
- if (!this.searchInput) return;
2087
-
2088
- // Create debounced search function
2089
- const debouncedSearch = debounce(e => {
2090
- this.search(e.target.value);
2091
- }, this.afs.options.get('debounceTime') || 300);
2092
-
2093
- // Bind input event
2094
- this.searchInput.addEventListener('input', debouncedSearch);
2095
-
2096
- // Bind clear event
2097
- this.searchInput.addEventListener('search', e => {
2098
- if (!e.target.value) {
2099
- this.clearSearch();
2100
- }
2101
- });
2102
-
2103
- // Handle Enter key
2104
- this.searchInput.addEventListener('keypress', e => {
2105
- if (e.key === 'Enter') {
2106
- e.preventDefault();
2107
- this.search(e.target.value);
2108
- }
2109
- });
2110
- }
2111
-
2112
- /**
2113
- * Perform search
2114
- * @public
2115
- * @param {string} query - Search query
2116
- */
2117
- search(query) {
2118
- this.afs.logger.debug('Performing search:', query);
2119
- const normalizedQuery = this.normalizeQuery(query);
2120
-
2121
- // Update state
2122
- this.afs.state.setState('search.query', normalizedQuery);
2123
- let matches = 0;
2124
-
2125
- // Special case for empty query
2126
- if (!normalizedQuery) {
2127
- this.clearSearch();
2128
- return;
2129
- }
2130
-
2131
- // Skip if query is too short
2132
- if (normalizedQuery.length < this.minSearchLength) {
2133
- this.afs.logger.debug('Search query too short');
2134
- return;
2135
- }
2136
- try {
2137
- // Create search regex
2138
- const regex = this.createSearchRegex(normalizedQuery);
2139
-
2140
- // Search through items
2141
- this.afs.items.forEach(item => {
2142
- const searchText = this.getItemSearchText(item);
2143
- const matchesSearch = regex.test(searchText);
2144
- if (matchesSearch) {
2145
- this.afs.showItem(item);
2146
- this.highlightMatches(item, regex);
2147
- matches++;
2148
- } else {
2149
- this.afs.hideItem(item);
2150
- this.removeHighlights(item);
2151
- }
2152
- });
2153
-
2154
- // Update URL and emit event
2155
- this.afs.urlManager.updateURL();
2156
- this.afs.emit('search', {
2157
- query: normalizedQuery,
2158
- matches,
2159
- total: this.afs.items.length
2160
- });
2161
- this.afs.logger.info(`Search complete. Found ${matches} matches`);
2162
- } catch (error) {
2163
- this.afs.logger.error('Search error:', error);
2164
- }
2165
-
2166
- // Update counter after animation
2167
- setTimeout(() => {
2168
- this.afs.updateCounter();
2169
- }, this.afs.options.get('animationDuration'));
2170
- }
2171
-
2172
- /**
2173
- * Normalize search query
2174
- * @private
2175
- * @param {string} query - Raw search query
2176
- * @returns {string} Normalized query
2177
- */
2178
- normalizeQuery(query) {
2179
- return query.toLowerCase().trim().replace(/\s+/g, ' ');
2180
- }
2181
-
2182
- /**
2183
- * Create search regex
2184
- * @private
2185
- * @param {string} query - Normalized search query
2186
- * @returns {RegExp} Search regex
2187
- */
2188
- createSearchRegex(query) {
2189
- // Escape special regex characters
2190
- const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2191
-
2192
- // Split into words for whole word matching
2193
- const words = escapedQuery.split(' ').filter(Boolean);
2194
-
2195
- // Create regex pattern
2196
- const pattern = words.map(word => `(?=.*\\b${word})`).join('');
2197
- return new RegExp(pattern, 'i');
2198
- }
2199
-
2200
- /**
2201
- * Get searchable text from item
2202
- * @private
2203
- * @param {HTMLElement} item - DOM element
2204
- * @returns {string} Searchable text
2205
- */
2206
- getItemSearchText(item) {
2207
- return this.searchKeys.map(key => item.dataset[key] || '').join(' ').toLowerCase();
2208
- }
2209
-
2210
- /**
2211
- * Highlight search matches
2212
- * @private
2213
- * @param {HTMLElement} item - DOM element
2214
- * @param {RegExp} regex - Search regex
2215
- */
2216
- highlightMatches(item, regex) {
2217
- if (!this.afs.options.get('highlightMatches')) return;
2218
- this.searchKeys.forEach(key => {
2219
- const target = item.querySelector(`[data-search-key="${key}"]`);
2220
- if (!target) return;
2221
- const text = target.textContent;
2222
- const words = this.afs.state.getState().search.query.split(' ');
2223
- let highlightedText = text;
2224
- words.forEach(word => {
2225
- if (!word) return;
2226
- const wordRegex = new RegExp(`(${word})`, 'gi');
2227
- highlightedText = highlightedText.replace(wordRegex, `<span class="${this.highlightClass}">$1</span>`);
2228
- });
2229
- target.innerHTML = highlightedText;
2230
- });
2231
- }
2232
-
2233
- /**
2234
- * Remove highlights
2235
- * @private
2236
- * @param {HTMLElement} item - DOM element
2237
- */
2238
- removeHighlights(item) {
2239
- if (!this.afs.options.get('highlightMatches')) return;
2240
- this.searchKeys.forEach(key => {
2241
- const target = item.querySelector(`[data-search-key="${key}"]`);
2242
- if (!target) return;
2243
- const highlights = target.querySelectorAll(`.${this.highlightClass}`);
2244
- highlights.forEach(highlight => {
2245
- const text = highlight.textContent;
2246
- highlight.replaceWith(text);
2247
- });
2248
- });
2249
- }
2250
-
2251
- /**
2252
- * Clear search
2253
- * @public
2254
- */
2255
- clearSearch() {
2256
- this.afs.logger.debug('Clearing search');
2257
-
2258
- // Clear input
2259
- if (this.searchInput) {
2260
- this.searchInput.value = '';
2261
- }
2262
-
2263
- // Clear state
2264
- this.afs.state.setState('search.query', '');
2265
-
2266
- // Show all items
2267
- this.afs.items.forEach(item => {
2268
- this.afs.showItem(item);
2269
- this.removeHighlights(item);
2270
- });
2271
-
2272
- // Update URL and emit event
2273
- this.afs.urlManager.updateURL();
2274
- this.afs.emit('searchCleared');
2275
-
2276
- // Update counter
2277
- setTimeout(() => {
2278
- this.afs.updateCounter();
2279
- }, this.afs.options.get('animationDuration'));
2280
- }
2281
-
2282
- /**
2283
- * Set search value
2284
- * @public
2285
- * @param {string} value - Search value
2286
- */
2287
- setValue(value) {
2288
- if (this.searchInput) {
2289
- this.searchInput.value = value;
2290
- }
2291
- this.search(value);
2292
- }
2293
-
2294
- /**
2295
- * Get current search value
2296
- * @public
2297
- * @returns {string} Current search value
2298
- */
2299
- getValue() {
2300
- return this.afs.state.getState().search.query;
2301
- }
2302
-
2303
- /**
2304
- * Update search configuration
2305
- * @public
2306
- * @param {Object} config - Search configuration
2307
- */
2308
- updateConfig() {
2309
- let {
2310
- searchKeys,
2311
- minSearchLength,
2312
- highlightClass,
2313
- debounceTime
2314
- } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
2315
- if (searchKeys) this.searchKeys = searchKeys;
2316
- if (minSearchLength) this.minSearchLength = minSearchLength;
2317
- if (highlightClass) this.highlightClass = highlightClass;
2318
- if (debounceTime) {
2319
- this.bindSearchEvents(); // Rebind with new debounce time
2320
- }
2321
- }
2322
-
2323
- /**
2324
- * Destroy search functionality
2325
- * @public
2326
- */
2327
- destroy() {
2328
- if (this.searchInput) {
2329
- this.searchInput.removeEventListener('input', this.debouncedSearch);
2330
- this.searchInput.removeEventListener('search', this.handleClear);
2331
- this.searchInput.removeEventListener('keypress', this.handleEnter);
2332
- }
2333
- this.clearSearch();
2334
- }
2335
- }
2336
-
2337
- /**
2338
- * @fileoverview Sort functionality for AFS
2339
- */
2340
-
2341
- class Sort {
2342
- /**
2343
- * @param {import('../AFS').AFS} afs - Main AFS instance
2344
- */
2345
- constructor(afs) {
2346
- this.afs = afs;
2347
- this.sortButtons = new Map();
2348
- this.setupSort();
2349
- }
2350
-
2351
- /**
2352
- * Setup sort functionality
2353
- * @private
2354
- */
2355
- setupSort() {
2356
- const sortSelector = this.afs.options.get("sortButtonSelector");
2357
- if (!sortSelector) return;
2358
-
2359
- // Find and setup sort buttons
2360
- document.querySelectorAll(sortSelector).forEach(button => {
2361
- const key = button.dataset.sortKey;
2362
- if (!key) {
2363
- this.afs.logger.warn("Sort button missing data-sort-key attribute:", button);
2364
- return;
2365
- }
2366
- this.sortButtons.set(button, {
2367
- key,
2368
- direction: button.dataset.sortDirection || "asc"
2369
- });
2370
- this.bindSortEvent(button);
2371
- });
2372
- this.afs.logger.debug("Sort functionality initialized");
2373
- }
2374
-
2375
- /**
2376
- * Bind sort event to button
2377
- * @private
2378
- * @param {HTMLElement} button - Sort button
2379
- */
2380
- bindSortEvent(button) {
2381
- this.afs.logger.debug("Binding sort event to button:", button);
2382
- button.addEventListener("click", () => {
2383
- const sortData = this.sortButtons.get(button);
2384
- if (!sortData) return;
2385
-
2386
- // Toggle direction
2387
- sortData.direction = sortData.direction === "asc" ? "desc" : "asc";
2388
- this.sortButtons.set(button, sortData);
2389
-
2390
- // Update button state
2391
- this.updateSortButtonState(button, sortData);
2392
-
2393
- // Perform sort
2394
- this.sort(sortData.key, sortData.direction);
2395
- });
2396
- }
2397
-
2398
- /**
2399
- * Update sort button visual state
2400
- * @private
2401
- * @param {HTMLElement} button - Sort button
2402
- * @param {Object} sortData - Sort data
2403
- */
2404
- updateSortButtonState(button, sortData) {
2405
- // Remove active class from all buttons
2406
- this.sortButtons.forEach((_, btn) => {
2407
- btn.classList.remove(this.afs.options.get("activeSortClass"));
2408
- });
2409
-
2410
- // Add active class to current button
2411
- button.classList.add(this.afs.options.get("activeSortClass"));
2412
-
2413
- // Update direction indicator
2414
- const indicator = button.querySelector(".sort-direction");
2415
- if (indicator) {
2416
- indicator.textContent = sortData.direction === "asc" ? "↑" : "↓";
2417
- }
2418
- }
2419
-
2420
- /**
2421
- * Sort items
2422
- * @public
2423
- * @param {string} key - Sort key
2424
- * @param {string} direction - Sort direction ('asc' or 'desc')
2425
- */
2426
- sort(key) {
2427
- let direction = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "asc";
2428
- this.afs.logger.debug(`Sorting by ${key} in ${direction} order`);
2429
- try {
2430
- // Update state
2431
- this.afs.state.setState("sort.current", {
2432
- key,
2433
- direction
2434
- });
2435
-
2436
- // Get all items as array
2437
- const items = Array.from(this.afs.items);
2438
-
2439
- // Determine sort type
2440
- const sortType = this.determineSortType(items[0], key);
2441
-
2442
- // Sort items
2443
- items.sort((a, b) => {
2444
- const valueA = this.getSortValue(a, key, sortType);
2445
- const valueB = this.getSortValue(b, key, sortType);
2446
- return this.compareValues(valueA, valueB, direction);
2447
- });
2448
-
2449
- // Reorder DOM elements
2450
- this.reorderItems(items);
2451
-
2452
- // Update URL and emit event
2453
- this.afs.urlManager.updateURL();
2454
- this.afs.emit("sort", {
2455
- key,
2456
- direction
2457
- });
2458
- this.afs.logger.info(`Sorted items by ${key} ${direction}`);
2459
- } catch (error) {
2460
- this.afs.logger.error("Sort error:", error);
2461
- }
2462
- }
2463
-
2464
- /**
2465
- * Determine sort type from first item
2466
- * @private
2467
- * @param {HTMLElement} item - First item
2468
- * @param {string} key - Sort key
2469
- * @returns {string} Sort type
2470
- */
2471
- determineSortType(item, key) {
2472
- const value = item.dataset[key];
2473
- if (!value) return "string";
2474
- if (!isNaN(value)) return "number";
2475
- if (/^\d{4}-\d{2}-\d{2}/.test(value)) return "date";
2476
- return "string";
2477
- }
2478
-
2479
- /**
2480
- * Get sort value from item
2481
- * @private
2482
- * @param {HTMLElement} item - DOM element
2483
- * @param {string} key - Sort key
2484
- * @param {string} type - Sort type
2485
- * @returns {any} Sort value
2486
- */
2487
- getSortValue(item, key, type) {
2488
- const value = item.dataset[key];
2489
- switch (type) {
2490
- case "number":
2491
- return parseFloat(value);
2492
- case "date":
2493
- return new Date(value).getTime();
2494
- default:
2495
- return value.toLowerCase();
2496
- }
2497
- }
2498
-
2499
- /**
2500
- * Compare two values
2501
- * @private
2502
- * @param {any} a - First value
2503
- * @param {any} b - Second value
2504
- * @param {string} direction - Sort direction
2505
- * @returns {number} Comparison result
2506
- */
2507
- compareValues(a, b, direction) {
2508
- const modifier = direction === "asc" ? 1 : -1;
2509
- if (a === b) return 0;
2510
- if (a === undefined || a === null) return 1;
2511
- if (b === undefined || b === null) return -1;
2512
- return a > b ? modifier : -modifier;
2513
- }
2514
-
2515
- /**
2516
- * Reorder DOM elements
2517
- * @private
2518
- * @param {HTMLElement[]} items - Sorted items
2519
- */
2520
- reorderItems(items) {
2521
- const container = this.afs.options.get("container");
2522
- const fragment = document.createDocumentFragment();
2523
- items.forEach(item => fragment.appendChild(item));
2524
- container.appendChild(fragment);
2525
- }
2526
- /**
2527
- * Sort with multiple criteria
2528
- * @public
2529
- * @param {Array<{key: string, direction: string}>} criteria - Sort criteria
2530
- */
2531
- sortMultiple(criteria) {
2532
- this.afs.logger.debug("Sorting by multiple criteria:", criteria);
2533
- try {
2534
- const items = Array.from(this.afs.items);
2535
- items.sort((a, b) => {
2536
- for (const {
2537
- key,
2538
- direction
2539
- } of criteria) {
2540
- const type = this.determineSortType(items[0], key);
2541
- const valueA = this.getSortValue(a, key, type);
2542
- const valueB = this.getSortValue(b, key, type);
2543
- const comparison = this.compareValues(valueA, valueB, direction);
2544
- if (comparison !== 0) return comparison;
2545
- }
2546
- return 0;
2547
- });
2548
- this.reorderItems(items);
2549
-
2550
- // Update state with primary sort
2551
- if (criteria.length > 0) {
2552
- this.afs.state.setState("sort.current", criteria[0]);
2553
- }
2554
- this.afs.urlManager.updateURL();
2555
- this.afs.emit("multiSort", {
2556
- criteria
2557
- });
2558
- } catch (error) {
2559
- this.afs.logger.error("Multiple sort error:", error);
2560
- }
2561
- }
2562
-
2563
- /**
2564
- * Sort with custom comparator
2565
- * @public
2566
- * @param {string} key - Sort key
2567
- * @param {Function} comparator - Custom comparison function
2568
- */
2569
- sortWithComparator(key, comparator) {
2570
- this.afs.logger.debug(`Sorting by ${key} with custom comparator`);
2571
- try {
2572
- const items = Array.from(this.afs.items);
2573
- items.sort((a, b) => {
2574
- const valueA = a.dataset[key];
2575
- const valueB = b.dataset[key];
2576
- return comparator(valueA, valueB);
2577
- });
2578
- this.reorderItems(items);
2579
- this.afs.emit("customSort", {
2580
- key,
2581
- comparator
2582
- });
2583
- } catch (error) {
2584
- this.afs.logger.error("Custom sort error:", error);
2585
- }
2586
- }
2587
-
2588
- /**
2589
- * Shuffle items randomly
2590
- * @public
2591
- */
2592
- shuffle() {
2593
- this.afs.logger.debug("Shuffling items");
2594
- try {
2595
- const items = Array.from(this.afs.items);
2596
-
2597
- // Fisher-Yates shuffle algorithm
2598
- for (let i = items.length - 1; i > 0; i--) {
2599
- const j = Math.floor(Math.random() * (i + 1));
2600
- [items[i], items[j]] = [items[j], items[i]];
2601
- }
2602
- this.reorderItems(items);
2603
-
2604
- // Clear current sort state
2605
- this.afs.state.setState("sort.current", null);
2606
-
2607
- // Update UI
2608
- this.sortButtons.forEach((_, button) => {
2609
- button.classList.remove(this.afs.options.get("activeSortClass"));
2610
- const indicator = button.querySelector(".sort-direction");
2611
- if (indicator) indicator.textContent = "";
2612
- });
2613
- this.afs.urlManager.updateURL();
2614
- this.afs.emit("shuffle");
2615
- } catch (error) {
2616
- this.afs.logger.error("Shuffle error:", error);
2617
- }
2618
- }
2619
-
2620
- /**
2621
- * Reset sort to default state
2622
- * @public
2623
- */
2624
- reset() {
2625
- this.afs.logger.debug("Resetting sort");
2626
-
2627
- // Clear sort state
2628
- this.afs.state.setState("sort.current", null);
2629
-
2630
- // Reset UI
2631
- this.sortButtons.forEach((_, button) => {
2632
- button.classList.remove(this.afs.options.get("activeSortClass"));
2633
- const indicator = button.querySelector(".sort-direction");
2634
- if (indicator) indicator.textContent = "";
2635
- });
2636
-
2637
- // Reset sort data
2638
- this.sortButtons.forEach((data, button) => {
2639
- data.direction = "asc";
2640
- this.sortButtons.set(button, data);
2641
- });
2642
- this.afs.urlManager.updateURL();
2643
- this.afs.emit("sortReset");
2644
- }
2645
-
2646
- /**
2647
- * Get current sort state
2648
- * @public
2649
- * @returns {Object|null} Current sort state
2650
- */
2651
- getCurrentSort() {
2652
- return this.afs.state.getState().sort.current;
2653
- }
2654
-
2655
- /**
2656
- * Add sort button dynamically
2657
- * @public
2658
- * @param {HTMLElement} button - Sort button element
2659
- * @param {string} key - Sort key
2660
- * @param {string} [direction='asc'] - Initial sort direction
2661
- */
2662
- addSortButton(button, key) {
2663
- let direction = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "asc";
2664
- if (!key) {
2665
- this.afs.logger.warn("Sort key required for new sort button");
2666
- return;
2667
- }
2668
- this.sortButtons.set(button, {
2669
- key,
2670
- direction
2671
- });
2672
- this.bindSortEvent(button);
2673
- this.afs.logger.debug(`Added sort button for ${key}`);
2674
- }
2675
-
2676
- /**
2677
- * Remove sort button
2678
- * @public
2679
- * @param {HTMLElement} button - Sort button to remove
2680
- */
2681
- removeSortButton(button) {
2682
- if (this.sortButtons.has(button)) {
2683
- button.removeEventListener("click", this.bindSortEvent);
2684
- this.sortButtons.delete(button);
2685
- this.afs.logger.debug("Removed sort button");
2686
- }
2687
- }
2688
-
2689
- /**
2690
- * Destroy sort functionality
2691
- * @public
2692
- */
2693
- destroy() {
2694
- this.sortButtons.forEach((_, button) => {
2695
- this.removeSortButton(button);
2696
- });
2697
- this.sortButtons.clear();
2698
- this.afs.logger.debug("Sort functionality destroyed");
2699
- }
2700
- }
2701
-
2702
- /**
2703
- * @fileoverview Pagination functionality for AFS
2704
- */
2705
- class Pagination {
2706
- /**
2707
- * @param {import('../AFS').AFS} afs - Main AFS instance
2708
- */
2709
- constructor(afs) {
2710
- this.afs = afs;
2711
- this.container = null;
2712
- this.animation = new Animation(afs);
2713
- this.options = this.afs.options.get("pagination");
2714
- this.setupPagination();
2715
- }
2716
-
2717
- /**
2718
- * Setup pagination
2719
- * @private
2720
- */
2721
- setupPagination() {
2722
- this.afs.logger.debug("Setting up pagination");
2723
- if (!this.afs.options.get('pagination.enabled')) return;
2724
- console.log('Pagination enabled');
2725
- this.container = document.createElement("div");
2726
- this.container.className = this.options.containerClass;
2727
- const itemsContainer = document.querySelector(this.afs.options.get("pagination.container"));
2728
- if (!itemsContainer) {
2729
- this.afs.logger.error("Items container not found.");
2730
- return;
2731
- }
2732
- itemsContainer.appendChild(this.container);
2733
-
2734
- // Initialize pagination state with defaults
2735
- this.afs.state.setState("pagination", {
2736
- currentPage: 1,
2737
- itemsPerPage: this.options.itemsPerPage,
2738
- totalPages: 0
2739
- });
2740
- this.bindEvents();
2741
- this.update();
2742
- this.afs.logger.debug("Pagination initialized");
2743
- }
2744
-
2745
- /**
2746
- * Bind pagination events
2747
- * @private
2748
- */
2749
- bindEvents() {
2750
- this.afs.on("filter", () => this.update());
2751
- this.afs.on("search", () => this.update());
2752
- this.afs.on("sort", () => this.update());
2753
- this.container.addEventListener("click", e => {
2754
- const button = e.target.closest("button");
2755
- if (!button) return;
2756
- const page = button.dataset.page;
2757
- if (page) {
2758
- this.goToPage(parseInt(page, 10));
2759
- }
2760
- });
2761
- }
2762
-
2763
- /**
2764
- * Update pagination
2765
- * @public
2766
- */
2767
- update() {
2768
- const visibleItems = Array.from(this.afs.state.getState().items.visible);
2769
- const itemsPerPage = this.afs.state.getState().pagination.itemsPerPage;
2770
- const totalPages = Math.max(1, Math.ceil(visibleItems.length / itemsPerPage));
2771
-
2772
- // Update state
2773
- const currentState = this.afs.state.getState().pagination;
2774
- let currentPage = currentState.currentPage;
2775
-
2776
- // Adjust current page if it's beyond the total pages
2777
- if (currentPage > totalPages) {
2778
- currentPage = totalPages;
2779
- }
2780
-
2781
- // Update pagination state
2782
- this.afs.state.setState("pagination", {
2783
- ...currentState,
2784
- currentPage,
2785
- totalPages
2786
- });
2787
-
2788
- // Update visibility before rendering pagination controls
2789
- this.updateVisibility(visibleItems);
2790
- this.renderPagination();
2791
- this.afs.urlManager.updateURL();
2792
- this.afs.emit("pagination", {
2793
- currentPage,
2794
- totalPages,
2795
- itemsPerPage,
2796
- visibleItems: visibleItems.length
2797
- });
2798
- }
2799
-
2800
- /**
2801
- * Update items visibility based on current page
2802
- * @private
2803
- */
2804
- updateVisibility(visibleItems) {
2805
- const {
2806
- currentPage,
2807
- itemsPerPage
2808
- } = this.afs.state.getState().pagination;
2809
- const startIndex = (currentPage - 1) * itemsPerPage;
2810
- const endIndex = startIndex + itemsPerPage;
2811
-
2812
- // First hide all items
2813
- this.afs.items.forEach(item => {
2814
- item.style.display = "none";
2815
- item.classList.add(this.afs.options.get("hiddenClass"));
2816
- });
2817
-
2818
- // Then show only the items for the current page
2819
- const itemsToShow = visibleItems.slice(startIndex, endIndex);
2820
-
2821
- // Ensure we're not trying to display non-existent items
2822
- if (itemsToShow.length === 0 && visibleItems.length > 0) {
2823
- // If we have no items to show but we do have visible items,
2824
- // we're probably on an invalid page - go to page 1
2825
- this.goToPage(1);
2826
- return;
2827
- }
2828
-
2829
- // Show items with animation
2830
- requestAnimationFrame(() => {
2831
- itemsToShow.forEach(item => {
2832
- // Remove hidden class and restore display
2833
- item.style.display = "";
2834
- item.classList.remove(this.afs.options.get("hiddenClass"));
2835
-
2836
- // Apply show animation in the next frame
2837
- requestAnimationFrame(() => {
2838
- this.animation.applyShowAnimation(item, this.options.animationType || "fade");
2839
- });
2840
- });
2841
- });
2842
- }
2843
-
2844
- /**
2845
- * Render pagination controls
2846
- * @private
2847
- */
2848
- renderPagination() {
2849
- const {
2850
- currentPage,
2851
- totalPages
2852
- } = this.afs.state.getState().pagination;
2853
- this.container.innerHTML = "";
2854
- if (totalPages <= 1) {
2855
- this.container.style.display = "none";
2856
- return;
2857
- }
2858
- this.container.style.display = "flex";
2859
- const controls = this.createPaginationControls(currentPage, totalPages);
2860
- this.container.appendChild(controls);
2861
- }
2862
-
2863
- /**
2864
- * Create pagination controls
2865
- * @private
2866
- * @param {number} currentPage - Current page
2867
- * @param {number} totalPages - Total pages
2868
- * @returns {DocumentFragment} Pagination controls
2869
- */
2870
- createPaginationControls(currentPage, totalPages) {
2871
- const fragment = document.createDocumentFragment();
2872
- if (this.options.showPrevNext) {
2873
- const prevButton = this.createPageButton("‹", currentPage - 1, {
2874
- disabled: currentPage === 1,
2875
- class: "afs-pagination-prev"
2876
- });
2877
- fragment.appendChild(prevButton);
2878
- }
2879
- fragment.appendChild(this.createPageButton("1", 1, {
2880
- active: currentPage === 1
2881
- }));
2882
- const range = this.calculatePageRange(currentPage, totalPages);
2883
- if (range.start > 2) fragment.appendChild(this.createEllipsis());
2884
- for (let i = range.start; i <= range.end; i++) {
2885
- if (i === 1 || i === totalPages) continue;
2886
- fragment.appendChild(this.createPageButton(i.toString(), i, {
2887
- active: currentPage === i
2888
- }));
2889
- }
2890
- if (range.end < totalPages - 1) fragment.appendChild(this.createEllipsis());
2891
- if (totalPages > 1) fragment.appendChild(this.createPageButton(totalPages.toString(), totalPages, {
2892
- active: currentPage === totalPages
2893
- }));
2894
- if (this.options.showPrevNext) {
2895
- const nextButton = this.createPageButton("›", currentPage + 1, {
2896
- disabled: currentPage === totalPages,
2897
- class: "afs-pagination-next"
2898
- });
2899
- fragment.appendChild(nextButton);
2900
- }
2901
- return fragment;
2902
- }
2903
-
2904
- /**
2905
- * Create page button
2906
- * @private
2907
- */
2908
- createPageButton(text, page) {
2909
- let {
2910
- active = false,
2911
- disabled = false,
2912
- class: className = ""
2913
- } = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
2914
- const button = document.createElement("button");
2915
- button.textContent = text;
2916
- button.dataset.page = page;
2917
- button.classList.add(this.options.pageButtonClass || "afs-page-button");
2918
- if (className) button.classList.add(className);
2919
- if (active) button.classList.add(this.options.activePageClass || "afs-page-active");
2920
- if (disabled) button.disabled = true;
2921
- return button;
2922
- }
2923
- createEllipsis() {
2924
- const span = document.createElement("span");
2925
- span.textContent = "...";
2926
- span.classList.add("afs-pagination-ellipsis");
2927
- return span;
2928
- }
2929
- calculatePageRange(currentPage, totalPages) {
2930
- const maxButtons = this.options.maxButtons || 7;
2931
- const sideButtons = Math.floor((maxButtons - 3) / 2);
2932
- let start = Math.max(2, currentPage - sideButtons);
2933
- let end = Math.min(totalPages - 1, start + maxButtons - 3);
2934
- if (end - start < maxButtons - 3) start = Math.max(2, end - (maxButtons - 3));
2935
- return {
2936
- start,
2937
- end
2938
- };
2939
- }
2940
-
2941
- /**
2942
- * Go to specific page
2943
- * @public
2944
- */
2945
- goToPage(page) {
2946
- const state = this.afs.state.getState().pagination;
2947
- const targetPage = Math.max(1, Math.min(page, state.totalPages));
2948
- if (targetPage === state.currentPage) return;
2949
-
2950
- // Update state
2951
- this.afs.state.setState("pagination.currentPage", targetPage);
2952
-
2953
- // Force a reflow before updating
2954
- this.afs.container.offsetHeight;
2955
-
2956
- // Update visibility and controls
2957
- this.update();
2958
-
2959
- // Scroll to top if enabled
2960
- if (this.options.scrollToTop) {
2961
- setTimeout(() => this.scrollToTop(), 100);
2962
- }
2963
-
2964
- // Emit page change event
2965
- this.afs.emit("pageChanged", {
2966
- previousPage: state.currentPage,
2967
- currentPage: targetPage,
2968
- totalPages: state.totalPages
2969
- });
2970
- }
2971
- scrollToTop() {
2972
- const container = document.querySelector(this.afs.options.get("pagination.container"));
2973
- if (!container) {
2974
- this.afs.logger.warn("Scroll container not found.");
2975
- return;
2976
- }
2977
- window.scrollTo({
2978
- top: container.offsetTop - this.options.scrollOffset,
2979
- behavior: "smooth"
2980
- });
2981
- }
2982
-
2983
- /**
2984
- * Set pagination mode
2985
- * @public
2986
- */
2987
- setPaginationMode(enabled) {
2988
- this.afs.logger.debug(`Setting pagination mode to: ${enabled}`);
2989
-
2990
- // Update options
2991
- this.afs.options.set('pagination.enabled', enabled);
2992
- if (enabled) {
2993
- // Enable pagination
2994
- this.setupPagination();
2995
- } else {
2996
- // Disable pagination and show all items
2997
- this.container.remove();
2998
- this.showAllItems();
2999
- }
3000
-
3001
- // Emit event
3002
- this.afs.emit('paginationModeChanged', {
3003
- enabled
3004
- });
3005
- }
3006
-
3007
- /**
3008
- * Show all items (for infinite scroll mode)
3009
- * @private
3010
- */
3011
- showAllItems() {
3012
- const visibleItems = Array.from(this.afs.state.getState().items.visible);
3013
- requestAnimationFrame(() => {
3014
- visibleItems.forEach(item => {
3015
- item.style.display = '';
3016
- item.classList.remove(this.afs.options.get('hiddenClass'));
3017
- requestAnimationFrame(() => {
3018
- this.animation.applyShowAnimation(item, this.options.animationType || 'fade');
3019
- });
3020
- });
3021
- });
3022
- }
3023
- }
3024
-
3025
- /**
3026
- * @fileoverview URL state management for AFS
3027
- */
3028
-
3029
- class URLManager {
3030
- /**
3031
- * @param {import('../AFS').AFS} afs - Main AFS instance
3032
- */
3033
- constructor(afs) {
3034
- this.afs = afs;
3035
- this.defaultParams = new URLSearchParams();
3036
- this.setupPopStateHandler();
3037
-
3038
- // Load URL state after a small delay to ensure all features are ready
3039
- setTimeout(() => {
3040
- this.loadFromURL();
3041
- }, 0);
3042
- }
3043
-
3044
- /**
3045
- * Setup history popstate handler
3046
- * @private
3047
- */
3048
- setupPopStateHandler() {
3049
- window.addEventListener('popstate', () => {
3050
- this.loadFromURL();
3051
- });
3052
- }
3053
-
3054
- /**
3055
- * Update URL with current filter state
3056
- * @public
3057
- */
3058
- /**
3059
- * Update URL with current filter state
3060
- * @public
3061
- */
3062
- updateURL() {
3063
- this.afs.logger.debug("Updating URL state");
3064
- const params = new URLSearchParams();
3065
- const state = this.afs.state.getState(); // Get current state of filters
3066
-
3067
- // Get active filters directly from the Filter instance
3068
- const activeFilters = this.afs.filter.getActiveFilters();
3069
-
3070
- // Update the state object with the active filters
3071
- state.filters.current = activeFilters;
3072
-
3073
- // Add filters to URL
3074
- this.addFiltersToURL(params, state);
3075
-
3076
- // Add ranges (if applicable) to URL
3077
- this.addRangesToURL(params, state);
3078
-
3079
- // Add search query to URL
3080
- this.addSearchToURL(params, state);
3081
-
3082
- // Add sort state to URL
3083
- this.addSortToURL(params, state);
3084
-
3085
- // Add pagination to URL
3086
- this.addPaginationToURL(params, state);
3087
-
3088
- // Push the updated URL
3089
- this.pushState(params); // Push the new URL state to the browser
3090
- }
3091
-
3092
- /**
3093
- * Add filters to URL parameters
3094
- * @private
3095
- * @param {URLSearchParams} params
3096
- * @param {Object} state
3097
- */
3098
- addFiltersToURL(params, state) {
3099
- const filters = state.filters;
3100
-
3101
- // Skip if only default filter is active
3102
- if (filters.current.size === 0 || filters.current.size === 1 && filters.current.has('*')) {
3103
- return;
3104
- }
3105
-
3106
- // Group filters by type
3107
- const filtersByType = {};
3108
- for (const filter of filters.current) {
3109
- if (filter !== '*') {
3110
- const [type, value] = filter.split(':');
3111
- if (!filtersByType[type]) {
3112
- filtersByType[type] = new Set();
3113
- }
3114
- filtersByType[type].add(value);
3115
- }
3116
- }
3117
-
3118
- // Add filters to params
3119
- Object.entries(filtersByType).forEach(_ref => {
3120
- let [type, values] = _ref;
3121
- params.set(type, Array.from(values).join(','));
3122
- });
3123
-
3124
- // Add filter mode if not default
3125
- if (filters.mode !== 'OR') {
3126
- params.set('filterMode', filters.mode.toLowerCase());
3127
- }
3128
-
3129
- // Add group mode if groups exist and mode isn't default
3130
- if (filters.groups.size > 0 && filters.groupMode !== 'OR') {
3131
- params.set('groupMode', filters.groupMode.toLowerCase());
3132
- }
3133
-
3134
- // Add filter groups if they exist
3135
- filters.groups.forEach((group, groupId) => {
3136
- params.set(`group_${groupId}`, Array.from(group.filters).join(','));
3137
- if (group.operator !== 'OR') {
3138
- params.set(`groupOp_${groupId}`, group.operator.toLowerCase());
3139
- }
3140
- });
3141
- }
3142
-
3143
- /**
3144
- * Add range filters to URL parameters
3145
- * @private
3146
- * @param {URLSearchParams} params
3147
- * @param {Object} state
3148
- */
3149
- addRangesToURL(params, state) {
3150
- // Handle numeric ranges
3151
- state.filters.ranges.forEach((range, key) => {
3152
- const {
3153
- currentMin,
3154
- currentMax
3155
- } = range;
3156
- if (currentMin !== range.min || currentMax !== range.max) {
3157
- params.set(`range_${key}`, `${currentMin},${currentMax}`);
3158
- }
3159
- });
3160
-
3161
- // Handle date ranges
3162
- state.filters.dateRanges.forEach((range, key) => {
3163
- const {
3164
- start,
3165
- end
3166
- } = range;
3167
- params.set(`dateRange_${key}`, `${start.toISOString()},${end.toISOString()}`);
3168
- });
3169
- }
3170
-
3171
- /**
3172
- * Add search parameters to URL
3173
- * @private
3174
- * @param {URLSearchParams} params
3175
- * @param {Object} state
3176
- */
3177
- addSearchToURL(params, state) {
3178
- if (state.search.query) {
3179
- params.set('search', state.search.query);
3180
- }
3181
- }
3182
-
3183
- /**
3184
- * Add sort parameters to URL
3185
- * @private
3186
- * @param {URLSearchParams} params
3187
- * @param {Object} state
3188
- */
3189
- addSortToURL(params, state) {
3190
- if (state.sort.current) {
3191
- const {
3192
- key,
3193
- direction
3194
- } = state.sort.current;
3195
- params.set('sort', `${key},${direction}`);
3196
- }
3197
- }
3198
-
3199
- /**
3200
- * Add pagination parameters to URL
3201
- * @private
3202
- * @param {URLSearchParams} params
3203
- * @param {Object} state
3204
- */
3205
- addPaginationToURL(params, state) {
3206
- const {
3207
- currentPage,
3208
- itemsPerPage
3209
- } = state.pagination;
3210
- if (!this.afs.options.get('pagination.enabled')) {
3211
- return;
3212
- }
3213
- if (currentPage > 1) {
3214
- params.set('page', currentPage.toString());
3215
- }
3216
- if (itemsPerPage !== this.afs.options.get('pagination.itemsPerPage')) {
3217
- params.set('perPage', itemsPerPage.toString());
3218
- }
3219
- }
3220
-
3221
- /**
3222
- * Update browser URL
3223
- * @private
3224
- * @param {URLSearchParams} params
3225
- */
3226
- pushState(params) {
3227
- const queryString = params.toString();
3228
- const newURL = `${window.location.pathname}${queryString ? '?' + queryString : ''}`;
3229
-
3230
- // Only update if URL actually changed
3231
- if (newURL !== window.location.href) {
3232
- window.history.pushState({}, '', newURL);
3233
- this.afs.logger.debug('URL updated:', newURL);
3234
- }
3235
- }
3236
-
3237
- /**
3238
- * Load filter state from URL
3239
- * @public
3240
- */
3241
- loadFromURL() {
3242
- this.afs.logger.debug('Loading state from URL');
3243
- const params = new URLSearchParams(window.location.search);
3244
- try {
3245
- // Clear existing state first
3246
- if (this.afs.filter) {
3247
- this.afs.filter.clearAllFilters();
3248
- }
3249
-
3250
- // Process filter mode first
3251
- const filterMode = params.get('filterMode');
3252
- if (filterMode && this.afs.filter) {
3253
- this.afs.filter.setFilterMode(filterMode.toUpperCase());
3254
- }
3255
-
3256
- // Process regular filters
3257
- const filterParams = Array.from(params.entries()).filter(_ref2 => {
3258
- let [key] = _ref2;
3259
- return this.isRegularFilter(key);
3260
- });
3261
- if (filterParams.length > 0 && this.afs.filter) {
3262
- // Remove default '*' filter
3263
- this.afs.filter.activeFilters.clear();
3264
-
3265
- // Add filters and update button states
3266
- filterParams.forEach(_ref3 => {
3267
- let [type, values] = _ref3;
3268
- values.split(',').forEach(value => {
3269
- const filter = `${type}:${value}`;
3270
- this.afs.filter.activeFilters.add(filter);
3271
-
3272
- // Update button UI
3273
- this.afs.filter.filterButtons.forEach((buttonFilter, button) => {
3274
- if (buttonFilter === filter) {
3275
- button.classList.add(this.afs.options.get('activeClass'));
3276
- } else if (buttonFilter === '*') {
3277
- button.classList.remove(this.afs.options.get('activeClass'));
3278
- }
3279
- });
3280
- });
3281
- });
3282
- }
3283
-
3284
- // Apply filters before processing other parameters
3285
- if (this.afs.filter) {
3286
- this.afs.filter.applyFilters();
3287
- }
3288
-
3289
- // Process search
3290
- const searchQuery = params.get('search');
3291
- if (searchQuery && this.afs.search) {
3292
- this.afs.search.setValue(searchQuery);
3293
- }
3294
-
3295
- // Process sort
3296
- const sortParam = params.get('sort');
3297
- if (sortParam && this.afs.sort) {
3298
- const [key, direction] = sortParam.split(',');
3299
- this.afs.sort.sort(key, direction);
3300
- }
3301
-
3302
- // Process pagination
3303
- const page = parseInt(params.get('page'));
3304
- if (!isNaN(page) && this.afs.pagination) {
3305
- this.afs.pagination.goToPage(page);
3306
- }
3307
- this.afs.emit('urlStateLoaded', {
3308
- params: Object.fromEntries(params)
3309
- });
3310
- this.afs.logger.info('State loaded from URL');
3311
- } catch (error) {
3312
- this.afs.logger.error('Error loading state from URL:', error);
3313
- // Reset to default state on error
3314
- if (this.afs.filter) {
3315
- this.afs.filter.clearAllFilters();
3316
- }
3317
- }
3318
- }
3319
-
3320
- /**
3321
- * Process filters from URL parameters
3322
- * @private
3323
- * @param {URLSearchParams} params
3324
- */
3325
- processFiltersFromURL(params) {
3326
- const state = this.afs.state.getState();
3327
- let hasFilters = false;
3328
-
3329
- // Process filter mode
3330
- const filterMode = params.get('filterMode');
3331
- if (filterMode) {
3332
- state.filters.mode = filterMode.toUpperCase();
3333
- }
3334
-
3335
- // Process group mode
3336
- const groupMode = params.get('groupMode');
3337
- if (groupMode) {
3338
- state.filters.groupMode = groupMode.toUpperCase();
3339
- }
3340
-
3341
- // Process regular filters
3342
- for (const [type, values] of params.entries()) {
3343
- if (this.isRegularFilter(type)) {
3344
- values.split(',').filter(Boolean).forEach(value => {
3345
- hasFilters = true;
3346
- state.filters.current.add(`${type}:${value}`);
3347
- });
3348
- }
3349
- }
3350
-
3351
- // Process filter groups
3352
- for (const [key, value] of params.entries()) {
3353
- if (key.startsWith('group_')) {
3354
- const groupId = key.replace('group_', '');
3355
- const operator = params.get(`groupOp_${groupId}`)?.toUpperCase() || 'OR';
3356
- state.filters.groups.set(groupId, {
3357
- filters: new Set(value.split(',')),
3358
- operator
3359
- });
3360
- }
3361
- }
3362
-
3363
- // Set default if no filters
3364
- if (!hasFilters && state.filters.groups.size === 0) {
3365
- state.filters.current.add('*');
3366
- }
3367
- }
3368
-
3369
- /**
3370
- * Process range filters from URL parameters
3371
- * @private
3372
- * @param {URLSearchParams} params
3373
- */
3374
- processRangesFromURL(params) {
3375
- const state = this.afs.state.getState();
3376
-
3377
- // Process numeric ranges
3378
- for (const [key, value] of params.entries()) {
3379
- if (key.startsWith('range_')) {
3380
- const rangeKey = key.replace('range_', '');
3381
- const [min, max] = value.split(',').map(Number);
3382
- state.filters.ranges.set(rangeKey, {
3383
- currentMin: min,
3384
- currentMax: max
3385
- });
3386
- }
3387
- }
3388
-
3389
- // Process date ranges
3390
- for (const [key, value] of params.entries()) {
3391
- if (key.startsWith('dateRange_')) {
3392
- const rangeKey = key.replace('dateRange_', '');
3393
- const [start, end] = value.split(',').map(str => new Date(str));
3394
- state.filters.dateRanges.set(rangeKey, {
3395
- start,
3396
- end
3397
- });
3398
- }
3399
- }
3400
- }
3401
-
3402
- /**
3403
- * Process search from URL parameters
3404
- * @private
3405
- * @param {URLSearchParams} params
3406
- */
3407
- processSearchFromURL(params) {
3408
- const searchQuery = params.get('search') || '';
3409
- this.afs.state.setState('search.query', searchQuery);
3410
- if (this.afs.options.get('searchInput')) {
3411
- this.afs.options.get('searchInput').value = searchQuery;
3412
- }
3413
- }
3414
-
3415
- /**
3416
- * Process sort from URL parameters
3417
- * @private
3418
- * @param {URLSearchParams} params
3419
- */
3420
- processSortFromURL(params) {
3421
- const sortParam = params.get('sort');
3422
- if (sortParam) {
3423
- const [key, direction] = sortParam.split(',');
3424
- this.afs.state.setState('sort.current', {
3425
- key,
3426
- direction
3427
- });
3428
- }
3429
- }
3430
-
3431
- /**
3432
- * Process pagination from URL parameters
3433
- * @private
3434
- * @param {URLSearchParams} params
3435
- */
3436
- processPaginationFromURL(params) {
3437
- const page = parseInt(params.get('page')) || 1;
3438
- const perPage = parseInt(params.get('perPage')) || this.afs.options.get('pagination.itemsPerPage');
3439
- this.afs.state.setState('pagination', {
3440
- currentPage: page,
3441
- itemsPerPage: perPage
3442
- });
3443
- }
3444
-
3445
- /**
3446
- * Check if parameter is a regular filter
3447
- * @private
3448
- * @param {string} param - Parameter name
3449
- * @returns {boolean}
3450
- */
3451
- isRegularFilter(param) {
3452
- const excludedParams = ['search', 'sort', 'page', 'perPage', 'filterMode', 'groupMode'];
3453
- return !excludedParams.includes(param) && !param.startsWith('group_') && !param.startsWith('groupOp_') && !param.startsWith('range_') && !param.startsWith('dateRange_');
3454
- }
3455
-
3456
- /**
3457
- * Clear URL parameters
3458
- * @public
3459
- */
3460
- clearURL() {
3461
- window.history.pushState({}, '', window.location.pathname);
3462
- this.afs.state.reset();
3463
- if (this.afs.filter) {
3464
- this.afs.filter.clearAllFilters();
3465
- }
3466
- }
3467
-
3468
- /**
3469
- * Get current URL parameters
3470
- * @public
3471
- * @returns {URLSearchParams}
3472
- */
3473
- getURLParams() {
3474
- return new URLSearchParams(window.location.search);
3475
- }
3476
-
3477
- /**
3478
- * Check if URL has parameters
3479
- * @public
3480
- * @returns {boolean}
3481
- */
3482
- hasParams() {
3483
- return window.location.search.length > 1;
3484
- }
3485
-
3486
- /**
3487
- * Get parameter value
3488
- * @public
3489
- * @param {string} param - Parameter name
3490
- * @returns {string|null}
3491
- */
3492
- getParam(param) {
3493
- return new URLSearchParams(window.location.search).get(param);
3494
- }
3495
- }
3496
-
3497
- /**
3498
- * @fileoverview Range filter implementation for AFS
3499
- */
3500
-
3501
- class RangeFilter {
3502
- constructor(afs) {
3503
- this.afs = afs;
3504
- this.activeRanges = new Map();
3505
- this.options = this.afs.options.get("slider");
3506
- if (!this.afs.styleManager) {
3507
- this.afs.styleManager = new StyleManager(this.afs.options);
3508
- }
3509
-
3510
- // Apply styles immediately
3511
- this.afs.styleManager.applyStyles();
3512
- }
3513
-
3514
- /**
3515
- * @typedef {Object} RangeOptions
3516
- * @property {string} key - Data attribute key
3517
- * @property {string} type - Type of range ('number' or 'date')
3518
- * @property {HTMLElement} container - Container element
3519
- * @property {number|string} [min] - Minimum value
3520
- * @property {number|string} [max] - Maximum value
3521
- * @property {number} [step] - Step value
3522
- */
3523
-
3524
- /**
3525
- * Add range slider
3526
- * @param {RangeOptions} options - Range slider options
3527
- */
3528
- addRangeSlider(_ref) {
3529
- let {
3530
- key,
3531
- type,
3532
- container,
3533
- min,
3534
- max,
3535
- step = 1,
3536
- ui
3537
- } = _ref;
3538
- this.afs.logger.debug(`Adding range slider for ${key}`);
3539
- if (!container) {
3540
- this.afs.logger.error("Container element required for range slider");
3541
- return;
3542
- }
3543
-
3544
- // Calculate min/max if not provided
3545
- const values = this.calculateMinMax(key, type);
3546
- min = min ?? values.min;
3547
- max = max ?? values.max;
3548
-
3549
- // Get global UI options and merge with slider-specific options
3550
- const globalUiOptions = this.afs.options.get("styles.slider.ui") || {
3551
- showHistogram: false,
3552
- bins: 10
3553
- };
3554
- const sliderUiOptions = {
3555
- ...globalUiOptions,
3556
- ...ui // Override with slider-specific options if provided
3557
- };
3558
-
3559
- // Only calculate histogram data if enabled
3560
- const histogramData = sliderUiOptions.showHistogram ? this.calculateHistogramData(key, sliderUiOptions.bins) : {
3561
- counts: [],
3562
- binEdges: [],
3563
- max: 0
3564
- };
3565
-
3566
- // Create slider elements
3567
- const elements = this.createSliderElements(histogramData, sliderUiOptions);
3568
- const state = this.initializeState(min, max, step, type);
3569
-
3570
- // Store histogram data and UI options in state
3571
- state.ui = sliderUiOptions;
3572
- if (sliderUiOptions.showHistogram) {
3573
- state.histogram = histogramData;
3574
- }
3575
-
3576
- // Add elements to container
3577
- this.appendElements(container, elements);
3578
-
3579
- // Setup event handlers
3580
- this.setupEventHandlers(elements, state, key);
3581
-
3582
- // Store state
3583
- this.activeRanges.set(key, {
3584
- state,
3585
- elements
3586
- });
3587
-
3588
- // Initial update
3589
- this.updateSliderUI(key);
3590
-
3591
- // Setup histogram highlight updates only if enabled
3592
- if (sliderUiOptions.showHistogram) {
3593
- this.setupHistogramHighlight(elements, state, histogramData.binEdges);
3594
- }
3595
- this.afs.logger.info(`Range slider added for ${key}`);
3596
- }
3597
- /**
3598
- * Calculate min and max values from items
3599
- * @private
3600
- */
3601
- calculateMinMax(key, type) {
3602
- const values = Array.from(this.afs.items).map(item => {
3603
- const value = item.dataset[key];
3604
- return type === "date" ? new Date(value).getTime() : parseFloat(value);
3605
- }).filter(value => !isNaN(value));
3606
- return {
3607
- min: Math.min(...values),
3608
- max: Math.max(...values)
3609
- };
3610
- }
3611
-
3612
- /**
3613
- * Create slider DOM elements
3614
- * @private
3615
- * @param {Object} histogramData - Histogram data
3616
- * @param {Object} sliderUiOptions - UI options for this slider
3617
- */
3618
- createSliderElements(histogramData, sliderUiOptions) {
3619
- const styles = this.afs.options.get("styles") || this.afs.styleManager.defaultStyles;
3620
- const colors = styles.colors || this.afs.styleManager.defaultStyles.colors;
3621
- const sliderOptions = this.afs.options.get("slider") || {};
3622
- const container = document.createElement("div");
3623
- container.className = "afs-range-container";
3624
- const slider = document.createElement("div");
3625
- slider.className = sliderOptions.containerClass;
3626
- const track = document.createElement("div");
3627
- track.className = sliderOptions.trackClass;
3628
-
3629
- // Only add histogram if enabled in the slider-specific options
3630
- if (sliderUiOptions?.showHistogram && histogramData?.counts?.length > 0) {
3631
- const histogram = this.createHistogramBars(histogramData, colors);
3632
- slider.appendChild(histogram);
3633
- }
3634
- const selectedRange = document.createElement("div");
3635
- selectedRange.className = sliderOptions.selectedClass;
3636
- const minThumb = document.createElement("div");
3637
- minThumb.className = sliderOptions.thumbClass;
3638
- const maxThumb = document.createElement("div");
3639
- maxThumb.className = sliderOptions.thumbClass;
3640
- const minValue = document.createElement("div");
3641
- minValue.className = sliderOptions.valueClass;
3642
- const maxValue = document.createElement("div");
3643
- maxValue.className = sliderOptions.valueClass;
3644
-
3645
- // Build the slider
3646
- slider.appendChild(track);
3647
- slider.appendChild(selectedRange);
3648
- slider.appendChild(minThumb);
3649
- slider.appendChild(maxThumb);
3650
- slider.appendChild(minValue);
3651
- slider.appendChild(maxValue);
3652
- container.appendChild(slider);
3653
- return {
3654
- container,
3655
- slider,
3656
- track,
3657
- selectedRange,
3658
- minThumb,
3659
- maxThumb,
3660
- minValue,
3661
- maxValue
3662
- };
3663
- }
3664
-
3665
- // Add helper method for creating histogram
3666
- createHistogram(data, colors) {
3667
- const histogram = document.createElement("div");
3668
- histogram.className = "afs-histogram";
3669
-
3670
- // Create histogram bars
3671
- data.forEach((value, index) => {
3672
- const bar = document.createElement("div");
3673
- bar.className = "afs-histogram-bar";
3674
- bar.style.height = `${value}%`;
3675
- bar.style.backgroundColor = colors.histogram;
3676
- histogram.appendChild(bar);
3677
- });
3678
- return histogram;
3679
- }
3680
-
3681
- /**
3682
- * Calculate histogram data from items
3683
- * @private
3684
- * @param {string} key - Data attribute key (e.g., 'price')
3685
- * @param {number} [bins=10] - Number of bins for histogram
3686
- * @returns {Object} Histogram data and metadata
3687
- */
3688
- calculateHistogramData(key) {
3689
- let bins = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 10;
3690
- try {
3691
- const values = Array.from(this.afs.items).map(item => parseFloat(item.dataset[key])).filter(value => !isNaN(value));
3692
- if (values.length === 0) {
3693
- return {
3694
- counts: [],
3695
- binEdges: [],
3696
- max: 0
3697
- };
3698
- }
3699
- const min = Math.min(...values);
3700
- const max = Math.max(...values);
3701
- const binWidth = (max - min) / bins;
3702
- const counts = new Array(bins).fill(0);
3703
- const binEdges = new Array(bins + 1);
3704
-
3705
- // Calculate bin edges
3706
- for (let i = 0; i <= bins; i++) {
3707
- binEdges[i] = min + i * binWidth;
3708
- }
3709
-
3710
- // Count values in each bin
3711
- values.forEach(value => {
3712
- if (value === max) {
3713
- counts[counts.length - 1]++;
3714
- return;
3715
- }
3716
- const binIndex = Math.floor((value - min) / binWidth);
3717
- counts[binIndex]++;
3718
- });
3719
-
3720
- // Normalize heights to be more subtle
3721
- const maxCount = Math.max(...counts);
3722
- const normalizedCounts = counts.map(count => Math.max(20, Math.round(count / maxCount * 100)) // Minimum height of 20%
3723
- );
3724
- return {
3725
- counts: normalizedCounts,
3726
- binEdges,
3727
- max: maxCount,
3728
- min,
3729
- max
3730
- };
3731
- } catch (error) {
3732
- this.afs.logger.error("Error calculating histogram:", error);
3733
- return {
3734
- counts: [],
3735
- binEdges: [],
3736
- max: 0
3737
- };
3738
- }
3739
- }
3740
-
3741
- /**
3742
- * Setup histogram highlight on range changes
3743
- * @private
3744
- */
3745
- setupHistogramHighlight(elements, state, binEdges) {
3746
- const bars = elements.slider.querySelectorAll(".afs-histogram-bar");
3747
- const updateHistogram = () => {
3748
- const minVal = state.currentMin;
3749
- const maxVal = state.currentMax;
3750
- bars.forEach((bar, index) => {
3751
- const binStart = binEdges[index];
3752
- const binEnd = binEdges[index + 1];
3753
-
3754
- // Highlight bars within the selected range
3755
- if (binStart >= minVal && binEnd <= maxVal) {
3756
- bar.classList.add("active");
3757
- } else {
3758
- bar.classList.remove("active");
3759
- }
3760
- });
3761
- };
3762
-
3763
- // Update histogram on range changes using AFS instance
3764
- this.afs.on("rangeFilter", () => updateHistogram());
3765
-
3766
- // Initial update
3767
- updateHistogram();
3768
- }
3769
-
3770
- /**
3771
- * Create histogram bars
3772
- * @private
3773
- */
3774
- createHistogramBars(histogramData, colors) {
3775
- const {
3776
- counts
3777
- } = histogramData;
3778
- const histogram = document.createElement("div");
3779
- histogram.className = "afs-histogram";
3780
- counts.forEach(height => {
3781
- const bar = document.createElement("div");
3782
- bar.className = "afs-histogram-bar";
3783
- bar.style.height = `${height}%`;
3784
- histogram.appendChild(bar);
3785
- });
3786
- return histogram;
3787
- }
3788
-
3789
- /**
3790
- * Initialize slider state
3791
- * @private
3792
- */
3793
- initializeState(min, max, step, type) {
3794
- return {
3795
- min,
3796
- max,
3797
- currentMin: min,
3798
- currentMax: max,
3799
- step,
3800
- type,
3801
- isDragging: false
3802
- };
3803
- }
3804
-
3805
- /**
3806
- * Append elements to container
3807
- * @private
3808
- */
3809
- appendElements(container, elements) {
3810
- const {
3811
- slider,
3812
- track,
3813
- selectedRange,
3814
- minThumb,
3815
- maxThumb,
3816
- minValue,
3817
- maxValue
3818
- } = elements;
3819
- slider.appendChild(track);
3820
- slider.appendChild(selectedRange);
3821
- slider.appendChild(minThumb);
3822
- slider.appendChild(maxThumb);
3823
- slider.appendChild(minValue);
3824
- slider.appendChild(maxValue);
3825
- container.appendChild(slider);
3826
- }
3827
-
3828
- /**
3829
- * Setup event handlers for slider
3830
- * @private
3831
- */
3832
- setupEventHandlers(elements, state, key) {
3833
- const {
3834
- minThumb,
3835
- maxThumb
3836
- } = elements;
3837
- const handleStart = isMin => e => {
3838
- e.preventDefault(); // Prevent scrolling while dragging on mobile
3839
- state.isDragging = true;
3840
-
3841
- // Get the correct event coordinates whether mouse or touch
3842
- const getEventXY = event => {
3843
- return event.touches ? event.touches[0] : event;
3844
- };
3845
- const moveHandler = moveEvent => {
3846
- const evt = getEventXY(moveEvent);
3847
- this.createMoveHandler(elements, state, key, isMin)(evt);
3848
- };
3849
- const stopHandler = () => {
3850
- state.isDragging = false;
3851
-
3852
- // Remove both mouse and touch event listeners
3853
- window.removeEventListener('mousemove', moveHandler);
3854
- window.removeEventListener('mouseup', stopHandler);
3855
- window.removeEventListener('touchmove', moveHandler);
3856
- window.removeEventListener('touchend', stopHandler);
3857
- window.removeEventListener('touchcancel', stopHandler);
3858
- this.applyFilter(key);
3859
- };
3860
-
3861
- // Add both mouse and touch event listeners
3862
- window.addEventListener('mousemove', moveHandler);
3863
- window.addEventListener('mouseup', stopHandler);
3864
- window.addEventListener('touchmove', moveHandler, {
3865
- passive: false
3866
- });
3867
- window.addEventListener('touchend', stopHandler);
3868
- window.addEventListener('touchcancel', stopHandler);
3869
- };
3870
-
3871
- // Add both mouse and touch event listeners to thumbs
3872
- minThumb.addEventListener('mousedown', handleStart(true));
3873
- minThumb.addEventListener('touchstart', handleStart(true), {
3874
- passive: false
3875
- });
3876
- maxThumb.addEventListener('mousedown', handleStart(false));
3877
- maxThumb.addEventListener('touchstart', handleStart(false), {
3878
- passive: false
3879
- });
3880
- }
3881
-
3882
- /**
3883
- * Update slider UI
3884
- * @private
3885
- */
3886
- updateSliderUI(key) {
3887
- const {
3888
- state,
3889
- elements
3890
- } = this.activeRanges.get(key);
3891
- const {
3892
- minThumb,
3893
- maxThumb,
3894
- selectedRange,
3895
- minValue,
3896
- maxValue
3897
- } = elements;
3898
-
3899
- // Calculate positions with padding consideration
3900
- const range = state.max - state.min;
3901
- const minPos = (state.currentMin - state.min) / range * 100;
3902
- const maxPos = (state.currentMax - state.min) / range * 100;
3903
-
3904
- // Ensure thumbs stay within bounds
3905
- const clampedMinPos = Math.max(0, Math.min(minPos, 100));
3906
- const clampedMaxPos = Math.max(0, Math.min(maxPos, 100));
3907
-
3908
- // Position thumbs
3909
- minThumb.style.left = `${clampedMinPos}%`;
3910
- maxThumb.style.left = `${clampedMaxPos}%`;
3911
-
3912
- // Position selected range
3913
- selectedRange.style.left = `${clampedMinPos}%`;
3914
- selectedRange.style.width = `${clampedMaxPos - clampedMinPos}%`;
3915
-
3916
- // Format values
3917
- const formatValue = state.type === "date" ? value => new Date(value).toLocaleDateString() : value => value.toFixed(2);
3918
-
3919
- // Update value labels
3920
- minValue.textContent = formatValue(state.currentMin);
3921
- maxValue.textContent = formatValue(state.currentMax);
3922
-
3923
- // Position value labels considering bounds
3924
- minValue.style.left = `${clampedMinPos}%`;
3925
- maxValue.style.left = `${clampedMaxPos}%`;
3926
-
3927
- // Handle edge cases for value label positioning
3928
- if (clampedMinPos < 5) {
3929
- minValue.style.transform = "translateX(0)";
3930
- } else if (clampedMinPos > 95) {
3931
- minValue.style.transform = "translateX(-100%)";
3932
- } else {
3933
- minValue.style.transform = "translateX(-50%)";
3934
- }
3935
- if (clampedMaxPos < 5) {
3936
- maxValue.style.transform = "translateX(0)";
3937
- } else if (clampedMaxPos > 95) {
3938
- maxValue.style.transform = "translateX(-100%)";
3939
- } else {
3940
- maxValue.style.transform = "translateX(-50%)";
3941
- }
3942
- }
3943
-
3944
- // Also update the createMoveHandler to respect the padding
3945
- createMoveHandler(elements, state, key, isMin) {
3946
- this.afs.logger.debug(`Creating move handler for ${key}`);
3947
- const {
3948
- track
3949
- } = elements;
3950
- const PADDING = 5;
3951
- return debounce(e => {
3952
- const clientX = e.touches ? e.touches[0].clientX : e.clientX;
3953
- const rect = track.getBoundingClientRect();
3954
- const totalWidth = rect.width;
3955
- const paddingPixels = PADDING / 100 * totalWidth;
3956
-
3957
- // Calculate percent with padding consideration
3958
- const rawPercent = (clientX - rect.left - paddingPixels) / (totalWidth - 2 * paddingPixels);
3959
- const percent = Math.min(Math.max(0, rawPercent), 1);
3960
-
3961
- // Calculate value considering the full range
3962
- const value = state.min + (state.max - state.min) * percent;
3963
- const stepped = Math.round(value / state.step) * state.step;
3964
- if (isMin) {
3965
- state.currentMin = Math.min(stepped, state.currentMax);
3966
- } else {
3967
- state.currentMax = Math.max(stepped, state.currentMin);
3968
- }
3969
- this.updateSliderUI(key);
3970
- }, 16); // ~60fps
3971
- }
3972
-
3973
- /**
3974
- * Apply range filter
3975
- * @private
3976
- */
3977
- applyFilter(key) {
3978
- this.afs.logger.debug(`Applying range filter for ${key}`);
3979
- const {
3980
- state
3981
- } = this.activeRanges.get(key);
3982
- this.afs.items.forEach(item => {
3983
- const value = state.type === "date" ? new Date(item.dataset[key]).getTime() : parseFloat(item.dataset[key]);
3984
- if (value >= state.currentMin && value <= state.currentMax) {
3985
- this.afs.showItem(item);
3986
- } else {
3987
- this.afs.hideItem(item);
3988
- }
3989
- });
3990
- this.afs.updateCounter();
3991
- this.afs.urlManager.updateURL();
3992
- this.afs.emit("rangeFilter", {
3993
- key,
3994
- min: state.currentMin,
3995
- max: state.currentMax
3996
- });
3997
- }
3998
-
3999
- /**
4000
- * Get current range values
4001
- * @param {string} key - Range key
4002
- * @returns {Object} Current range values
4003
- */
4004
- getRangeValues(key) {
4005
- const range = this.activeRanges.get(key);
4006
- if (!range) return null;
4007
- return {
4008
- min: range.state.currentMin,
4009
- max: range.state.currentMax,
4010
- type: range.state.type
4011
- };
4012
- }
4013
-
4014
- /**
4015
- * Set range values
4016
- * @param {string} key - Range key
4017
- * @param {number} min - Minimum value
4018
- * @param {number} max - Maximum value
4019
- */
4020
- setRangeValues(key, min, max) {
4021
- const range = this.activeRanges.get(key);
4022
- if (!range) return;
4023
- range.state.currentMin = min;
4024
- range.state.currentMax = max;
4025
- this.updateSliderUI(key);
4026
- this.applyFilter(key);
4027
- }
4028
-
4029
- /**
4030
- * Remove range slider
4031
- * @param {string} key - Range key
4032
- */
4033
- removeRangeSlider(key) {
4034
- const range = this.activeRanges.get(key);
4035
- if (!range) return;
4036
- range.elements.slider.remove();
4037
- this.activeRanges.delete(key);
4038
- this.afs.logger.info(`Range slider removed for ${key}`);
4039
- }
4040
- }
4041
-
4042
- /**
4043
- * @fileoverview Date filter implementation for AFS
4044
- */
4045
-
4046
- class DateFilter {
4047
- constructor(afs) {
4048
- this.afs = afs;
4049
- this.activeDateRanges = new Map();
4050
- this.defaultFormat = this.afs.options.get('dateFormat') || 'YYYY-MM-DD';
4051
- }
4052
-
4053
- /**
4054
- * @typedef {Object} DateRangeOptions
4055
- * @property {string} key - Data attribute key
4056
- * @property {HTMLElement} container - Container element
4057
- * @property {Date} [minDate] - Minimum date
4058
- * @property {Date} [maxDate] - Maximum date
4059
- * @property {string} [format] - Date format (default: YYYY-MM-DD)
4060
- */
4061
-
4062
- /**
4063
- * Add date range filter
4064
- * @param {DateRangeOptions} options - Date range options
4065
- */
4066
- addDateRange(_ref) {
4067
- let {
4068
- key,
4069
- container,
4070
- minDate,
4071
- maxDate,
4072
- format = this.defaultFormat
4073
- } = _ref;
4074
- this.afs.logger.debug(`Adding date range for ${key}`);
4075
- if (!container) {
4076
- this.afs.logger.error('Container element required for date range');
4077
- return;
4078
- }
4079
-
4080
- // Calculate min/max dates if not provided
4081
- const dates = this.calculateMinMaxDates(key);
4082
- minDate = minDate ?? dates.min;
4083
- maxDate = maxDate ?? dates.max;
4084
-
4085
- // Create date picker elements
4086
- const elements = this.createDateElements();
4087
- const state = this.initializeState(minDate, maxDate, format);
4088
-
4089
- // Add elements to container
4090
- this.appendElements(container, elements);
4091
-
4092
- // Setup event handlers
4093
- this.setupEventHandlers(elements, state, key);
4094
-
4095
- // Store state
4096
- this.activeDateRanges.set(key, {
4097
- state,
4098
- elements
4099
- });
4100
-
4101
- // Initial update
4102
- this.updateDateUI(key);
4103
- this.afs.logger.info(`Date range added for ${key}`);
4104
- }
4105
-
4106
- /**
4107
- * Calculate min and max dates from items
4108
- * @private
4109
- */
4110
- calculateMinMaxDates(key) {
4111
- try {
4112
- const validDates = Array.from(this.afs.items).map(item => {
4113
- if (!item || !item.dataset || !item.dataset[key]) {
4114
- return null;
4115
- }
4116
- const date = new Date(item.dataset[key]);
4117
- return isNaN(date.getTime()) ? null : date;
4118
- }).filter(date => date !== null);
4119
- if (validDates.length === 0) {
4120
- // Return default date range if no valid dates found
4121
- const today = new Date();
4122
- return {
4123
- min: new Date(today.getFullYear(), 0, 1),
4124
- // January 1st of current year
4125
- max: new Date(today.getFullYear(), 11, 31) // December 31st of current year
4126
- };
4127
- }
4128
- return {
4129
- min: new Date(Math.min(...validDates)),
4130
- max: new Date(Math.max(...validDates))
4131
- };
4132
- } catch (error) {
4133
- this.afs.logger.error('Error calculating date range:', error);
4134
- // Return default date range on error
4135
- const today = new Date();
4136
- return {
4137
- min: new Date(today.getFullYear(), 0, 1),
4138
- max: new Date(today.getFullYear(), 11, 31)
4139
- };
4140
- }
4141
- }
4142
-
4143
- /**
4144
- * Create date picker elements
4145
- * @private
4146
- */
4147
- createDateElements() {
4148
- this.afs.logger.debug('Creating date picker elements');
4149
- const container = document.createElement('div');
4150
- container.className = 'afs-date-range-container';
4151
- const startContainer = document.createElement('div');
4152
- startContainer.className = 'afs-date-input-wrapper';
4153
- const startLabel = document.createElement('label');
4154
- startLabel.textContent = 'Start Date';
4155
- const startInput = document.createElement('input');
4156
- startInput.type = 'date';
4157
- startInput.className = 'afs-date-input start-date';
4158
- const endContainer = document.createElement('div');
4159
- endContainer.className = 'afs-date-input-wrapper';
4160
- const endLabel = document.createElement('label');
4161
- endLabel.textContent = 'End Date';
4162
- const endInput = document.createElement('input');
4163
- endInput.type = 'date';
4164
- endInput.className = 'afs-date-input end-date';
4165
- startContainer.appendChild(startLabel);
4166
- startContainer.appendChild(startInput);
4167
- endContainer.appendChild(endLabel);
4168
- endContainer.appendChild(endInput);
4169
- container.appendChild(startContainer);
4170
- container.appendChild(endContainer);
4171
- return {
4172
- container,
4173
- startInput,
4174
- endInput
4175
- };
4176
- }
4177
-
4178
- /**
4179
- * Initialize date filter state
4180
- * @private
4181
- */
4182
- initializeState(minDate, maxDate, format) {
4183
- return {
4184
- minDate,
4185
- maxDate,
4186
- currentStartDate: minDate,
4187
- currentEndDate: maxDate,
4188
- format
4189
- };
4190
- }
4191
-
4192
- /**
4193
- * Append elements to container
4194
- * @private
4195
- */
4196
- appendElements(container, elements) {
4197
- container.appendChild(elements.container);
4198
- }
4199
-
4200
- /**
4201
- * Setup event handlers
4202
- * @private
4203
- */
4204
- setupEventHandlers(elements, state, key) {
4205
- this.afs.logger.debug(`Setting up event handlers for date range ${key}`);
4206
- const {
4207
- startInput,
4208
- endInput
4209
- } = elements;
4210
- const handleDateChange = debounce(() => {
4211
- const startDate = new Date(startInput.value);
4212
- const endDate = new Date(endInput.value);
4213
- if (!isNaN(startDate.getTime()) && !isNaN(endDate.getTime())) {
4214
- state.currentStartDate = startDate;
4215
- state.currentEndDate = endDate;
4216
- this.applyDateFilter(key);
4217
- }
4218
- }, 300);
4219
- startInput.addEventListener('change', handleDateChange);
4220
- endInput.addEventListener('change', handleDateChange);
4221
- }
4222
-
4223
- /**
4224
- * Update date picker UI
4225
- * @private
4226
- */
4227
- updateDateUI(key) {
4228
- try {
4229
- const {
4230
- state,
4231
- elements
4232
- } = this.activeDateRanges.get(key);
4233
- const {
4234
- startInput,
4235
- endInput
4236
- } = elements;
4237
-
4238
- // Format dates for input
4239
- const formatDate = date => {
4240
- try {
4241
- const d = new Date(date);
4242
- if (isNaN(d.getTime())) {
4243
- throw new Error('Invalid date');
4244
- }
4245
- return d.toISOString().split('T')[0];
4246
- } catch (error) {
4247
- this.afs.logger.error('Error formatting date:', error);
4248
- return '';
4249
- }
4250
- };
4251
-
4252
- // Set min/max constraints
4253
- startInput.min = formatDate(state.minDate);
4254
- startInput.max = formatDate(state.maxDate);
4255
- endInput.min = formatDate(state.minDate);
4256
- endInput.max = formatDate(state.maxDate);
4257
-
4258
- // Set current values
4259
- startInput.value = formatDate(state.currentStartDate);
4260
- endInput.value = formatDate(state.currentEndDate);
4261
- } catch (error) {
4262
- this.afs.logger.error('Error updating date UI:', error);
4263
- }
4264
- }
4265
-
4266
- /**
4267
- * Apply date filter
4268
- * @private
4269
- */
4270
- applyDateFilter(key) {
4271
- this.afs.logger.info(`Applying date filter for ${key}`);
4272
- const {
4273
- state
4274
- } = this.activeDateRanges.get(key);
4275
- this.afs.items.forEach(item => {
4276
- try {
4277
- // Check if item and dataset exist
4278
- if (!item || !item.dataset || !item.dataset[key]) {
4279
- this.afs.hideItem(item);
4280
- return;
4281
- }
4282
- const itemDate = new Date(item.dataset[key]);
4283
-
4284
- // Check if date is valid
4285
- if (isNaN(itemDate.getTime())) {
4286
- this.afs.hideItem(item);
4287
- return;
4288
- }
4289
-
4290
- // Set time to midnight for consistent comparison
4291
- const startDate = new Date(state.currentStartDate);
4292
- startDate.setHours(0, 0, 0, 0);
4293
- const endDate = new Date(state.currentEndDate);
4294
- endDate.setHours(23, 59, 59, 999);
4295
- const compareDate = new Date(itemDate);
4296
- compareDate.setHours(0, 0, 0, 0);
4297
- if (compareDate >= startDate && compareDate <= endDate) {
4298
- this.afs.showItem(item);
4299
- } else {
4300
- this.afs.hideItem(item);
4301
- }
4302
- } catch (error) {
4303
- this.afs.logger.error('Error filtering item by date:', error);
4304
- this.afs.hideItem(item);
4305
- }
4306
- });
4307
- this.afs.updateCounter();
4308
- this.afs.urlManager.updateURL();
4309
- this.afs.emit('dateFilter', {
4310
- key,
4311
- startDate: state.currentStartDate,
4312
- endDate: state.currentEndDate
4313
- });
4314
- }
4315
-
4316
- /**
4317
- * Get current date range
4318
- * @param {string} key - Date range key
4319
- * @returns {Object} Current date range
4320
- */
4321
- getDateRange(key) {
4322
- const range = this.activeDateRanges.get(key);
4323
- if (!range) return null;
4324
- return {
4325
- startDate: range.state.currentStartDate,
4326
- endDate: range.state.currentEndDate
4327
- };
4328
- }
4329
-
4330
- /**
4331
- * Set date range
4332
- * @param {string} key - Date range key
4333
- * @param {Date} startDate - Start date
4334
- * @param {Date} endDate - End date
4335
- */
4336
- setDateRange(key, startDate, endDate) {
4337
- const range = this.activeDateRanges.get(key);
4338
- if (!range) return;
4339
- range.state.currentStartDate = startDate;
4340
- range.state.currentEndDate = endDate;
4341
- this.updateDateUI(key);
4342
- this.applyDateFilter(key);
4343
- }
4344
-
4345
- /**
4346
- * Remove date range
4347
- * @param {string} key - Date range key
4348
- */
4349
- removeDateRange(key) {
4350
- const range = this.activeDateRanges.get(key);
4351
- if (!range) return;
4352
- range.elements.container.remove();
4353
- this.activeDateRanges.delete(key);
4354
- this.afs.logger.info(`Date range removed for ${key}`);
4355
- }
4356
- }
4357
-
4358
- /**
4359
- * @fileoverview Advanced Filter System - Main Class
4360
- */
4361
-
4362
- // Version
4363
- const VERSION = '1.0.8';
4364
- class AFS extends EventEmitter {
4365
- /**
4366
- * @param {Object} options - Configuration options
4367
- */
4368
- constructor() {
4369
- let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
4370
- super();
4371
- this.initializeCore(options);
4372
- }
4373
-
4374
- /**
4375
- * Initialize core components
4376
- * @private
4377
- */
4378
- initializeCore(options) {
4379
- try {
4380
- this.options = new Options(options);
4381
- this.logger = new Logger(this.options.get("debug") || false, this.options.get("logLevel") || "info");
4382
- this.state = new State();
4383
- this.styleManager = new StyleManager(this.options);
4384
- this.initializeDOM();
4385
- this.initializeFeatures();
4386
- this.setupLifecycle();
4387
- } catch (error) {
4388
- console.error("AFS initialization error:", error);
4389
- throw error;
4390
- }
4391
- }
4392
-
4393
- /**
4394
- * Initialize DOM elements
4395
- * @private
4396
- */
4397
- initializeDOM() {
4398
- this.logger.debug("Initializing DOM elements");
4399
- this.container = document.querySelector(this.options.get("containerSelector"));
4400
- if (!this.container) {
4401
- throw new Error(`Container not found: ${this.options.get("containerSelector")}`);
4402
- }
4403
- this.items = this.container.querySelectorAll(this.options.get("itemSelector"));
4404
- if (this.items.length === 0) {
4405
- this.logger.warn("No items found in container");
4406
- }
4407
- this.state.setState("items.total", this.items.length);
4408
- this.state.setState("items.visible", new Set(this.items));
4409
- }
4410
-
4411
- /**
4412
- * Initialize features
4413
- * @private
4414
- */
4415
- initializeFeatures() {
4416
- this.logger.debug("Initializing features");
4417
-
4418
- // Then initialize other features
4419
- this.filter = new Filter(this);
4420
- this.search = new Search(this);
4421
- this.sort = new Sort(this);
4422
- this.rangeFilter = new RangeFilter(this);
4423
- this.urlManager = new URLManager(this);
4424
- this.dateFilter = new DateFilter(this);
4425
- this.pagination = new Pagination(this);
4426
-
4427
- // Apply styles
4428
- this.styleManager.applyStyles();
4429
-
4430
- // Load initial state from URL
4431
- this.urlManager.loadFromURL();
4432
- }
4433
-
4434
- /**
4435
- * Setup lifecycle events
4436
- * @private
4437
- */
4438
- setupLifecycle() {
4439
- if (this.options.get("responsive")) {
4440
- window.addEventListener("resize", this.handleResize.bind(this));
4441
- }
4442
- if (this.options.get("preserveState")) {
4443
- document.addEventListener("visibilitychange", this.handleVisibilityChange.bind(this));
4444
- }
4445
- if (this.options.get("observeDOM")) {
4446
- this.setupMutationObserver();
4447
- }
4448
- this.emit("initialized", {
4449
- itemCount: this.items.length,
4450
- options: this.options.export()
4451
- });
4452
- }
4453
-
4454
- // Item Management Methods
4455
- /**
4456
- * Show item with animation
4457
- * @public
4458
- * @param {HTMLElement} item - Item to show
4459
- */
4460
- showItem(item) {
4461
- // Update state first
4462
- const visibleItems = this.state.getState().items.visible;
4463
- visibleItems.add(item);
4464
- this.state.setState("items.visible", visibleItems);
4465
-
4466
- // Remove hidden class but keep opacity 0 initially
4467
- item.classList.remove(this.options.get("hiddenClass"));
4468
-
4469
- // Set initial animation state
4470
- item.style.opacity = "0";
4471
- item.style.transform = "scale(0.95)";
4472
- item.style.display = ""; // Ensure item is not display: none
4473
-
4474
- // Force reflow before starting animation
4475
- item.offsetHeight;
4476
-
4477
- // Add transition class if not present
4478
- const transitionClass = this.options.get("transitionClass") || "afs-transition";
4479
- if (!item.classList.contains(transitionClass)) {
4480
- item.classList.add(transitionClass);
4481
- }
4482
-
4483
- // Start animation in next frame
4484
- requestAnimationFrame(() => {
4485
- requestAnimationFrame(() => {
4486
- // Double RAF for reliable animation
4487
- item.style.opacity = "1";
4488
- item.style.transform = "scale(1)";
4489
- });
4490
- });
4491
-
4492
- // Clean up after animation
4493
- const duration = this.options.get("animationDuration") || 300;
4494
- setTimeout(() => {
4495
- // Only clean up if item is still meant to be visible
4496
- if (visibleItems.has(item)) {
4497
- item.style.transform = "";
4498
- item.style.opacity = "";
4499
- }
4500
- }, duration);
4501
- }
4502
-
4503
- /**
4504
- * Hide item with animation
4505
- * @public
4506
- * @param {HTMLElement} item - Item to hide
4507
- */
4508
- hideItem(item) {
4509
- // Update state first
4510
- const visibleItems = this.state.getState().items.visible;
4511
- visibleItems.delete(item);
4512
- this.state.setState("items.visible", visibleItems);
4513
-
4514
- // Add transition class if not present
4515
- const transitionClass = this.options.get("transitionClass") || "afs-transition";
4516
- if (!item.classList.contains(transitionClass)) {
4517
- item.classList.add(transitionClass);
4518
- }
4519
-
4520
- // Start hide animation
4521
- requestAnimationFrame(() => {
4522
- item.style.opacity = "0";
4523
- item.style.transform = "scale(0.95)";
4524
- });
4525
-
4526
- // Add hidden class and clean up after animation
4527
- const duration = this.options.get("animationDuration") || 300;
4528
- setTimeout(() => {
4529
- // Only hide if the item is still meant to be hidden
4530
- if (!visibleItems.has(item)) {
4531
- item.classList.add(this.options.get("hiddenClass"));
4532
- // Clean up styles
4533
- item.style.transform = "";
4534
- item.style.opacity = "";
4535
- }
4536
- }, duration);
4537
- }
4538
-
4539
- /**
4540
- * Add new items
4541
- * @public
4542
- */
4543
- addItems(newItems) {
4544
- const items = Array.isArray(newItems) ? newItems : [newItems];
4545
- const fragment = document.createDocumentFragment();
4546
- items.forEach(item => {
4547
- fragment.appendChild(item);
4548
- this.state.getState().items.visible.add(item);
4549
- });
4550
- this.container.appendChild(fragment);
4551
- this.items = this.container.querySelectorAll(this.options.get("itemSelector"));
4552
- this.filter.applyFilters();
4553
- }
4554
-
4555
- /**
4556
- * Remove items
4557
- * @public
4558
- */
4559
- removeItems(itemsToRemove) {
4560
- const items = Array.isArray(itemsToRemove) ? itemsToRemove : [itemsToRemove];
4561
- const visibleItems = this.state.getState().items.visible;
4562
- items.forEach(item => {
4563
- visibleItems.delete(item);
4564
- item.remove();
4565
- });
4566
- this.items = this.container.querySelectorAll(this.options.get("itemSelector"));
4567
- this.updateCounter();
4568
- }
4569
-
4570
- // State Management Methods
4571
- /**
4572
- * Save current state
4573
- * @public
4574
- */
4575
- saveState() {
4576
- if (!this.options.get("preserveState")) return;
4577
- const state = {
4578
- filters: Array.from(this.filter.getActiveFilters()),
4579
- search: this.search.getValue(),
4580
- sort: this.sort.getCurrentSort(),
4581
- pagination: this.pagination.getPageInfo(),
4582
- timestamp: Date.now()
4583
- };
4584
- localStorage.setItem("afs_state", JSON.stringify(state));
4585
- this.logger.debug("State saved");
4586
- }
4587
-
4588
- /**
4589
- * Restore saved state
4590
- * @public
4591
- */
4592
- restoreState() {
4593
- if (!this.options.get("preserveState")) return;
4594
- try {
4595
- const saved = localStorage.getItem("afs_state");
4596
- if (!saved) return;
4597
- const state = JSON.parse(saved);
4598
- if (Date.now() - state.timestamp > this.options.get("stateExpiry")) {
4599
- localStorage.removeItem("afs_state");
4600
- return;
4601
- }
4602
- this.setState(state);
4603
- this.logger.debug("State restored");
4604
- } catch (error) {
4605
- this.logger.error("Error restoring state:", error);
4606
- }
4607
- }
4608
-
4609
- /**
4610
- * Get current state
4611
- * @public
4612
- */
4613
- getState() {
4614
- return this.state.export();
4615
- }
4616
-
4617
- /**
4618
- * Set new state
4619
- * @public
4620
- */
4621
- setState(newState) {
4622
- this.state.import(newState);
4623
- this.refresh();
4624
- }
4625
-
4626
- // Update Methods
4627
- /**
4628
- * Update counter display
4629
- * @public
4630
- */
4631
- updateCounter() {
4632
- // Get counter element
4633
- const counterElement = document.querySelector(this.options.get("counterSelector"));
4634
- if (!counterElement) return;
4635
- try {
4636
- // Get count values
4637
- const total = this.items.length;
4638
- const visible = this.state.getState().items.visible.size;
4639
- const filtered = total - visible;
4640
-
4641
- // Get counter options with defaults
4642
- const counterOpts = this.options.get("counter") || Options.defaults.counter;
4643
- const template = counterOpts.template || "Showing {visible} of {total}";
4644
- const formatter = counterOpts.formatter || (num => num.toLocaleString());
4645
-
4646
- // Format numbers
4647
- const formattedVisible = formatter(visible);
4648
- const formattedTotal = formatter(total);
4649
- const formattedFiltered = formatter(filtered);
4650
-
4651
- // Build counter text
4652
- let counterText = template.replace("{visible}", formattedVisible).replace("{total}", formattedTotal).replace("{filtered}", formattedFiltered);
4653
-
4654
- // Add filtered count if enabled
4655
- if (filtered > 0 && counterOpts.showFiltered) {
4656
- const filteredTemplate = counterOpts.filteredTemplate || "({filtered} filtered)";
4657
- counterText += " " + filteredTemplate.replace("{filtered}", formattedFiltered);
4658
- }
4659
-
4660
- // Show no results message if applicable
4661
- if (visible === 0 && counterOpts.noResultsTemplate) {
4662
- counterText = counterOpts.noResultsTemplate;
4663
- }
4664
-
4665
- // Update counter element
4666
- counterElement.textContent = counterText;
4667
-
4668
- // Emit event
4669
- this.emit("counterUpdated", {
4670
- total,
4671
- visible,
4672
- filtered,
4673
- formattedTotal,
4674
- formattedVisible,
4675
- formattedFiltered
4676
- });
4677
- } catch (error) {
4678
- // Fallback to basic counter if anything fails
4679
- this.logger.error("Error updating counter:", error);
4680
- counterElement.textContent = `${this.state.getState().items.visible.size}/${this.items.length}`;
4681
- }
4682
- }
4683
-
4684
- /**
4685
- * Update options
4686
- * @public
4687
- */
4688
- updateOptions(newOptions) {
4689
- this.options.update(newOptions);
4690
- this.styleManager.updateStyles(newOptions);
4691
- this.refresh();
4692
- }
4693
-
4694
- /**
4695
- * Refresh system
4696
- * @public
4697
- */
4698
- refresh() {
4699
- this.logger.debug("Refreshing AFS");
4700
- this.items = this.container.querySelectorAll(this.options.get("itemSelector"));
4701
- this.state.setState("items.total", this.items.length);
4702
- this.filter.applyFilters();
4703
- this.search.search(this.search.getValue());
4704
- this.pagination.update();
4705
- this.emit("refreshed", {
4706
- itemCount: this.items.length
4707
- });
4708
- }
4709
-
4710
- // Event Handlers
4711
- /**
4712
- * Handle resize
4713
- * @private
4714
- */
4715
- handleResize = (() => debounce(() => {
4716
- this.emit("resize");
4717
- this.refresh();
4718
- }, 250))();
4719
-
4720
- /**
4721
- * Handle visibility change
4722
- * @private
4723
- */
4724
- handleVisibilityChange() {
4725
- if (document.hidden) {
4726
- this.emit("hidden");
4727
- this.saveState();
4728
- } else {
4729
- this.emit("visible");
4730
- this.restoreState();
4731
- }
4732
- }
4733
-
4734
- /**
4735
- * Setup mutation observer
4736
- * @private
4737
- */
4738
- setupMutationObserver() {
4739
- const observer = new MutationObserver(mutations => {
4740
- if (mutations.some(mutation => mutation.type === "childList")) {
4741
- this.refresh();
4742
- }
4743
- });
4744
- observer.observe(this.container, {
4745
- childList: true,
4746
- subtree: true
4747
- });
4748
- }
4749
-
4750
- // Utility Methods
4751
- /**
4752
- * Get version
4753
- * @public
4754
- */
4755
- getVersion() {
4756
- return VERSION;
4757
- }
4758
-
4759
- /**
4760
- * Check feature support
4761
- * @public
4762
- */
4763
- isFeatureSupported(feature) {
4764
- const supportedFeatures = {
4765
- search: !!this.search,
4766
- pagination: true,
4767
- animation: typeof document.createElement("div").style.transition !== "undefined",
4768
- urlState: typeof window.history.pushState === "function",
4769
- localStorage: (() => {
4770
- try {
4771
- localStorage.setItem("test", "test");
4772
- localStorage.removeItem("test");
4773
- return true;
4774
- } catch (e) {
4775
- return false;
4776
- }
4777
- })()
4778
- };
4779
- return !!supportedFeatures[feature];
4780
- }
4781
-
4782
- /**
4783
- * Destroy instance
4784
- * @public
4785
- */
4786
- destroy() {
4787
- this.logger.debug("Destroying AFS instance");
4788
-
4789
- // Remove event listeners
4790
- window.removeEventListener("resize", this.handleResize);
4791
- document.removeEventListener("visibilitychange", this.handleVisibilityChange);
4792
-
4793
- // Destroy features
4794
- this.filter.destroy();
4795
- this.search.destroy();
4796
- this.sort.destroy();
4797
- this.pagination.destroy();
4798
- this.rangeFilter.destroy();
4799
-
4800
- // Cleanup
4801
- this.styleManager.removeStyles();
4802
- this.state.reset();
4803
- localStorage.removeItem("afs_state");
4804
-
4805
- // Reset items
4806
- this.items.forEach(item => {
4807
- item.style = "";
4808
- item.classList.remove(this.options.get("hiddenClass"), this.options.get("activeClass"));
4809
- });
4810
- this.emit("destroyed");
4811
- }
4812
- }
4813
-
4814
- exports.AFS = AFS;
4815
- exports.VERSION = VERSION;
4816
-
4817
- }));
4818
- //# sourceMappingURL=AFS.js.map