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