@liedekef/ftable 1.1.52 → 1.3.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 +458 -625
- package/ftable.js +458 -625
- package/ftable.min.js +2 -2
- package/ftable.umd.js +458 -625
- package/package.json +1 -1
- package/themes/basic/ftable_basic.css +0 -4
- package/themes/basic/ftable_basic.min.css +1 -1
- package/themes/ftable_theme_base.less +0 -5
- package/themes/lightcolor/blue/ftable.css +0 -4
- package/themes/lightcolor/blue/ftable.min.css +1 -1
- package/themes/lightcolor/gray/ftable.css +0 -4
- package/themes/lightcolor/gray/ftable.min.css +1 -1
- package/themes/lightcolor/green/ftable.css +0 -4
- package/themes/lightcolor/green/ftable.min.css +1 -1
- package/themes/lightcolor/orange/ftable.css +0 -4
- package/themes/lightcolor/orange/ftable.min.css +1 -1
- package/themes/lightcolor/red/ftable.css +0 -4
- package/themes/lightcolor/red/ftable.min.css +1 -1
- package/themes/metro/blue/ftable.css +0 -4
- package/themes/metro/blue/ftable.min.css +1 -1
- package/themes/metro/brown/ftable.css +0 -4
- package/themes/metro/brown/ftable.min.css +1 -1
- package/themes/metro/crimson/ftable.css +0 -4
- package/themes/metro/crimson/ftable.min.css +1 -1
- package/themes/metro/darkgray/ftable.css +0 -4
- package/themes/metro/darkgray/ftable.min.css +1 -1
- package/themes/metro/darkorange/ftable.css +0 -4
- package/themes/metro/darkorange/ftable.min.css +1 -1
- package/themes/metro/green/ftable.css +0 -4
- package/themes/metro/green/ftable.min.css +1 -1
- package/themes/metro/lightgray/ftable.css +0 -4
- package/themes/metro/lightgray/ftable.min.css +1 -1
- package/themes/metro/pink/ftable.css +0 -4
- package/themes/metro/pink/ftable.min.css +1 -1
- package/themes/metro/purple/ftable.css +0 -4
- package/themes/metro/purple/ftable.min.css +1 -1
- package/themes/metro/red/ftable.css +0 -4
- 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,108 @@ 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 ?? false;
|
|
1418
|
+
|
|
1419
|
+
return this._buildCustomMultiSelect({
|
|
1420
|
+
containerId: fieldName,
|
|
1421
|
+
hiddenSelectId: `Edit-${fieldName}`,
|
|
1422
|
+
hiddenSelectName: name,
|
|
1423
|
+
extraClasses: '',
|
|
1424
|
+
containerDataFieldName: fieldName,
|
|
1425
|
+
hiddenSelectAttributes: {},
|
|
1426
|
+
optionsSource,
|
|
1427
|
+
initialValues: Array.isArray(value) ? value :
|
|
1428
|
+
(value ? value.toString().split(',').filter(v => v) : []),
|
|
1429
|
+
placeholderText: field.placeholder || this.options.messages?.multiSelectPlaceholder || 'Click to select options...',
|
|
1430
|
+
livesearch,
|
|
1431
|
+
onChangeExtra: (hiddenSelect) => {
|
|
1432
|
+
hiddenSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1433
|
+
},
|
|
1434
|
+
buildHiddenSelectOnUpdate: true, // form-mode: rebuild hidden options each time
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* Shared builder for both form-mode and search-mode custom multi-selects.
|
|
1440
|
+
*
|
|
1441
|
+
* config {
|
|
1442
|
+
* containerId string – used for data-field-name on the outer container
|
|
1443
|
+
* hiddenSelectId string – id attribute of the hidden <select>
|
|
1444
|
+
* hiddenSelectName string – name attribute of the hidden <select>
|
|
1445
|
+
* extraClasses string – extra CSS classes on the container (space-separated)
|
|
1446
|
+
* containerDataFieldName string – value for container's data-field-name
|
|
1447
|
+
* hiddenSelectAttributes object – extra attributes for the hidden <select>
|
|
1448
|
+
* optionsSource array – resolved array of option objects: { Value, DisplayText, Group? }
|
|
1449
|
+
* initialValues array – pre-selected values
|
|
1450
|
+
* placeholderText string
|
|
1451
|
+
* livesearch bool – show filter input inside dropdown
|
|
1452
|
+
* onChangeExtra fn(hiddenSelect) – called after every selection change
|
|
1453
|
+
* buildHiddenSelectOnUpdate bool – if true, rebuild hidden <option>s in updateDisplay (form mode);
|
|
1454
|
+
* if false, all options are pre-populated (search mode)
|
|
1455
|
+
* }
|
|
1456
|
+
*/
|
|
1457
|
+
_buildCustomMultiSelect(config) {
|
|
1458
|
+
const {
|
|
1459
|
+
hiddenSelectId,
|
|
1460
|
+
hiddenSelectName,
|
|
1461
|
+
extraClasses,
|
|
1462
|
+
containerDataFieldName,
|
|
1463
|
+
hiddenSelectAttributes,
|
|
1464
|
+
optionsSource,
|
|
1465
|
+
initialValues,
|
|
1466
|
+
placeholderText,
|
|
1467
|
+
livesearch,
|
|
1468
|
+
onChangeExtra,
|
|
1469
|
+
buildHiddenSelectOnUpdate,
|
|
1470
|
+
} = config;
|
|
1471
|
+
|
|
1472
|
+
// Normalise options to a flat array of { optValue, optText, groupLabel }
|
|
1473
|
+
const normaliseOptions = (src) => {
|
|
1474
|
+
if (!src) return [];
|
|
1475
|
+
const arr = Array.isArray(src) ? src :
|
|
1476
|
+
Object.entries(src).map(([k, v]) => ({ Value: k, DisplayText: v }));
|
|
1477
|
+
return arr
|
|
1478
|
+
.map(o => ({
|
|
1479
|
+
optValue: (o.Value !== undefined ? o.Value :
|
|
1480
|
+
o.value !== undefined ? o.value : o),
|
|
1481
|
+
optText: o.DisplayText || o.text || o,
|
|
1482
|
+
groupLabel: o.Group || o.group || null,
|
|
1483
|
+
}))
|
|
1484
|
+
.filter(o => o.optValue != null && o.optValue !== '');
|
|
1485
|
+
};
|
|
1486
|
+
|
|
1487
|
+
const allOptions = normaliseOptions(optionsSource);
|
|
1488
|
+
|
|
1489
|
+
// Build a value→text lookup (used by updateDisplay)
|
|
1490
|
+
const optionsMap = new Map(allOptions.map(o => [o.optValue.toString(), o.optText]));
|
|
1491
|
+
|
|
1492
|
+
// ---------- DOM skeleton ----------
|
|
1493
|
+
const containerClasses = ['ftable-multiselect-container', ...extraClasses.split(' ').filter(Boolean)].join(' ');
|
|
1416
1494
|
const container = FTableDOMHelper.create('div', {
|
|
1417
|
-
className:
|
|
1418
|
-
attributes: { 'data-field-name':
|
|
1495
|
+
className: containerClasses,
|
|
1496
|
+
attributes: { 'data-field-name': containerDataFieldName }
|
|
1419
1497
|
});
|
|
1420
1498
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1499
|
+
const hiddenSelect = FTableDOMHelper.create('select', {
|
|
1500
|
+
id: hiddenSelectId,
|
|
1501
|
+
name: hiddenSelectName,
|
|
1502
|
+
multiple: true,
|
|
1503
|
+
style: 'display: none;',
|
|
1504
|
+
attributes: hiddenSelectAttributes
|
|
1427
1505
|
});
|
|
1428
|
-
container.appendChild(
|
|
1506
|
+
container.appendChild(hiddenSelect);
|
|
1507
|
+
container.hiddenSelect = hiddenSelect;
|
|
1429
1508
|
|
|
1430
|
-
// Create display area
|
|
1431
1509
|
const display = FTableDOMHelper.create('div', {
|
|
1432
1510
|
className: 'ftable-multiselect-display',
|
|
1433
1511
|
parent: container,
|
|
1434
|
-
attributes: {
|
|
1435
|
-
tabindex: '0' // Makes it focusable and in tab order
|
|
1436
|
-
}
|
|
1512
|
+
attributes: { tabindex: '0' }
|
|
1437
1513
|
});
|
|
1438
1514
|
|
|
1439
1515
|
const selectedDisplay = FTableDOMHelper.create('div', {
|
|
@@ -1441,294 +1517,288 @@ class FTableFormBuilder {
|
|
|
1441
1517
|
parent: display
|
|
1442
1518
|
});
|
|
1443
1519
|
|
|
1444
|
-
const
|
|
1445
|
-
const placeholder = FTableDOMHelper.create('span', {
|
|
1520
|
+
const placeholderEl = FTableDOMHelper.create('span', {
|
|
1446
1521
|
className: 'ftable-multiselect-placeholder',
|
|
1447
1522
|
textContent: placeholderText,
|
|
1448
1523
|
parent: selectedDisplay
|
|
1449
1524
|
});
|
|
1450
1525
|
|
|
1451
|
-
|
|
1452
|
-
const toggleBtn = FTableDOMHelper.create('button', {
|
|
1526
|
+
FTableDOMHelper.create('button', {
|
|
1453
1527
|
type: 'button',
|
|
1454
1528
|
className: 'ftable-multiselect-toggle',
|
|
1455
1529
|
innerHTML: '▼',
|
|
1456
1530
|
parent: display,
|
|
1457
|
-
attributes: {
|
|
1458
|
-
tabindex: '-1' // this skips regular focus when tabbing
|
|
1459
|
-
}
|
|
1531
|
+
attributes: { tabindex: '-1' }
|
|
1460
1532
|
});
|
|
1461
1533
|
|
|
1462
|
-
//
|
|
1463
|
-
let dropdown
|
|
1534
|
+
// ---------- State ----------
|
|
1535
|
+
let dropdown = null;
|
|
1464
1536
|
let dropdownOverlay = null;
|
|
1537
|
+
const selectedValues = new Set(initialValues.map(v => v.toString()));
|
|
1538
|
+
const checkboxMap = new Map(); // value → checkbox element
|
|
1465
1539
|
|
|
1466
|
-
//
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1540
|
+
// In search mode, pre-populate the hidden select once with all options
|
|
1541
|
+
if (!buildHiddenSelectOnUpdate) {
|
|
1542
|
+
allOptions.forEach(({ optValue, optText }) => {
|
|
1543
|
+
FTableDOMHelper.create('option', {
|
|
1544
|
+
value: optValue,
|
|
1545
|
+
textContent: optText,
|
|
1546
|
+
parent: hiddenSelect
|
|
1547
|
+
});
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1472
1550
|
|
|
1473
|
-
//
|
|
1551
|
+
// ---------- updateDisplay ----------
|
|
1474
1552
|
const updateDisplay = () => {
|
|
1475
1553
|
selectedDisplay.innerHTML = '';
|
|
1476
|
-
|
|
1554
|
+
|
|
1555
|
+
if (buildHiddenSelectOnUpdate) {
|
|
1556
|
+
// Form mode: rebuild hidden <option>s to reflect current selection
|
|
1557
|
+
hiddenSelect.innerHTML = '';
|
|
1558
|
+
selectedValues.forEach(val => {
|
|
1559
|
+
const text = optionsMap.get(val) ?? val;
|
|
1560
|
+
FTableDOMHelper.create('option', {
|
|
1561
|
+
value: val,
|
|
1562
|
+
textContent: text,
|
|
1563
|
+
selected: true,
|
|
1564
|
+
parent: hiddenSelect
|
|
1565
|
+
});
|
|
1566
|
+
});
|
|
1567
|
+
} else {
|
|
1568
|
+
// Search mode: just flip selected state on existing options
|
|
1569
|
+
Array.from(hiddenSelect.options).forEach(opt => {
|
|
1570
|
+
opt.selected = selectedValues.has(opt.value);
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1477
1574
|
if (selectedValues.size === 0) {
|
|
1478
|
-
|
|
1479
|
-
selectedDisplay.appendChild(
|
|
1575
|
+
placeholderEl.textContent = placeholderText;
|
|
1576
|
+
selectedDisplay.appendChild(placeholderEl);
|
|
1480
1577
|
} 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 => {
|
|
1578
|
+
selectedValues.forEach(val => {
|
|
1498
1579
|
const tag = FTableDOMHelper.create('span', {
|
|
1499
1580
|
className: 'ftable-multiselect-tag',
|
|
1500
1581
|
parent: selectedDisplay
|
|
1501
1582
|
});
|
|
1502
|
-
|
|
1503
1583
|
FTableDOMHelper.create('span', {
|
|
1504
1584
|
className: 'ftable-multiselect-tag-text',
|
|
1505
|
-
textContent: optionsMap.get(val
|
|
1585
|
+
textContent: optionsMap.get(val) || val,
|
|
1506
1586
|
parent: tag
|
|
1507
1587
|
});
|
|
1508
|
-
|
|
1509
1588
|
const removeBtn = FTableDOMHelper.create('span', {
|
|
1510
1589
|
className: 'ftable-multiselect-tag-remove',
|
|
1511
1590
|
innerHTML: '×',
|
|
1512
1591
|
parent: tag
|
|
1513
1592
|
});
|
|
1514
|
-
|
|
1515
1593
|
removeBtn.addEventListener('click', (e) => {
|
|
1516
1594
|
e.stopPropagation();
|
|
1517
1595
|
selectedValues.delete(val);
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
if (checkbox) {
|
|
1521
|
-
checkbox.checked = false;
|
|
1522
|
-
}
|
|
1596
|
+
const cb = checkboxMap.get(val);
|
|
1597
|
+
if (cb) cb.checked = false;
|
|
1523
1598
|
updateDisplay();
|
|
1524
|
-
|
|
1599
|
+
if (onChangeExtra) onChangeExtra(hiddenSelect);
|
|
1525
1600
|
});
|
|
1526
1601
|
});
|
|
1527
1602
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1603
|
+
|
|
1604
|
+
if (onChangeExtra) onChangeExtra(hiddenSelect);
|
|
1530
1605
|
};
|
|
1531
1606
|
|
|
1532
|
-
//
|
|
1607
|
+
// ---------- Dropdown helpers ----------
|
|
1533
1608
|
const closeDropdown = () => {
|
|
1534
|
-
display.focus();
|
|
1535
|
-
if (dropdown)
|
|
1536
|
-
|
|
1537
|
-
dropdown = null;
|
|
1538
|
-
}
|
|
1539
|
-
if (dropdownOverlay) {
|
|
1540
|
-
dropdownOverlay.remove();
|
|
1541
|
-
dropdownOverlay = null;
|
|
1542
|
-
}
|
|
1609
|
+
display.focus();
|
|
1610
|
+
if (dropdown) { dropdown.remove(); dropdown = null; }
|
|
1611
|
+
if (dropdownOverlay) { dropdownOverlay.remove(); dropdownOverlay = null; }
|
|
1543
1612
|
if (container._cleanupHandlers) {
|
|
1544
1613
|
container._cleanupHandlers();
|
|
1545
1614
|
container._cleanupHandlers = null;
|
|
1546
1615
|
}
|
|
1547
1616
|
};
|
|
1548
1617
|
|
|
1549
|
-
// Function to position dropdown
|
|
1550
1618
|
const positionDropdown = () => {
|
|
1551
1619
|
if (!dropdown) return;
|
|
1552
|
-
|
|
1553
|
-
const
|
|
1554
|
-
const
|
|
1555
|
-
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
|
|
1556
|
-
|
|
1620
|
+
const rect = display.getBoundingClientRect();
|
|
1621
|
+
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
1622
|
+
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
|
|
1557
1623
|
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,
|
|
1624
|
+
let top = rect.bottom + scrollTop + 4;
|
|
1625
|
+
|
|
1626
|
+
Object.assign(dropdown.style, {
|
|
1627
|
+
position: 'absolute',
|
|
1628
|
+
left: `${left}px`,
|
|
1629
|
+
top: `${top}px`,
|
|
1630
|
+
width: `${rect.width}px`,
|
|
1631
|
+
minWidth: 'fit-content',
|
|
1632
|
+
boxSizing: 'border-box',
|
|
1633
|
+
zIndex: '10000',
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1636
|
+
const ddRect = dropdown.getBoundingClientRect();
|
|
1637
|
+
if (ddRect.right > window.innerWidth) {
|
|
1638
|
+
left = Math.max(10, window.innerWidth - ddRect.width - 10);
|
|
1573
1639
|
dropdown.style.left = `${left}px`;
|
|
1574
1640
|
}
|
|
1575
1641
|
};
|
|
1576
1642
|
|
|
1577
|
-
//
|
|
1578
|
-
const
|
|
1579
|
-
if (!
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1643
|
+
// Render options (or a filtered subset) into the open dropdown
|
|
1644
|
+
const renderDropdownOptions = (filterText = '') => {
|
|
1645
|
+
if (!dropdown) return;
|
|
1646
|
+
// Remove existing option rows (but keep the search bar if present)
|
|
1647
|
+
Array.from(dropdown.querySelectorAll('.ftable-multiselect-option, .ftable-multiselect-optgroup'))
|
|
1648
|
+
.forEach(el => el.remove());
|
|
1649
|
+
|
|
1650
|
+
const lc = filterText.toLowerCase();
|
|
1651
|
+
const visible = filterText
|
|
1652
|
+
? allOptions.filter(o => o.optText.toLowerCase().includes(lc))
|
|
1653
|
+
: allOptions;
|
|
1654
|
+
|
|
1655
|
+
// Group rendering
|
|
1656
|
+
const usedGroups = new Set();
|
|
1657
|
+
let currentOptgroup = null;
|
|
1658
|
+
|
|
1659
|
+
visible.forEach(({ optValue, optText, groupLabel }) => {
|
|
1660
|
+
if (groupLabel && groupLabel !== usedGroups[usedGroups.size - 1]) {
|
|
1661
|
+
if (!usedGroups.has(groupLabel)) {
|
|
1662
|
+
usedGroups.add(groupLabel);
|
|
1663
|
+
currentOptgroup = FTableDOMHelper.create('div', {
|
|
1664
|
+
className: 'ftable-multiselect-optgroup',
|
|
1665
|
+
textContent: groupLabel,
|
|
1666
|
+
parent: dropdown
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
} else if (!groupLabel) {
|
|
1670
|
+
currentOptgroup = null;
|
|
1591
1671
|
}
|
|
1592
1672
|
|
|
1593
|
-
const optText = option.DisplayText || option.text || option;
|
|
1594
|
-
|
|
1595
1673
|
const optionDiv = FTableDOMHelper.create('div', {
|
|
1596
1674
|
className: 'ftable-multiselect-option',
|
|
1597
1675
|
parent: dropdown
|
|
1598
1676
|
});
|
|
1599
|
-
|
|
1677
|
+
|
|
1600
1678
|
const checkbox = FTableDOMHelper.create('input', {
|
|
1601
1679
|
type: 'checkbox',
|
|
1602
1680
|
className: 'ftable-multiselect-checkbox',
|
|
1603
1681
|
checked: selectedValues.has(optValue.toString()),
|
|
1604
1682
|
parent: optionDiv
|
|
1605
1683
|
});
|
|
1606
|
-
|
|
1607
|
-
// Store checkbox reference
|
|
1684
|
+
|
|
1608
1685
|
checkboxMap.set(optValue.toString(), checkbox);
|
|
1609
|
-
|
|
1610
|
-
|
|
1686
|
+
|
|
1687
|
+
FTableDOMHelper.create('label', {
|
|
1611
1688
|
className: 'ftable-multiselect-label',
|
|
1612
1689
|
textContent: optText,
|
|
1613
1690
|
parent: optionDiv
|
|
1614
1691
|
});
|
|
1615
|
-
|
|
1616
|
-
// Click anywhere on the option to toggle
|
|
1692
|
+
|
|
1617
1693
|
optionDiv.addEventListener('click', (e) => {
|
|
1618
1694
|
e.stopPropagation();
|
|
1619
|
-
|
|
1620
|
-
if (selectedValues.has(
|
|
1621
|
-
selectedValues.delete(
|
|
1695
|
+
const key = optValue.toString();
|
|
1696
|
+
if (selectedValues.has(key)) {
|
|
1697
|
+
selectedValues.delete(key);
|
|
1622
1698
|
checkbox.checked = false;
|
|
1623
1699
|
} else {
|
|
1624
|
-
selectedValues.add(
|
|
1700
|
+
selectedValues.add(key);
|
|
1625
1701
|
checkbox.checked = true;
|
|
1626
1702
|
}
|
|
1627
|
-
|
|
1628
1703
|
updateDisplay();
|
|
1629
1704
|
});
|
|
1630
1705
|
});
|
|
1631
1706
|
};
|
|
1632
1707
|
|
|
1633
|
-
//
|
|
1708
|
+
// ---------- toggleDropdown ----------
|
|
1634
1709
|
const toggleDropdown = (e) => {
|
|
1635
1710
|
if (e) e.stopPropagation();
|
|
1636
|
-
|
|
1711
|
+
|
|
1637
1712
|
if (dropdown) {
|
|
1638
|
-
// Dropdown is open, close it
|
|
1639
1713
|
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
|
-
});
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1650
1716
|
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
parent: document.body,
|
|
1655
|
-
attributes: {
|
|
1656
|
-
tabindex: '-1',
|
|
1657
|
-
role: 'listbox',
|
|
1658
|
-
'aria-multiselectable': 'true'
|
|
1659
|
-
}
|
|
1660
|
-
});
|
|
1717
|
+
// Close any other open dropdowns
|
|
1718
|
+
document.querySelectorAll('.ftable-multiselect-dropdown').forEach(dd => dd.remove());
|
|
1719
|
+
document.querySelectorAll('.ftable-multiselect-overlay').forEach(ov => ov.remove());
|
|
1661
1720
|
|
|
1662
|
-
|
|
1663
|
-
|
|
1721
|
+
dropdownOverlay = FTableDOMHelper.create('div', {
|
|
1722
|
+
className: 'ftable-multiselect-overlay',
|
|
1723
|
+
parent: document.body
|
|
1724
|
+
});
|
|
1664
1725
|
|
|
1665
|
-
|
|
1666
|
-
|
|
1726
|
+
dropdown = FTableDOMHelper.create('div', {
|
|
1727
|
+
className: 'ftable-multiselect-dropdown',
|
|
1728
|
+
parent: document.body,
|
|
1729
|
+
attributes: { tabindex: '-1', role: 'listbox', 'aria-multiselectable': 'true' }
|
|
1730
|
+
});
|
|
1667
1731
|
|
|
1668
|
-
|
|
1669
|
-
|
|
1732
|
+
// Optional live-search bar
|
|
1733
|
+
if (livesearch) {
|
|
1734
|
+
const searchWrap = FTableDOMHelper.create('div', {
|
|
1735
|
+
className: 'ftable-multiselect-livesearch-wrap',
|
|
1736
|
+
parent: dropdown
|
|
1737
|
+
});
|
|
1738
|
+
const searchInput = FTableDOMHelper.create('input', {
|
|
1739
|
+
type: 'search',
|
|
1740
|
+
className: 'ftable-multiselect-livesearch',
|
|
1741
|
+
placeholder: 'Search...',
|
|
1742
|
+
parent: searchWrap,
|
|
1743
|
+
attributes: { autocomplete: 'off' }
|
|
1744
|
+
});
|
|
1745
|
+
searchInput.addEventListener('input', () => {
|
|
1746
|
+
renderDropdownOptions(searchInput.value);
|
|
1747
|
+
});
|
|
1748
|
+
searchInput.addEventListener('click', e => e.stopPropagation());
|
|
1749
|
+
// Focus search input automatically
|
|
1750
|
+
setTimeout(() => searchInput.focus(), 0);
|
|
1751
|
+
}
|
|
1670
1752
|
|
|
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
|
-
}
|
|
1753
|
+
renderDropdownOptions();
|
|
1754
|
+
positionDropdown();
|
|
1688
1755
|
|
|
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
|
-
});
|
|
1756
|
+
if (!livesearch) dropdown.focus();
|
|
1698
1757
|
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1758
|
+
// Keyboard navigation
|
|
1759
|
+
dropdown.addEventListener('keydown', (e) => {
|
|
1760
|
+
if (e.key === 'Escape') {
|
|
1761
|
+
closeDropdown();
|
|
1762
|
+
} else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
1763
|
+
e.preventDefault();
|
|
1764
|
+
const checkboxes = Array.from(dropdown.querySelectorAll('.ftable-multiselect-checkbox'));
|
|
1765
|
+
const currentIndex = checkboxes.indexOf(document.activeElement);
|
|
1766
|
+
const nextIndex = e.key === 'ArrowDown'
|
|
1767
|
+
? (currentIndex < checkboxes.length - 1 ? currentIndex + 1 : 0)
|
|
1768
|
+
: (currentIndex > 0 ? currentIndex - 1 : checkboxes.length - 1);
|
|
1769
|
+
checkboxes[nextIndex]?.focus();
|
|
1770
|
+
} else if (e.key === ' ' || e.key === 'Enter') {
|
|
1771
|
+
e.preventDefault();
|
|
1772
|
+
if (document.activeElement.classList.contains('ftable-multiselect-checkbox')) {
|
|
1773
|
+
document.activeElement.click();
|
|
1703
1774
|
}
|
|
1704
|
-
}
|
|
1775
|
+
}
|
|
1776
|
+
});
|
|
1705
1777
|
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
};
|
|
1727
|
-
}
|
|
1778
|
+
dropdownOverlay.addEventListener('click', (event) => {
|
|
1779
|
+
if (event.target === dropdownOverlay) closeDropdown();
|
|
1780
|
+
});
|
|
1781
|
+
|
|
1782
|
+
// Reposition on scroll / resize
|
|
1783
|
+
const repositionHandler = () => positionDropdown();
|
|
1784
|
+
const scrollHandler = (e) => {
|
|
1785
|
+
if (dropdown && dropdown.contains(e.target)) return;
|
|
1786
|
+
positionDropdown();
|
|
1787
|
+
};
|
|
1788
|
+
const resizeObserver = new ResizeObserver(() => positionDropdown());
|
|
1789
|
+
window.addEventListener('scroll', scrollHandler, true);
|
|
1790
|
+
window.addEventListener('resize', repositionHandler);
|
|
1791
|
+
resizeObserver.observe(selectedDisplay);
|
|
1792
|
+
|
|
1793
|
+
container._cleanupHandlers = () => {
|
|
1794
|
+
window.removeEventListener('scroll', scrollHandler, true);
|
|
1795
|
+
window.removeEventListener('resize', repositionHandler);
|
|
1796
|
+
resizeObserver.disconnect();
|
|
1797
|
+
};
|
|
1728
1798
|
};
|
|
1729
1799
|
|
|
1730
1800
|
display.addEventListener('click', toggleDropdown);
|
|
1731
|
-
|
|
1801
|
+
display.querySelector('.ftable-multiselect-toggle').addEventListener('click', toggleDropdown);
|
|
1732
1802
|
display.addEventListener('keydown', (e) => {
|
|
1733
1803
|
if (e.key === 'ArrowDown' || e.key === 'Enter') {
|
|
1734
1804
|
e.preventDefault();
|
|
@@ -1736,29 +1806,32 @@ class FTableFormBuilder {
|
|
|
1736
1806
|
}
|
|
1737
1807
|
});
|
|
1738
1808
|
|
|
1739
|
-
//
|
|
1740
|
-
|
|
1809
|
+
// Reset method (used by search toolbar)
|
|
1810
|
+
container.resetMultiSelect = () => {
|
|
1811
|
+
selectedValues.clear();
|
|
1812
|
+
checkboxMap.forEach(cb => { cb.checked = false; });
|
|
1813
|
+
closeDropdown();
|
|
1814
|
+
updateDisplay();
|
|
1815
|
+
};
|
|
1816
|
+
|
|
1817
|
+
// Cleanup when container is removed from DOM
|
|
1818
|
+
const domObserver = new MutationObserver((mutations) => {
|
|
1741
1819
|
mutations.forEach((mutation) => {
|
|
1742
1820
|
mutation.removedNodes.forEach((node) => {
|
|
1743
|
-
if (node === container || node.contains && node.contains(container)) {
|
|
1821
|
+
if (node === container || (node.contains && node.contains(container))) {
|
|
1744
1822
|
closeDropdown();
|
|
1745
|
-
|
|
1823
|
+
domObserver.disconnect();
|
|
1746
1824
|
}
|
|
1747
1825
|
});
|
|
1748
1826
|
});
|
|
1749
1827
|
});
|
|
1750
|
-
|
|
1751
|
-
// Start observing once container is in the DOM
|
|
1752
1828
|
setTimeout(() => {
|
|
1753
1829
|
if (container.parentNode) {
|
|
1754
|
-
|
|
1830
|
+
domObserver.observe(container.parentNode, { childList: true, subtree: true });
|
|
1755
1831
|
}
|
|
1756
1832
|
}, 0);
|
|
1757
1833
|
|
|
1758
|
-
// Initialize
|
|
1759
|
-
populateOptions();
|
|
1760
1834
|
updateDisplay();
|
|
1761
|
-
|
|
1762
1835
|
return container;
|
|
1763
1836
|
}
|
|
1764
1837
|
|
|
@@ -1868,7 +1941,21 @@ class FTableFormBuilder {
|
|
|
1868
1941
|
select.innerHTML = ''; // Clear existing options
|
|
1869
1942
|
|
|
1870
1943
|
if (Array.isArray(options)) {
|
|
1944
|
+
// Group options by their Group property (if any)
|
|
1945
|
+
const groups = new Map(); // groupLabel -> [options]
|
|
1946
|
+
const ungrouped = [];
|
|
1947
|
+
|
|
1871
1948
|
options.forEach(option => {
|
|
1949
|
+
const groupLabel = option.Group || option.group || null;
|
|
1950
|
+
if (groupLabel) {
|
|
1951
|
+
if (!groups.has(groupLabel)) groups.set(groupLabel, []);
|
|
1952
|
+
groups.get(groupLabel).push(option);
|
|
1953
|
+
} else {
|
|
1954
|
+
ungrouped.push(option);
|
|
1955
|
+
}
|
|
1956
|
+
});
|
|
1957
|
+
|
|
1958
|
+
const appendOption = (option, parent) => {
|
|
1872
1959
|
const value = option.Value !== undefined ? option.Value :
|
|
1873
1960
|
option.value !== undefined ? option.value :
|
|
1874
1961
|
option; // fallback for string
|
|
@@ -1876,19 +1963,30 @@ class FTableFormBuilder {
|
|
|
1876
1963
|
value: value,
|
|
1877
1964
|
textContent: option.DisplayText || option.text || option,
|
|
1878
1965
|
selected: value == selectedValue,
|
|
1879
|
-
parent:
|
|
1966
|
+
parent: parent
|
|
1880
1967
|
});
|
|
1881
|
-
|
|
1882
1968
|
if (option.Data && typeof option.Data === 'object') {
|
|
1883
1969
|
Object.entries(option.Data).forEach(([key, dataValue]) => {
|
|
1884
1970
|
optionElement.setAttribute(`data-${key}`, dataValue);
|
|
1885
1971
|
});
|
|
1886
1972
|
}
|
|
1973
|
+
};
|
|
1974
|
+
|
|
1975
|
+
// Render ungrouped options first
|
|
1976
|
+
ungrouped.forEach(option => appendOption(option, select));
|
|
1887
1977
|
|
|
1978
|
+
// Render grouped options inside <optgroup> elements
|
|
1979
|
+
groups.forEach((groupOptions, label) => {
|
|
1980
|
+
const optgroup = FTableDOMHelper.create('optgroup', {
|
|
1981
|
+
attributes: { label },
|
|
1982
|
+
parent: select
|
|
1983
|
+
});
|
|
1984
|
+
groupOptions.forEach(option => appendOption(option, optgroup));
|
|
1888
1985
|
});
|
|
1986
|
+
|
|
1889
1987
|
} else if (typeof options === 'object') {
|
|
1890
1988
|
Object.entries(options).forEach(([key, text]) => {
|
|
1891
|
-
|
|
1989
|
+
FTableDOMHelper.create('option', {
|
|
1892
1990
|
value: key,
|
|
1893
1991
|
textContent: text,
|
|
1894
1992
|
selected: key == selectedValue,
|
|
@@ -2088,7 +2186,7 @@ class FTable extends FTableEventEmitter {
|
|
|
2088
2186
|
initColumnWidths() {
|
|
2089
2187
|
const visibleFields = this.columnList.filter(fieldName => {
|
|
2090
2188
|
const field = this.options.fields[fieldName];
|
|
2091
|
-
return field.visibility !== 'hidden' && field.visibility !== 'separator';
|
|
2189
|
+
return !field.action && field.visibility !== 'hidden' && field.visibility !== 'separator';
|
|
2092
2190
|
});
|
|
2093
2191
|
|
|
2094
2192
|
const count = visibleFields.length;
|
|
@@ -2107,7 +2205,7 @@ class FTable extends FTableEventEmitter {
|
|
|
2107
2205
|
th: this.elements.table.querySelector(`[data-field-name="${fieldName}"]`),
|
|
2108
2206
|
field: this.options.fields[fieldName]
|
|
2109
2207
|
}))
|
|
2110
|
-
.filter(item => item.th && item.field.visibility !== 'hidden' && item.field.visibility !== 'separator');
|
|
2208
|
+
.filter(item => item.th && !item.field.action && item.field.visibility !== 'hidden' && item.field.visibility !== 'separator');
|
|
2111
2209
|
|
|
2112
2210
|
if (visibleHeaders.length === 0) return;
|
|
2113
2211
|
|
|
@@ -2242,8 +2340,17 @@ class FTable extends FTableEventEmitter {
|
|
|
2242
2340
|
this.fieldList.forEach(fieldName => {
|
|
2243
2341
|
const field = this.options.fields[fieldName];
|
|
2244
2342
|
const isKeyField = field.key === true;
|
|
2245
|
-
|
|
2246
|
-
|
|
2343
|
+
const isActionField = !!field.action; // action: 'select' | 'update' | 'clone' | 'delete'
|
|
2344
|
+
|
|
2345
|
+
if (isActionField) {
|
|
2346
|
+
// Action columns are always listed but never part of forms or sorting
|
|
2347
|
+
field.list = true;
|
|
2348
|
+
field.create = false;
|
|
2349
|
+
field.edit = false;
|
|
2350
|
+
field.sorting = false;
|
|
2351
|
+
field.searchable = false;
|
|
2352
|
+
field.visibility = field.visibility ?? 'visible';
|
|
2353
|
+
} else if (isKeyField) {
|
|
2247
2354
|
if (field.create === undefined || !field.create) {
|
|
2248
2355
|
field.create = true;
|
|
2249
2356
|
field.type = 'hidden';
|
|
@@ -2268,6 +2375,13 @@ class FTable extends FTableEventEmitter {
|
|
|
2268
2375
|
return field.list !== false;
|
|
2269
2376
|
});
|
|
2270
2377
|
|
|
2378
|
+
// Track which actions are user-placed (via action columns in fields)
|
|
2379
|
+
this._userPlacedActions = new Set(
|
|
2380
|
+
this.fieldList
|
|
2381
|
+
.filter(name => this.options.fields[name].action)
|
|
2382
|
+
.map(name => this.options.fields[name].action)
|
|
2383
|
+
);
|
|
2384
|
+
|
|
2271
2385
|
// Find key field
|
|
2272
2386
|
this.keyField = this.fieldList.find(name => this.options.fields[name].key === true);
|
|
2273
2387
|
if (!this.keyField) {
|
|
@@ -2278,6 +2392,7 @@ class FTable extends FTableEventEmitter {
|
|
|
2278
2392
|
async resolveAsyncFieldOptions() {
|
|
2279
2393
|
const promises = this.columnList.map(async (fieldName) => {
|
|
2280
2394
|
const field = this.options.fields[fieldName];
|
|
2395
|
+
if (field.action) return; // Skip action columns
|
|
2281
2396
|
const originalOptions = this.formBuilder.originalFieldOptions.get(fieldName);
|
|
2282
2397
|
|
|
2283
2398
|
if (this.formBuilder.shouldResolveOptions(originalOptions)) {
|
|
@@ -2303,7 +2418,7 @@ class FTable extends FTableEventEmitter {
|
|
|
2303
2418
|
for (const row of rows) {
|
|
2304
2419
|
for (const fieldName of this.columnList) {
|
|
2305
2420
|
const field = this.options.fields[fieldName];
|
|
2306
|
-
if (!field.options) continue;
|
|
2421
|
+
if (field.action || !field.options) continue;
|
|
2307
2422
|
|
|
2308
2423
|
const cell = row.querySelector(`td[data-field-name="${fieldName}"]`);
|
|
2309
2424
|
if (!cell) continue;
|
|
@@ -2374,8 +2489,8 @@ class FTable extends FTableEventEmitter {
|
|
|
2374
2489
|
parent: thead
|
|
2375
2490
|
});
|
|
2376
2491
|
|
|
2377
|
-
// Add selecting column if enabled
|
|
2378
|
-
if (this.options.selecting && this.options.selectingCheckboxes) {
|
|
2492
|
+
// Add selecting column if enabled (only if not user-placed)
|
|
2493
|
+
if (this.options.selecting && this.options.selectingCheckboxes && !this._userPlacedActions.has('select')) {
|
|
2379
2494
|
const selectHeader = FTableDOMHelper.create('th', {
|
|
2380
2495
|
className: `ftable-command-column-header ftable-column-header-select`,
|
|
2381
2496
|
parent: headerRow
|
|
@@ -2393,9 +2508,41 @@ class FTable extends FTableEventEmitter {
|
|
|
2393
2508
|
}
|
|
2394
2509
|
}
|
|
2395
2510
|
|
|
2396
|
-
// Add data columns
|
|
2511
|
+
// Add data columns (including any user-placed action columns)
|
|
2397
2512
|
this.columnList.forEach(fieldName => {
|
|
2398
2513
|
const field = this.options.fields[fieldName];
|
|
2514
|
+
|
|
2515
|
+
// If this column is a user-placed action column, render an action header
|
|
2516
|
+
if (field.action) {
|
|
2517
|
+
const actionClassMap = {
|
|
2518
|
+
select: 'ftable-column-header-select',
|
|
2519
|
+
update: 'ftable-column-header-edit',
|
|
2520
|
+
clone: 'ftable-column-header-clone',
|
|
2521
|
+
delete: 'ftable-column-header-delete',
|
|
2522
|
+
};
|
|
2523
|
+
const th = FTableDOMHelper.create('th', {
|
|
2524
|
+
className: `ftable-command-column-header ${actionClassMap[field.action] || ''}`,
|
|
2525
|
+
parent: headerRow
|
|
2526
|
+
});
|
|
2527
|
+
if (field.title) {
|
|
2528
|
+
th.textContent = field.title;
|
|
2529
|
+
}
|
|
2530
|
+
// For select action with multiselect, add the select-all checkbox
|
|
2531
|
+
if (field.action === 'select' && this.options.selecting && this.options.selectingCheckboxes && this.options.multiselect) {
|
|
2532
|
+
const selectAllCheckbox = FTableDOMHelper.create('input', {
|
|
2533
|
+
attributes: { type: 'checkbox' },
|
|
2534
|
+
parent: th
|
|
2535
|
+
});
|
|
2536
|
+
selectAllCheckbox.addEventListener('change', () => {
|
|
2537
|
+
this.toggleSelectAll(selectAllCheckbox.checked);
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2540
|
+
if (field.width) {
|
|
2541
|
+
th.style.width = field.width;
|
|
2542
|
+
}
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2399
2546
|
const th = FTableDOMHelper.create('th', {
|
|
2400
2547
|
className: `ftable-column-header ${field.listClass || ''} ${field.listClassHeader || ''}`,
|
|
2401
2548
|
attributes: { 'data-field-name': fieldName },
|
|
@@ -2444,22 +2591,22 @@ class FTable extends FTableEventEmitter {
|
|
|
2444
2591
|
}
|
|
2445
2592
|
});
|
|
2446
2593
|
|
|
2447
|
-
// Add action columns
|
|
2448
|
-
if (this.options.actions.updateAction) {
|
|
2594
|
+
// Add default action columns only if not user-placed
|
|
2595
|
+
if (this.options.actions.updateAction && !this._userPlacedActions.has('update')) {
|
|
2449
2596
|
FTableDOMHelper.create('th', {
|
|
2450
2597
|
className: 'ftable-command-column-header ftable-column-header-edit',
|
|
2451
2598
|
parent: headerRow
|
|
2452
2599
|
});
|
|
2453
2600
|
}
|
|
2454
2601
|
|
|
2455
|
-
if (this.options.actions.cloneAction) {
|
|
2602
|
+
if (this.options.actions.cloneAction && !this._userPlacedActions.has('clone')) {
|
|
2456
2603
|
FTableDOMHelper.create('th', {
|
|
2457
2604
|
className: 'ftable-command-column-header ftable-column-header-clone',
|
|
2458
2605
|
parent: headerRow
|
|
2459
2606
|
});
|
|
2460
2607
|
}
|
|
2461
2608
|
|
|
2462
|
-
if (this.options.actions.deleteAction) {
|
|
2609
|
+
if (this.options.actions.deleteAction && !this._userPlacedActions.has('delete')) {
|
|
2463
2610
|
FTableDOMHelper.create('th', {
|
|
2464
2611
|
className: 'ftable-command-column-header ftable-column-header-delete',
|
|
2465
2612
|
parent: headerRow
|
|
@@ -2480,17 +2627,27 @@ class FTable extends FTableEventEmitter {
|
|
|
2480
2627
|
parent: theadParent
|
|
2481
2628
|
});
|
|
2482
2629
|
|
|
2483
|
-
// Add empty cell for selecting column if enabled
|
|
2484
|
-
if (this.options.selecting && this.options.selectingCheckboxes) {
|
|
2630
|
+
// Add empty cell for selecting column if enabled (only if not user-placed)
|
|
2631
|
+
if (this.options.selecting && this.options.selectingCheckboxes && !this._userPlacedActions.has('select')) {
|
|
2485
2632
|
FTableDOMHelper.create('th', {
|
|
2486
2633
|
className: 'ftable-toolbarsearch-column-header',
|
|
2487
2634
|
parent: searchRow
|
|
2488
2635
|
});
|
|
2489
2636
|
}
|
|
2490
2637
|
|
|
2491
|
-
// Add search input cells for data columns
|
|
2638
|
+
// Add search input cells for data columns (including user-placed action columns)
|
|
2492
2639
|
for (const fieldName of this.columnList) {
|
|
2493
2640
|
const field = this.options.fields[fieldName];
|
|
2641
|
+
|
|
2642
|
+
// Action columns get an empty search cell
|
|
2643
|
+
if (field.action) {
|
|
2644
|
+
FTableDOMHelper.create('th', {
|
|
2645
|
+
className: 'ftable-toolbarsearch-column-header ftable-command-column-header',
|
|
2646
|
+
parent: searchRow
|
|
2647
|
+
});
|
|
2648
|
+
continue;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2494
2651
|
const isSearchable = field.searchable !== false;
|
|
2495
2652
|
|
|
2496
2653
|
const th = FTableDOMHelper.create('th', {
|
|
@@ -2662,9 +2819,9 @@ class FTable extends FTableEventEmitter {
|
|
|
2662
2819
|
parent: searchRow
|
|
2663
2820
|
});
|
|
2664
2821
|
|
|
2665
|
-
const actionCount = (this.options.actions.updateAction ? 1 : 0) +
|
|
2666
|
-
(this.options.actions.deleteAction ? 1 : 0) +
|
|
2667
|
-
(this.options.actions.cloneAction ? 1 : 0);
|
|
2822
|
+
const actionCount = (this.options.actions.updateAction && !this._userPlacedActions.has('update') ? 1 : 0) +
|
|
2823
|
+
(this.options.actions.deleteAction && !this._userPlacedActions.has('delete') ? 1 : 0) +
|
|
2824
|
+
(this.options.actions.cloneAction && !this._userPlacedActions.has('clone') ? 1 : 0);
|
|
2668
2825
|
|
|
2669
2826
|
if (actionCount > 0) {
|
|
2670
2827
|
resetTh.colSpan = actionCount;
|
|
@@ -2772,379 +2929,26 @@ class FTable extends FTableEventEmitter {
|
|
|
2772
2929
|
}
|
|
2773
2930
|
|
|
2774
2931
|
createCustomMultiSelectForSearch(fieldSearchName, fieldName, field, optionsSource, attributes) {
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
// Create display area
|
|
2794
|
-
const display = FTableDOMHelper.create('div', {
|
|
2795
|
-
className: 'ftable-multiselect-display',
|
|
2796
|
-
parent: container,
|
|
2797
|
-
attributes: {
|
|
2798
|
-
tabindex: '0' // Makes it focusable and in tab order
|
|
2799
|
-
}
|
|
2800
|
-
});
|
|
2801
|
-
|
|
2802
|
-
const selectedDisplay = FTableDOMHelper.create('div', {
|
|
2803
|
-
className: 'ftable-multiselect-selected',
|
|
2804
|
-
parent: display
|
|
2805
|
-
});
|
|
2806
|
-
|
|
2807
|
-
const placeholderText = field.searchPlaceholder || field.placeholder || this.options.messages.multiSelectPlaceholder || 'Click to select options...';
|
|
2808
|
-
const placeholder = FTableDOMHelper.create('span', {
|
|
2809
|
-
className: 'ftable-multiselect-placeholder',
|
|
2810
|
-
textContent: placeholderText,
|
|
2811
|
-
parent: selectedDisplay
|
|
2812
|
-
});
|
|
2813
|
-
|
|
2814
|
-
// Create dropdown toggle button
|
|
2815
|
-
const toggleBtn = FTableDOMHelper.create('button', {
|
|
2816
|
-
type: 'button',
|
|
2817
|
-
className: 'ftable-multiselect-toggle',
|
|
2818
|
-
innerHTML: '▼',
|
|
2819
|
-
parent: display,
|
|
2820
|
-
attributes: {
|
|
2821
|
-
tabindex: '-1' // this skips regular focus when tabbing
|
|
2822
|
-
}
|
|
2823
|
-
});
|
|
2824
|
-
|
|
2825
|
-
// Dropdown and overlay will be created on demand and appended to body
|
|
2826
|
-
let dropdown = null;
|
|
2827
|
-
let dropdownOverlay = null;
|
|
2828
|
-
|
|
2829
|
-
// Store selected values and checkbox references
|
|
2830
|
-
const selectedValues = new Set();
|
|
2831
|
-
const checkboxMap = new Map(); // Map of value -> checkbox element
|
|
2832
|
-
|
|
2833
|
-
// Function to update display and hidden select
|
|
2834
|
-
const updateDisplay = () => {
|
|
2835
|
-
selectedDisplay.innerHTML = '';
|
|
2836
|
-
|
|
2837
|
-
// Update hidden select
|
|
2838
|
-
Array.from(hiddenSelect.options).forEach(opt => {
|
|
2839
|
-
opt.selected = selectedValues.has(opt.value);
|
|
2840
|
-
});
|
|
2841
|
-
|
|
2842
|
-
// Trigger change event on hidden select for search functionality
|
|
2843
|
-
hiddenSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
|
2844
|
-
|
|
2845
|
-
if (selectedValues.size === 0) {
|
|
2846
|
-
placeholder.textContent = placeholderText;
|
|
2847
|
-
selectedDisplay.appendChild(placeholder);
|
|
2848
|
-
} else {
|
|
2849
|
-
const selectedArray = Array.from(selectedValues);
|
|
2850
|
-
const optionsMap = new Map();
|
|
2851
|
-
|
|
2852
|
-
// Build options map
|
|
2853
|
-
if (optionsSource && Array.isArray(optionsSource)) {
|
|
2854
|
-
optionsSource.forEach(opt => {
|
|
2855
|
-
const val = opt.Value !== undefined ? opt.Value :
|
|
2856
|
-
opt.value !== undefined ? opt.value : opt;
|
|
2857
|
-
const text = opt.DisplayText || opt.text || opt;
|
|
2858
|
-
optionsMap.set(val.toString(), text);
|
|
2859
|
-
});
|
|
2860
|
-
}
|
|
2861
|
-
|
|
2862
|
-
selectedArray.forEach(val => {
|
|
2863
|
-
const tag = FTableDOMHelper.create('span', {
|
|
2864
|
-
className: 'ftable-multiselect-tag',
|
|
2865
|
-
parent: selectedDisplay
|
|
2866
|
-
});
|
|
2867
|
-
|
|
2868
|
-
FTableDOMHelper.create('span', {
|
|
2869
|
-
className: 'ftable-multiselect-tag-text',
|
|
2870
|
-
textContent: optionsMap.get(val.toString()) || val,
|
|
2871
|
-
parent: tag
|
|
2872
|
-
});
|
|
2873
|
-
|
|
2874
|
-
const removeBtn = FTableDOMHelper.create('span', {
|
|
2875
|
-
className: 'ftable-multiselect-tag-remove',
|
|
2876
|
-
innerHTML: '×',
|
|
2877
|
-
parent: tag
|
|
2878
|
-
});
|
|
2879
|
-
|
|
2880
|
-
removeBtn.addEventListener('click', (e) => {
|
|
2881
|
-
e.stopPropagation();
|
|
2882
|
-
selectedValues.delete(val);
|
|
2883
|
-
// Update the checkbox state
|
|
2884
|
-
const checkbox = checkboxMap.get(val.toString());
|
|
2885
|
-
if (checkbox) {
|
|
2886
|
-
checkbox.checked = false;
|
|
2887
|
-
}
|
|
2888
|
-
updateDisplay();
|
|
2889
|
-
});
|
|
2890
|
-
});
|
|
2891
|
-
}
|
|
2892
|
-
};
|
|
2893
|
-
|
|
2894
|
-
// Function to close dropdown
|
|
2895
|
-
const closeDropdown = () => {
|
|
2896
|
-
display.focus(); // Return focus to the trigger
|
|
2897
|
-
if (dropdown) {
|
|
2898
|
-
dropdown.remove();
|
|
2899
|
-
dropdown = null;
|
|
2900
|
-
}
|
|
2901
|
-
if (dropdownOverlay) {
|
|
2902
|
-
dropdownOverlay.remove();
|
|
2903
|
-
dropdownOverlay = null;
|
|
2904
|
-
}
|
|
2905
|
-
if (container._cleanupHandlers) {
|
|
2906
|
-
container._cleanupHandlers();
|
|
2907
|
-
container._cleanupHandlers = null;
|
|
2908
|
-
}
|
|
2909
|
-
};
|
|
2910
|
-
|
|
2911
|
-
// Function to position dropdown
|
|
2912
|
-
const positionDropdown = () => {
|
|
2913
|
-
if (!dropdown) return;
|
|
2914
|
-
|
|
2915
|
-
const rect = display.getBoundingClientRect();
|
|
2916
|
-
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
2917
|
-
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
|
|
2918
|
-
|
|
2919
|
-
let left = rect.left + scrollLeft;
|
|
2920
|
-
let top = rect.bottom + scrollTop + 4; // 4px gap
|
|
2921
|
-
|
|
2922
|
-
dropdown.style.position = 'absolute';
|
|
2923
|
-
dropdown.style.left = `${left}px`;
|
|
2924
|
-
dropdown.style.top = `${top}px`;
|
|
2925
|
-
dropdown.style.width = `${rect.width}px`;
|
|
2926
|
-
dropdown.style.minWidth = `${rect.width}px`;
|
|
2927
|
-
dropdown.style.boxSizing = 'border-box';
|
|
2928
|
-
dropdown.style.zIndex = '10000';
|
|
2929
|
-
|
|
2930
|
-
// Adjust horizontal position if needed
|
|
2931
|
-
const dropdownRect = dropdown.getBoundingClientRect();
|
|
2932
|
-
const viewportWidth = window.innerWidth;
|
|
2933
|
-
if (dropdownRect.right > viewportWidth) {
|
|
2934
|
-
left = Math.max(10, viewportWidth - dropdownRect.width - 10);
|
|
2935
|
-
dropdown.style.left = `${left}px`;
|
|
2936
|
-
}
|
|
2937
|
-
};
|
|
2938
|
-
|
|
2939
|
-
// Populate options in both hidden select and dropdown
|
|
2940
|
-
const populateOptions = () => {
|
|
2941
|
-
if (!optionsSource || !dropdown) return;
|
|
2942
|
-
|
|
2943
|
-
const options = Array.isArray(optionsSource) ? optionsSource :
|
|
2944
|
-
Object.entries(optionsSource).map(([k, v]) => ({Value: k, DisplayText: v}));
|
|
2945
|
-
|
|
2946
|
-
options.forEach(option => {
|
|
2947
|
-
const optValue = option.Value !== undefined ? option.Value :
|
|
2948
|
-
option.value !== undefined ? option.value : option;
|
|
2949
|
-
|
|
2950
|
-
// Skip if value is empty
|
|
2951
|
-
if (optValue == null || optValue === '') {
|
|
2952
|
-
return; // This continues to the next iteration
|
|
2953
|
-
}
|
|
2954
|
-
|
|
2955
|
-
const optText = option.DisplayText || option.text || option;
|
|
2956
|
-
|
|
2957
|
-
// Add to hidden select (only once)
|
|
2958
|
-
if (!hiddenSelect.querySelector(`option[value="${optValue}"]`)) {
|
|
2959
|
-
FTableDOMHelper.create('option', {
|
|
2960
|
-
value: optValue,
|
|
2961
|
-
textContent: optText,
|
|
2962
|
-
parent: hiddenSelect
|
|
2963
|
-
});
|
|
2964
|
-
}
|
|
2965
|
-
|
|
2966
|
-
// Add to visual dropdown
|
|
2967
|
-
const optionDiv = FTableDOMHelper.create('div', {
|
|
2968
|
-
className: 'ftable-multiselect-option',
|
|
2969
|
-
parent: dropdown
|
|
2970
|
-
});
|
|
2971
|
-
|
|
2972
|
-
const checkbox = FTableDOMHelper.create('input', {
|
|
2973
|
-
type: 'checkbox',
|
|
2974
|
-
className: 'ftable-multiselect-checkbox',
|
|
2975
|
-
parent: optionDiv
|
|
2976
|
-
});
|
|
2977
|
-
|
|
2978
|
-
// Set initial checked state
|
|
2979
|
-
checkbox.checked = selectedValues.has(optValue.toString());
|
|
2980
|
-
|
|
2981
|
-
// Store checkbox reference
|
|
2982
|
-
checkboxMap.set(optValue.toString(), checkbox);
|
|
2983
|
-
|
|
2984
|
-
const label = FTableDOMHelper.create('label', {
|
|
2985
|
-
className: 'ftable-multiselect-label',
|
|
2986
|
-
textContent: optText,
|
|
2987
|
-
parent: optionDiv
|
|
2988
|
-
});
|
|
2989
|
-
|
|
2990
|
-
// Click anywhere on the option to toggle
|
|
2991
|
-
optionDiv.addEventListener('click', (e) => {
|
|
2992
|
-
e.stopPropagation();
|
|
2993
|
-
|
|
2994
|
-
if (selectedValues.has(optValue.toString())) {
|
|
2995
|
-
selectedValues.delete(optValue.toString());
|
|
2996
|
-
checkbox.checked = false;
|
|
2997
|
-
} else {
|
|
2998
|
-
selectedValues.add(optValue.toString());
|
|
2999
|
-
checkbox.checked = true;
|
|
3000
|
-
}
|
|
3001
|
-
|
|
3002
|
-
updateDisplay();
|
|
3003
|
-
});
|
|
3004
|
-
});
|
|
3005
|
-
};
|
|
3006
|
-
|
|
3007
|
-
// Toggle dropdown
|
|
3008
|
-
const toggleDropdown = (e) => {
|
|
3009
|
-
if (e) e.stopPropagation();
|
|
3010
|
-
|
|
3011
|
-
if (dropdown) {
|
|
3012
|
-
// Dropdown is open, close it
|
|
3013
|
-
closeDropdown();
|
|
3014
|
-
} else {
|
|
3015
|
-
// Close any other open multiselect dropdowns
|
|
3016
|
-
document.querySelectorAll('.ftable-multiselect-dropdown').forEach(dd => dd.remove());
|
|
3017
|
-
document.querySelectorAll('.ftable-multiselect-overlay').forEach(ov => ov.remove());
|
|
3018
|
-
|
|
3019
|
-
// Create overlay
|
|
3020
|
-
dropdownOverlay = FTableDOMHelper.create('div', {
|
|
3021
|
-
className: 'ftable-multiselect-overlay',
|
|
3022
|
-
parent: document.body
|
|
3023
|
-
});
|
|
3024
|
-
|
|
3025
|
-
// Create dropdown
|
|
3026
|
-
dropdown = FTableDOMHelper.create('div', {
|
|
3027
|
-
className: 'ftable-multiselect-dropdown',
|
|
3028
|
-
parent: document.body,
|
|
3029
|
-
attributes: {
|
|
3030
|
-
tabindex: '-1',
|
|
3031
|
-
role: 'listbox',
|
|
3032
|
-
'aria-multiselectable': 'true'
|
|
3033
|
-
}
|
|
3034
|
-
});
|
|
3035
|
-
|
|
3036
|
-
// Populate options
|
|
3037
|
-
populateOptions();
|
|
3038
|
-
|
|
3039
|
-
// Position dropdown
|
|
3040
|
-
positionDropdown();
|
|
3041
|
-
|
|
3042
|
-
// dropdown focus
|
|
3043
|
-
dropdown.focus();
|
|
3044
|
-
|
|
3045
|
-
// Add keyboard navigation
|
|
3046
|
-
dropdown.addEventListener('keydown', (e) => {
|
|
3047
|
-
if (e.key === 'Escape') {
|
|
3048
|
-
closeDropdown();
|
|
3049
|
-
} else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
3050
|
-
e.preventDefault();
|
|
3051
|
-
// Navigate between options
|
|
3052
|
-
const checkboxes = Array.from(dropdown.querySelectorAll('.ftable-multiselect-checkbox'));
|
|
3053
|
-
const current = document.activeElement;
|
|
3054
|
-
const currentIndex = checkboxes.indexOf(current);
|
|
3055
|
-
|
|
3056
|
-
let nextIndex;
|
|
3057
|
-
if (e.key === 'ArrowDown') {
|
|
3058
|
-
nextIndex = currentIndex < checkboxes.length - 1 ? currentIndex + 1 : 0;
|
|
3059
|
-
} else {
|
|
3060
|
-
nextIndex = currentIndex > 0 ? currentIndex - 1 : checkboxes.length - 1;
|
|
3061
|
-
}
|
|
3062
|
-
|
|
3063
|
-
checkboxes[nextIndex].focus();
|
|
3064
|
-
} else if (e.key === ' ' || e.key === 'Enter') {
|
|
3065
|
-
e.preventDefault();
|
|
3066
|
-
// Toggle the focused checkbox
|
|
3067
|
-
if (document.activeElement.classList.contains('ftable-multiselect-checkbox')) {
|
|
3068
|
-
document.activeElement.click();
|
|
3069
|
-
}
|
|
3070
|
-
}
|
|
3071
|
-
});
|
|
3072
|
-
|
|
3073
|
-
// Handle clicks outside
|
|
3074
|
-
dropdownOverlay.addEventListener('click', (event) => {
|
|
3075
|
-
if (event.target === dropdownOverlay) {
|
|
3076
|
-
closeDropdown();
|
|
3077
|
-
}
|
|
3078
|
-
});
|
|
3079
|
-
|
|
3080
|
-
// Reposition on scroll/resize
|
|
3081
|
-
const repositionHandler = () => positionDropdown();
|
|
3082
|
-
const scrollHandler = (e) => {
|
|
3083
|
-
if (dropdown && dropdown.contains(e.target)) {
|
|
3084
|
-
return; // Allow scrolling inside dropdown
|
|
3085
|
-
}
|
|
3086
|
-
positionDropdown();
|
|
3087
|
-
};
|
|
3088
|
-
const selectedResizeObserver = new ResizeObserver(() => {
|
|
3089
|
-
positionDropdown();
|
|
3090
|
-
});
|
|
3091
|
-
window.addEventListener('scroll', scrollHandler, true);
|
|
3092
|
-
window.addEventListener('resize', repositionHandler);
|
|
3093
|
-
selectedResizeObserver.observe(selectedDisplay);
|
|
3094
|
-
|
|
3095
|
-
// Store cleanup function
|
|
3096
|
-
container._cleanupHandlers = () => {
|
|
3097
|
-
window.removeEventListener('scroll', scrollHandler, true);
|
|
3098
|
-
window.removeEventListener('resize', repositionHandler);
|
|
3099
|
-
selectedResizeObserver.disconnect();
|
|
3100
|
-
};
|
|
3101
|
-
}
|
|
3102
|
-
};
|
|
3103
|
-
|
|
3104
|
-
display.addEventListener('click', toggleDropdown);
|
|
3105
|
-
toggleBtn.addEventListener('click', toggleDropdown);
|
|
3106
|
-
display.addEventListener('keydown', (e) => {
|
|
3107
|
-
if (e.key === 'ArrowDown' || e.key === 'Enter') {
|
|
3108
|
-
e.preventDefault();
|
|
3109
|
-
toggleDropdown();
|
|
3110
|
-
}
|
|
3111
|
-
});
|
|
3112
|
-
|
|
3113
|
-
// Add reset method to container
|
|
3114
|
-
container.resetMultiSelect = () => {
|
|
3115
|
-
selectedValues.clear();
|
|
3116
|
-
checkboxMap.forEach(checkbox => {
|
|
3117
|
-
checkbox.checked = false;
|
|
3118
|
-
});
|
|
3119
|
-
closeDropdown();
|
|
3120
|
-
updateDisplay();
|
|
3121
|
-
};
|
|
3122
|
-
|
|
3123
|
-
// Clean up when container is removed from DOM
|
|
3124
|
-
const observer = new MutationObserver((mutations) => {
|
|
3125
|
-
mutations.forEach((mutation) => {
|
|
3126
|
-
mutation.removedNodes.forEach((node) => {
|
|
3127
|
-
if (node === container || node.contains && node.contains(container)) {
|
|
3128
|
-
closeDropdown();
|
|
3129
|
-
observer.disconnect();
|
|
3130
|
-
}
|
|
3131
|
-
});
|
|
3132
|
-
});
|
|
2932
|
+
const livesearch = field.livesearch ?? false;
|
|
2933
|
+
|
|
2934
|
+
return this.formBuilder._buildCustomMultiSelect({
|
|
2935
|
+
hiddenSelectId: fieldSearchName,
|
|
2936
|
+
hiddenSelectName: attributes['data-field-name'] || fieldSearchName,
|
|
2937
|
+
extraClasses: 'ftable-multiselect-search ftable-toolbarsearch',
|
|
2938
|
+
containerDataFieldName: attributes['data-field-name'] || fieldSearchName,
|
|
2939
|
+
hiddenSelectAttributes: attributes,
|
|
2940
|
+
optionsSource: optionsSource,
|
|
2941
|
+
initialValues: [],
|
|
2942
|
+
placeholderText: field.searchPlaceholder || field.placeholder
|
|
2943
|
+
|| this.options.messages?.multiSelectPlaceholder
|
|
2944
|
+
|| 'Click to select options...',
|
|
2945
|
+
livesearch,
|
|
2946
|
+
onChangeExtra: (hiddenSelect) => {
|
|
2947
|
+
hiddenSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
|
2948
|
+
},
|
|
2949
|
+
buildHiddenSelectOnUpdate: false, // search mode: options pre-populated
|
|
3133
2950
|
});
|
|
3134
|
-
|
|
3135
|
-
// Start observing once container is in the DOM
|
|
3136
|
-
setTimeout(() => {
|
|
3137
|
-
if (container.parentNode) {
|
|
3138
|
-
observer.observe(container.parentNode, { childList: true, subtree: true });
|
|
3139
|
-
}
|
|
3140
|
-
}, 0);
|
|
3141
|
-
|
|
3142
|
-
// Initialize
|
|
3143
|
-
updateDisplay();
|
|
3144
|
-
|
|
3145
|
-
return container;
|
|
3146
2951
|
}
|
|
3147
|
-
|
|
3148
2952
|
async createDatalistForSearch(fieldName, field) {
|
|
3149
2953
|
const fieldSearchName = 'ftable-toolbarsearch-' + fieldName;
|
|
3150
2954
|
|
|
@@ -3702,6 +3506,7 @@ class FTable extends FTableEventEmitter {
|
|
|
3702
3506
|
|
|
3703
3507
|
this.columnList.forEach(fieldName => {
|
|
3704
3508
|
const field = this.options.fields[fieldName];
|
|
3509
|
+
if (field.action) return; // Action columns don't appear in column picker
|
|
3705
3510
|
const isVisible = field.visibility !== 'hidden';
|
|
3706
3511
|
const isFixed = field.visibility === 'fixed';
|
|
3707
3512
|
const isSeparator = field.visibility === 'separator';
|
|
@@ -3993,9 +3798,18 @@ class FTable extends FTableEventEmitter {
|
|
|
3993
3798
|
const searchFields = [];
|
|
3994
3799
|
|
|
3995
3800
|
Object.entries(this.state.searchQueries).forEach(([fieldName, query]) => {
|
|
3996
|
-
if (query
|
|
3997
|
-
|
|
3998
|
-
|
|
3801
|
+
if (Array.isArray(query)) {
|
|
3802
|
+
query.forEach(value => {
|
|
3803
|
+
if (value !== '' && value != null) {
|
|
3804
|
+
queries.push(value);
|
|
3805
|
+
searchFields.push(fieldName);
|
|
3806
|
+
}
|
|
3807
|
+
});
|
|
3808
|
+
} else {
|
|
3809
|
+
if (query !== '') {
|
|
3810
|
+
queries.push(query);
|
|
3811
|
+
searchFields.push(fieldName);
|
|
3812
|
+
}
|
|
3999
3813
|
}
|
|
4000
3814
|
});
|
|
4001
3815
|
|
|
@@ -4105,26 +3919,45 @@ class FTable extends FTableEventEmitter {
|
|
|
4105
3919
|
// Store record data
|
|
4106
3920
|
row.recordData = record;
|
|
4107
3921
|
|
|
4108
|
-
// Add selecting checkbox if enabled
|
|
4109
|
-
if (this.options.selecting && this.options.selectingCheckboxes) {
|
|
3922
|
+
// Add selecting checkbox if enabled (only if not user-placed)
|
|
3923
|
+
if (this.options.selecting && this.options.selectingCheckboxes && !this._userPlacedActions.has('select')) {
|
|
4110
3924
|
this.addSelectingCell(row);
|
|
4111
3925
|
}
|
|
4112
3926
|
|
|
4113
|
-
// Add data cells
|
|
3927
|
+
// Add data cells (including user-placed action columns)
|
|
4114
3928
|
this.columnList.forEach(fieldName => {
|
|
4115
|
-
this.
|
|
3929
|
+
const field = this.options.fields[fieldName];
|
|
3930
|
+
if (field.action) {
|
|
3931
|
+
// Render inline action cell
|
|
3932
|
+
switch (field.action) {
|
|
3933
|
+
case 'select':
|
|
3934
|
+
this.addSelectingCell(row);
|
|
3935
|
+
break;
|
|
3936
|
+
case 'update':
|
|
3937
|
+
this.addEditCell(row);
|
|
3938
|
+
break;
|
|
3939
|
+
case 'clone':
|
|
3940
|
+
this.addCloneCell(row);
|
|
3941
|
+
break;
|
|
3942
|
+
case 'delete':
|
|
3943
|
+
this.addDeleteCell(row);
|
|
3944
|
+
break;
|
|
3945
|
+
}
|
|
3946
|
+
} else {
|
|
3947
|
+
this.addDataCell(row, record, fieldName);
|
|
3948
|
+
}
|
|
4116
3949
|
});
|
|
4117
3950
|
|
|
4118
|
-
// Add action cells
|
|
4119
|
-
if (this.options.actions.updateAction) {
|
|
3951
|
+
// Add default action cells only if not user-placed
|
|
3952
|
+
if (this.options.actions.updateAction && !this._userPlacedActions.has('update')) {
|
|
4120
3953
|
this.addEditCell(row);
|
|
4121
3954
|
}
|
|
4122
3955
|
|
|
4123
|
-
if (this.options.actions.cloneAction) {
|
|
3956
|
+
if (this.options.actions.cloneAction && !this._userPlacedActions.has('clone')) {
|
|
4124
3957
|
this.addCloneCell(row);
|
|
4125
3958
|
}
|
|
4126
3959
|
|
|
4127
|
-
if (this.options.actions.deleteAction) {
|
|
3960
|
+
if (this.options.actions.deleteAction && !this._userPlacedActions.has('delete')) {
|
|
4128
3961
|
this.addDeleteCell(row);
|
|
4129
3962
|
}
|
|
4130
3963
|
|