@liedekef/ftable 1.1.52 → 1.2.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.
Files changed (38) hide show
  1. package/ftable.esm.js +357 -599
  2. package/ftable.js +357 -599
  3. package/ftable.min.js +2 -2
  4. package/ftable.umd.js +357 -599
  5. package/package.json +1 -1
  6. package/themes/basic/ftable_basic.css +0 -4
  7. package/themes/basic/ftable_basic.min.css +1 -1
  8. package/themes/ftable_theme_base.less +0 -5
  9. package/themes/lightcolor/blue/ftable.css +0 -4
  10. package/themes/lightcolor/blue/ftable.min.css +1 -1
  11. package/themes/lightcolor/gray/ftable.css +0 -4
  12. package/themes/lightcolor/gray/ftable.min.css +1 -1
  13. package/themes/lightcolor/green/ftable.css +0 -4
  14. package/themes/lightcolor/green/ftable.min.css +1 -1
  15. package/themes/lightcolor/orange/ftable.css +0 -4
  16. package/themes/lightcolor/orange/ftable.min.css +1 -1
  17. package/themes/lightcolor/red/ftable.css +0 -4
  18. package/themes/lightcolor/red/ftable.min.css +1 -1
  19. package/themes/metro/blue/ftable.css +0 -4
  20. package/themes/metro/blue/ftable.min.css +1 -1
  21. package/themes/metro/brown/ftable.css +0 -4
  22. package/themes/metro/brown/ftable.min.css +1 -1
  23. package/themes/metro/crimson/ftable.css +0 -4
  24. package/themes/metro/crimson/ftable.min.css +1 -1
  25. package/themes/metro/darkgray/ftable.css +0 -4
  26. package/themes/metro/darkgray/ftable.min.css +1 -1
  27. package/themes/metro/darkorange/ftable.css +0 -4
  28. package/themes/metro/darkorange/ftable.min.css +1 -1
  29. package/themes/metro/green/ftable.css +0 -4
  30. package/themes/metro/green/ftable.min.css +1 -1
  31. package/themes/metro/lightgray/ftable.css +0 -4
  32. package/themes/metro/lightgray/ftable.min.css +1 -1
  33. package/themes/metro/pink/ftable.css +0 -4
  34. package/themes/metro/pink/ftable.min.css +1 -1
  35. package/themes/metro/purple/ftable.css +0 -4
  36. package/themes/metro/purple/ftable.min.css +1 -1
  37. package/themes/metro/red/ftable.css +0 -4
  38. package/themes/metro/red/ftable.min.css +1 -1
package/ftable.js CHANGED
@@ -322,15 +322,13 @@ class FTableHttpClient {
322
322
  }
323
323
 
