@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.esm.js
CHANGED
|
@@ -321,15 +321,13 @@ class FTableHttpClient {
|
|
|
321
321
|
}
|
|
322
322
|
|
|
323
323
|
if (Array.isArray(value)) {
|
|
324
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
1417
|
-
attributes: { 'data-field-name':
|
|
1496
|
+
className: containerClasses,
|
|
1497
|
+
attributes: { 'data-field-name': containerDataFieldName }
|
|
1418
1498
|
});
|
|
1419
1499
|
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
1462
|
-
let dropdown
|
|
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
|
-
//
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
1478
|
-
selectedDisplay.appendChild(
|
|
1576
|
+
placeholderEl.textContent = placeholderText;
|
|
1577
|
+
selectedDisplay.appendChild(placeholderEl);
|
|
1479
1578
|
} else {
|
|
1480
|
-
|
|
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
|
|
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
|
-
|
|
1518
|
-
|
|
1519
|
-
if (checkbox) {
|
|
1520
|
-
checkbox.checked = false;
|
|
1521
|
-
}
|
|
1597
|
+
const cb = checkboxMap.get(val);
|
|
1598
|
+
if (cb) cb.checked = false;
|
|
1522
1599
|
updateDisplay();
|
|
1523
|
-
|
|
1600
|
+
if (onChangeExtra) onChangeExtra(hiddenSelect);
|
|
1524
1601
|
});
|
|
1525
1602
|
});
|
|
1526
1603
|
}
|
|
1527
|
-
|
|
1528
|
-
|
|
1604
|
+
|
|
1605
|
+
if (onChangeExtra) onChangeExtra(hiddenSelect);
|
|
1529
1606
|
};
|
|
1530
1607
|
|
|
1531
|
-
//
|
|
1608
|
+
// ---------- Dropdown helpers ----------
|
|
1532
1609
|
const closeDropdown = () => {
|
|
1533
|
-
display.focus();
|
|
1534
|
-
if (dropdown)
|
|
1535
|
-
|
|
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
|
|
1553
|
-
const
|
|
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
|
|
1558
|
-
|
|
1559
|
-
dropdown.style
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
const
|
|
1570
|
-
if (
|
|
1571
|
-
left = Math.max(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
|
-
//
|
|
1577
|
-
const
|
|
1578
|
-
if (!
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
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
|
-
|
|
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(
|
|
1620
|
-
selectedValues.delete(
|
|
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(
|
|
1701
|
+
selectedValues.add(key);
|
|
1624
1702
|
checkbox.checked = true;
|
|
1625
1703
|
}
|
|
1626
|
-
|
|
1627
1704
|
updateDisplay();
|
|
1628
1705
|
});
|
|
1629
1706
|
});
|
|
1630
1707
|
};
|
|
1631
1708
|
|
|
1632
|
-
//
|
|
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
|
-
|
|
1640
|
-
|
|
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
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
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
|
-
|
|
1662
|
-
|
|
1722
|
+
dropdownOverlay = FTableDOMHelper.create('div', {
|
|
1723
|
+
className: 'ftable-multiselect-overlay',
|
|
1724
|
+
parent: document.body
|
|
1725
|
+
});
|
|
1663
1726
|
|
|
1664
|
-
|
|
1665
|
-
|
|
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
|
-
|
|
1668
|
-
|
|
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
|
-
|
|
1671
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
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
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1734
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
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
|
|
3986
|
-
|
|
3987
|
-
|
|
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
|
|