@liedekef/ftable 1.1.48 → 1.1.50

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.
Files changed (38) hide show
  1. package/ftable.esm.js +650 -48
  2. package/ftable.js +650 -48
  3. package/ftable.min.js +2 -24
  4. package/ftable.umd.js +650 -48
  5. package/package.json +1 -1
  6. package/themes/basic/ftable_basic.css +148 -0
  7. package/themes/basic/ftable_basic.min.css +1 -1
  8. package/themes/ftable_theme_base.less +172 -0
  9. package/themes/lightcolor/blue/ftable.css +148 -0
  10. package/themes/lightcolor/blue/ftable.min.css +1 -1
  11. package/themes/lightcolor/gray/ftable.css +148 -0
  12. package/themes/lightcolor/gray/ftable.min.css +1 -1
  13. package/themes/lightcolor/green/ftable.css +148 -0
  14. package/themes/lightcolor/green/ftable.min.css +1 -1
  15. package/themes/lightcolor/orange/ftable.css +148 -0
  16. package/themes/lightcolor/orange/ftable.min.css +1 -1
  17. package/themes/lightcolor/red/ftable.css +148 -0
  18. package/themes/lightcolor/red/ftable.min.css +1 -1
  19. package/themes/metro/blue/ftable.css +148 -0
  20. package/themes/metro/blue/ftable.min.css +1 -1
  21. package/themes/metro/brown/ftable.css +148 -0
  22. package/themes/metro/brown/ftable.min.css +1 -1
  23. package/themes/metro/crimson/ftable.css +148 -0
  24. package/themes/metro/crimson/ftable.min.css +1 -1
  25. package/themes/metro/darkgray/ftable.css +148 -0
  26. package/themes/metro/darkgray/ftable.min.css +1 -1
  27. package/themes/metro/darkorange/ftable.css +148 -0
  28. package/themes/metro/darkorange/ftable.min.css +1 -1
  29. package/themes/metro/green/ftable.css +148 -0
  30. package/themes/metro/green/ftable.min.css +1 -1
  31. package/themes/metro/lightgray/ftable.css +148 -0
  32. package/themes/metro/lightgray/ftable.min.css +1 -1
  33. package/themes/metro/pink/ftable.css +148 -0
  34. package/themes/metro/pink/ftable.min.css +1 -1
  35. package/themes/metro/purple/ftable.css +148 -0
  36. package/themes/metro/purple/ftable.min.css +1 -1
  37. package/themes/metro/red/ftable.css +148 -0
  38. package/themes/metro/red/ftable.min.css +1 -1
