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