@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.
- package/ftable.esm.js +357 -589
- package/ftable.js +357 -589
- package/ftable.min.js +2 -2
- package/ftable.umd.js +357 -589
- package/package.json +1 -1
- package/themes/basic/ftable_basic.css +1 -6
- package/themes/basic/ftable_basic.min.css +1 -1
- package/themes/ftable_theme_base.less +1 -7
- package/themes/lightcolor/blue/ftable.css +1 -6
- package/themes/lightcolor/blue/ftable.min.css +1 -1
- package/themes/lightcolor/gray/ftable.css +1 -6
- package/themes/lightcolor/gray/ftable.min.css +1 -1
- package/themes/lightcolor/green/ftable.css +1 -6
- package/themes/lightcolor/green/ftable.min.css +1 -1
- package/themes/lightcolor/orange/ftable.css +1 -6
- package/themes/lightcolor/orange/ftable.min.css +1 -1
- package/themes/lightcolor/red/ftable.css +1 -6
- package/themes/lightcolor/red/ftable.min.css +1 -1
- package/themes/metro/blue/ftable.css +1 -6
- package/themes/metro/blue/ftable.min.css +1 -1
- package/themes/metro/brown/ftable.css +1 -6
- package/themes/metro/brown/ftable.min.css +1 -1
- package/themes/metro/crimson/ftable.css +1 -6
- package/themes/metro/crimson/ftable.min.css +1 -1
- package/themes/metro/darkgray/ftable.css +1 -6
- package/themes/metro/darkgray/ftable.min.css +1 -1
- package/themes/metro/darkorange/ftable.css +1 -6
- package/themes/metro/darkorange/ftable.min.css +1 -1
- package/themes/metro/green/ftable.css +1 -6
- package/themes/metro/green/ftable.min.css +1 -1
- package/themes/metro/lightgray/ftable.css +1 -6
- package/themes/metro/lightgray/ftable.min.css +1 -1
- package/themes/metro/pink/ftable.css +1 -6
- package/themes/metro/pink/ftable.min.css +1 -1
- package/themes/metro/purple/ftable.css +1 -6
- package/themes/metro/purple/ftable.min.css +1 -1
- package/themes/metro/red/ftable.css +1 -6
- package/themes/metro/red/ftable.min.css +1 -1
package/ftable.js
CHANGED
|
@@ -322,15 +322,13 @@ class FTableHttpClient {
|
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
if (Array.isArray(value)) {
|
|
325
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
1418
|
-
attributes: { 'data-field-name':
|
|
1497
|
+
className: containerClasses,
|
|
1498
|
+
attributes: { 'data-field-name': containerDataFieldName }
|
|
1419
1499
|
});
|
|
1420
1500
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
1463
|
-
let dropdown
|
|
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
|
-
//
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
1479
|
-
selectedDisplay.appendChild(
|
|
1577
|
+
placeholderEl.textContent = placeholderText;
|
|
1578
|
+
selectedDisplay.appendChild(placeholderEl);
|
|
1480
1579
|
} else {
|
|
1481
|
-
|
|
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
|
|
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
|
-
|
|
1519
|
-
|
|
1520
|
-
if (checkbox) {
|
|
1521
|
-
checkbox.checked = false;
|
|
1522
|
-
}
|
|
1598
|
+
const cb = checkboxMap.get(val);
|
|
1599
|
+
if (cb) cb.checked = false;
|
|
1523
1600
|
updateDisplay();
|
|
1524
|
-
|
|
1601
|
+
if (onChangeExtra) onChangeExtra(hiddenSelect);
|
|
1525
1602
|
});
|
|
1526
1603
|
});
|
|
1527
1604
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1605
|
+
|
|
1606
|
+
if (onChangeExtra) onChangeExtra(hiddenSelect);
|
|
1530
1607
|
};
|
|
1531
1608
|
|
|
1532
|
-
//
|
|
1609
|
+
// ---------- Dropdown helpers ----------
|
|
1533
1610
|
const closeDropdown = () => {
|
|
1534
|
-
display.focus();
|
|
1535
|
-
if (dropdown)
|
|
1536
|
-
|
|
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
|
|
1554
|
-
const
|
|
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
|
|
1559
|
-
|
|
1560
|
-
dropdown.style
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
const
|
|
1571
|
-
if (
|
|
1572
|
-
left = Math.max(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
|
-
//
|
|
1578
|
-
const
|
|
1579
|
-
if (!
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
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
|
-
|
|
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(
|
|
1621
|
-
selectedValues.delete(
|
|
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(
|
|
1702
|
+
selectedValues.add(key);
|
|
1625
1703
|
checkbox.checked = true;
|
|
1626
1704
|
}
|
|
1627
|
-
|
|
1628
1705
|
updateDisplay();
|
|
1629
1706
|
});
|
|
1630
1707
|
});
|
|
1631
1708
|
};
|
|
1632
1709
|
|
|
1633
|
-
//
|
|
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
|
-
|
|
1641
|
-
|
|
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
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
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
|
-
|
|
1663
|
-
|
|
1723
|
+
dropdownOverlay = FTableDOMHelper.create('div', {
|
|
1724
|
+
className: 'ftable-multiselect-overlay',
|
|
1725
|
+
parent: document.body
|
|
1726
|
+
});
|
|
1664
1727
|
|
|
1665
|
-
|
|
1666
|
-
|
|
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
|
-
|
|
1669
|
-
|
|
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
|
-
|
|
1672
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
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
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1735
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
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
|
|
3987
|
-
|
|
3988
|
-
|
|
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
|
|