package/ftable.esm.js CHANGED
@@ -1374,10 +1374,10 @@ class FTableFormBuilder {
1374
1374
 
1375
1375
  // extra check for name and multiple
1376
1376
  let name = fieldName;
1377
+ let hasMultiple = false;
1378
+
1377
1379
  // Apply inputAttributes from field definition
1378
1380
  if (field.inputAttributes) {
1379
- let hasMultiple = false;
1380
-
1381
1381
  const parsed = this.parseInputAttributes(field.inputAttributes);
1382
1382
  Object.assign(attributes, parsed);
1383
1383
 
@@ -1387,6 +1387,13 @@ class FTableFormBuilder {
1387
1387
  name = `${fieldName}[]`;
1388
1388
  }
1389
1389
  }
1390
+
1391
+ // If multiple select, create custom UI
1392
+ if (hasMultiple) {
1393
+ return this.createCustomMultiSelect(fieldName, field, value, attributes, name);
1394
+ }
1395
+
1396
+ // Standard single select
1390
1397
  attributes.name = name;
1391
1398
 
1392
1399
  const select = FTableDOMHelper.create('select', {
@@ -1403,6 +1410,297 @@ class FTableFormBuilder {
1403
1410
  return select;
1404
1411
  }
1405
1412
 
1413
+ createCustomMultiSelect(fieldName, field, value, attributes, name) {
1414
+ // Create container
1415
+ const container = FTableDOMHelper.create('div', {
1416
+ className: 'ftable-multiselect-container',
1417
+ attributes: { 'data-field-name': fieldName }
1418
+ });
1419
+
1420
+ // Create hidden input to store selected values
1421
+ const hiddenInput = FTableDOMHelper.create('input', {
1422
+ type: 'hidden',
1423
+ name: name,
1424
+ id: `Edit-${fieldName}`,
1425
+ value: Array.isArray(value) ? value.join(',') : value || ''
1426
+ });
1427
+ container.appendChild(hiddenInput);
1428
+
1429
+ // Create display area
1430
+ const display = FTableDOMHelper.create('div', {
1431
+ className: 'ftable-multiselect-display',
1432
+ parent: container
1433
+ });
1434
+
1435
+ const selectedDisplay = FTableDOMHelper.create('div', {
1436
+ className: 'ftable-multiselect-selected',
1437
+ parent: display
1438
+ });
1439
+
1440
+ const placeholderText = field.placeholder || this.options.messages.multiSelectPlaceholder || 'Click to select options...';
1441
+ const placeholder = FTableDOMHelper.create('span', {
1442
+ className: 'ftable-multiselect-placeholder',
1443
+ textContent: placeholderText,
1444
+ parent: selectedDisplay
1445
+ });
1446
+
1447
+ // Create dropdown toggle button
1448
+ const toggleBtn = FTableDOMHelper.create('button', {
1449
+ type: 'button',
1450
+ className: 'ftable-multiselect-toggle',
1451
+ innerHTML: '▼',
1452
+ parent: display
1453
+ });
1454
+
1455
+ // Dropdown and overlay will be created on demand and appended to body
1456
+ let dropdown = null;
1457
+ let dropdownOverlay = null;
1458
+
1459
+ // Store selected values and checkbox references
1460
+ const selectedValues = new Set(
1461
+ Array.isArray(value) ? value :
1462
+ value ? value.toString().split(',').filter(v => v) : []
1463
+ );
1464
+ const checkboxMap = new Map(); // Map of value -> checkbox element
1465
+
1466
+ // Function to update display
1467
+ const updateDisplay = () => {
1468
+ selectedDisplay.innerHTML = '';
1469
+
1470
+ if (selectedValues.size === 0) {
1471
+ placeholder.textContent = placeholderText;
1472
+ selectedDisplay.appendChild(placeholder);
1473
+ } else {
1474
+ const selectedArray = Array.from(selectedValues);
1475
+ const optionsMap = new Map();
1476
+
1477
+ // Build options map
1478
+ if (field.options) {
1479
+ const options = Array.isArray(field.options) ? field.options :
1480
+ Object.entries(field.options).map(([k, v]) => ({Value: k, DisplayText: v}));
1481
+
1482
+ options.forEach(opt => {
1483
+ const val = opt.Value !== undefined ? opt.Value :
1484
+ opt.value !== undefined ? opt.value : opt;
1485
+ const text = opt.DisplayText || opt.text || opt;
1486
+ optionsMap.set(val.toString(), text);
1487
+ });
1488
+ }
1489
+
1490
+ selectedArray.forEach(val => {
1491
+ const tag = FTableDOMHelper.create('span', {
1492
+ className: 'ftable-multiselect-tag',
1493
+ parent: selectedDisplay
1494
+ });
1495
+
1496
+ FTableDOMHelper.create('span', {
1497
+ className: 'ftable-multiselect-tag-text',
1498
+ textContent: optionsMap.get(val.toString()) || val,
1499
+ parent: tag
1500
+ });
1501
+
1502
+ const removeBtn = FTableDOMHelper.create('span', {
1503
+ className: 'ftable-multiselect-tag-remove',
1504
+ innerHTML: '×',
1505
+ parent: tag
1506
+ });
1507
+
1508
+ removeBtn.addEventListener('click', (e) => {
1509
+ e.stopPropagation();
1510
+ selectedValues.delete(val);
1511
+ // Update the checkbox state
1512
+ const checkbox = checkboxMap.get(val.toString());
1513
+ if (checkbox) {
1514
+ checkbox.checked = false;
1515
+ }
1516
+ updateDisplay();
1517
+ hiddenInput.value = Array.from(selectedValues).join(',');
1518
+ });
1519
+ });
1520
+ }
1521
+
1522
+ hiddenInput.value = Array.from(selectedValues).join(',');
1523
+ };
1524
+
1525
+ // Function to close dropdown
1526
+ const closeDropdown = () => {
1527
+ if (dropdown) {
1528
+ dropdown.remove();
1529
+ dropdown = null;
1530
+ }
1531
+ if (dropdownOverlay) {
1532
+ dropdownOverlay.remove();
1533
+ dropdownOverlay = null;
1534
+ }
1535
+ if (container._cleanupHandlers) {
1536
+ container._cleanupHandlers();
1537
+ container._cleanupHandlers = null;
1538
+ }
1539
+ };
1540
+
1541
+ // Function to position dropdown
1542
+ const positionDropdown = () => {
1543
+ if (!dropdown) return;
1544
+
1545
+ const rect = display.getBoundingClientRect();
1546
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
1547
+ const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
1548
+
1549
+ let left = rect.left + scrollLeft;
1550
+ let top = rect.bottom + scrollTop + 4; // 4px gap
1551
+
1552
+ dropdown.style.position = 'absolute';
1553
+ dropdown.style.left = `${left}px`;
1554
+ dropdown.style.top = `${top}px`;
1555
+ dropdown.style.width = `${rect.width}px`;
1556
+ dropdown.style.minWidth = `${rect.width}px`;
1557
+ dropdown.style.boxSizing = 'border-box';
1558
+ dropdown.style.zIndex = '10000';
1559
+
1560
+ // Adjust horizontal position if needed
1561
+ const dropdownRect = dropdown.getBoundingClientRect();
1562
+ const viewportWidth = window.innerWidth;
1563
+ if (dropdownRect.right > viewportWidth) {
1564
+ left = Math.max(10, viewportWidth - dropdownRect.width - 10);
1565
+ dropdown.style.left = `${left}px`;
1566
+ }
1567
+ };
1568
+
1569
+ // Populate options
1570
+ const populateOptions = () => {
1571
+ if (!field.options || !dropdown) return;
1572
+
1573
+ const options = Array.isArray(field.options) ? field.options :
1574
+ Object.entries(field.options).map(([k, v]) => ({Value: k, DisplayText: v}));
1575
+
1576
+ options.forEach(option => {
1577
+ const optValue = option.Value !== undefined ? option.Value :
1578
+ option.value !== undefined ? option.value : option;
1579
+
1580
+ // Skip if value is empty
1581
+ if (optValue == null || optValue === '') {
1582
+ return; // This continues to the next iteration
1583
+ }
1584
+
1585
+ const optText = option.DisplayText || option.text || option;
1586
+
1587
+ const optionDiv = FTableDOMHelper.create('div', {
1588
+ className: 'ftable-multiselect-option',
1589
+ parent: dropdown
1590
+ });
1591
+
1592
+ const checkbox = FTableDOMHelper.create('input', {
1593
+ type: 'checkbox',
1594
+ className: 'ftable-multiselect-checkbox',
1595
+ checked: selectedValues.has(optValue.toString()),
1596
+ parent: optionDiv
1597
+ });
1598
+
1599
+ // Store checkbox reference
1600
+ checkboxMap.set(optValue.toString(), checkbox);
1601
+
1602
+ const label = FTableDOMHelper.create('label', {
1603
+ className: 'ftable-multiselect-label',
1604
+ textContent: optText,
1605
+ parent: optionDiv
1606
+ });
1607
+
1608
+ // Click anywhere on the option to toggle
1609
+ optionDiv.addEventListener('click', (e) => {
1610
+ e.stopPropagation();
1611
+
1612
+ if (selectedValues.has(optValue.toString())) {
1613
+ selectedValues.delete(optValue.toString());
1614
+ checkbox.checked = false;
1615
+ } else {
1616
+ selectedValues.add(optValue.toString());
1617
+ checkbox.checked = true;
1618
+ }
1619
+
1620
+ updateDisplay();
1621
+ });
1622
+ });
1623
+ };
1624
+
1625
+ // Toggle dropdown
1626
+ const toggleDropdown = (e) => {
1627
+ if (e) e.stopPropagation();
1628
+
1629
+ if (dropdown) {
1630
+ // Dropdown is open, close it
1631
+ closeDropdown();
1632
+ } else {
1633
+ // Close any other open multiselect dropdowns
1634
+ document.querySelectorAll('.ftable-multiselect-dropdown').forEach(dd => dd.remove());
1635
+ document.querySelectorAll('.ftable-multiselect-overlay').forEach(ov => ov.remove());
1636
+
1637
+ // Create overlay
1638
+ dropdownOverlay = FTableDOMHelper.create('div', {
1639
+ className: 'ftable-multiselect-overlay',
1640
+ parent: document.body
1641
+ });
1642
+
1643
+ // Create dropdown
1644
+ dropdown = FTableDOMHelper.create('div', {
1645
+ className: 'ftable-multiselect-dropdown',
1646
+ parent: document.body
1647
+ });
1648
+
1649
+ // Populate options
1650
+ populateOptions();
1651
+
1652
+ // Position dropdown
1653
+ positionDropdown();
1654
+
1655
+ // Handle clicks outside
1656
+ dropdownOverlay.addEventListener('click', (event) => {
1657
+ if (event.target === dropdownOverlay) {
1658
+ closeDropdown();
1659
+ }
1660
+ });
1661
+
1662
+ // Reposition on scroll/resize
1663
+ const repositionHandler = () => positionDropdown();
1664
+ window.addEventListener('scroll', repositionHandler, true);
1665
+ window.addEventListener('resize', repositionHandler);
1666
+
1667
+ // Store cleanup function
1668
+ container._cleanupHandlers = () => {
1669
+ window.removeEventListener('scroll', repositionHandler, true);
1670
+ window.removeEventListener('resize', repositionHandler);
1671
+ };
1672
+ }
1673
+ };
1674
+
1675
+ display.addEventListener('click', toggleDropdown);
1676
+ toggleBtn.addEventListener('click', toggleDropdown);
1677
+
1678
+ // Clean up when container is removed from DOM
1679
+ const observer = new MutationObserver((mutations) => {
1680
+ mutations.forEach((mutation) => {
1681
+ mutation.removedNodes.forEach((node) => {
1682
+ if (node === container || node.contains && node.contains(container)) {
1683
+ closeDropdown();
1684
+ observer.disconnect();
1685
+ }
1686
+ });
1687
+ });
1688
+ });
1689
+
1690
+ // Start observing once container is in the DOM
1691
+ setTimeout(() => {
1692
+ if (container.parentNode) {
1693
+ observer.observe(container.parentNode, { childList: true, subtree: true });
1694
+ }
1695
+ }, 0);
1696
+
1697
+ // Initialize
1698
+ populateOptions();
1699
+ updateDisplay();
1700
+
1701
+ return container;
1702
+ }
1703
+
1406
1704
  createRadioGroup(fieldName, field, value) {
1407
1705
  const wrapper = FTableDOMHelper.create('div', {
1408
1706
  className: 'ftable-radio-group'
@@ -1722,9 +2020,6 @@ class FTable extends FTableEventEmitter {
1722
2020
  this.updateSortingHeaders();
1723
2021
  this.renderSortingInfo();
1724
2022
 
1725
- // Add essential CSS if not already present
1726
- //this.addEssentialCSS();
1727
-
1728
2023
  // now make sure all tables have a % width
1729
2024
  this.initColumnWidths();
1730
2025
  }
@@ -1804,40 +2099,6 @@ class FTable extends FTableEventEmitter {
1804
2099
  return result;
1805
2100
  }
1806
2101
 
1807
- addEssentialCSS() {
1808
- // Check if our CSS is already added
1809
- if (document.querySelector('#ftable-essential-css')) return;
1810
-
1811
- const css = `
1812
- .ftable-row-animation {
1813
- transition: background-color 0.3s ease;
1814
- }
1815
-
1816
- .ftable-row-added {
1817
- background-color: #d4edda !important;
1818
- }
1819
-
1820
- .ftable-row-edited {
1821
- background-color: #d1ecf1 !important;
1822
- }
1823
-
1824
- .ftable-row-deleted {
1825
- opacity: 0;
1826
- transform: translateY(-10px);
1827
- transition: opacity 0.3s ease, transform 0.3s ease;
1828
- }
1829
-
1830
- .ftable-toolbarsearch {
1831
- width: 90%;
1832
- }
1833
- `;
1834
-
1835
- const style = document.createElement('style');
1836
- style.id = 'ftable-essential-css';
1837
- style.textContent = css;
1838
- document.head.appendChild(style);
1839
- }
1840
-
1841
2102
  createPagingUI() {
1842
2103
  this.elements.bottomPanel = FTableDOMHelper.create('div', {
1843
2104
  className: 'ftable-bottom-panel',
@@ -2308,12 +2569,19 @@ class FTable extends FTableEventEmitter {
2308
2569
  container.appendChild(input.datalistElement);
2309
2570
  }
2310
2571
 
2311
- if (input.tagName === 'SELECT') {
2312
- input.addEventListener('change', (e) => {
2572
+ // Handle event listeners - check if it's a custom multiselect container
2573
+ let targetElement = input;
2574
+ if (input.classList && input.classList.contains('ftable-multiselect-container') && input.hiddenSelect) {
2575
+ // It's a custom multiselect - attach listener to the hidden select
2576
+ targetElement = input.hiddenSelect;
2577
+ }
2578
+
2579
+ if (targetElement.tagName === 'SELECT') {
2580
+ targetElement.addEventListener('change', (e) => {
2313
2581
  this.handleSearchInputChange(e);
2314
2582
  });
2315
2583
  } else {
2316
- input.addEventListener('input', (e) => {
2584
+ targetElement.addEventListener('input', (e) => {
2317
2585
  this.handleSearchInputChange(e);
2318
2586
  });
2319
2587
  }
@@ -2378,12 +2646,6 @@ class FTable extends FTableEventEmitter {
2378
2646
  }
2379
2647
  attributes['data-field-name'] = name;
2380
2648
 
2381
- const select = FTableDOMHelper.create('select', {
2382
- attributes: attributes,
2383
- id: fieldSearchName,
2384
- className: 'ftable-toolbarsearch'
2385
- });
2386
-
2387
2649
  let optionsSource;
2388
2650
  if (isCheckboxValues && field.values) {
2389
2651
  optionsSource = Object.entries(field.values).map(([value, displayText]) => ({
@@ -2394,6 +2656,24 @@ class FTable extends FTableEventEmitter {
2394
2656
  optionsSource = await this.formBuilder.getFieldOptions(fieldName);
2395
2657
  }
2396
2658
 
2659
+ // If multiple, create custom UI
2660
+ if (hasMultiple) {
2661
+ return this.createCustomMultiSelectForSearch(
2662
+ fieldSearchName,
2663
+ fieldName,
2664
+ field,
2665
+ optionsSource,
2666
+ attributes
2667
+ );
2668
+ }
2669
+
2670
+ // Standard single select
2671
+ const select = FTableDOMHelper.create('select', {
2672
+ attributes: attributes,
2673
+ id: fieldSearchName,
2674
+ className: 'ftable-toolbarsearch'
2675
+ });
2676
+
2397
2677
  // Add empty option only if first option is not already empty
2398
2678
  const hasEmptyFirst = optionsSource?.length > 0 &&
2399
2679
  (optionsSource[0].Value === '' ||
@@ -2430,6 +2710,320 @@ class FTable extends FTableEventEmitter {
2430
2710
  return select;
2431
2711
  }
2432
2712
 
2713
+ createCustomMultiSelectForSearch(fieldSearchName, fieldName, field, optionsSource, attributes) {
2714
+ // Create container
2715
+ const container = FTableDOMHelper.create('div', {
2716
+ className: 'ftable-multiselect-container ftable-multiselect-search ftable-toolbarsearch',
2717
+ attributes: { 'data-field-name': attributes['data-field-name'] }
2718
+ });
2719
+
2720
+ // Create hidden select to maintain compatibility with existing search logic
2721
+ const hiddenSelect = FTableDOMHelper.create('select', {
2722
+ id: fieldSearchName,
2723
+ multiple: true,
2724
+ style: 'display: none;',
2725
+ attributes: attributes
2726
+ });
2727
+ container.appendChild(hiddenSelect);
2728
+
2729
+ // Expose hidden select for external access (needed for event listeners and reset)
2730
+ container.hiddenSelect = hiddenSelect;
2731
+
2732
+ // Create display area
2733
+ const display = FTableDOMHelper.create('div', {
2734
+ className: 'ftable-multiselect-display',
2735
+ parent: container
2736
+ });
2737
+
2738
+ const selectedDisplay = FTableDOMHelper.create('div', {
2739
+ className: 'ftable-multiselect-selected',
2740
+ parent: display
2741
+ });
2742
+
2743
+ const placeholderText = field.searchPlaceholder || field.placeholder || this.options.messages.multiSelectPlaceholder || 'Click to select options...';
2744
+ const placeholder = FTableDOMHelper.create('span', {
2745
+ className: 'ftable-multiselect-placeholder',
2746
+ textContent: placeholderText,
2747
+ parent: selectedDisplay
2748
+ });
2749
+
2750
+ // Create dropdown toggle button
2751
+ const toggleBtn = FTableDOMHelper.create('button', {
2752
+ type: 'button',
2753
+ className: 'ftable-multiselect-toggle',
2754
+ innerHTML: '▼',
2755
+ parent: display
2756
+ });
2757
+
2758
+ // Dropdown and overlay will be created on demand and appended to body
2759
+ let dropdown = null;
2760
+ let dropdownOverlay = null;
2761
+
2762
+ // Store selected values and checkbox references
2763
+ const selectedValues = new Set();
2764
+ const checkboxMap = new Map(); // Map of value -> checkbox element
2765
+
2766
+ // Function to update display and hidden select
2767
+ const updateDisplay = () => {
2768
+ selectedDisplay.innerHTML = '';
2769
+
2770
+ // Update hidden select
2771
+ Array.from(hiddenSelect.options).forEach(opt => {
2772
+ opt.selected = selectedValues.has(opt.value);
2773
+ });
2774
+
2775
+ // Trigger change event on hidden select for search functionality
2776
+ hiddenSelect.dispatchEvent(new Event('change', { bubbles: true }));
2777
+
2778
+ if (selectedValues.size === 0) {
2779
+ placeholder.textContent = placeholderText;
2780
+ selectedDisplay.appendChild(placeholder);
2781
+ } else {
2782
+ const selectedArray = Array.from(selectedValues);
2783
+ const optionsMap = new Map();
2784
+
2785
+ // Build options map
2786
+ if (optionsSource && Array.isArray(optionsSource)) {
2787
+ optionsSource.forEach(opt => {
2788
+ const val = opt.Value !== undefined ? opt.Value :
2789
+ opt.value !== undefined ? opt.value : opt;
2790
+ const text = opt.DisplayText || opt.text || opt;
2791
+ optionsMap.set(val.toString(), text);
2792
+ });
2793
+ }
2794
+
2795
+ selectedArray.forEach(val => {
2796
+ const tag = FTableDOMHelper.create('span', {
2797
+ className: 'ftable-multiselect-tag',
2798
+ parent: selectedDisplay
2799
+ });
2800
+
2801
+ FTableDOMHelper.create('span', {
2802
+ className: 'ftable-multiselect-tag-text',
2803
+ textContent: optionsMap.get(val.toString()) || val,
2804
+ parent: tag
2805
+ });
2806
+
2807
+ const removeBtn = FTableDOMHelper.create('span', {
2808
+ className: 'ftable-multiselect-tag-remove',
2809
+ innerHTML: '×',
2810
+ parent: tag
2811
+ });
2812
+
2813
+ removeBtn.addEventListener('click', (e) => {
2814
+ e.stopPropagation();
2815
+ selectedValues.delete(val);
2816
+ // Update the checkbox state
2817
+ const checkbox = checkboxMap.get(val.toString());
2818
+ if (checkbox) {
2819
+ checkbox.checked = false;
2820
+ }
2821
+ updateDisplay();
2822
+ });
2823
+ });
2824
+ }
2825
+ };
2826
+
2827
+ // Function to close dropdown
2828
+ const closeDropdown = () => {
2829
+ if (dropdown) {
2830
+ dropdown.remove();
2831
+ dropdown = null;
2832
+ }
2833
+ if (dropdownOverlay) {
2834
+ dropdownOverlay.remove();
2835
+ dropdownOverlay = null;
2836
+ }
2837
+ if (container._cleanupHandlers) {
2838
+ container._cleanupHandlers();
2839
+ container._cleanupHandlers = null;
2840
+ }
2841
+ };
2842
+
2843
+ // Function to position dropdown
2844
+ const positionDropdown = () => {
2845
+ if (!dropdown) return;
2846
+
2847
+ const rect = display.getBoundingClientRect();
2848
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
2849
+ const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
2850
+
2851
+ let left = rect.left + scrollLeft;
2852
+ let top = rect.bottom + scrollTop + 4; // 4px gap
2853
+
2854
+ dropdown.style.position = 'absolute';
2855
+ dropdown.style.left = `${left}px`;
2856
+ dropdown.style.top = `${top}px`;
2857
+ dropdown.style.width = `${rect.width}px`;
2858
+ dropdown.style.minWidth = `${rect.width}px`;
2859
+ dropdown.style.boxSizing = 'border-box';
2860
+ dropdown.style.zIndex = '10000';
2861
+
2862
+ // Adjust horizontal position if needed
2863
+ const dropdownRect = dropdown.getBoundingClientRect();
2864
+ const viewportWidth = window.innerWidth;
2865
+ if (dropdownRect.right > viewportWidth) {
2866
+ left = Math.max(10, viewportWidth - dropdownRect.width - 10);
2867
+ dropdown.style.left = `${left}px`;
2868
+ }
2869
+ };
2870
+
2871
+ // Populate options in both hidden select and dropdown
2872
+ const populateOptions = () => {
2873
+ if (!optionsSource || !dropdown) return;
2874
+
2875
+ const options = Array.isArray(optionsSource) ? optionsSource :
2876
+ Object.entries(optionsSource).map(([k, v]) => ({Value: k, DisplayText: v}));
2877
+
2878
+ options.forEach(option => {
2879
+ const optValue = option.Value !== undefined ? option.Value :
2880
+ option.value !== undefined ? option.value : option;
2881
+
2882
+ // Skip if value is empty
2883
+ if (optValue == null || optValue === '') {
2884
+ return; // This continues to the next iteration
2885
+ }
2886
+
2887
+ const optText = option.DisplayText || option.text || option;
2888
+
2889
+ // Add to hidden select (only once)
2890
+ if (!hiddenSelect.querySelector(`option[value="${optValue}"]`)) {
2891
+ FTableDOMHelper.create('option', {
2892
+ value: optValue,
2893
+ textContent: optText,
2894
+ parent: hiddenSelect
2895
+ });
2896
+ }
2897
+
2898
+ // Add to visual dropdown
2899
+ const optionDiv = FTableDOMHelper.create('div', {
2900
+ className: 'ftable-multiselect-option',
2901
+ parent: dropdown
2902
+ });
2903
+
2904
+ const checkbox = FTableDOMHelper.create('input', {
2905
+ type: 'checkbox',
2906
+ className: 'ftable-multiselect-checkbox',
2907
+ parent: optionDiv
2908
+ });
2909
+
2910
+ // Set initial checked state
2911
+ checkbox.checked = selectedValues.has(optValue.toString());
2912
+
2913
+ // Store checkbox reference
2914
+ checkboxMap.set(optValue.toString(), checkbox);
2915
+
2916
+ const label = FTableDOMHelper.create('label', {
2917
+ className: 'ftable-multiselect-label',
2918
+ textContent: optText,
2919
+ parent: optionDiv
2920
+ });
2921
+
2922
+ // Click anywhere on the option to toggle
2923
+ optionDiv.addEventListener('click', (e) => {
2924
+ e.stopPropagation();
2925
+
2926
+ if (selectedValues.has(optValue.toString())) {
2927
+ selectedValues.delete(optValue.toString());
2928
+ checkbox.checked = false;
2929
+ } else {
2930
+ selectedValues.add(optValue.toString());
2931
+ checkbox.checked = true;
2932
+ }
2933
+
2934
+ updateDisplay();
2935
+ });
2936
+ });
2937
+ };
2938
+
2939
+ // Toggle dropdown
2940
+ const toggleDropdown = (e) => {
2941
+ if (e) e.stopPropagation();
2942
+
2943
+ if (dropdown) {
2944
+ // Dropdown is open, close it
2945
+ closeDropdown();
2946
+ } else {
2947
+ // Close any other open multiselect dropdowns
2948
+ document.querySelectorAll('.ftable-multiselect-dropdown').forEach(dd => dd.remove());
2949
+ document.querySelectorAll('.ftable-multiselect-overlay').forEach(ov => ov.remove());
2950
+
2951
+ // Create overlay
2952
+ dropdownOverlay = FTableDOMHelper.create('div', {
2953
+ className: 'ftable-multiselect-overlay',
2954
+ parent: document.body
2955
+ });
2956
+
2957
+ // Create dropdown
2958
+ dropdown = FTableDOMHelper.create('div', {
2959
+ className: 'ftable-multiselect-dropdown',
2960
+ parent: document.body
2961
+ });
2962
+
2963
+ // Populate options
2964
+ populateOptions();
2965
+
2966
+ // Position dropdown
2967
+ positionDropdown();
2968
+
2969
+ // Handle clicks outside
2970
+ dropdownOverlay.addEventListener('click', (event) => {
2971
+ if (event.target === dropdownOverlay) {
2972
+ closeDropdown();
2973
+ }
2974
+ });
2975
+
2976
+ // Reposition on scroll/resize
2977
+ const repositionHandler = () => positionDropdown();
2978
+ window.addEventListener('scroll', repositionHandler, true);
2979
+ window.addEventListener('resize', repositionHandler);
2980
+
2981
+ // Store cleanup function
2982
+ container._cleanupHandlers = () => {
2983
+ window.removeEventListener('scroll', repositionHandler, true);
2984
+ window.removeEventListener('resize', repositionHandler);
2985
+ };
2986
+ }
2987
+ };
2988
+
2989
+ display.addEventListener('click', toggleDropdown);
2990
+ toggleBtn.addEventListener('click', toggleDropdown);
2991
+
2992
+ // Add reset method to container
2993
+ container.resetMultiSelect = () => {
2994
+ selectedValues.clear();
2995
+ checkboxMap.forEach(checkbox => {
2996
+ checkbox.checked = false;
2997
+ });
2998
+ closeDropdown();
2999
+ updateDisplay();
3000
+ };
3001
+
3002
+ // Clean up when container is removed from DOM
3003
+ const observer = new MutationObserver((mutations) => {
3004
+ mutations.forEach((mutation) => {
3005
+ mutation.removedNodes.forEach((node) => {
3006
+ if (node === container || node.contains && node.contains(container)) {
3007
+ closeDropdown();
3008
+ observer.disconnect();
3009
+ }
3010
+ });
3011
+ });
3012
+ });
3013
+
3014
+ // Start observing once container is in the DOM
3015
+ setTimeout(() => {
3016
+ if (container.parentNode) {
3017
+ observer.observe(container.parentNode, { childList: true, subtree: true });
3018
+ }
3019
+ }, 0);
3020
+
3021
+ // Initialize
3022
+ updateDisplay();
3023
+
3024
+ return container;
3025
+ }
3026
+
2433
3027
  async createDatalistForSearch(fieldName, field) {
2434
3028
  const fieldSearchName = 'ftable-toolbarsearch-' + fieldName;
2435
3029
 
@@ -2520,6 +3114,14 @@ class FTable extends FTableEventEmitter {
2520
3114
  }
2521
3115
  });
2522
3116
 
3117
+ // Clear custom multiselect containers
3118
+ const multiSelectContainers = this.elements.table.querySelectorAll('.ftable-multiselect-container');
3119
+ multiSelectContainers.forEach(container => {
3120
+ if (typeof container.resetMultiSelect === 'function') {
3121
+ container.resetMultiSelect();
3122
+ }
3123
+ });
3124
+
2523
3125
  // Reload data without search parameters
2524
3126
  this.load();
2525
3127
  }