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