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