@liedekef/ftable 1.1.51 → 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 -589
  2. package/ftable.js +357 -589
  3. package/ftable.min.js +2 -2
  4. package/ftable.umd.js +357 -589
  5. package/package.json +1 -1
  6. package/themes/basic/ftable_basic.css +1 -6
  7. package/themes/basic/ftable_basic.min.css +1 -1
  8. package/themes/ftable_theme_base.less +1 -7
  9. package/themes/lightcolor/blue/ftable.css +1 -6
  10. package/themes/lightcolor/blue/ftable.min.css +1 -1
  11. package/themes/lightcolor/gray/ftable.css +1 -6
  12. package/themes/lightcolor/gray/ftable.min.css +1 -1
  13. package/themes/lightcolor/green/ftable.css +1 -6
  14. package/themes/lightcolor/green/ftable.min.css +1 -1
  15. package/themes/lightcolor/orange/ftable.css +1 -6
  16. package/themes/lightcolor/orange/ftable.min.css +1 -1
  17. package/themes/lightcolor/red/ftable.css +1 -6
  18. package/themes/lightcolor/red/ftable.min.css +1 -1
  19. package/themes/metro/blue/ftable.css +1 -6
  20. package/themes/metro/blue/ftable.min.css +1 -1
  21. package/themes/metro/brown/ftable.css +1 -6
  22. package/themes/metro/brown/ftable.min.css +1 -1
  23. package/themes/metro/crimson/ftable.css +1 -6
  24. package/themes/metro/crimson/ftable.min.css +1 -1
  25. package/themes/metro/darkgray/ftable.css +1 -6
  26. package/themes/metro/darkgray/ftable.min.css +1 -1
  27. package/themes/metro/darkorange/ftable.css +1 -6
  28. package/themes/metro/darkorange/ftable.min.css +1 -1
  29. package/themes/metro/green/ftable.css +1 -6
  30. package/themes/metro/green/ftable.min.css +1 -1
  31. package/themes/metro/lightgray/ftable.css +1 -6
  32. package/themes/metro/lightgray/ftable.min.css +1 -1
  33. package/themes/metro/pink/ftable.css +1 -6
  34. package/themes/metro/pink/ftable.min.css +1 -1
  35. package/themes/metro/purple/ftable.css +1 -6
  36. package/themes/metro/purple/ftable.min.css +1 -1
  37. package/themes/metro/red/ftable.css +1 -6
  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,289 +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 scrollHandler = (e) => {
1707
- if (dropdown && dropdown.contains(e.target)) {
1708
- return; // Allow scrolling inside dropdown
1709
- }
1710
- positionDropdown();
1711
- };
1712
- const repositionHandler = () => positionDropdown();
1713
- window.addEventListener('scroll', scrollHandler, true);
1714
- window.addEventListener('resize', repositionHandler);
1715
-
1716
- // Store cleanup function
1717
- container._cleanupHandlers = () => {
1718
- window.removeEventListener('scroll', scrollHandler, true);
1719
- window.removeEventListener('resize', repositionHandler);
1720
- };
1721
- }
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
+ };
1722
1799
  };
1723
1800
 
1724
1801
  display.addEventListener('click', toggleDropdown);
1725
- toggleBtn.addEventListener('click', toggleDropdown);
1802
+ display.querySelector('.ftable-multiselect-toggle').addEventListener('click', toggleDropdown);
1726
1803
  display.addEventListener('keydown', (e) => {
1727
1804
  if (e.key === 'ArrowDown' || e.key === 'Enter') {
1728
1805
  e.preventDefault();
@@ -1730,29 +1807,32 @@ class FTableFormBuilder {
1730
1807
  }
1731
1808
  });
1732
1809
 
1733
- // Clean up when container is removed from DOM
1734
- 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) => {
1735
1820
  mutations.forEach((mutation) => {
1736
1821
  mutation.removedNodes.forEach((node) => {
1737
- if (node === container || node.contains && node.contains(container)) {
1822
+ if (node === container || (node.contains && node.contains(container))) {
1738
1823
  closeDropdown();
1739
- observer.disconnect();
1824
+ domObserver.disconnect();
1740
1825
  }
1741
1826
  });
1742
1827
  });
1743
1828
  });
