@smilodon/core 1.0.15 → 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/index.cjs +885 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +885 -4
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/index.umd.js +885 -3
- package/dist/index.umd.js.map +1 -1
- package/dist/index.umd.min.js +1 -1
- package/dist/index.umd.min.js.map +1 -1
- package/dist/types/src/components/angular-enhanced-select.d.ts +88 -0
- package/dist/types/src/index.d.ts +1 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1495,10 +1495,17 @@ class EnhancedSelect extends HTMLElement {
|
|
|
1495
1495
|
}
|
|
1496
1496
|
`;
|
|
1497
1497
|
console.log('[EnhancedSelect] _initializeStyles: Created style element, content length:', style.textContent?.length || 0);
|
|
1498
|
-
console.log('[EnhancedSelect] _initializeStyles:
|
|
1499
|
-
|
|
1500
|
-
|
|
1498
|
+
console.log('[EnhancedSelect] _initializeStyles: Shadow root children BEFORE:', this._shadow.children.length);
|
|
1499
|
+
// Insert as first child to ensure styles are processed first
|
|
1500
|
+
if (this._shadow.firstChild) {
|
|
1501
|
+
this._shadow.insertBefore(style, this._shadow.firstChild);
|
|
1502
|
+
}
|
|
1503
|
+
else {
|
|
1504
|
+
this._shadow.appendChild(style);
|
|
1505
|
+
}
|
|
1506
|
+
console.log('[EnhancedSelect] _initializeStyles: Style inserted, shadow root children AFTER:', this._shadow.children.length);
|
|
1501
1507
|
console.log('[EnhancedSelect] _initializeStyles: Shadow root has style element:', !!this._shadow.querySelector('style'));
|
|
1508
|
+
console.log('[EnhancedSelect] _initializeStyles: Style sheet rules:', style.sheet?.cssRules?.length || 'NOT PARSED');
|
|
1502
1509
|
}
|
|
1503
1510
|
_attachEventListeners() {
|
|
1504
1511
|
// Arrow click handler
|
|
@@ -2469,6 +2476,880 @@ if (!customElements.get('enhanced-select')) {
|
|
|
2469
2476
|
customElements.define('enhanced-select', EnhancedSelect);
|
|
2470
2477
|
}
|
|
2471
2478
|
|
|
2479
|
+
/**
|
|
2480
|
+
* Angular-Optimized Enhanced Select Component
|
|
2481
|
+
*
|
|
2482
|
+
* This is a specialized variant that uses light DOM instead of shadow DOM
|
|
2483
|
+
* to ensure perfect compatibility with Angular's rendering pipeline and
|
|
2484
|
+
* view encapsulation system.
|
|
2485
|
+
*
|
|
2486
|
+
* Key differences from standard EnhancedSelect:
|
|
2487
|
+
* - Uses light DOM with scoped CSS classes
|
|
2488
|
+
* - Integrates seamlessly with Angular's change detection
|
|
2489
|
+
* - Maintains all core features (virtualization, accessibility, performance)
|
|
2490
|
+
* - Uses unique class prefixes to avoid style conflicts
|
|
2491
|
+
*
|
|
2492
|
+
* @performance Optimized for Angular's zone.js and rendering cycle
|
|
2493
|
+
* @accessibility Full WCAG 2.1 AAA compliance maintained
|
|
2494
|
+
*/
|
|
2495
|
+
/**
|
|
2496
|
+
* Angular-Enhanced Select Web Component
|
|
2497
|
+
* Uses light DOM for Angular compatibility
|
|
2498
|
+
*/
|
|
2499
|
+
class AngularEnhancedSelect extends HTMLElement {
|
|
2500
|
+
constructor() {
|
|
2501
|
+
super();
|
|
2502
|
+
this._pageCache = {};
|
|
2503
|
+
this._typeBuffer = '';
|
|
2504
|
+
this._hasError = false;
|
|
2505
|
+
this._errorMessage = '';
|
|
2506
|
+
this._boundArrowClick = null;
|
|
2507
|
+
// Unique class prefix to avoid conflicts
|
|
2508
|
+
this.PREFIX = 'smilodon-ang-';
|
|
2509
|
+
this._uniqueId = `angular-select-${Math.random().toString(36).substr(2, 9)}`;
|
|
2510
|
+
// Merge global config
|
|
2511
|
+
this._config = selectConfig.getConfig();
|
|
2512
|
+
// Initialize state
|
|
2513
|
+
this._state = {
|
|
2514
|
+
isOpen: false,
|
|
2515
|
+
isBusy: false,
|
|
2516
|
+
isSearching: false,
|
|
2517
|
+
currentPage: this._config.infiniteScroll.initialPage || 1,
|
|
2518
|
+
totalPages: 1,
|
|
2519
|
+
selectedIndices: new Set(),
|
|
2520
|
+
selectedItems: new Map(),
|
|
2521
|
+
activeIndex: -1,
|
|
2522
|
+
searchQuery: '',
|
|
2523
|
+
loadedItems: [],
|
|
2524
|
+
groupedItems: [],
|
|
2525
|
+
preserveScrollPosition: false,
|
|
2526
|
+
lastScrollPosition: 0,
|
|
2527
|
+
lastNotifiedQuery: null,
|
|
2528
|
+
lastNotifiedResultCount: 0,
|
|
2529
|
+
isExpanded: false,
|
|
2530
|
+
};
|
|
2531
|
+
// Create DOM structure in light DOM
|
|
2532
|
+
this._initializeStyles();
|
|
2533
|
+
this._container = this._createContainer();
|
|
2534
|
+
this._inputContainer = this._createInputContainer();
|
|
2535
|
+
this._input = this._createInput();
|
|
2536
|
+
this._arrowContainer = this._createArrowContainer();
|
|
2537
|
+
this._dropdown = this._createDropdown();
|
|
2538
|
+
this._optionsContainer = this._createOptionsContainer();
|
|
2539
|
+
this._liveRegion = this._createLiveRegion();
|
|
2540
|
+
this._assembleDOM();
|
|
2541
|
+
this._attachEventListeners();
|
|
2542
|
+
this._initializeObservers();
|
|
2543
|
+
}
|
|
2544
|
+
connectedCallback() {
|
|
2545
|
+
// Ensure host has proper layout
|
|
2546
|
+
if (!this.style.display) {
|
|
2547
|
+
this.style.display = 'block';
|
|
2548
|
+
}
|
|
2549
|
+
if (!this.style.position) {
|
|
2550
|
+
this.style.position = 'relative';
|
|
2551
|
+
}
|
|
2552
|
+
// Load initial data if server-side is enabled
|
|
2553
|
+
if (this._config.serverSide.enabled && this._config.serverSide.initialSelectedValues) {
|
|
2554
|
+
this._loadInitialSelectedItems();
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
disconnectedCallback() {
|
|
2558
|
+
// Cleanup observers
|
|
2559
|
+
this._resizeObserver?.disconnect();
|
|
2560
|
+
this._intersectionObserver?.disconnect();
|
|
2561
|
+
if (this._busyTimeout)
|
|
2562
|
+
clearTimeout(this._busyTimeout);
|
|
2563
|
+
if (this._typeTimeout)
|
|
2564
|
+
clearTimeout(this._typeTimeout);
|
|
2565
|
+
if (this._searchTimeout)
|
|
2566
|
+
clearTimeout(this._searchTimeout);
|
|
2567
|
+
// Cleanup arrow click listener
|
|
2568
|
+
if (this._boundArrowClick && this._arrowContainer) {
|
|
2569
|
+
this._arrowContainer.removeEventListener('click', this._boundArrowClick);
|
|
2570
|
+
}
|
|
2571
|
+
// Remove style element
|
|
2572
|
+
if (this._styleElement && this._styleElement.parentNode) {
|
|
2573
|
+
this._styleElement.parentNode.removeChild(this._styleElement);
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
_initializeStyles() {
|
|
2577
|
+
// Create scoped styles for this component instance
|
|
2578
|
+
this._styleElement = document.createElement('style');
|
|
2579
|
+
this._styleElement.setAttribute('data-component', 'angular-enhanced-select');
|
|
2580
|
+
this._styleElement.setAttribute('data-id', this._uniqueId);
|
|
2581
|
+
const p = this.PREFIX; // shorthand
|
|
2582
|
+
this._styleElement.textContent = `
|
|
2583
|
+
/* Host styles - applied to <angular-enhanced-select> */
|
|
2584
|
+
angular-enhanced-select {
|
|
2585
|
+
display: block;
|
|
2586
|
+
position: relative;
|
|
2587
|
+
width: 100%;
|
|
2588
|
+
min-height: 44px;
|
|
2589
|
+
box-sizing: border-box;
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
/* Container */
|
|
2593
|
+
.${p}container {
|
|
2594
|
+
position: relative;
|
|
2595
|
+
width: 100%;
|
|
2596
|
+
min-height: 44px;
|
|
2597
|
+
box-sizing: border-box;
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
/* Input Container */
|
|
2601
|
+
.${p}input-container {
|
|
2602
|
+
position: relative;
|
|
2603
|
+
width: 100%;
|
|
2604
|
+
display: flex;
|
|
2605
|
+
align-items: center;
|
|
2606
|
+
flex-wrap: wrap;
|
|
2607
|
+
gap: 6px;
|
|
2608
|
+
padding: 6px 52px 6px 8px;
|
|
2609
|
+
min-height: 44px;
|
|
2610
|
+
background: white;
|
|
2611
|
+
border: 1px solid #d1d5db;
|
|
2612
|
+
border-radius: 6px;
|
|
2613
|
+
box-sizing: border-box;
|
|
2614
|
+
transition: all 0.2s ease;
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
.${p}input-container:focus-within {
|
|
2618
|
+
border-color: #667eea;
|
|
2619
|
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
/* Gradient separator before arrow */
|
|
2623
|
+
.${p}input-container::after {
|
|
2624
|
+
content: '';
|
|
2625
|
+
position: absolute;
|
|
2626
|
+
top: 50%;
|
|
2627
|
+
right: 40px;
|
|
2628
|
+
transform: translateY(-50%);
|
|
2629
|
+
width: 1px;
|
|
2630
|
+
height: 60%;
|
|
2631
|
+
background: linear-gradient(
|
|
2632
|
+
to bottom,
|
|
2633
|
+
transparent 0%,
|
|
2634
|
+
rgba(0, 0, 0, 0.1) 20%,
|
|
2635
|
+
rgba(0, 0, 0, 0.1) 80%,
|
|
2636
|
+
transparent 100%
|
|
2637
|
+
);
|
|
2638
|
+
pointer-events: none;
|
|
2639
|
+
z-index: 1;
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
/* Dropdown Arrow Container */
|
|
2643
|
+
.${p}arrow-container {
|
|
2644
|
+
position: absolute;
|
|
2645
|
+
top: 0;
|
|
2646
|
+
right: 0;
|
|
2647
|
+
bottom: 0;
|
|
2648
|
+
width: 40px;
|
|
2649
|
+
display: flex;
|
|
2650
|
+
align-items: center;
|
|
2651
|
+
justify-content: center;
|
|
2652
|
+
cursor: pointer;
|
|
2653
|
+
transition: background-color 0.2s ease;
|
|
2654
|
+
border-radius: 0 4px 4px 0;
|
|
2655
|
+
z-index: 2;
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
.${p}arrow-container:hover {
|
|
2659
|
+
background-color: rgba(102, 126, 234, 0.08);
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
.${p}arrow {
|
|
2663
|
+
width: 16px;
|
|
2664
|
+
height: 16px;
|
|
2665
|
+
color: #667eea;
|
|
2666
|
+
transition: transform 0.2s ease, color 0.2s ease;
|
|
2667
|
+
transform: translateY(0);
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
.${p}arrow-container:hover .${p}arrow {
|
|
2671
|
+
color: #667eea;
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
.${p}arrow.${p}open {
|
|
2675
|
+
transform: rotate(180deg);
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
/* Input */
|
|
2679
|
+
.${p}input {
|
|
2680
|
+
flex: 1;
|
|
2681
|
+
min-width: 120px;
|
|
2682
|
+
padding: 4px;
|
|
2683
|
+
border: none;
|
|
2684
|
+
font-size: 14px;
|
|
2685
|
+
line-height: 1.5;
|
|
2686
|
+
color: #1f2937;
|
|
2687
|
+
background: transparent;
|
|
2688
|
+
box-sizing: border-box;
|
|
2689
|
+
outline: none;
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
.${p}input::placeholder {
|
|
2693
|
+
color: #9ca3af;
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
/* Selection Badges */
|
|
2697
|
+
.${p}badge {
|
|
2698
|
+
display: inline-flex;
|
|
2699
|
+
align-items: center;
|
|
2700
|
+
gap: 4px;
|
|
2701
|
+
padding: 4px 8px;
|
|
2702
|
+
margin: 2px;
|
|
2703
|
+
background: #667eea;
|
|
2704
|
+
color: white;
|
|
2705
|
+
border-radius: 4px;
|
|
2706
|
+
font-size: 13px;
|
|
2707
|
+
line-height: 1;
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
.${p}badge-remove {
|
|
2711
|
+
display: inline-flex;
|
|
2712
|
+
align-items: center;
|
|
2713
|
+
justify-content: center;
|
|
2714
|
+
width: 16px;
|
|
2715
|
+
height: 16px;
|
|
2716
|
+
padding: 0;
|
|
2717
|
+
margin-left: 4px;
|
|
2718
|
+
background: rgba(255, 255, 255, 0.3);
|
|
2719
|
+
border: none;
|
|
2720
|
+
border-radius: 50%;
|
|
2721
|
+
color: white;
|
|
2722
|
+
font-size: 16px;
|
|
2723
|
+
line-height: 1;
|
|
2724
|
+
cursor: pointer;
|
|
2725
|
+
transition: background 0.2s;
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
.${p}badge-remove:hover {
|
|
2729
|
+
background: rgba(255, 255, 255, 0.5);
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
/* Dropdown */
|
|
2733
|
+
.${p}dropdown {
|
|
2734
|
+
position: absolute;
|
|
2735
|
+
scroll-behavior: smooth;
|
|
2736
|
+
top: 100%;
|
|
2737
|
+
left: 0;
|
|
2738
|
+
right: 0;
|
|
2739
|
+
margin-top: 4px;
|
|
2740
|
+
max-height: 300px;
|
|
2741
|
+
overflow: hidden;
|
|
2742
|
+
background: white;
|
|
2743
|
+
border: 1px solid #ccc;
|
|
2744
|
+
border-radius: 4px;
|
|
2745
|
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
2746
|
+
z-index: 1000;
|
|
2747
|
+
box-sizing: border-box;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
.${p}dropdown[style*="display: none"] {
|
|
2751
|
+
display: none !important;
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
/* Options Container */
|
|
2755
|
+
.${p}options-container {
|
|
2756
|
+
position: relative;
|
|
2757
|
+
max-height: 300px;
|
|
2758
|
+
overflow: auto;
|
|
2759
|
+
transition: opacity 0.2s ease-in-out;
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
/* Option */
|
|
2763
|
+
.${p}option {
|
|
2764
|
+
padding: 8px 12px;
|
|
2765
|
+
cursor: pointer;
|
|
2766
|
+
color: inherit;
|
|
2767
|
+
transition: background-color 0.15s ease;
|
|
2768
|
+
user-select: none;
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
.${p}option:hover {
|
|
2772
|
+
background-color: #f3f4f6;
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
.${p}option.${p}selected {
|
|
2776
|
+
background-color: #e0e7ff;
|
|
2777
|
+
color: #4338ca;
|
|
2778
|
+
font-weight: 500;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
.${p}option.${p}active {
|
|
2782
|
+
background-color: #f3f4f6;
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
/* Load More */
|
|
2786
|
+
.${p}load-more-container {
|
|
2787
|
+
padding: 12px;
|
|
2788
|
+
text-align: center;
|
|
2789
|
+
border-top: 1px solid #e0e0e0;
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
.${p}load-more-button {
|
|
2793
|
+
padding: 8px 16px;
|
|
2794
|
+
border: 1px solid #1976d2;
|
|
2795
|
+
background: white;
|
|
2796
|
+
color: #1976d2;
|
|
2797
|
+
border-radius: 4px;
|
|
2798
|
+
cursor: pointer;
|
|
2799
|
+
font-size: 14px;
|
|
2800
|
+
transition: all 0.2s ease;
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
.${p}load-more-button:hover {
|
|
2804
|
+
background: #1976d2;
|
|
2805
|
+
color: white;
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
.${p}load-more-button:disabled {
|
|
2809
|
+
opacity: 0.5;
|
|
2810
|
+
cursor: not-allowed;
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
/* Busy State */
|
|
2814
|
+
.${p}busy-bucket {
|
|
2815
|
+
padding: 16px;
|
|
2816
|
+
text-align: center;
|
|
2817
|
+
color: #666;
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
.${p}spinner {
|
|
2821
|
+
display: inline-block;
|
|
2822
|
+
width: 20px;
|
|
2823
|
+
height: 20px;
|
|
2824
|
+
border: 2px solid #ccc;
|
|
2825
|
+
border-top-color: #1976d2;
|
|
2826
|
+
border-radius: 50%;
|
|
2827
|
+
animation: ${p}spin 0.6s linear infinite;
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
@keyframes ${p}spin {
|
|
2831
|
+
to { transform: rotate(360deg); }
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
/* Empty State */
|
|
2835
|
+
.${p}empty-state {
|
|
2836
|
+
padding: 24px;
|
|
2837
|
+
text-align: center;
|
|
2838
|
+
color: #999;
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
/* Searching State */
|
|
2842
|
+
.${p}searching-state {
|
|
2843
|
+
padding: 24px;
|
|
2844
|
+
text-align: center;
|
|
2845
|
+
color: #667eea;
|
|
2846
|
+
font-style: italic;
|
|
2847
|
+
animation: ${p}pulse 1.5s ease-in-out infinite;
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
@keyframes ${p}pulse {
|
|
2851
|
+
0%, 100% { opacity: 1; }
|
|
2852
|
+
50% { opacity: 0.5; }
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
/* Error states */
|
|
2856
|
+
.${p}input[aria-invalid="true"] {
|
|
2857
|
+
border-color: #dc2626;
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
.${p}input[aria-invalid="true"]:focus {
|
|
2861
|
+
border-color: #dc2626;
|
|
2862
|
+
box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.1);
|
|
2863
|
+
outline-color: #dc2626;
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
/* Live Region (Screen reader only) */
|
|
2867
|
+
.${p}live-region {
|
|
2868
|
+
position: absolute;
|
|
2869
|
+
left: -10000px;
|
|
2870
|
+
width: 1px;
|
|
2871
|
+
height: 1px;
|
|
2872
|
+
overflow: hidden;
|
|
2873
|
+
clip: rect(0, 0, 0, 0);
|
|
2874
|
+
white-space: nowrap;
|
|
2875
|
+
border-width: 0;
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
/* Accessibility: Reduced motion */
|
|
2879
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2880
|
+
.${p}arrow,
|
|
2881
|
+
.${p}badge-remove,
|
|
2882
|
+
.${p}option,
|
|
2883
|
+
.${p}dropdown {
|
|
2884
|
+
animation-duration: 0.01ms !important;
|
|
2885
|
+
animation-iteration-count: 1 !important;
|
|
2886
|
+
transition-duration: 0.01ms !important;
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
/* Touch targets (WCAG 2.5.5) */
|
|
2891
|
+
.${p}load-more-button,
|
|
2892
|
+
.${p}option {
|
|
2893
|
+
min-height: 44px;
|
|
2894
|
+
}
|
|
2895
|
+
`;
|
|
2896
|
+
// Append to document head
|
|
2897
|
+
document.head.appendChild(this._styleElement);
|
|
2898
|
+
}
|
|
2899
|
+
_createContainer() {
|
|
2900
|
+
const container = document.createElement('div');
|
|
2901
|
+
container.className = `${this.PREFIX}container`;
|
|
2902
|
+
return container;
|
|
2903
|
+
}
|
|
2904
|
+
_createInputContainer() {
|
|
2905
|
+
const container = document.createElement('div');
|
|
2906
|
+
container.className = `${this.PREFIX}input-container`;
|
|
2907
|
+
return container;
|
|
2908
|
+
}
|
|
2909
|
+
_createInput() {
|
|
2910
|
+
const input = document.createElement('input');
|
|
2911
|
+
input.type = 'text';
|
|
2912
|
+
input.className = `${this.PREFIX}input`;
|
|
2913
|
+
input.placeholder = this.getAttribute('placeholder') || 'Select an option...';
|
|
2914
|
+
input.setAttribute('readonly', '');
|
|
2915
|
+
input.setAttribute('role', 'combobox');
|
|
2916
|
+
input.setAttribute('aria-expanded', 'false');
|
|
2917
|
+
input.setAttribute('aria-haspopup', 'listbox');
|
|
2918
|
+
input.setAttribute('aria-autocomplete', 'none');
|
|
2919
|
+
input.setAttribute('aria-controls', `${this._uniqueId}-listbox`);
|
|
2920
|
+
input.setAttribute('aria-owns', `${this._uniqueId}-listbox`);
|
|
2921
|
+
input.tabIndex = 0;
|
|
2922
|
+
return input;
|
|
2923
|
+
}
|
|
2924
|
+
_createArrowContainer() {
|
|
2925
|
+
const container = document.createElement('div');
|
|
2926
|
+
container.className = `${this.PREFIX}arrow-container`;
|
|
2927
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
2928
|
+
svg.setAttribute('class', `${this.PREFIX}arrow`);
|
|
2929
|
+
svg.setAttribute('width', '16');
|
|
2930
|
+
svg.setAttribute('height', '16');
|
|
2931
|
+
svg.setAttribute('viewBox', '0 0 16 16');
|
|
2932
|
+
svg.setAttribute('fill', 'none');
|
|
2933
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
2934
|
+
path.setAttribute('d', 'M4 6l4 4 4-4');
|
|
2935
|
+
path.setAttribute('stroke', 'currentColor');
|
|
2936
|
+
path.setAttribute('stroke-width', '2');
|
|
2937
|
+
path.setAttribute('stroke-linecap', 'round');
|
|
2938
|
+
path.setAttribute('stroke-linejoin', 'round');
|
|
2939
|
+
svg.appendChild(path);
|
|
2940
|
+
container.appendChild(svg);
|
|
2941
|
+
return container;
|
|
2942
|
+
}
|
|
2943
|
+
_createDropdown() {
|
|
2944
|
+
const dropdown = document.createElement('div');
|
|
2945
|
+
dropdown.id = `${this._uniqueId}-listbox`;
|
|
2946
|
+
dropdown.className = `${this.PREFIX}dropdown`;
|
|
2947
|
+
dropdown.style.display = 'none';
|
|
2948
|
+
dropdown.setAttribute('role', 'listbox');
|
|
2949
|
+
return dropdown;
|
|
2950
|
+
}
|
|
2951
|
+
_createOptionsContainer() {
|
|
2952
|
+
const container = document.createElement('div');
|
|
2953
|
+
container.className = `${this.PREFIX}options-container`;
|
|
2954
|
+
return container;
|
|
2955
|
+
}
|
|
2956
|
+
_createLiveRegion() {
|
|
2957
|
+
const region = document.createElement('div');
|
|
2958
|
+
region.className = `${this.PREFIX}live-region`;
|
|
2959
|
+
region.setAttribute('role', 'status');
|
|
2960
|
+
region.setAttribute('aria-live', 'polite');
|
|
2961
|
+
region.setAttribute('aria-atomic', 'true');
|
|
2962
|
+
return region;
|
|
2963
|
+
}
|
|
2964
|
+
_assembleDOM() {
|
|
2965
|
+
// Assemble in light DOM
|
|
2966
|
+
this._inputContainer.appendChild(this._input);
|
|
2967
|
+
this._inputContainer.appendChild(this._arrowContainer);
|
|
2968
|
+
this._container.appendChild(this._inputContainer);
|
|
2969
|
+
this._dropdown.appendChild(this._optionsContainer);
|
|
2970
|
+
this._container.appendChild(this._dropdown);
|
|
2971
|
+
this._container.appendChild(this._liveRegion);
|
|
2972
|
+
this.appendChild(this._container);
|
|
2973
|
+
}
|
|
2974
|
+
_attachEventListeners() {
|
|
2975
|
+
// Arrow click handler
|
|
2976
|
+
if (this._arrowContainer) {
|
|
2977
|
+
this._boundArrowClick = (e) => {
|
|
2978
|
+
e.stopPropagation();
|
|
2979
|
+
e.preventDefault();
|
|
2980
|
+
const wasOpen = this._state.isOpen;
|
|
2981
|
+
this._state.isOpen = !this._state.isOpen;
|
|
2982
|
+
this._updateDropdownVisibility();
|
|
2983
|
+
this._updateArrowRotation();
|
|
2984
|
+
if (this._state.isOpen && this._config.callbacks.onOpen) {
|
|
2985
|
+
this._config.callbacks.onOpen();
|
|
2986
|
+
}
|
|
2987
|
+
else if (!this._state.isOpen && this._config.callbacks.onClose) {
|
|
2988
|
+
this._config.callbacks.onClose();
|
|
2989
|
+
}
|
|
2990
|
+
if (!wasOpen && this._state.isOpen && this._state.selectedIndices.size > 0) {
|
|
2991
|
+
setTimeout(() => this._scrollToSelected(), 50);
|
|
2992
|
+
}
|
|
2993
|
+
};
|
|
2994
|
+
this._arrowContainer.addEventListener('click', this._boundArrowClick);
|
|
2995
|
+
}
|
|
2996
|
+
// Input focus handler
|
|
2997
|
+
this._input.addEventListener('focus', () => {
|
|
2998
|
+
if (!this._state.isOpen) {
|
|
2999
|
+
this._state.isOpen = true;
|
|
3000
|
+
this._updateDropdownVisibility();
|
|
3001
|
+
this._updateArrowRotation();
|
|
3002
|
+
if (this._config.callbacks.onOpen) {
|
|
3003
|
+
this._config.callbacks.onOpen();
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
});
|
|
3007
|
+
// Input keyboard handler
|
|
3008
|
+
this._input.addEventListener('keydown', (e) => this._handleKeydown(e));
|
|
3009
|
+
// Click outside to close
|
|
3010
|
+
document.addEventListener('click', (e) => {
|
|
3011
|
+
if (!this.contains(e.target) && this._state.isOpen) {
|
|
3012
|
+
this._state.isOpen = false;
|
|
3013
|
+
this._updateDropdownVisibility();
|
|
3014
|
+
this._updateArrowRotation();
|
|
3015
|
+
if (this._config.callbacks.onClose) {
|
|
3016
|
+
this._config.callbacks.onClose();
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
});
|
|
3020
|
+
// Search handler
|
|
3021
|
+
if (this.hasAttribute('searchable')) {
|
|
3022
|
+
this._input.removeAttribute('readonly');
|
|
3023
|
+
this._input.addEventListener('input', (e) => {
|
|
3024
|
+
const query = e.target.value;
|
|
3025
|
+
this._handleSearch(query);
|
|
3026
|
+
});
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
_initializeObservers() {
|
|
3030
|
+
// Resize observer for dropdown positioning
|
|
3031
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
3032
|
+
this._resizeObserver = new ResizeObserver(() => {
|
|
3033
|
+
if (this._state.isOpen) {
|
|
3034
|
+
this._updateDropdownPosition();
|
|
3035
|
+
}
|
|
3036
|
+
});
|
|
3037
|
+
this._resizeObserver.observe(this);
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
_updateDropdownVisibility() {
|
|
3041
|
+
if (this._state.isOpen) {
|
|
3042
|
+
this._dropdown.style.display = 'block';
|
|
3043
|
+
this._input.setAttribute('aria-expanded', 'true');
|
|
3044
|
+
this._updateDropdownPosition();
|
|
3045
|
+
}
|
|
3046
|
+
else {
|
|
3047
|
+
this._dropdown.style.display = 'none';
|
|
3048
|
+
this._input.setAttribute('aria-expanded', 'false');
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
_updateDropdownPosition() {
|
|
3052
|
+
// Ensure dropdown is positioned correctly relative to input
|
|
3053
|
+
const rect = this._inputContainer.getBoundingClientRect();
|
|
3054
|
+
const viewportHeight = window.innerHeight;
|
|
3055
|
+
const spaceBelow = viewportHeight - rect.bottom;
|
|
3056
|
+
const spaceAbove = rect.top;
|
|
3057
|
+
// Auto placement
|
|
3058
|
+
if (spaceBelow < 300 && spaceAbove > spaceBelow) {
|
|
3059
|
+
// Open upward
|
|
3060
|
+
this._dropdown.style.top = 'auto';
|
|
3061
|
+
this._dropdown.style.bottom = '100%';
|
|
3062
|
+
this._dropdown.style.marginTop = '0';
|
|
3063
|
+
this._dropdown.style.marginBottom = '4px';
|
|
3064
|
+
}
|
|
3065
|
+
else {
|
|
3066
|
+
// Open downward (default)
|
|
3067
|
+
this._dropdown.style.top = '100%';
|
|
3068
|
+
this._dropdown.style.bottom = 'auto';
|
|
3069
|
+
this._dropdown.style.marginTop = '4px';
|
|
3070
|
+
this._dropdown.style.marginBottom = '0';
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
_updateArrowRotation() {
|
|
3074
|
+
const arrow = this._arrowContainer?.querySelector(`.${this.PREFIX}arrow`);
|
|
3075
|
+
if (arrow) {
|
|
3076
|
+
if (this._state.isOpen) {
|
|
3077
|
+
arrow.classList.add(`${this.PREFIX}open`);
|
|
3078
|
+
}
|
|
3079
|
+
else {
|
|
3080
|
+
arrow.classList.remove(`${this.PREFIX}open`);
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
_handleKeydown(e) {
|
|
3085
|
+
// Implement keyboard navigation
|
|
3086
|
+
switch (e.key) {
|
|
3087
|
+
case 'ArrowDown':
|
|
3088
|
+
e.preventDefault();
|
|
3089
|
+
if (!this._state.isOpen) {
|
|
3090
|
+
this._state.isOpen = true;
|
|
3091
|
+
this._updateDropdownVisibility();
|
|
3092
|
+
this._updateArrowRotation();
|
|
3093
|
+
}
|
|
3094
|
+
else {
|
|
3095
|
+
this._moveActive(1);
|
|
3096
|
+
}
|
|
3097
|
+
break;
|
|
3098
|
+
case 'ArrowUp':
|
|
3099
|
+
e.preventDefault();
|
|
3100
|
+
if (this._state.isOpen) {
|
|
3101
|
+
this._moveActive(-1);
|
|
3102
|
+
}
|
|
3103
|
+
break;
|
|
3104
|
+
case 'Enter':
|
|
3105
|
+
e.preventDefault();
|
|
3106
|
+
if (this._state.isOpen && this._state.activeIndex >= 0) {
|
|
3107
|
+
this._selectByIndex(this._state.activeIndex);
|
|
3108
|
+
}
|
|
3109
|
+
else {
|
|
3110
|
+
this._state.isOpen = true;
|
|
3111
|
+
this._updateDropdownVisibility();
|
|
3112
|
+
this._updateArrowRotation();
|
|
3113
|
+
}
|
|
3114
|
+
break;
|
|
3115
|
+
case 'Escape':
|
|
3116
|
+
e.preventDefault();
|
|
3117
|
+
if (this._state.isOpen) {
|
|
3118
|
+
this._state.isOpen = false;
|
|
3119
|
+
this._updateDropdownVisibility();
|
|
3120
|
+
this._updateArrowRotation();
|
|
3121
|
+
}
|
|
3122
|
+
break;
|
|
3123
|
+
case 'Tab':
|
|
3124
|
+
if (this._state.isOpen) {
|
|
3125
|
+
this._state.isOpen = false;
|
|
3126
|
+
this._updateDropdownVisibility();
|
|
3127
|
+
this._updateArrowRotation();
|
|
3128
|
+
}
|
|
3129
|
+
break;
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
_moveActive(direction) {
|
|
3133
|
+
const options = this._optionsContainer.querySelectorAll(`.${this.PREFIX}option`);
|
|
3134
|
+
if (options.length === 0)
|
|
3135
|
+
return;
|
|
3136
|
+
let newIndex = this._state.activeIndex + direction;
|
|
3137
|
+
if (newIndex < 0)
|
|
3138
|
+
newIndex = 0;
|
|
3139
|
+
if (newIndex >= options.length)
|
|
3140
|
+
newIndex = options.length - 1;
|
|
3141
|
+
this._state.activeIndex = newIndex;
|
|
3142
|
+
// Update visual active state
|
|
3143
|
+
options.forEach((opt, idx) => {
|
|
3144
|
+
if (idx === newIndex) {
|
|
3145
|
+
opt.classList.add(`${this.PREFIX}active`);
|
|
3146
|
+
opt.scrollIntoView({ block: 'nearest' });
|
|
3147
|
+
}
|
|
3148
|
+
else {
|
|
3149
|
+
opt.classList.remove(`${this.PREFIX}active`);
|
|
3150
|
+
}
|
|
3151
|
+
});
|
|
3152
|
+
}
|
|
3153
|
+
_selectByIndex(index) {
|
|
3154
|
+
const item = this._state.loadedItems[index];
|
|
3155
|
+
if (!item)
|
|
3156
|
+
return;
|
|
3157
|
+
const isMultiple = this.hasAttribute('multiple');
|
|
3158
|
+
if (isMultiple) {
|
|
3159
|
+
// Toggle selection
|
|
3160
|
+
if (this._state.selectedIndices.has(index)) {
|
|
3161
|
+
this._state.selectedIndices.delete(index);
|
|
3162
|
+
this._state.selectedItems.delete(index);
|
|
3163
|
+
}
|
|
3164
|
+
else {
|
|
3165
|
+
this._state.selectedIndices.add(index);
|
|
3166
|
+
this._state.selectedItems.set(index, item);
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
else {
|
|
3170
|
+
// Single selection
|
|
3171
|
+
this._state.selectedIndices.clear();
|
|
3172
|
+
this._state.selectedItems.clear();
|
|
3173
|
+
this._state.selectedIndices.add(index);
|
|
3174
|
+
this._state.selectedItems.set(index, item);
|
|
3175
|
+
// Close dropdown
|
|
3176
|
+
this._state.isOpen = false;
|
|
3177
|
+
this._updateDropdownVisibility();
|
|
3178
|
+
this._updateArrowRotation();
|
|
3179
|
+
}
|
|
3180
|
+
this._updateInputDisplay();
|
|
3181
|
+
this._renderOptions();
|
|
3182
|
+
this._emitChangeEvent();
|
|
3183
|
+
}
|
|
3184
|
+
_updateInputDisplay() {
|
|
3185
|
+
const selectedItems = Array.from(this._state.selectedItems.values());
|
|
3186
|
+
const isMultiple = this.hasAttribute('multiple');
|
|
3187
|
+
if (isMultiple) {
|
|
3188
|
+
// Clear input, show badges
|
|
3189
|
+
this._input.value = '';
|
|
3190
|
+
// Remove existing badges
|
|
3191
|
+
this._inputContainer.querySelectorAll(`.${this.PREFIX}badge`).forEach(badge => badge.remove());
|
|
3192
|
+
// Add new badges
|
|
3193
|
+
selectedItems.forEach((item, idx) => {
|
|
3194
|
+
const badge = document.createElement('span');
|
|
3195
|
+
badge.className = `${this.PREFIX}badge`;
|
|
3196
|
+
badge.textContent = item.label;
|
|
3197
|
+
const removeBtn = document.createElement('button');
|
|
3198
|
+
removeBtn.className = `${this.PREFIX}badge-remove`;
|
|
3199
|
+
removeBtn.textContent = '×';
|
|
3200
|
+
removeBtn.addEventListener('click', (e) => {
|
|
3201
|
+
e.stopPropagation();
|
|
3202
|
+
const itemIndex = Array.from(this._state.selectedItems.keys())[idx];
|
|
3203
|
+
this._state.selectedIndices.delete(itemIndex);
|
|
3204
|
+
this._state.selectedItems.delete(itemIndex);
|
|
3205
|
+
this._updateInputDisplay();
|
|
3206
|
+
this._renderOptions();
|
|
3207
|
+
this._emitChangeEvent();
|
|
3208
|
+
});
|
|
3209
|
+
badge.appendChild(removeBtn);
|
|
3210
|
+
this._inputContainer.insertBefore(badge, this._input);
|
|
3211
|
+
});
|
|
3212
|
+
}
|
|
3213
|
+
else {
|
|
3214
|
+
// Single selection - show label in input
|
|
3215
|
+
if (selectedItems.length > 0) {
|
|
3216
|
+
this._input.value = selectedItems[0].label;
|
|
3217
|
+
}
|
|
3218
|
+
else {
|
|
3219
|
+
this._input.value = '';
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
_renderOptions() {
|
|
3224
|
+
// Clear existing options
|
|
3225
|
+
this._optionsContainer.innerHTML = '';
|
|
3226
|
+
const items = this._state.loadedItems;
|
|
3227
|
+
if (items.length === 0) {
|
|
3228
|
+
const emptyDiv = document.createElement('div');
|
|
3229
|
+
emptyDiv.className = `${this.PREFIX}empty-state`;
|
|
3230
|
+
emptyDiv.textContent = 'No options available';
|
|
3231
|
+
this._optionsContainer.appendChild(emptyDiv);
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
3234
|
+
// Render options
|
|
3235
|
+
items.forEach((item, index) => {
|
|
3236
|
+
const optionDiv = document.createElement('div');
|
|
3237
|
+
optionDiv.className = `${this.PREFIX}option`;
|
|
3238
|
+
optionDiv.textContent = item.label;
|
|
3239
|
+
optionDiv.setAttribute('role', 'option');
|
|
3240
|
+
optionDiv.setAttribute('data-index', String(index));
|
|
3241
|
+
if (this._state.selectedIndices.has(index)) {
|
|
3242
|
+
optionDiv.classList.add(`${this.PREFIX}selected`);
|
|
3243
|
+
optionDiv.setAttribute('aria-selected', 'true');
|
|
3244
|
+
}
|
|
3245
|
+
if (item.disabled) {
|
|
3246
|
+
optionDiv.style.opacity = '0.5';
|
|
3247
|
+
optionDiv.style.cursor = 'not-allowed';
|
|
3248
|
+
}
|
|
3249
|
+
else {
|
|
3250
|
+
optionDiv.addEventListener('click', () => {
|
|
3251
|
+
this._selectByIndex(index);
|
|
3252
|
+
});
|
|
3253
|
+
}
|
|
3254
|
+
this._optionsContainer.appendChild(optionDiv);
|
|
3255
|
+
});
|
|
3256
|
+
}
|
|
3257
|
+
_handleSearch(query) {
|
|
3258
|
+
this._state.searchQuery = query;
|
|
3259
|
+
// Filter items based on search
|
|
3260
|
+
const allItems = this._state.loadedItems;
|
|
3261
|
+
const filtered = allItems.filter(item => item.label.toLowerCase().includes(query.toLowerCase()));
|
|
3262
|
+
// Temporarily replace loaded items with filtered
|
|
3263
|
+
this._state.loadedItems;
|
|
3264
|
+
this._state.loadedItems = filtered;
|
|
3265
|
+
this._renderOptions();
|
|
3266
|
+
// Emit search event
|
|
3267
|
+
this.dispatchEvent(new CustomEvent('search', {
|
|
3268
|
+
detail: { query },
|
|
3269
|
+
bubbles: true,
|
|
3270
|
+
composed: true,
|
|
3271
|
+
}));
|
|
3272
|
+
}
|
|
3273
|
+
_emitChangeEvent() {
|
|
3274
|
+
const selectedItems = Array.from(this._state.selectedItems.values());
|
|
3275
|
+
const selectedValues = selectedItems.map(item => item.value);
|
|
3276
|
+
const selectedIndices = Array.from(this._state.selectedIndices);
|
|
3277
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
3278
|
+
detail: { selectedItems, selectedValues, selectedIndices },
|
|
3279
|
+
bubbles: true,
|
|
3280
|
+
composed: true,
|
|
3281
|
+
}));
|
|
3282
|
+
}
|
|
3283
|
+
_scrollToSelected() {
|
|
3284
|
+
const firstSelected = this._optionsContainer.querySelector(`.${this.PREFIX}selected`);
|
|
3285
|
+
if (firstSelected) {
|
|
3286
|
+
firstSelected.scrollIntoView({ block: 'nearest' });
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
_loadInitialSelectedItems() {
|
|
3290
|
+
// Placeholder for server-side data loading
|
|
3291
|
+
}
|
|
3292
|
+
_announce(message) {
|
|
3293
|
+
if (this._liveRegion) {
|
|
3294
|
+
this._liveRegion.textContent = message;
|
|
3295
|
+
setTimeout(() => {
|
|
3296
|
+
if (this._liveRegion)
|
|
3297
|
+
this._liveRegion.textContent = '';
|
|
3298
|
+
}, 1000);
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
// Public API methods
|
|
3302
|
+
setItems(items) {
|
|
3303
|
+
this._state.loadedItems = items;
|
|
3304
|
+
this._renderOptions();
|
|
3305
|
+
}
|
|
3306
|
+
setGroupedItems(groups) {
|
|
3307
|
+
this._state.groupedItems = groups;
|
|
3308
|
+
// Flatten for now
|
|
3309
|
+
const items = [];
|
|
3310
|
+
groups.forEach(group => {
|
|
3311
|
+
if (group.items) {
|
|
3312
|
+
items.push(...group.items);
|
|
3313
|
+
}
|
|
3314
|
+
});
|
|
3315
|
+
this.setItems(items);
|
|
3316
|
+
}
|
|
3317
|
+
setSelectedValues(values) {
|
|
3318
|
+
this._state.selectedIndices.clear();
|
|
3319
|
+
this._state.selectedItems.clear();
|
|
3320
|
+
values.forEach(value => {
|
|
3321
|
+
const index = this._state.loadedItems.findIndex(item => item.value === value);
|
|
3322
|
+
if (index >= 0) {
|
|
3323
|
+
this._state.selectedIndices.add(index);
|
|
3324
|
+
this._state.selectedItems.set(index, this._state.loadedItems[index]);
|
|
3325
|
+
}
|
|
3326
|
+
});
|
|
3327
|
+
this._updateInputDisplay();
|
|
3328
|
+
this._renderOptions();
|
|
3329
|
+
}
|
|
3330
|
+
getSelectedValues() {
|
|
3331
|
+
return Array.from(this._state.selectedItems.values()).map(item => item.value);
|
|
3332
|
+
}
|
|
3333
|
+
updateConfig(config) {
|
|
3334
|
+
this._config = { ...this._config, ...config };
|
|
3335
|
+
}
|
|
3336
|
+
setError(message) {
|
|
3337
|
+
this._hasError = true;
|
|
3338
|
+
this._errorMessage = message;
|
|
3339
|
+
this._input.setAttribute('aria-invalid', 'true');
|
|
3340
|
+
this._announce(`Error: ${message}`);
|
|
3341
|
+
}
|
|
3342
|
+
clearError() {
|
|
3343
|
+
this._hasError = false;
|
|
3344
|
+
this._errorMessage = '';
|
|
3345
|
+
this._input.removeAttribute('aria-invalid');
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
// Register the custom element
|
|
3349
|
+
if (typeof customElements !== 'undefined' && !customElements.get('angular-enhanced-select')) {
|
|
3350
|
+
customElements.define('angular-enhanced-select', AngularEnhancedSelect);
|
|
3351
|
+
}
|
|
3352
|
+
|
|
2472
3353
|
/**
|
|
2473
3354
|
* Independent Option Component
|
|
2474
3355
|
* High cohesion, low coupling - handles its own selection state and events
|
|
@@ -3731,6 +4612,7 @@ function warnCSPViolation(feature, fallback) {
|
|
|
3731
4612
|
}
|
|
3732
4613
|
}
|
|
3733
4614
|
|
|
4615
|
+
exports.AngularEnhancedSelect = AngularEnhancedSelect;
|
|
3734
4616
|
exports.CSPFeatures = CSPFeatures;
|
|
3735
4617
|
exports.DOMPool = DOMPool;
|
|
3736
4618
|
exports.EnhancedSelect = EnhancedSelect;
|