324
324
  if (Array.isArray(value)) {
325
- // Clean key: remove trailing [] if present
326
- const cleanKey = key.replace(/\[\]$/, '');
327
- const paramKey = cleanKey + '[]'; // Always use [] suffix once
325
+ const keyName = key.endsWith('[]') ? key : key + '[]';
328
326
 
329
327
  // Append each item in the array with the same key
330
328
  // This generates query strings like `key=val1&key=val2&key=val3`
331
329
  value.forEach(item => {
332
330
  if (item !== null && item !== undefined) { // Ensure array items are also not null/undefined
333
- fullUrl.searchParams.append(paramKey, item);
331
+ fullUrl.searchParams.append(keyName, item);
334
332
  }
335
333
  });
336
334
  } else {
@@ -356,15 +354,13 @@ class FTableHttpClient {
356
354
  return; // Skip null or undefined values
357
355
  }
358
356
  if (Array.isArray(value)) {
359
- // Clean key: remove trailing [] if present
360
- const cleanKey = key.replace(/\[\]$/, '');
361
- const paramKey = cleanKey + '[]'; // Always use [] suffix once
357
+ const keyName = key.endsWith('[]') ? key : key + '[]';
362
358
 
363
359
  // Append each item in the array with the same key
364
360
  // This generates query strings like `key=val1&key=val2&key=val3`
365
361
  value.forEach(item => {
366
362
  if (item !== null && item !== undefined) { // Ensure array items are also not null/undefined
367
- formData.append(paramKey, item);
363
+ formData.append(keyName, item);
368
364
  }
369
365
  });
370
366
  } else {
@@ -1412,28 +1408,110 @@ class FTableFormBuilder {
1412
1408
  }
1413
1409
 
1414
1410
  createCustomMultiSelect(fieldName, field, value, attributes, name) {
1415
- // Create container
1411
+ const optionsSource = Array.isArray(field.options) ? field.options :
1412
+ (field.options && typeof field.options === 'object')
1413
+ ? Object.entries(field.options).map(([k, v]) => ({ Value: k, DisplayText: v }))
1414
+ : [];
1415
+
1416
+ // Support data-livesearch attribute on the virtual select as well as field.livesearch
1417
+ const livesearch = field.livesearch
1418
+ ?? (attributes['data-livesearch'] === 'true' || attributes['data-livesearch'] === true)
1419
+ ?? false;
1420
+
1421
+ return this._buildCustomMultiSelect({
1422
+ containerId: fieldName,
1423
+ hiddenSelectId: `Edit-${fieldName}`,
1424
+ hiddenSelectName: name,
1425
+ extraClasses: '',
1426
+ containerDataFieldName: fieldName,
1427
+ hiddenSelectAttributes: {},
1428
+ optionsSource,
1429
+ initialValues: Array.isArray(value) ? value :
1430
+ (value ? value.toString().split(',').filter(v => v) : []),
1431
+ placeholderText: field.placeholder || this.options.messages?.multiSelectPlaceholder || 'Click to select options...',
1432
+ livesearch,
1433
+ onChangeExtra: (hiddenSelect) => {
1434
+ hiddenSelect.dispatchEvent(new Event('change', { bubbles: true }));
1435
+ },
1436
+ buildHiddenSelectOnUpdate: true, // form-mode: rebuild hidden options each time
1437
+ });
1438
+ }
1439
+
1440
+ /**
1441
+ * Shared builder for both form-mode and search-mode custom multi-selects.
1442
+ *
1443
+ * config {
1444
+ * containerId string – used for data-field-name on the outer container
1445
+ * hiddenSelectId string – id attribute of the hidden <select>
1446
+ * hiddenSelectName string – name attribute of the hidden <select>
1447
+ * extraClasses string – extra CSS classes on the container (space-separated)
1448
+ * containerDataFieldName string – value for container's data-field-name
1449
+ * hiddenSelectAttributes object – extra attributes for the hidden <select>
1450
+ * optionsSource array – resolved array of option objects: { Value, DisplayText, Group? }
1451
+ * initialValues array – pre-selected values
1452
+ * placeholderText string
1453
+ * livesearch bool – show filter input inside dropdown
1454
+ * onChangeExtra fn(hiddenSelect) – called after every selection change
1455
+ * buildHiddenSelectOnUpdate bool – if true, rebuild hidden <option>s in updateDisplay (form mode);
1456
+ * if false, all options are pre-populated (search mode)
1457
+ * }
1458
+ */
1459
+ _buildCustomMultiSelect(config) {
1460
+ const {
1461
+ hiddenSelectId,
1462
+ hiddenSelectName,
1463
+ extraClasses,
1464
+ containerDataFieldName,
1465
+ hiddenSelectAttributes,
1466
+ optionsSource,
1467
+ initialValues,
1468
+ placeholderText,
1469
+ livesearch,
1470
+ onChangeExtra,
1471
+ buildHiddenSelectOnUpdate,
1472
+ } = config;
1473
+
1474
+ // Normalise options to a flat array of { optValue, optText, groupLabel }
1475
+ const normaliseOptions = (src) => {
1476
+ if (!src) return [];
1477
+ const arr = Array.isArray(src) ? src :
1478
+ Object.entries(src).map(([k, v]) => ({ Value: k, DisplayText: v }));
1479
+ return arr
1480
+ .map(o => ({
1481
+ optValue: (o.Value !== undefined ? o.Value :
1482
+ o.value !== undefined ? o.value : o),
1483
+ optText: o.DisplayText || o.text || o,
1484
+ groupLabel: o.Group || o.group || null,
1485
+ }))
1486
+ .filter(o => o.optValue != null && o.optValue !== '');
1487
+ };
1488
+
1489
+ const allOptions = normaliseOptions(optionsSource);
1490
+
1491
+ // Build a value→text lookup (used by updateDisplay)
1492
+ const optionsMap = new Map(allOptions.map(o => [o.optValue.toString(), o.optText]));
1493
+
1494
+ // ---------- DOM skeleton ----------
1495
+ const containerClasses = ['ftable-multiselect-container', ...extraClasses.split(' ').filter(Boolean)].join(' ');
1416
1496
  const container = FTableDOMHelper.create('div', {
1417
- className: 'ftable-multiselect-container',
1418
- attributes: { 'data-field-name': fieldName }
1497
+ className: containerClasses,
1498
+ attributes: { 'data-field-name': containerDataFieldName }
1419
1499
  });
1420
1500
 
1421
- // Create hidden input to store selected values
1422
- const hiddenInput = FTableDOMHelper.create('input', {
1423
- type: 'hidden',
1424
- name: name,
1425
- id: `Edit-${fieldName}`,
1426
- value: Array.isArray(value) ? value.join(',') : value || ''
1501
+ const hiddenSelect = FTableDOMHelper.create('select', {
1502
+ id: hiddenSelectId,
1503
+ name: hiddenSelectName,
1504
+ multiple: true,
1505
+ style: 'display: none;',
1506
+ attributes: hiddenSelectAttributes
1427
1507
  });
1428
- container.appendChild(hiddenInput);
1508
+ container.appendChild(hiddenSelect);
1509
+ container.hiddenSelect = hiddenSelect;
1429
1510
 
1430
- // Create display area
1431
1511
  const display = FTableDOMHelper.create('div', {
1432
1512
  className: 'ftable-multiselect-display',
1433
1513
  parent: container,
1434
- attributes: {
1435
- tabindex: '0' // Makes it focusable and in tab order
1436
- }
1514
+ attributes: { tabindex: '0' }
1437
1515
  });
1438
1516
 
1439
1517
  const selectedDisplay = FTableDOMHelper.create('div', {
@@ -1441,294 +1519,288 @@ class FTableFormBuilder {
1441
1519
  parent: display
1442
1520
  });
1443
1521
 
1444
- const placeholderText = field.placeholder || this.options.messages.multiSelectPlaceholder || 'Click to select options...';
1445
- const placeholder = FTableDOMHelper.create('span', {
1522
+ const placeholderEl = FTableDOMHelper.create('span', {
1446
1523
  className: 'ftable-multiselect-placeholder',
1447
1524
  textContent: placeholderText,
1448
1525
  parent: selectedDisplay
1449
1526
  });
1450
1527
 
1451
- // Create dropdown toggle button
1452
- const toggleBtn = FTableDOMHelper.create('button', {
1528
+ FTableDOMHelper.create('button', {
1453
1529
  type: 'button',
1454
1530
  className: 'ftable-multiselect-toggle',
1455
1531
  innerHTML: '▼',
1456
1532
  parent: display,
1457
- attributes: {
1458
- tabindex: '-1' // this skips regular focus when tabbing
1459
- }
1533
+ attributes: { tabindex: '-1' }
1460
1534
  });
1461
1535
 
1462
- // Dropdown and overlay will be created on demand and appended to body
1463
- let dropdown = null;
1536
+ // ---------- State ----------
1537
+ let dropdown = null;
1464
1538
  let dropdownOverlay = null;
1539
+ const selectedValues = new Set(initialValues.map(v => v.toString()));
1540
+ const checkboxMap = new Map(); // value → checkbox element
1465
1541
 
1466
- // Store selected values and checkbox references
1467
- const selectedValues = new Set(
1468
- Array.isArray(value) ? value :
1469
- value ? value.toString().split(',').filter(v => v) : []
1470
- );
1471
- const checkboxMap = new Map(); // Map of value -> checkbox element
1542
+ // In search mode, pre-populate the hidden select once with all options
1543
+ if (!buildHiddenSelectOnUpdate) {
1544
+ allOptions.forEach(({ optValue, optText }) => {
1545
+ FTableDOMHelper.create('option', {
1546
+ value: optValue,
1547
+ textContent: optText,
1548
+ parent: hiddenSelect
1549
+ });
1550
+ });
1551
+ }
1472
1552
 
1473
- // Function to update display
1553
+ // ---------- updateDisplay ----------
1474
1554
  const updateDisplay = () => {
1475
1555
  selectedDisplay.innerHTML = '';
1476
-
1556
+
1557
+ if (buildHiddenSelectOnUpdate) {
1558
+ // Form mode: rebuild hidden <option>s to reflect current selection
1559
+ hiddenSelect.innerHTML = '';
1560
+ selectedValues.forEach(val => {
1561
+ const text = optionsMap.get(val) ?? val;
1562
+ FTableDOMHelper.create('option', {
1563
+ value: val,
1564
+ textContent: text,
1565
+ selected: true,
1566
+ parent: hiddenSelect
1567
+ });
1568
+ });
1569
+ } else {
1570
+ // Search mode: just flip selected state on existing options
1571
+ Array.from(hiddenSelect.options).forEach(opt => {
1572
+ opt.selected = selectedValues.has(opt.value);
1573
+ });
1574
+ }
1575
+
1477
1576
  if (selectedValues.size === 0) {
1478
- placeholder.textContent = placeholderText;
1479
- selectedDisplay.appendChild(placeholder);
1577
+ placeholderEl.textContent = placeholderText;
1578
+ selectedDisplay.appendChild(placeholderEl);
1480
1579
  } else {
1481
- const selectedArray = Array.from(selectedValues);
1482
- const optionsMap = new Map();
1483
-
1484
- // Build options map
1485
- if (field.options) {
1486
- const options = Array.isArray(field.options) ? field.options :
1487
- Object.entries(field.options).map(([k, v]) => ({Value: k, DisplayText: v}));
1488
-
1489
- options.forEach(opt => {
1490
- const val = opt.Value !== undefined ? opt.Value :
1491
- opt.value !== undefined ? opt.value : opt;
1492
- const text = opt.DisplayText || opt.text || opt;
1493
- optionsMap.set(val.toString(), text);
1494
- });
1495
- }
1496
-
1497
- selectedArray.forEach(val => {
1580
+ selectedValues.forEach(val => {
1498
1581
  const tag = FTableDOMHelper.create('span', {
1499
1582
  className: 'ftable-multiselect-tag',
1500
1583
  parent: selectedDisplay
1501
1584
  });
1502
-
1503
1585
  FTableDOMHelper.create('span', {
1504
1586
  className: 'ftable-multiselect-tag-text',
1505
- textContent: optionsMap.get(val.toString()) || val,
1587
+ textContent: optionsMap.get(val) || val,
1506
1588
  parent: tag
1507
1589
  });
1508
-
1509
1590
  const removeBtn = FTableDOMHelper.create('span', {
1510
1591
  className: 'ftable-multiselect-tag-remove',
1511
1592
  innerHTML: '×',
1512
1593
  parent: tag
1513
1594
  });
1514
-
1515
1595
  removeBtn.addEventListener('click', (e) => {
1516
1596
  e.stopPropagation();
1517
1597
  selectedValues.delete(val);
1518
- // Update the checkbox state
1519
- const checkbox = checkboxMap.get(val.toString());
1520
- if (checkbox) {
1521
- checkbox.checked = false;
1522
- }
1598
+ const cb = checkboxMap.get(val);
1599
+ if (cb) cb.checked = false;
1523
1600
  updateDisplay();
1524
- hiddenInput.value = Array.from(selectedValues).join(',');
1601
+ if (onChangeExtra) onChangeExtra(hiddenSelect);
1525
1602
  });
1526
1603
  });
1527
1604
  }
1528
-
1529
- hiddenInput.value = Array.from(selectedValues).join(',');
1605
+
1606
+ if (onChangeExtra) onChangeExtra(hiddenSelect);
1530
1607
  };
1531
1608
 
1532
- // Function to close dropdown
1609
+ // ---------- Dropdown helpers ----------
1533
1610
  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
- }
1611
+ display.focus();
1612
+ if (dropdown) { dropdown.remove(); dropdown = null; }
1613
+ if (dropdownOverlay) { dropdownOverlay.remove(); dropdownOverlay = null; }
1543
1614
  if (container._cleanupHandlers) {
1544
1615
  container._cleanupHandlers();
1545
1616
  container._cleanupHandlers = null;
1546
1617
  }
1547
1618
  };
1548
1619
 
1549
- // Function to position dropdown
1550
1620
  const positionDropdown = () => {
1551
1621
  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
-
1622
+ const rect = display.getBoundingClientRect();
1623
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
1624
+ const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
1557
1625
  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);
1626
+ let top = rect.bottom + scrollTop + 4;
1627
+
1628
+ Object.assign(dropdown.style, {
1629
+ position: 'absolute',
1630
+ left: `${left}px`,
1631
+ top: `${top}px`,
1632
+ width: `${rect.width}px`,
1633
+ minWidth: 'fit-content',
1634
+ boxSizing: 'border-box',
1635
+ zIndex: '10000',
1636
+ });
1637
+
1638
+ const ddRect = dropdown.getBoundingClientRect();
1639
+ if (ddRect.right > window.innerWidth) {
1640
+ left = Math.max(10, window.innerWidth - ddRect.width - 10);
1573
1641
  dropdown.style.left = `${left}px`;
1574
1642
  }
1575
1643
  };
1576
1644
 
1577
- // Populate options
1578
- const populateOptions = () => {
1579
- if (!field.options || !dropdown) return;
1580
-
1581
- const options = Array.isArray(field.options) ? field.options :
1582
- Object.entries(field.options).map(([k, v]) => ({Value: k, DisplayText: v}));
1583
-
1584
- options.forEach(option => {
1585
- const optValue = option.Value !== undefined ? option.Value :
1586
- option.value !== undefined ? option.value : option;
1587
-
1588
- // Skip if value is empty
1589
- if (optValue == null || optValue === '') {
1590
- return; // This continues to the next iteration
1645
+ // Render options (or a filtered subset) into the open dropdown
1646
+ const renderDropdownOptions = (filterText = '') => {
1647
+ if (!dropdown) return;
1648
+ // Remove existing option rows (but keep the search bar if present)
1649
+ Array.from(dropdown.querySelectorAll('.ftable-multiselect-option, .ftable-multiselect-optgroup'))
1650
+ .forEach(el => el.remove());
1651
+
1652
+ const lc = filterText.toLowerCase();
1653
+ const visible = filterText
1654
+ ? allOptions.filter(o => o.optText.toLowerCase().includes(lc))
1655
+ : allOptions;
1656
+
1657
+ // Group rendering
1658
+ const usedGroups = new Set();
1659
+ let currentOptgroup = null;
1660
+
1661
+ visible.forEach(({ optValue, optText, groupLabel }) => {
1662
+ if (groupLabel && groupLabel !== usedGroups[usedGroups.size - 1]) {
1663
+ if (!usedGroups.has(groupLabel)) {
1664
+ usedGroups.add(groupLabel);
1665
+ currentOptgroup = FTableDOMHelper.create('div', {
1666
+ className: 'ftable-multiselect-optgroup',
1667
+ textContent: groupLabel,
1668
+ parent: dropdown
1669
+ });
1670
+ }
1671
+ } else if (!groupLabel) {
1672
+ currentOptgroup = null;
1591
1673
  }
1592
1674
 
1593
- const optText = option.DisplayText || option.text || option;
1594
-
1595
1675
  const optionDiv = FTableDOMHelper.create('div', {
1596
1676
  className: 'ftable-multiselect-option',
1597
1677
  parent: dropdown
1598
1678
  });
1599
-
1679
+
1600
1680
  const checkbox = FTableDOMHelper.create('input', {
1601
1681
  type: 'checkbox',
1602
1682
  className: 'ftable-multiselect-checkbox',
1603
1683
  checked: selectedValues.has(optValue.toString()),
1604
1684
  parent: optionDiv
1605
1685
  });
1606
-
1607
- // Store checkbox reference
1686
+
1608
1687
  checkboxMap.set(optValue.toString(), checkbox);
1609
-
1610
- const label = FTableDOMHelper.create('label', {
1688
+
1689
+ FTableDOMHelper.create('label', {
1611
1690
  className: 'ftable-multiselect-label',
1612
1691
  textContent: optText,
1613
1692
  parent: optionDiv
1614
1693
  });
1615
-
1616
- // Click anywhere on the option to toggle
1694
+
1617
1695
  optionDiv.addEventListener('click', (e) => {
1618
1696
  e.stopPropagation();
1619
-
1620
- if (selectedValues.has(optValue.toString())) {
1621
- selectedValues.delete(optValue.toString());
1697
+ const key = optValue.toString();
1698
+ if (selectedValues.has(key)) {
1699
+ selectedValues.delete(key);
1622
1700
  checkbox.checked = false;
1623
1701
  } else {
1624
- selectedValues.add(optValue.toString());
1702
+ selectedValues.add(key);
1625
1703
  checkbox.checked = true;
1626
1704
  }
1627
-
1628
1705
  updateDisplay();
1629
1706
  });
1630
1707
  });
1631
1708
  };
1632
1709
 
1633
- // Toggle dropdown
1710
+ // ---------- toggleDropdown ----------
1634
1711
  const toggleDropdown = (e) => {
1635
1712
  if (e) e.stopPropagation();
1636
-
1713
+
1637
1714
  if (dropdown) {
1638
- // Dropdown is open, close it
1639
1715
  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
- });
1716
+ return;
1717
+ }
1650
1718
 
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
- });
1719
+ // Close any other open dropdowns
1720
+ document.querySelectorAll('.ftable-multiselect-dropdown').forEach(dd => dd.remove());
1721
+ document.querySelectorAll('.ftable-multiselect-overlay').forEach(ov => ov.remove());
1661
1722
 
1662
- // Populate options
1663
- populateOptions();
1723
+ dropdownOverlay = FTableDOMHelper.create('div', {
1724
+ className: 'ftable-multiselect-overlay',
1725
+ parent: document.body
1726
+ });
1664
1727
 
1665
- // Position dropdown
1666
- positionDropdown();
1728
+ dropdown = FTableDOMHelper.create('div', {
1729
+ className: 'ftable-multiselect-dropdown',
1730
+ parent: document.body,
1731
+ attributes: { tabindex: '-1', role: 'listbox', 'aria-multiselectable': 'true' }
1732
+ });
1667
1733
 
1668
- // dropdown focus
1669
- dropdown.focus();
1734
+ // Optional live-search bar
1735
+ if (livesearch) {
1736
+ const searchWrap = FTableDOMHelper.create('div', {
1737
+ className: 'ftable-multiselect-livesearch-wrap',
1738
+ parent: dropdown
1739
+ });
1740
+ const searchInput = FTableDOMHelper.create('input', {
1741
+ type: 'search',
1742
+ className: 'ftable-multiselect-livesearch',
1743
+ placeholder: 'Search...',
1744
+ parent: searchWrap,
1745
+ attributes: { autocomplete: 'off' }
1746
+ });
1747
+ searchInput.addEventListener('input', () => {
1748
+ renderDropdownOptions(searchInput.value);
1749
+ });
1750
+ searchInput.addEventListener('click', e => e.stopPropagation());
1751
+ // Focus search input automatically
1752
+ setTimeout(() => searchInput.focus(), 0);
1753
+ }
1670
1754
 
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
- }
1755
+ renderDropdownOptions();
1756
+ positionDropdown();
1688
1757
 
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
- });
1758
+ if (!livesearch) dropdown.focus();
1698
1759
 
1699
- // Handle clicks outside
1700
- dropdownOverlay.addEventListener('click', (event) => {
1701
- if (event.target === dropdownOverlay) {
1702
- closeDropdown();
1760
+ // Keyboard navigation
1761
+ dropdown.addEventListener('keydown', (e) => {
1762
+ if (e.key === 'Escape') {
1763
+ closeDropdown();
1764
+ } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
1765
+ e.preventDefault();
1766
+ const checkboxes = Array.from(dropdown.querySelectorAll('.ftable-multiselect-checkbox'));
1767
+ const currentIndex = checkboxes.indexOf(document.activeElement);
1768
+ const nextIndex = e.key === 'ArrowDown'
1769
+ ? (currentIndex < checkboxes.length - 1 ? currentIndex + 1 : 0)
1770
+ : (currentIndex > 0 ? currentIndex - 1 : checkboxes.length - 1);
1771
+ checkboxes[nextIndex]?.focus();
1772
+ } else if (e.key === ' ' || e.key === 'Enter') {
1773
+ e.preventDefault();
1774
+ if (document.activeElement.classList.contains('ftable-multiselect-checkbox')) {
1775
+ document.activeElement.click();
1703
1776
  }
1704
- });
1777
+ }
1778
+ });
1705
1779
 
1706
- // Reposition on scroll/resize
1707
- const repositionHandler = () => positionDropdown();
1708
- const scrollHandler = (e) => {
1709
- if (dropdown && dropdown.contains(e.target)) {
1710
- return; // Allow scrolling inside dropdown
1711
- }
1712
- positionDropdown();
1713
- };
1714
- const selectedResizeObserver = new ResizeObserver(() => {
1715
- positionDropdown();
1716
- });
1717
- window.addEventListener('scroll', scrollHandler, true);
1718
- window.addEventListener('resize', repositionHandler);
1719
- selectedResizeObserver.observe(selectedDisplay);
1720
-
1721
- // Store cleanup function
1722
- container._cleanupHandlers = () => {
1723
- window.removeEventListener('scroll', scrollHandler, true);
1724
- window.removeEventListener('resize', repositionHandler);
1725
- selectedResizeObserver.disconnect();
1726
- };
1727
- }
1780
+ dropdownOverlay.addEventListener('click', (event) => {
1781
+ if (event.target === dropdownOverlay) closeDropdown();
1782
+ });
1783
+
1784
+ // Reposition on scroll / resize
1785
+ const repositionHandler = () => positionDropdown();
1786
+ const scrollHandler = (e) => {
1787
+ if (dropdown && dropdown.contains(e.target)) return;
1788
+ positionDropdown();
1789
+ };
1790
+ const resizeObserver = new ResizeObserver(() => positionDropdown());
1791
+ window.addEventListener('scroll', scrollHandler, true);
1792
+ window.addEventListener('resize', repositionHandler);
1793
+ resizeObserver.observe(selectedDisplay);
1794
+
1795
+ container._cleanupHandlers = () => {
1796
+ window.removeEventListener('scroll', scrollHandler, true);
1797
+ window.removeEventListener('resize', repositionHandler);
1798
+ resizeObserver.disconnect();
1799
+ };
1728
1800
  };
1729
1801
 
1730
1802
  display.addEventListener('click', toggleDropdown);
1731
- toggleBtn.addEventListener('click', toggleDropdown);
1803
+ display.querySelector('.ftable-multiselect-toggle').addEventListener('click', toggleDropdown);
1732
1804
  display.addEventListener('keydown', (e) => {
1733
1805
  if (e.key === 'ArrowDown' || e.key === 'Enter') {
1734
1806
  e.preventDefault();
@@ -1736,29 +1808,32 @@ class FTableFormBuilder {
1736
1808
  }
1737
1809
  });
1738
1810
 
1739
- // Clean up when container is removed from DOM
1740
- const observer = new MutationObserver((mutations) => {
1811
+ // Reset method (used by search toolbar)
1812
+ container.resetMultiSelect = () => {
1813
+ selectedValues.clear();
1814
+ checkboxMap.forEach(cb => { cb.checked = false; });
1815
+ closeDropdown();
1816
+ updateDisplay();
1817
+ };
1818
+
1819
+ // Cleanup when container is removed from DOM
1820
+ const domObserver = new MutationObserver((mutations) => {
1741
1821
  mutations.forEach((mutation) => {
1742
1822
  mutation.removedNodes.forEach((node) => {
1743
- if (node === container || node.contains && node.contains(container)) {
1823
+ if (node === container || (node.contains && node.contains(container))) {
1744
1824
  closeDropdown();
1745
- observer.disconnect();
1825
+ domObserver.disconnect();
1746
1826
  }
1747
1827
  });
1748
1828
  });
1749
1829
  });
1750
-
1751
- // Start observing once container is in the DOM
1752
1830
  setTimeout(() => {
1753
1831
  if (container.parentNode) {
1754
- observer.observe(container.parentNode, { childList: true, subtree: true });
1832
+ domObserver.observe(container.parentNode, { childList: true, subtree: true });
1755
1833
  }
1756
1834
  }, 0);
1757
1835
 
1758
- // Initialize
1759
- populateOptions();
1760
1836
  updateDisplay();
1761
-
1762
1837
  return container;
1763
1838
  }
1764
1839
 
@@ -1868,7 +1943,21 @@ class FTableFormBuilder {
1868
1943
  select.innerHTML = ''; // Clear existing options
1869
1944
 
1870
1945
  if (Array.isArray(options)) {
1946
+ // Group options by their Group property (if any)
1947
+ const groups = new Map(); // groupLabel -> [options]
1948
+ const ungrouped = [];
1949
+
1871
1950
  options.forEach(option => {
1951
+ const groupLabel = option.Group || option.group || null;
1952
+ if (groupLabel) {
1953
+ if (!groups.has(groupLabel)) groups.set(groupLabel, []);
1954
+ groups.get(groupLabel).push(option);
1955
+ } else {
1956
+ ungrouped.push(option);
1957
+ }
1958
+ });
1959
+
1960
+ const appendOption = (option, parent) => {
1872
1961
  const value = option.Value !== undefined ? option.Value :
1873
1962
  option.value !== undefined ? option.value :
1874
1963
  option; // fallback for string
@@ -1876,19 +1965,30 @@ class FTableFormBuilder {
1876
1965
  value: value,
1877
1966
  textContent: option.DisplayText || option.text || option,
1878
1967
  selected: value == selectedValue,
1879
- parent: select
1968
+ parent: parent
1880
1969
  });
1881
-
1882
1970
  if (option.Data && typeof option.Data === 'object') {
1883
1971
  Object.entries(option.Data).forEach(([key, dataValue]) => {
1884
1972
  optionElement.setAttribute(`data-${key}`, dataValue);
1885
1973
  });
1886
1974
  }
1975
+ };
1976
+
1977
+ // Render ungrouped options first
1978
+ ungrouped.forEach(option => appendOption(option, select));
1887
1979
 
1980
+ // Render grouped options inside <optgroup> elements
1981
+ groups.forEach((groupOptions, label) => {
1982
+ const optgroup = FTableDOMHelper.create('optgroup', {
1983
+ attributes: { label },
1984
+ parent: select
1985
+ });
1986
+ groupOptions.forEach(option => appendOption(option, optgroup));
1888
1987
  });
1988
+
1889
1989
  } else if (typeof options === 'object') {
1890
1990
  Object.entries(options).forEach(([key, text]) => {
1891
- const optionElement = FTableDOMHelper.create('option', {
1991
+ FTableDOMHelper.create('option', {
1892
1992
  value: key,
1893
1993
  textContent: text,
1894
1994
  selected: key == selectedValue,
@@ -2772,379 +2872,28 @@ class FTable extends FTableEventEmitter {
2772
2872
  }
2773
2873
 
2774
2874
  createCustomMultiSelectForSearch(fieldSearchName, fieldName, field, optionsSource, attributes) {
2775
- // Create container
2776
- const container = FTableDOMHelper.create('div', {
2777
- className: 'ftable-multiselect-container ftable-multiselect-search ftable-toolbarsearch',
2778
- attributes: { 'data-field-name': attributes['data-field-name'] }
2779
- });
2780
-
2781
- // Create hidden select to maintain compatibility with existing search logic
2782
- const hiddenSelect = FTableDOMHelper.create('select', {
2783
- id: fieldSearchName,
2784
- multiple: true,
2785
- style: 'display: none;',
2786
- attributes: attributes
2787
- });
2788
- container.appendChild(hiddenSelect);
2789
-
2790
- // Expose hidden select for external access (needed for event listeners and reset)
2791
- container.hiddenSelect = hiddenSelect;
2792
-
2793
- // Create display area
2794
- const display = FTableDOMHelper.create('div', {
2795
- className: 'ftable-multiselect-display',
2796
- parent: container,
2797
- attributes: {
2798
- tabindex: '0' // Makes it focusable and in tab order
2799
- }
2800
- });
2801
-
2802
- const selectedDisplay = FTableDOMHelper.create('div', {
2803
- className: 'ftable-multiselect-selected',
2804
- parent: display
2805
- });
2806
-
2807
- const placeholderText = field.searchPlaceholder || field.placeholder || this.options.messages.multiSelectPlaceholder || 'Click to select options...';
2808
- const placeholder = FTableDOMHelper.create('span', {
2809
- className: 'ftable-multiselect-placeholder',
2810
- textContent: placeholderText,
2811
- parent: selectedDisplay
2812
- });
2813
-
2814
- // Create dropdown toggle button
2815
- const toggleBtn = FTableDOMHelper.create('button', {
2816
- type: 'button',
2817
- className: 'ftable-multiselect-toggle',
2818
- innerHTML: '▼',
2819
- parent: display,
2820
- attributes: {
2821
- tabindex: '-1' // this skips regular focus when tabbing
2822
- }
2823
- });
2824
-
2825
- // Dropdown and overlay will be created on demand and appended to body
2826
- let dropdown = null;
2827
- let dropdownOverlay = null;
2828
-
2829
- // Store selected values and checkbox references
2830
- const selectedValues = new Set();
2831
- const checkboxMap = new Map(); // Map of value -> checkbox element
2832
-
2833
- // Function to update display and hidden select
2834
- const updateDisplay = () => {
2835
- selectedDisplay.innerHTML = '';
2836
-
2837
- // Update hidden select
2838
- Array.from(hiddenSelect.options).forEach(opt => {
2839
- opt.selected = selectedValues.has(opt.value);
2840
- });
2841
-
2842
- // Trigger change event on hidden select for search functionality
2843
- hiddenSelect.dispatchEvent(new Event('change', { bubbles: true }));
2844
-
2845
- if (selectedValues.size === 0) {
2846
- placeholder.textContent = placeholderText;
2847
- selectedDisplay.appendChild(placeholder);
2848
- } else {
2849
- const selectedArray = Array.from(selectedValues);
2850
- const optionsMap = new Map();
2851
-
2852
- // Build options map
2853
- if (optionsSource && Array.isArray(optionsSource)) {
2854
- optionsSource.forEach(opt => {
2855
- const val = opt.Value !== undefined ? opt.Value :
2856
- opt.value !== undefined ? opt.value : opt;
2857
- const text = opt.DisplayText || opt.text || opt;
2858
- optionsMap.set(val.toString(), text);
2859
- });
2860
- }
2861
-
2862
- selectedArray.forEach(val => {
2863
- const tag = FTableDOMHelper.create('span', {
2864
- className: 'ftable-multiselect-tag',
2865
- parent: selectedDisplay
2866
- });
2867
-
2868
- FTableDOMHelper.create('span', {
2869
- className: 'ftable-multiselect-tag-text',
2870
- textContent: optionsMap.get(val.toString()) || val,
2871
- parent: tag
2872
- });
2873
-
2874
- const removeBtn = FTableDOMHelper.create('span', {
2875
- className: 'ftable-multiselect-tag-remove',
2876
- innerHTML: '×',
2877
- parent: tag
2878
- });
2879
-
2880
- removeBtn.addEventListener('click', (e) => {
2881
- e.stopPropagation();
2882
- selectedValues.delete(val);
2883
- // Update the checkbox state
2884
- const checkbox = checkboxMap.get(val.toString());
2885
- if (checkbox) {
2886
- checkbox.checked = false;
2887
- }
2888
- updateDisplay();
2889
- });
2890
- });
2891
- }
2892
- };
2893
-
2894
- // Function to close dropdown
2895
- const closeDropdown = () => {
2896
- display.focus(); // Return focus to the trigger
2897
- if (dropdown) {
2898
- dropdown.remove();
2899
- dropdown = null;
2900
- }
2901
- if (dropdownOverlay) {
2902
- dropdownOverlay.remove();
2903
- dropdownOverlay = null;
2904
- }
2905
- if (container._cleanupHandlers) {
2906
- container._cleanupHandlers();
2907
- container._cleanupHandlers = null;
2908
- }
2909
- };
2910
-
2911
- // Function to position dropdown
2912
- const positionDropdown = () => {
2913
- if (!dropdown) return;
2914
-
2915
- const rect = display.getBoundingClientRect();
2916
- const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
2917
- const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
2918
-
2919
- let left = rect.left + scrollLeft;
2920
- let top = rect.bottom + scrollTop + 4; // 4px gap
2921
-
2922
- dropdown.style.position = 'absolute';
2923
- dropdown.style.left = `${left}px`;
2924
- dropdown.style.top = `${top}px`;
2925
- dropdown.style.width = `${rect.width}px`;
2926
- dropdown.style.minWidth = `${rect.width}px`;
2927
- dropdown.style.boxSizing = 'border-box';
2928
- dropdown.style.zIndex = '10000';
2929
-
2930
- // Adjust horizontal position if needed
2931
- const dropdownRect = dropdown.getBoundingClientRect();
2932
- const viewportWidth = window.innerWidth;
2933
- if (dropdownRect.right > viewportWidth) {
2934
- left = Math.max(10, viewportWidth - dropdownRect.width - 10);
2935
- dropdown.style.left = `${left}px`;
2936
- }
2937
- };
2938
-
2939
- // Populate options in both hidden select and dropdown
2940
- const populateOptions = () => {
2941
- if (!optionsSource || !dropdown) return;
2942
-
2943
- const options = Array.isArray(optionsSource) ? optionsSource :
2944
- Object.entries(optionsSource).map(([k, v]) => ({Value: k, DisplayText: v}));
2945
-
2946
- options.forEach(option => {
2947
- const optValue = option.Value !== undefined ? option.Value :
2948
- option.value !== undefined ? option.value : option;
2949
-
2950
- // Skip if value is empty
2951
- if (optValue == null || optValue === '') {
2952
- return; // This continues to the next iteration
2953
- }
2954
-
2955
- const optText = option.DisplayText || option.text || option;
2956
-
2957
- // Add to hidden select (only once)
2958
- if (!hiddenSelect.querySelector(`option[value="${optValue}"]`)) {
2959
- FTableDOMHelper.create('option', {
2960
- value: optValue,
2961
- textContent: optText,
2962
- parent: hiddenSelect
2963
- });
2964
- }
2965
-
2966
- // Add to visual dropdown
2967
- const optionDiv = FTableDOMHelper.create('div', {
2968
- className: 'ftable-multiselect-option',
2969
- parent: dropdown
2970
- });
2971
-
2972
- const checkbox = FTableDOMHelper.create('input', {
2973
- type: 'checkbox',
2974
- className: 'ftable-multiselect-checkbox',
2975
- parent: optionDiv
2976
- });
2977
-
2978
- // Set initial checked state
2979
- checkbox.checked = selectedValues.has(optValue.toString());
2980
-
2981
- // Store checkbox reference
2982
- checkboxMap.set(optValue.toString(), checkbox);
2983
-
2984
- const label = FTableDOMHelper.create('label', {
2985
- className: 'ftable-multiselect-label',
2986
- textContent: optText,
2987
- parent: optionDiv
2988
- });
2989
-
2990
- // Click anywhere on the option to toggle
2991
- optionDiv.addEventListener('click', (e) => {
2992
- e.stopPropagation();
2993
-
2994
- if (selectedValues.has(optValue.toString())) {
2995
- selectedValues.delete(optValue.toString());
2996
- checkbox.checked = false;
2997
- } else {
2998
- selectedValues.add(optValue.toString());
2999
- checkbox.checked = true;
3000
- }
3001
-
3002
- updateDisplay();
3003
- });
3004
- });
3005
- };
3006
-
3007
- // Toggle dropdown
3008
- const toggleDropdown = (e) => {
3009
- if (e) e.stopPropagation();
3010
-
3011
- if (dropdown) {
3012
- // Dropdown is open, close it
3013
- closeDropdown();
3014
- } else {
3015
- // Close any other open multiselect dropdowns
3016
- document.querySelectorAll('.ftable-multiselect-dropdown').forEach(dd => dd.remove());
3017
- document.querySelectorAll('.ftable-multiselect-overlay').forEach(ov => ov.remove());
3018
-
3019
- // Create overlay
3020
- dropdownOverlay = FTableDOMHelper.create('div', {
3021
- className: 'ftable-multiselect-overlay',
3022
- parent: document.body
3023
- });
3024
-
3025
- // Create dropdown
3026
- dropdown = FTableDOMHelper.create('div', {
3027
- className: 'ftable-multiselect-dropdown',
3028
- parent: document.body,
3029
- attributes: {
3030
- tabindex: '-1',
3031
- role: 'listbox',
3032
- 'aria-multiselectable': 'true'
3033
- }
3034
- });
3035
-
3036
- // Populate options
3037
- populateOptions();
3038
-
3039
- // Position dropdown
3040
- positionDropdown();
3041
-
3042
- // dropdown focus
3043
- dropdown.focus();
3044
-
3045
- // Add keyboard navigation
3046
- dropdown.addEventListener('keydown', (e) => {
3047
- if (e.key === 'Escape') {
3048
- closeDropdown();
3049
- } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
3050
- e.preventDefault();
3051
- // Navigate between options
3052
- const checkboxes = Array.from(dropdown.querySelectorAll('.ftable-multiselect-checkbox'));
3053
- const current = document.activeElement;
3054
- const currentIndex = checkboxes.indexOf(current);
3055
-
3056
- let nextIndex;
3057
- if (e.key === 'ArrowDown') {
3058
- nextIndex = currentIndex < checkboxes.length - 1 ? currentIndex + 1 : 0;
3059
- } else {
3060
- nextIndex = currentIndex > 0 ? currentIndex - 1 : checkboxes.length - 1;
3061
- }
3062
-
3063
- checkboxes[nextIndex].focus();
3064
- } else if (e.key === ' ' || e.key === 'Enter') {
3065
- e.preventDefault();
3066
- // Toggle the focused checkbox
3067
- if (document.activeElement.classList.contains('ftable-multiselect-checkbox')) {
3068
- document.activeElement.click();
3069
- }
3070
- }
3071
- });
3072
-
3073
- // Handle clicks outside
3074
- dropdownOverlay.addEventListener('click', (event) => {
3075
- if (event.target === dropdownOverlay) {
3076
- closeDropdown();
3077
- }
3078
- });
3079
-
3080
- // Reposition on scroll/resize
3081
- const repositionHandler = () => positionDropdown();
3082
- const scrollHandler = (e) => {
3083
- if (dropdown && dropdown.contains(e.target)) {
3084
- return; // Allow scrolling inside dropdown
3085
- }
3086
- positionDropdown();
3087
- };
3088
- const selectedResizeObserver = new ResizeObserver(() => {
3089
- positionDropdown();
3090
- });
3091
- window.addEventListener('scroll', scrollHandler, true);
3092
- window.addEventListener('resize', repositionHandler);
3093
- selectedResizeObserver.observe(selectedDisplay);
3094
-
3095
- // Store cleanup function
3096
- container._cleanupHandlers = () => {
3097
- window.removeEventListener('scroll', scrollHandler, true);
3098
- window.removeEventListener('resize', repositionHandler);
3099
- selectedResizeObserver.disconnect();
3100
- };
3101
- }
3102
- };
3103
-
3104
- display.addEventListener('click', toggleDropdown);
3105
- toggleBtn.addEventListener('click', toggleDropdown);
3106
- display.addEventListener('keydown', (e) => {
3107
- if (e.key === 'ArrowDown' || e.key === 'Enter') {
3108
- e.preventDefault();
3109
- toggleDropdown();
3110
- }
3111
- });
3112
-
3113
- // Add reset method to container
3114
- container.resetMultiSelect = () => {
3115
- selectedValues.clear();
3116
- checkboxMap.forEach(checkbox => {
3117
- checkbox.checked = false;
3118
- });
3119
- closeDropdown();
3120
- updateDisplay();
3121
- };
3122
-
3123
- // Clean up when container is removed from DOM
3124
- const observer = new MutationObserver((mutations) => {
3125
- mutations.forEach((mutation) => {
3126
- mutation.removedNodes.forEach((node) => {
3127
- if (node === container || node.contains && node.contains(container)) {
3128
- closeDropdown();
3129
- observer.disconnect();
3130
- }
3131
- });
3132
- });
2875
+ const livesearch = field.livesearch
2876
+ ?? (attributes['data-livesearch'] === 'true' || attributes['data-livesearch'] === true)
2877
+ ?? false;
2878
+
2879
+ return this.formBuilder._buildCustomMultiSelect({
2880
+ hiddenSelectId: fieldSearchName,
2881
+ hiddenSelectName: attributes['data-field-name'] || fieldSearchName,
2882
+ extraClasses: 'ftable-multiselect-search ftable-toolbarsearch',
2883
+ containerDataFieldName: attributes['data-field-name'] || fieldSearchName,
2884
+ hiddenSelectAttributes: attributes,
2885
+ optionsSource: optionsSource,
2886
+ initialValues: [],
2887
+ placeholderText: field.searchPlaceholder || field.placeholder
2888
+ || this.options.messages?.multiSelectPlaceholder
2889
+ || 'Click to select options...',
2890
+ livesearch,
2891
+ onChangeExtra: (hiddenSelect) => {
2892
+ hiddenSelect.dispatchEvent(new Event('change', { bubbles: true }));
2893
+ },
2894
+ buildHiddenSelectOnUpdate: false, // search mode: options pre-populated
3133
2895
  });
3134
-
3135
- // Start observing once container is in the DOM
3136
- setTimeout(() => {
3137
- if (container.parentNode) {
3138
- observer.observe(container.parentNode, { childList: true, subtree: true });
3139
- }
3140
- }, 0);
3141
-
3142
- // Initialize
3143
- updateDisplay();
3144
-
3145
- return container;
3146
2896
  }
3147
-
3148
2897
  async createDatalistForSearch(fieldName, field) {
3149
2898
  const fieldSearchName = 'ftable-toolbarsearch-' + fieldName;
3150
2899
 
@@ -3993,9 +3742,18 @@ class FTable extends FTableEventEmitter {
3993
3742
  const searchFields = [];
3994
3743
 
3995
3744
  Object.entries(this.state.searchQueries).forEach(([fieldName, query]) => {
3996
- if (query !== '') { // Double check it's not empty
3997
- queries.push(query);
3998
- searchFields.push(fieldName);
3745
+ if (Array.isArray(query)) {
3746
+ query.forEach(value => {
3747
+ if (value !== '' && value != null) {
3748
+ queries.push(value);
3749
+ searchFields.push(fieldName);
3750
+ }
3751
+ });
3752
+ } else {
3753
+ if (query !== '') {
3754
+ queries.push(query);
3755
+ searchFields.push(fieldName);
3756
+ }
3999
3757
  }
4000
3758
  });
4001
3759