1744
-
1745
- // Start observing once container is in the DOM
1746
1829
  setTimeout(() => {
1747
1830
  if (container.parentNode) {
1748
- observer.observe(container.parentNode, { childList: true, subtree: true });
1831
+ domObserver.observe(container.parentNode, { childList: true, subtree: true });
1749
1832
  }
1750
1833
  }, 0);
1751
1834
 
1752
- // Initialize
1753
- populateOptions();
1754
1835
  updateDisplay();
1755
-
1756
1836
  return container;
1757
1837
  }
1758
1838
 
@@ -1862,7 +1942,21 @@ class FTableFormBuilder {
1862
1942
  select.innerHTML = ''; // Clear existing options
1863
1943
 
1864
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
+
1865
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) => {
1866
1960
  const value = option.Value !== undefined ? option.Value :
1867
1961
  option.value !== undefined ? option.value :
1868
1962
  option; // fallback for string
@@ -1870,19 +1964,30 @@ class FTableFormBuilder {
1870
1964
  value: value,
1871
1965
  textContent: option.DisplayText || option.text || option,
1872
1966
  selected: value == selectedValue,
1873
- parent: select
1967
+ parent: parent
1874
1968
  });
1875
-
1876
1969
  if (option.Data && typeof option.Data === 'object') {
1877
1970
  Object.entries(option.Data).forEach(([key, dataValue]) => {
1878
1971
  optionElement.setAttribute(`data-${key}`, dataValue);
1879
1972
  });
1880
1973
  }
1974
+ };
1975
+
1976
+ // Render ungrouped options first
1977
+ ungrouped.forEach(option => appendOption(option, select));
1881
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));
1882
1986
  });
