@liedekef/ftable 1.1.49 → 1.1.51
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/ftable.esm.js +347 -91
- package/ftable.js +347 -91
- package/ftable.min.js +2 -24
- package/ftable.umd.js +347 -91
- package/package.json +1 -1
- package/themes/basic/ftable_basic.css +18 -7
- package/themes/basic/ftable_basic.min.css +1 -1
- package/themes/ftable_theme_base.less +21 -10
- package/themes/lightcolor/blue/ftable.css +18 -7
- package/themes/lightcolor/blue/ftable.min.css +1 -1
- package/themes/lightcolor/gray/ftable.css +18 -7
- package/themes/lightcolor/gray/ftable.min.css +1 -1
- package/themes/lightcolor/green/ftable.css +18 -7
- package/themes/lightcolor/green/ftable.min.css +1 -1
- package/themes/lightcolor/orange/ftable.css +18 -7
- package/themes/lightcolor/orange/ftable.min.css +1 -1
- package/themes/lightcolor/red/ftable.css +18 -7
- package/themes/lightcolor/red/ftable.min.css +1 -1
- package/themes/metro/blue/ftable.css +18 -7
- package/themes/metro/blue/ftable.min.css +1 -1
- package/themes/metro/brown/ftable.css +18 -7
- package/themes/metro/brown/ftable.min.css +1 -1
- package/themes/metro/crimson/ftable.css +18 -7
- package/themes/metro/crimson/ftable.min.css +1 -1
- package/themes/metro/darkgray/ftable.css +18 -7
- package/themes/metro/darkgray/ftable.min.css +1 -1
- package/themes/metro/darkorange/ftable.css +18 -7
- package/themes/metro/darkorange/ftable.min.css +1 -1
- package/themes/metro/green/ftable.css +18 -7
- package/themes/metro/green/ftable.min.css +1 -1
- package/themes/metro/lightgray/ftable.css +18 -7
- package/themes/metro/lightgray/ftable.min.css +1 -1
- package/themes/metro/pink/ftable.css +18 -7
- package/themes/metro/pink/ftable.min.css +1 -1
- package/themes/metro/purple/ftable.css +18 -7
- package/themes/metro/purple/ftable.min.css +1 -1
- package/themes/metro/red/ftable.css +18 -7
- package/themes/metro/red/ftable.min.css +1 -1
package/ftable.esm.js
CHANGED
|
@@ -1429,7 +1429,10 @@ class FTableFormBuilder {
|
|
|
1429
1429
|
// Create display area
|
|
1430
1430
|
const display = FTableDOMHelper.create('div', {
|
|
1431
1431
|
className: 'ftable-multiselect-display',
|
|
1432
|
-
parent: container
|
|
1432
|
+
parent: container,
|
|
1433
|
+
attributes: {
|
|
1434
|
+
tabindex: '0' // Makes it focusable and in tab order
|
|
1435
|
+
}
|
|
1433
1436
|
});
|
|
1434
1437
|
|
|
1435
1438
|
const selectedDisplay = FTableDOMHelper.create('div', {
|
|
@@ -1449,15 +1452,15 @@ class FTableFormBuilder {
|
|
|
1449
1452
|
type: 'button',
|
|
1450
1453
|
className: 'ftable-multiselect-toggle',
|
|
1451
1454
|
innerHTML: '▼',
|
|
1452
|
-
parent: display
|
|
1455
|
+
parent: display,
|
|
1456
|
+
attributes: {
|
|
1457
|
+
tabindex: '-1' // this skips regular focus when tabbing
|
|
1458
|
+
}
|
|
1453
1459
|
});
|
|
1454
1460
|
|
|
1455
|
-
//
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
parent: container,
|
|
1459
|
-
style: 'display: none;'
|
|
1460
|
-
});
|
|
1461
|
+
// Dropdown and overlay will be created on demand and appended to body
|
|
1462
|
+
let dropdown = null;
|
|
1463
|
+
let dropdownOverlay = null;
|
|
1461
1464
|
|
|
1462
1465
|
// Store selected values and checkbox references
|
|
1463
1466
|
const selectedValues = new Set(
|
|
@@ -1525,9 +1528,54 @@ class FTableFormBuilder {
|
|
|
1525
1528
|
hiddenInput.value = Array.from(selectedValues).join(',');
|
|
1526
1529
|
};
|
|
1527
1530
|
|
|
1531
|
+
// Function to close dropdown
|
|
1532
|
+
const closeDropdown = () => {
|
|
1533
|
+
display.focus(); // Return focus to the trigger
|
|
1534
|
+
if (dropdown) {
|
|
1535
|
+
dropdown.remove();
|
|
1536
|
+
dropdown = null;
|
|
1537
|
+
}
|
|
1538
|
+
if (dropdownOverlay) {
|
|
1539
|
+
dropdownOverlay.remove();
|
|
1540
|
+
dropdownOverlay = null;
|
|
1541
|
+
}
|
|
1542
|
+
if (container._cleanupHandlers) {
|
|
1543
|
+
container._cleanupHandlers();
|
|
1544
|
+
container._cleanupHandlers = null;
|
|
1545
|
+
}
|
|
1546
|
+
};
|
|
1547
|
+
|
|
1548
|
+
// Function to position dropdown
|
|
1549
|
+
const positionDropdown = () => {
|
|
1550
|
+
if (!dropdown) return;
|
|
1551
|
+
|
|
1552
|
+
const rect = display.getBoundingClientRect();
|
|
1553
|
+
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
1554
|
+
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
|
|
1555
|
+
|
|
1556
|
+
let left = rect.left + scrollLeft;
|
|
1557
|
+
let top = rect.bottom + scrollTop + 4; // 4px gap
|
|
1558
|
+
|
|
1559
|
+
dropdown.style.position = 'absolute';
|
|
1560
|
+
dropdown.style.left = `${left}px`;
|
|
1561
|
+
dropdown.style.top = `${top}px`;
|
|
1562
|
+
dropdown.style.width = `${rect.width}px`;
|
|
1563
|
+
dropdown.style.minWidth = `${rect.width}px`;
|
|
1564
|
+
dropdown.style.boxSizing = 'border-box';
|
|
1565
|
+
dropdown.style.zIndex = '10000';
|
|
1566
|
+
|
|
1567
|
+
// Adjust horizontal position if needed
|
|
1568
|
+
const dropdownRect = dropdown.getBoundingClientRect();
|
|
1569
|
+
const viewportWidth = window.innerWidth;
|
|
1570
|
+
if (dropdownRect.right > viewportWidth) {
|
|
1571
|
+
left = Math.max(10, viewportWidth - dropdownRect.width - 10);
|
|
1572
|
+
dropdown.style.left = `${left}px`;
|
|
1573
|
+
}
|
|
1574
|
+
};
|
|
1575
|
+
|
|
1528
1576
|
// Populate options
|
|
1529
1577
|
const populateOptions = () => {
|
|
1530
|
-
if (!field.options) return;
|
|
1578
|
+
if (!field.options || !dropdown) return;
|
|
1531
1579
|
|
|
1532
1580
|
const options = Array.isArray(field.options) ? field.options :
|
|
1533
1581
|
Object.entries(field.options).map(([k, v]) => ({Value: k, DisplayText: v}));
|
|
@@ -1584,27 +1632,123 @@ class FTableFormBuilder {
|
|
|
1584
1632
|
// Toggle dropdown
|
|
1585
1633
|
const toggleDropdown = (e) => {
|
|
1586
1634
|
if (e) e.stopPropagation();
|
|
1587
|
-
const isVisible = dropdown.style.display !== 'none';
|
|
1588
|
-
dropdown.style.display = isVisible ? 'none' : 'block';
|
|
1589
1635
|
|
|
1590
|
-
if (
|
|
1591
|
-
//
|
|
1592
|
-
|
|
1593
|
-
|
|
1636
|
+
if (dropdown) {
|
|
1637
|
+
// Dropdown is open, close it
|
|
1638
|
+
closeDropdown();
|
|
1639
|
+
} else {
|
|
1640
|
+
// Close any other open multiselect dropdowns
|
|
1641
|
+
document.querySelectorAll('.ftable-multiselect-dropdown').forEach(dd => dd.remove());
|
|
1642
|
+
document.querySelectorAll('.ftable-multiselect-overlay').forEach(ov => ov.remove());
|
|
1643
|
+
|
|
1644
|
+
// Create overlay
|
|
1645
|
+
dropdownOverlay = FTableDOMHelper.create('div', {
|
|
1646
|
+
className: 'ftable-multiselect-overlay',
|
|
1647
|
+
parent: document.body
|
|
1648
|
+
});
|
|
1649
|
+
|
|
1650
|
+
// Create dropdown
|
|
1651
|
+
dropdown = FTableDOMHelper.create('div', {
|
|
1652
|
+
className: 'ftable-multiselect-dropdown',
|
|
1653
|
+
parent: document.body,
|
|
1654
|
+
attributes: {
|
|
1655
|
+
tabindex: '-1',
|
|
1656
|
+
role: 'listbox',
|
|
1657
|
+
'aria-multiselectable': 'true'
|
|
1658
|
+
}
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
// Populate options
|
|
1662
|
+
populateOptions();
|
|
1663
|
+
|
|
1664
|
+
// Position dropdown
|
|
1665
|
+
positionDropdown();
|
|
1666
|
+
|
|
1667
|
+
// dropdown focus
|
|
1668
|
+
dropdown.focus();
|
|
1669
|
+
|
|
1670
|
+
// Add keyboard navigation
|
|
1671
|
+
dropdown.addEventListener('keydown', (e) => {
|
|
1672
|
+
if (e.key === 'Escape') {
|
|
1673
|
+
closeDropdown();
|
|
1674
|
+
} else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
1675
|
+
e.preventDefault();
|
|
1676
|
+
// Navigate between options
|
|
1677
|
+
const checkboxes = Array.from(dropdown.querySelectorAll('.ftable-multiselect-checkbox'));
|
|
1678
|
+
const current = document.activeElement;
|
|
1679
|
+
const currentIndex = checkboxes.indexOf(current);
|
|
1680
|
+
|
|
1681
|
+
let nextIndex;
|
|
1682
|
+
if (e.key === 'ArrowDown') {
|
|
1683
|
+
nextIndex = currentIndex < checkboxes.length - 1 ? currentIndex + 1 : 0;
|
|
1684
|
+
} else {
|
|
1685
|
+
nextIndex = currentIndex > 0 ? currentIndex - 1 : checkboxes.length - 1;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
checkboxes[nextIndex].focus();
|
|
1689
|
+
} else if (e.key === ' ' || e.key === 'Enter') {
|
|
1690
|
+
e.preventDefault();
|
|
1691
|
+
// Toggle the focused checkbox
|
|
1692
|
+
if (document.activeElement.classList.contains('ftable-multiselect-checkbox')) {
|
|
1693
|
+
document.activeElement.click();
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
});
|
|
1697
|
+
|
|
1698
|
+
// Handle clicks outside
|
|
1699
|
+
dropdownOverlay.addEventListener('click', (event) => {
|
|
1700
|
+
if (event.target === dropdownOverlay) {
|
|
1701
|
+
closeDropdown();
|
|
1702
|
+
}
|
|
1594
1703
|
});
|
|
1704
|
+
|
|
1705
|
+
// Reposition on scroll/resize
|
|
1706
|
+
const scrollHandler = (e) => {
|
|
1707
|
+
if (dropdown && dropdown.contains(e.target)) {
|
|
1708
|
+
return; // Allow scrolling inside dropdown
|
|
1709
|
+
}
|
|
1710
|
+
positionDropdown();
|
|
1711
|
+
};
|
|
1712
|
+
const repositionHandler = () => positionDropdown();
|
|
1713
|
+
window.addEventListener('scroll', scrollHandler, true);
|
|
1714
|
+
window.addEventListener('resize', repositionHandler);
|
|
1715
|
+
|
|
1716
|
+
// Store cleanup function
|
|
1717
|
+
container._cleanupHandlers = () => {
|
|
1718
|
+
window.removeEventListener('scroll', scrollHandler, true);
|
|
1719
|
+
window.removeEventListener('resize', repositionHandler);
|
|
1720
|
+
};
|
|
1595
1721
|
}
|
|
1596
1722
|
};
|
|
1597
1723
|
|
|
1598
1724
|
display.addEventListener('click', toggleDropdown);
|
|
1599
1725
|
toggleBtn.addEventListener('click', toggleDropdown);
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
dropdown.style.display = 'none';
|
|
1726
|
+
display.addEventListener('keydown', (e) => {
|
|
1727
|
+
if (e.key === 'ArrowDown' || e.key === 'Enter') {
|
|
1728
|
+
e.preventDefault();
|
|
1729
|
+
toggleDropdown();
|
|
1605
1730
|
}
|
|
1606
1731
|
});
|
|
1607
1732
|
|
|
1733
|
+
// Clean up when container is removed from DOM
|
|
1734
|
+
const observer = new MutationObserver((mutations) => {
|
|
1735
|
+
mutations.forEach((mutation) => {
|
|
1736
|
+
mutation.removedNodes.forEach((node) => {
|
|
1737
|
+
if (node === container || node.contains && node.contains(container)) {
|
|
1738
|
+
closeDropdown();
|
|
1739
|
+
observer.disconnect();
|
|
1740
|
+
}
|
|
1741
|
+
});
|
|
1742
|
+
});
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1745
|
+
// Start observing once container is in the DOM
|
|
1746
|
+
setTimeout(() => {
|
|
1747
|
+
if (container.parentNode) {
|
|
1748
|
+
observer.observe(container.parentNode, { childList: true, subtree: true });
|
|
1749
|
+
}
|
|
1750
|
+
}, 0);
|
|
1751
|
+
|
|
1608
1752
|
// Initialize
|
|
1609
1753
|
populateOptions();
|
|
1610
1754
|
updateDisplay();
|
|
@@ -1931,9 +2075,6 @@ class FTable extends FTableEventEmitter {
|
|
|
1931
2075
|
this.updateSortingHeaders();
|
|
1932
2076
|
this.renderSortingInfo();
|
|
1933
2077
|
|
|
1934
|
-
// Add essential CSS if not already present
|
|
1935
|
-
//this.addEssentialCSS();
|
|
1936
|
-
|
|
1937
2078
|
// now make sure all tables have a % width
|
|
1938
2079
|
this.initColumnWidths();
|
|
1939
2080
|
}
|
|
@@ -2013,40 +2154,6 @@ class FTable extends FTableEventEmitter {
|
|
|
2013
2154
|
return result;
|
|
2014
2155
|
}
|
|
2015
2156
|
|
|
2016
|
-
addEssentialCSS() {
|
|
2017
|
-
// Check if our CSS is already added
|
|
2018
|
-
if (document.querySelector('#ftable-essential-css')) return;
|
|
2019
|
-
|
|
2020
|
-
const css = `
|
|
2021
|
-
.ftable-row-animation {
|
|
2022
|
-
transition: background-color 0.3s ease;
|
|
2023
|
-
}
|
|
2024
|
-
|
|
2025
|
-
.ftable-row-added {
|
|
2026
|
-
background-color: #d4edda !important;
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
.ftable-row-edited {
|
|
2030
|
-
background-color: #d1ecf1 !important;
|
|
2031
|
-
}
|
|
2032
|
-
|
|
2033
|
-
.ftable-row-deleted {
|
|
2034
|
-
opacity: 0;
|
|
2035
|
-
transform: translateY(-10px);
|
|
2036
|
-
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
.ftable-toolbarsearch {
|
|
2040
|
-
width: 90%;
|
|
2041
|
-
}
|
|
2042
|
-
`;
|
|
2043
|
-
|
|
2044
|
-
const style = document.createElement('style');
|
|
2045
|
-
style.id = 'ftable-essential-css';
|
|
2046
|
-
style.textContent = css;
|
|
2047
|
-
document.head.appendChild(style);
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
2157
|
createPagingUI() {
|
|
2051
2158
|
this.elements.bottomPanel = FTableDOMHelper.create('div', {
|
|
2052
2159
|
className: 'ftable-bottom-panel',
|
|
@@ -2680,7 +2787,10 @@ class FTable extends FTableEventEmitter {
|
|
|
2680
2787
|
// Create display area
|
|
2681
2788
|
const display = FTableDOMHelper.create('div', {
|
|
2682
2789
|
className: 'ftable-multiselect-display',
|
|
2683
|
-
parent: container
|
|
2790
|
+
parent: container,
|
|
2791
|
+
attributes: {
|
|
2792
|
+
tabindex: '0' // Makes it focusable and in tab order
|
|
2793
|
+
}
|
|
2684
2794
|
});
|
|
2685
2795
|
|
|
2686
2796
|
const selectedDisplay = FTableDOMHelper.create('div', {
|
|
@@ -2700,15 +2810,15 @@ class FTable extends FTableEventEmitter {
|
|
|
2700
2810
|
type: 'button',
|
|
2701
2811
|
className: 'ftable-multiselect-toggle',
|
|
2702
2812
|
innerHTML: '▼',
|
|
2703
|
-
parent: display
|
|
2813
|
+
parent: display,
|
|
2814
|
+
attributes: {
|
|
2815
|
+
tabindex: '-1' // this skips regular focus when tabbing
|
|
2816
|
+
}
|
|
2704
2817
|
});
|
|
2705
2818
|
|
|
2706
|
-
//
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
parent: container,
|
|
2710
|
-
style: 'display: none;'
|
|
2711
|
-
});
|
|
2819
|
+
// Dropdown and overlay will be created on demand and appended to body
|
|
2820
|
+
let dropdown = null;
|
|
2821
|
+
let dropdownOverlay = null;
|
|
2712
2822
|
|
|
2713
2823
|
// Store selected values and checkbox references
|
|
2714
2824
|
const selectedValues = new Set();
|
|
@@ -2775,18 +2885,54 @@ class FTable extends FTableEventEmitter {
|
|
|
2775
2885
|
}
|
|
2776
2886
|
};
|
|
2777
2887
|
|
|
2778
|
-
//
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2888
|
+
// Function to close dropdown
|
|
2889
|
+
const closeDropdown = () => {
|
|
2890
|
+
display.focus(); // Return focus to the trigger
|
|
2891
|
+
if (dropdown) {
|
|
2892
|
+
dropdown.remove();
|
|
2893
|
+
dropdown = null;
|
|
2894
|
+
}
|
|
2895
|
+
if (dropdownOverlay) {
|
|
2896
|
+
dropdownOverlay.remove();
|
|
2897
|
+
dropdownOverlay = null;
|
|
2898
|
+
}
|
|
2899
|
+
if (container._cleanupHandlers) {
|
|
2900
|
+
container._cleanupHandlers();
|
|
2901
|
+
container._cleanupHandlers = null;
|
|
2902
|
+
}
|
|
2903
|
+
};
|
|
2904
|
+
|
|
2905
|
+
// Function to position dropdown
|
|
2906
|
+
const positionDropdown = () => {
|
|
2907
|
+
if (!dropdown) return;
|
|
2908
|
+
|
|
2909
|
+
const rect = display.getBoundingClientRect();
|
|
2910
|
+
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
2911
|
+
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
|
|
2912
|
+
|
|
2913
|
+
let left = rect.left + scrollLeft;
|
|
2914
|
+
let top = rect.bottom + scrollTop + 4; // 4px gap
|
|
2915
|
+
|
|
2916
|
+
dropdown.style.position = 'absolute';
|
|
2917
|
+
dropdown.style.left = `${left}px`;
|
|
2918
|
+
dropdown.style.top = `${top}px`;
|
|
2919
|
+
dropdown.style.width = `${rect.width}px`;
|
|
2920
|
+
dropdown.style.minWidth = `${rect.width}px`;
|
|
2921
|
+
dropdown.style.boxSizing = 'border-box';
|
|
2922
|
+
dropdown.style.zIndex = '10000';
|
|
2923
|
+
|
|
2924
|
+
// Adjust horizontal position if needed
|
|
2925
|
+
const dropdownRect = dropdown.getBoundingClientRect();
|
|
2926
|
+
const viewportWidth = window.innerWidth;
|
|
2927
|
+
if (dropdownRect.right > viewportWidth) {
|
|
2928
|
+
left = Math.max(10, viewportWidth - dropdownRect.width - 10);
|
|
2929
|
+
dropdown.style.left = `${left}px`;
|
|
2930
|
+
}
|
|
2785
2931
|
};
|
|
2786
2932
|
|
|
2787
2933
|
// Populate options in both hidden select and dropdown
|
|
2788
2934
|
const populateOptions = () => {
|
|
2789
|
-
if (!optionsSource) return;
|
|
2935
|
+
if (!optionsSource || !dropdown) return;
|
|
2790
2936
|
|
|
2791
2937
|
const options = Array.isArray(optionsSource) ? optionsSource :
|
|
2792
2938
|
Object.entries(optionsSource).map(([k, v]) => ({Value: k, DisplayText: v}));
|
|
@@ -2802,12 +2948,14 @@ class FTable extends FTableEventEmitter {
|
|
|
2802
2948
|
|
|
2803
2949
|
const optText = option.DisplayText || option.text || option;
|
|
2804
2950
|
|
|
2805
|
-
// Add to hidden select
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2951
|
+
// Add to hidden select (only once)
|
|
2952
|
+
if (!hiddenSelect.querySelector(`option[value="${optValue}"]`)) {
|
|
2953
|
+
FTableDOMHelper.create('option', {
|
|
2954
|
+
value: optValue,
|
|
2955
|
+
textContent: optText,
|
|
2956
|
+
parent: hiddenSelect
|
|
2957
|
+
});
|
|
2958
|
+
}
|
|
2811
2959
|
|
|
2812
2960
|
// Add to visual dropdown
|
|
2813
2961
|
const optionDiv = FTableDOMHelper.create('div', {
|
|
@@ -2821,6 +2969,9 @@ class FTable extends FTableEventEmitter {
|
|
|
2821
2969
|
parent: optionDiv
|
|
2822
2970
|
});
|
|
2823
2971
|
|
|
2972
|
+
// Set initial checked state
|
|
2973
|
+
checkbox.checked = selectedValues.has(optValue.toString());
|
|
2974
|
+
|
|
2824
2975
|
// Store checkbox reference
|
|
2825
2976
|
checkboxMap.set(optValue.toString(), checkbox);
|
|
2826
2977
|
|
|
@@ -2850,29 +3001,134 @@ class FTable extends FTableEventEmitter {
|
|
|
2850
3001
|
// Toggle dropdown
|
|
2851
3002
|
const toggleDropdown = (e) => {
|
|
2852
3003
|
if (e) e.stopPropagation();
|
|
2853
|
-
const isVisible = dropdown.style.display !== 'none';
|
|
2854
|
-
dropdown.style.display = isVisible ? 'none' : 'block';
|
|
2855
3004
|
|
|
2856
|
-
if (
|
|
2857
|
-
//
|
|
2858
|
-
|
|
2859
|
-
|
|
3005
|
+
if (dropdown) {
|
|
3006
|
+
// Dropdown is open, close it
|
|
3007
|
+
closeDropdown();
|
|
3008
|
+
} else {
|
|
3009
|
+
// Close any other open multiselect dropdowns
|
|
3010
|
+
document.querySelectorAll('.ftable-multiselect-dropdown').forEach(dd => dd.remove());
|
|
3011
|
+
document.querySelectorAll('.ftable-multiselect-overlay').forEach(ov => ov.remove());
|
|
3012
|
+
|
|
3013
|
+
// Create overlay
|
|
3014
|
+
dropdownOverlay = FTableDOMHelper.create('div', {
|
|
3015
|
+
className: 'ftable-multiselect-overlay',
|
|
3016
|
+
parent: document.body
|
|
3017
|
+
});
|
|
3018
|
+
|
|
3019
|
+
// Create dropdown
|
|
3020
|
+
dropdown = FTableDOMHelper.create('div', {
|
|
3021
|
+
className: 'ftable-multiselect-dropdown',
|
|
3022
|
+
parent: document.body,
|
|
3023
|
+
attributes: {
|
|
3024
|
+
tabindex: '-1',
|
|
3025
|
+
role: 'listbox',
|
|
3026
|
+
'aria-multiselectable': 'true'
|
|
3027
|
+
}
|
|
3028
|
+
});
|
|
3029
|
+
|
|
3030
|
+
// Populate options
|
|
3031
|
+
populateOptions();
|
|
3032
|
+
|
|
3033
|
+
// Position dropdown
|
|
3034
|
+
positionDropdown();
|
|
3035
|
+
|
|
3036
|
+
// dropdown focus
|
|
3037
|
+
dropdown.focus();
|
|
3038
|
+
|
|
3039
|
+
// Add keyboard navigation
|
|
3040
|
+
dropdown.addEventListener('keydown', (e) => {
|
|
3041
|
+
if (e.key === 'Escape') {
|
|
3042
|
+
closeDropdown();
|
|
3043
|
+
} else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
3044
|
+
e.preventDefault();
|
|
3045
|
+
// Navigate between options
|
|
3046
|
+
const checkboxes = Array.from(dropdown.querySelectorAll('.ftable-multiselect-checkbox'));
|
|
3047
|
+
const current = document.activeElement;
|
|
3048
|
+
const currentIndex = checkboxes.indexOf(current);
|
|
3049
|
+
|
|
3050
|
+
let nextIndex;
|
|
3051
|
+
if (e.key === 'ArrowDown') {
|
|
3052
|
+
nextIndex = currentIndex < checkboxes.length - 1 ? currentIndex + 1 : 0;
|
|
3053
|
+
} else {
|
|
3054
|
+
nextIndex = currentIndex > 0 ? currentIndex - 1 : checkboxes.length - 1;
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
checkboxes[nextIndex].focus();
|
|
3058
|
+
} else if (e.key === ' ' || e.key === 'Enter') {
|
|
3059
|
+
e.preventDefault();
|
|
3060
|
+
// Toggle the focused checkbox
|
|
3061
|
+
if (document.activeElement.classList.contains('ftable-multiselect-checkbox')) {
|
|
3062
|
+
document.activeElement.click();
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
2860
3065
|
});
|
|
3066
|
+
|
|
3067
|
+
// Handle clicks outside
|
|
3068
|
+
dropdownOverlay.addEventListener('click', (event) => {
|
|
3069
|
+
if (event.target === dropdownOverlay) {
|
|
3070
|
+
closeDropdown();
|
|
3071
|
+
}
|
|
3072
|
+
});
|
|
3073
|
+
|
|
3074
|
+
// Reposition on scroll/resize
|
|
3075
|
+
const scrollHandler = (e) => {
|
|
3076
|
+
if (dropdown && dropdown.contains(e.target)) {
|
|
3077
|
+
return; // Allow scrolling inside dropdown
|
|
3078
|
+
}
|
|
3079
|
+
positionDropdown();
|
|
3080
|
+
};
|
|
3081
|
+
const repositionHandler = () => positionDropdown();
|
|
3082
|
+
window.addEventListener('scroll', scrollHandler, true);
|
|
3083
|
+
window.addEventListener('resize', repositionHandler);
|
|
3084
|
+
|
|
3085
|
+
// Store cleanup function
|
|
3086
|
+
container._cleanupHandlers = () => {
|
|
3087
|
+
window.removeEventListener('scroll', scrollHandler, true);
|
|
3088
|
+
window.removeEventListener('resize', repositionHandler);
|
|
3089
|
+
};
|
|
2861
3090
|
}
|
|
2862
3091
|
};
|
|
2863
3092
|
|
|
2864
3093
|
display.addEventListener('click', toggleDropdown);
|
|
2865
3094
|
toggleBtn.addEventListener('click', toggleDropdown);
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
dropdown.style.display = 'none';
|
|
3095
|
+
display.addEventListener('keydown', (e) => {
|
|
3096
|
+
if (e.key === 'ArrowDown' || e.key === 'Enter') {
|
|
3097
|
+
e.preventDefault();
|
|
3098
|
+
toggleDropdown();
|
|
2871
3099
|
}
|
|
2872
3100
|
});
|
|
2873
3101
|
|
|
3102
|
+
// Add reset method to container
|
|
3103
|
+
container.resetMultiSelect = () => {
|
|
3104
|
+
selectedValues.clear();
|
|
3105
|
+
checkboxMap.forEach(checkbox => {
|
|
3106
|
+
checkbox.checked = false;
|
|
3107
|
+
});
|
|
3108
|
+
closeDropdown();
|
|
3109
|
+
updateDisplay();
|
|
3110
|
+
};
|
|
3111
|
+
|
|
3112
|
+
// Clean up when container is removed from DOM
|
|
3113
|
+
const observer = new MutationObserver((mutations) => {
|
|
3114
|
+
mutations.forEach((mutation) => {
|
|
3115
|
+
mutation.removedNodes.forEach((node) => {
|
|
3116
|
+
if (node === container || node.contains && node.contains(container)) {
|
|
3117
|
+
closeDropdown();
|
|
3118
|
+
observer.disconnect();
|
|
3119
|
+
}
|
|
3120
|
+
});
|
|
3121
|
+
});
|
|
3122
|
+
});
|
|
3123
|
+
|
|
3124
|
+
// Start observing once container is in the DOM
|
|
3125
|
+
setTimeout(() => {
|
|
3126
|
+
if (container.parentNode) {
|
|
3127
|
+
observer.observe(container.parentNode, { childList: true, subtree: true });
|
|
3128
|
+
}
|
|
3129
|
+
}, 0);
|
|
3130
|
+
|
|
2874
3131
|
// Initialize
|
|
2875
|
-
populateOptions();
|
|
2876
3132
|
updateDisplay();
|
|
2877
3133
|
|
|
2878
3134
|
return container;
|