1987
+
1883
1988
  } else if (typeof options === 'object') {
1884
1989
  Object.entries(options).forEach(([key, text]) => {
1885
- const optionElement = FTableDOMHelper.create('option', {
1990
+ FTableDOMHelper.create('option', {
1886
1991
  value: key,
1887
1992
  textContent: text,
1888
1993
  selected: key == selectedValue,
@@ -2766,374 +2871,28 @@ class FTable extends FTableEventEmitter {
2766
2871
  }
2767
2872
 
2768
2873
  createCustomMultiSelectForSearch(fieldSearchName, fieldName, field, optionsSource, attributes) {
2769
- // Create container
2770
- const container = FTableDOMHelper.create('div', {
2771
- className: 'ftable-multiselect-container ftable-multiselect-search ftable-toolbarsearch',
2772
- attributes: { 'data-field-name': attributes['data-field-name'] }
2773
- });
2774
-
2775
- // Create hidden select to maintain compatibility with existing search logic
2776
- const hiddenSelect = FTableDOMHelper.create('select', {
2777
- id: fieldSearchName,
2778
- multiple: true,
2779
- style: 'display: none;',
2780
- attributes: attributes
2781
- });
2782
- container.appendChild(hiddenSelect);
2783
-
2784
- // Expose hidden select for external access (needed for event listeners and reset)
2785
- container.hiddenSelect = hiddenSelect;
2786
-
2787
- // Create display area
2788
- const display = FTableDOMHelper.create('div', {
2789
- className: 'ftable-multiselect-display',
2790
- parent: container,
2791
- attributes: {
2792
- tabindex: '0' // Makes it focusable and in tab order
2793
- }
2794
- });
2795
-
2796
- const selectedDisplay = FTableDOMHelper.create('div', {
2797
- className: 'ftable-multiselect-selected',
2798
- parent: display
2799
- });
2800
-
2801
- const placeholderText = field.searchPlaceholder || field.placeholder || this.options.messages.multiSelectPlaceholder || 'Click to select options...';
2802
- const placeholder = FTableDOMHelper.create('span', {
2803
- className: 'ftable-multiselect-placeholder',
2804
- textContent: placeholderText,
2805
- parent: selectedDisplay
2806
- });
2807
-
2808
- // Create dropdown toggle button
2809
- const toggleBtn = FTableDOMHelper.create('button', {
2810
- type: 'button',
2811
- className: 'ftable-multiselect-toggle',
2812
- innerHTML: '▼',
2813
- parent: display,
2814
- attributes: {
2815
- tabindex: '-1' // this skips regular focus when tabbing
2816
- }
2817
- });
2818
-
2819
- // Dropdown and overlay will be created on demand and appended to body
2820
- let dropdown = null;
2821
- let dropdownOverlay = null;
2822
-
2823
- // Store selected values and checkbox references
2824
- const selectedValues = new Set();
2825
- const checkboxMap = new Map(); // Map of value -> checkbox element
2826
-
2827
- // Function to update display and hidden select
2828
- const updateDisplay = () => {
2829
- selectedDisplay.innerHTML = '';
2830
-
2831
- // Update hidden select
2832
- Array.from(hiddenSelect.options).forEach(opt => {
2833
- opt.selected = selectedValues.has(opt.value);
2834
- });
2835
-
2836
- // Trigger change event on hidden select for search functionality
2837
- hiddenSelect.dispatchEvent(new Event('change', { bubbles: true }));
2838
-
2839
- if (selectedValues.size === 0) {
2840
- placeholder.textContent = placeholderText;
2841
- selectedDisplay.appendChild(placeholder);
2842
- } else {
2843
- const selectedArray = Array.from(selectedValues);
2844
- const optionsMap = new Map();
2845
-
2846
- // Build options map
2847
- if (optionsSource && Array.isArray(optionsSource)) {
2848
- optionsSource.forEach(opt => {
2849
- const val = opt.Value !== undefined ? opt.Value :
2850
- opt.value !== undefined ? opt.value : opt;
2851
- const text = opt.DisplayText || opt.text || opt;
2852
- optionsMap.set(val.toString(), text);
2853
- });
2854
- }
2855
-
2856
- selectedArray.forEach(val => {
2857
- const tag = FTableDOMHelper.create('span', {
2858
- className: 'ftable-multiselect-tag',
2859
- parent: selectedDisplay
2860
- });
2861
-
2862
- FTableDOMHelper.create('span', {
2863
- className: 'ftable-multiselect-tag-text',
2864
- textContent: optionsMap.get(val.toString()) || val,
2865
- parent: tag
2866
- });
2867
-
2868
- const removeBtn = FTableDOMHelper.create('span', {
2869
- className: 'ftable-multiselect-tag-remove',
2870
- innerHTML: '×',
2871
- parent: tag
2872
- });
2873
-
2874
- removeBtn.addEventListener('click', (e) => {
2875
- e.stopPropagation();
2876
- selectedValues.delete(val);
2877
- // Update the checkbox state
2878
- const checkbox = checkboxMap.get(val.toString());
2879
- if (checkbox) {
2880
- checkbox.checked = false;
2881
- }
2882
- updateDisplay();
2883
- });
2884
- });
2885
- }
2886
- };
2887
-
2888
- // Function to close dropdown
2889
- const closeDropdown = () => {
2890
- display.focus(); // Return focus to the trigger
2891
- if (dropdown) {
2892
- dropdown.remove();
2893
- dropdown = null;
2894
- }
2895
- if (dropdownOverlay) {
2896
- dropdownOverlay.remove();
2897
- dropdownOverlay = null;
2898
- }
2899
- if (container._cleanupHandlers) {
2900
- container._cleanupHandlers();
2901
- container._cleanupHandlers = null;
2902
- }
2903
- };
2904
-
2905
- // Function to position dropdown
2906
- const positionDropdown = () => {
2907
- if (!dropdown) return;
2908
-
2909
- const rect = display.getBoundingClientRect();
2910
- const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
2911
- const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
2912
-
2913
- let left = rect.left + scrollLeft;
2914
- let top = rect.bottom + scrollTop + 4; // 4px gap
2915
-
2916
- dropdown.style.position = 'absolute';
2917
- dropdown.style.left = `${left}px`;
2918
- dropdown.style.top = `${top}px`;
2919
- dropdown.style.width = `${rect.width}px`;
2920
- dropdown.style.minWidth = `${rect.width}px`;
2921
- dropdown.style.boxSizing = 'border-box';
2922
- dropdown.style.zIndex = '10000';
2923
-
2924
- // Adjust horizontal position if needed
2925
- const dropdownRect = dropdown.getBoundingClientRect();
2926
- const viewportWidth = window.innerWidth;
2927
- if (dropdownRect.right > viewportWidth) {
2928
- left = Math.max(10, viewportWidth - dropdownRect.width - 10);
2929
- dropdown.style.left = `${left}px`;
2930
- }
2931
- };
2932
-
2933
- // Populate options in both hidden select and dropdown
2934
- const populateOptions = () => {
2935
- if (!optionsSource || !dropdown) return;
2936
-
2937
- const options = Array.isArray(optionsSource) ? optionsSource :
2938
- Object.entries(optionsSource).map(([k, v]) => ({Value: k, DisplayText: v}));
2939
-
2940
- options.forEach(option => {
2941
- const optValue = option.Value !== undefined ? option.Value :
2942
- option.value !== undefined ? option.value : option;
2943
-
2944
- // Skip if value is empty
2945
- if (optValue == null || optValue === '') {
2946
- return; // This continues to the next iteration
2947
- }
2948
-
2949
- const optText = option.DisplayText || option.text || option;
2950
-
2951
- // Add to hidden select (only once)
2952
- if (!hiddenSelect.querySelector(`option[value="${optValue}"]`)) {
2953
- FTableDOMHelper.create('option', {
2954
- value: optValue,
2955
- textContent: optText,
2956
- parent: hiddenSelect
2957
- });
2958
- }
2959
-
2960
- // Add to visual dropdown
2961
- const optionDiv = FTableDOMHelper.create('div', {
2962
- className: 'ftable-multiselect-option',
2963
- parent: dropdown
2964
- });
2965
-
2966
- const checkbox = FTableDOMHelper.create('input', {
2967
- type: 'checkbox',
2968
- className: 'ftable-multiselect-checkbox',
2969
- parent: optionDiv
2970
- });
2971
-
2972
- // Set initial checked state
2973
- checkbox.checked = selectedValues.has(optValue.toString());
2974
-
2975
- // Store checkbox reference
2976
- checkboxMap.set(optValue.toString(), checkbox);
2977
-
2978
- const label = FTableDOMHelper.create('label', {
2979
- className: 'ftable-multiselect-label',
2980
- textContent: optText,
2981
- parent: optionDiv
2982
- });
2983
-
2984
- // Click anywhere on the option to toggle
2985
- optionDiv.addEventListener('click', (e) => {
2986
- e.stopPropagation();
2987
-
2988
- if (selectedValues.has(optValue.toString())) {
2989
- selectedValues.delete(optValue.toString());
2990
- checkbox.checked = false;
2991
- } else {
2992
- selectedValues.add(optValue.toString());
2993
- checkbox.checked = true;
2994
- }
2995
-
2996
- updateDisplay();
2997
- });
2998
- });
2999
- };
3000
-
3001
- // Toggle dropdown
3002
- const toggleDropdown = (e) => {
3003
- if (e) e.stopPropagation();
3004
-
3005
- if (dropdown) {
3006
- // Dropdown is open, close it
3007
- closeDropdown();
3008
- } else {
3009
- // Close any other open multiselect dropdowns
3010
- document.querySelectorAll('.ftable-multiselect-dropdown').forEach(dd => dd.remove());
3011
- document.querySelectorAll('.ftable-multiselect-overlay').forEach(ov => ov.remove());
3012
-
3013
- // Create overlay
3014
- dropdownOverlay = FTableDOMHelper.create('div', {
3015
- className: 'ftable-multiselect-overlay',
3016
- parent: document.body
3017
- });
3018
-
3019
- // Create dropdown
3020
- dropdown = FTableDOMHelper.create('div', {
3021
- className: 'ftable-multiselect-dropdown',
3022
- parent: document.body,
3023
- attributes: {
3024
- tabindex: '-1',
3025
- role: 'listbox',
3026
- 'aria-multiselectable': 'true'
3027
- }
3028
- });
3029
-
3030
- // Populate options
3031
- populateOptions();
3032
-
3033
- // Position dropdown
3034
- positionDropdown();
3035
-
3036
- // dropdown focus
3037
- dropdown.focus();
3038
-
3039
- // Add keyboard navigation
3040
- dropdown.addEventListener('keydown', (e) => {
3041
- if (e.key === 'Escape') {
3042
- closeDropdown();
3043
- } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
3044
- e.preventDefault();
3045
- // Navigate between options
3046
- const checkboxes = Array.from(dropdown.querySelectorAll('.ftable-multiselect-checkbox'));
3047
- const current = document.activeElement;
3048
- const currentIndex = checkboxes.indexOf(current);
3049
-
3050
- let nextIndex;
3051
- if (e.key === 'ArrowDown') {
3052
- nextIndex = currentIndex < checkboxes.length - 1 ? currentIndex + 1 : 0;
3053
- } else {
3054
- nextIndex = currentIndex > 0 ? currentIndex - 1 : checkboxes.length - 1;
3055
- }
3056
-
3057
- checkboxes[nextIndex].focus();
3058
- } else if (e.key === ' ' || e.key === 'Enter') {
3059
- e.preventDefault();
3060
- // Toggle the focused checkbox
3061
- if (document.activeElement.classList.contains('ftable-multiselect-checkbox')) {
3062
- document.activeElement.click();
3063
- }
3064
- }
3065
- });
3066
-
3067
- // Handle clicks outside
3068
- dropdownOverlay.addEventListener('click', (event) => {
3069
- if (event.target === dropdownOverlay) {
3070
- closeDropdown();
3071
- }
3072
- });
3073
-
3074
- // Reposition on scroll/resize
3075
- const scrollHandler = (e) => {
3076
- if (dropdown && dropdown.contains(e.target)) {
3077
- return; // Allow scrolling inside dropdown
3078
- }
3079
- positionDropdown();
3080
- };
3081
- const repositionHandler = () => positionDropdown();
3082
- window.addEventListener('scroll', scrollHandler, true);
3083
- window.addEventListener('resize', repositionHandler);
3084
-
3085
- // Store cleanup function
3086
- container._cleanupHandlers = () => {
3087
- window.removeEventListener('scroll', scrollHandler, true);
3088
- window.removeEventListener('resize', repositionHandler);
3089
- };
3090
- }
3091
- };
3092
-
3093
- display.addEventListener('click', toggleDropdown);
3094
- toggleBtn.addEventListener('click', toggleDropdown);
3095
- display.addEventListener('keydown', (e) => {
3096
- if (e.key === 'ArrowDown' || e.key === 'Enter') {
3097
- e.preventDefault();
3098
- toggleDropdown();
3099
- }
3100
- });
3101
-
3102
- // Add reset method to container
3103
- container.resetMultiSelect = () => {
3104
- selectedValues.clear();
3105
- checkboxMap.forEach(checkbox => {
3106
- checkbox.checked = false;
3107
- });
3108
- closeDropdown();
3109
- updateDisplay();
3110
- };
3111
-
3112
- // Clean up when container is removed from DOM
3113
- const observer = new MutationObserver((mutations) => {
3114
- mutations.forEach((mutation) => {
3115
- mutation.removedNodes.forEach((node) => {
3116
- if (node === container || node.contains && node.contains(container)) {
3117
- closeDropdown();
3118
- observer.disconnect();
3119
- }
3120
- });
3121
- });
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
3122
2894
  });
3123
-
3124
- // Start observing once container is in the DOM
3125
- setTimeout(() => {
3126
- if (container.parentNode) {
3127
- observer.observe(container.parentNode, { childList: true, subtree: true });
3128
- }
3129
- }, 0);
3130
-
3131
- // Initialize
3132
- updateDisplay();
3133
-
3134
- return container;
3135
2895
  }
3136
-
3137
2896
  async createDatalistForSearch(fieldName, field) {
3138
2897
  const fieldSearchName = 'ftable-toolbarsearch-' + fieldName;
3139
2898
 
@@ -3982,9 +3741,18 @@ class FTable extends FTableEventEmitter {
3982
3741
  const searchFields = [];
3983
3742
 
3984
3743
  Object.entries(this.state.searchQueries).forEach(([fieldName, query]) => {
3985
- if (query !== '') { // Double check it's not empty
3986
- queries.push(query);
3987
- 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
+ }
3988
3756
  }
3989
3757
  });
3990
3758