@liedekef/ftable 1.1.48 → 1.1.50
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 +650 -48
- package/ftable.js +650 -48
- package/ftable.min.js +2 -24
- package/ftable.umd.js +650 -48
- package/package.json +1 -1
- package/themes/basic/ftable_basic.css +148 -0
- package/themes/basic/ftable_basic.min.css +1 -1
- package/themes/ftable_theme_base.less +172 -0
- package/themes/lightcolor/blue/ftable.css +148 -0
- package/themes/lightcolor/blue/ftable.min.css +1 -1
- package/themes/lightcolor/gray/ftable.css +148 -0
- package/themes/lightcolor/gray/ftable.min.css +1 -1
- package/themes/lightcolor/green/ftable.css +148 -0
- package/themes/lightcolor/green/ftable.min.css +1 -1
- package/themes/lightcolor/orange/ftable.css +148 -0
- package/themes/lightcolor/orange/ftable.min.css +1 -1
- package/themes/lightcolor/red/ftable.css +148 -0
- package/themes/lightcolor/red/ftable.min.css +1 -1
- package/themes/metro/blue/ftable.css +148 -0
- package/themes/metro/blue/ftable.min.css +1 -1
- package/themes/metro/brown/ftable.css +148 -0
- package/themes/metro/brown/ftable.min.css +1 -1
- package/themes/metro/crimson/ftable.css +148 -0
- package/themes/metro/crimson/ftable.min.css +1 -1
- package/themes/metro/darkgray/ftable.css +148 -0
- package/themes/metro/darkgray/ftable.min.css +1 -1
- package/themes/metro/darkorange/ftable.css +148 -0
- package/themes/metro/darkorange/ftable.min.css +1 -1
- package/themes/metro/green/ftable.css +148 -0
- package/themes/metro/green/ftable.min.css +1 -1
- package/themes/metro/lightgray/ftable.css +148 -0
- package/themes/metro/lightgray/ftable.min.css +1 -1
- package/themes/metro/pink/ftable.css +148 -0
- package/themes/metro/pink/ftable.min.css +1 -1
- package/themes/metro/purple/ftable.css +148 -0
- package/themes/metro/purple/ftable.min.css +1 -1
- package/themes/metro/red/ftable.css +148 -0
- package/themes/metro/red/ftable.min.css +1 -1
package/ftable.esm.js
CHANGED
|
@@ -1374,10 +1374,10 @@ class FTableFormBuilder {
|
|
|
1374
1374
|
|
|
1375
1375
|
// extra check for name and multiple
|
|
1376
1376
|
let name = fieldName;
|
|
1377
|
+
let hasMultiple = false;
|
|
1378
|
+
|
|
1377
1379
|
// Apply inputAttributes from field definition
|
|
1378
1380
|
if (field.inputAttributes) {
|
|
1379
|
-
let hasMultiple = false;
|
|
1380
|
-
|
|
1381
1381
|
const parsed = this.parseInputAttributes(field.inputAttributes);
|
|
1382
1382
|
Object.assign(attributes, parsed);
|
|
1383
1383
|
|
|
@@ -1387,6 +1387,13 @@ class FTableFormBuilder {
|
|
|
1387
1387
|
name = `${fieldName}[]`;
|
|
1388
1388
|
}
|
|
1389
1389
|
}
|
|
1390
|
+
|
|
1391
|
+
// If multiple select, create custom UI
|
|
1392
|
+
if (hasMultiple) {
|
|
1393
|
+
return this.createCustomMultiSelect(fieldName, field, value, attributes, name);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// Standard single select
|
|
1390
1397
|
attributes.name = name;
|
|
1391
1398
|
|
|
1392
1399
|
const select = FTableDOMHelper.create('select', {
|
|
@@ -1403,6 +1410,297 @@ class FTableFormBuilder {
|
|
|
1403
1410
|
return select;
|
|
1404
1411
|
}
|
|
1405
1412
|
|
|
1413
|
+
createCustomMultiSelect(fieldName, field, value, attributes, name) {
|
|
1414
|
+
// Create container
|
|
1415
|
+
const container = FTableDOMHelper.create('div', {
|
|
1416
|
+
className: 'ftable-multiselect-container',
|
|
1417
|
+
attributes: { 'data-field-name': fieldName }
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
// Create hidden input to store selected values
|
|
1421
|
+
const hiddenInput = FTableDOMHelper.create('input', {
|
|
1422
|
+
type: 'hidden',
|
|
1423
|
+
name: name,
|
|
1424
|
+
id: `Edit-${fieldName}`,
|
|
1425
|
+
value: Array.isArray(value) ? value.join(',') : value || ''
|
|
1426
|
+
});
|
|
1427
|
+
container.appendChild(hiddenInput);
|
|
1428
|
+
|
|
1429
|
+
// Create display area
|
|
1430
|
+
const display = FTableDOMHelper.create('div', {
|
|
1431
|
+
className: 'ftable-multiselect-display',
|
|
1432
|
+
parent: container
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
const selectedDisplay = FTableDOMHelper.create('div', {
|
|
1436
|
+
className: 'ftable-multiselect-selected',
|
|
1437
|
+
parent: display
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
const placeholderText = field.placeholder || this.options.messages.multiSelectPlaceholder || 'Click to select options...';
|
|
1441
|
+
const placeholder = FTableDOMHelper.create('span', {
|
|
1442
|
+
className: 'ftable-multiselect-placeholder',
|
|
1443
|
+
textContent: placeholderText,
|
|
1444
|
+
parent: selectedDisplay
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
// Create dropdown toggle button
|
|
1448
|
+
const toggleBtn = FTableDOMHelper.create('button', {
|
|
1449
|
+
type: 'button',
|
|
1450
|
+
className: 'ftable-multiselect-toggle',
|
|
1451
|
+
innerHTML: '▼',
|
|
1452
|
+
parent: display
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
// Dropdown and overlay will be created on demand and appended to body
|
|
1456
|
+
let dropdown = null;
|
|
1457
|
+
let dropdownOverlay = null;
|
|
1458
|
+
|
|
1459
|
+
// Store selected values and checkbox references
|
|
1460
|
+
const selectedValues = new Set(
|
|
1461
|
+
Array.isArray(value) ? value :
|
|
1462
|
+
value ? value.toString().split(',').filter(v => v) : []
|
|
1463
|
+
);
|
|
1464
|
+
const checkboxMap = new Map(); // Map of value -> checkbox element
|
|
1465
|
+
|
|
1466
|
+
// Function to update display
|
|
1467
|
+
const updateDisplay = () => {
|
|
1468
|
+
selectedDisplay.innerHTML = '';
|
|
1469
|
+
|
|
1470
|
+
if (selectedValues.size === 0) {
|
|
1471
|
+
placeholder.textContent = placeholderText;
|
|
1472
|
+
selectedDisplay.appendChild(placeholder);
|
|
1473
|
+
} else {
|
|
1474
|
+
const selectedArray = Array.from(selectedValues);
|
|
1475
|
+
const optionsMap = new Map();
|
|
1476
|
+
|
|
1477
|
+
// Build options map
|
|
1478
|
+
if (field.options) {
|
|
1479
|
+
const options = Array.isArray(field.options) ? field.options :
|
|
1480
|
+
Object.entries(field.options).map(([k, v]) => ({Value: k, DisplayText: v}));
|
|
1481
|
+
|
|
1482
|
+
options.forEach(opt => {
|
|
1483
|
+
const val = opt.Value !== undefined ? opt.Value :
|
|
1484
|
+
opt.value !== undefined ? opt.value : opt;
|
|
1485
|
+
const text = opt.DisplayText || opt.text || opt;
|
|
1486
|
+
optionsMap.set(val.toString(), text);
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
selectedArray.forEach(val => {
|
|
1491
|
+
const tag = FTableDOMHelper.create('span', {
|
|
1492
|
+
className: 'ftable-multiselect-tag',
|
|
1493
|
+
parent: selectedDisplay
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
FTableDOMHelper.create('span', {
|
|
1497
|
+
className: 'ftable-multiselect-tag-text',
|
|
1498
|
+
textContent: optionsMap.get(val.toString()) || val,
|
|
1499
|
+
parent: tag
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
const removeBtn = FTableDOMHelper.create('span', {
|
|
1503
|
+
className: 'ftable-multiselect-tag-remove',
|
|
1504
|
+
innerHTML: '×',
|
|
1505
|
+
parent: tag
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
removeBtn.addEventListener('click', (e) => {
|
|
1509
|
+
e.stopPropagation();
|
|
1510
|
+
selectedValues.delete(val);
|
|
1511
|
+
// Update the checkbox state
|
|
1512
|
+
const checkbox = checkboxMap.get(val.toString());
|
|
1513
|
+
if (checkbox) {
|
|
1514
|
+
checkbox.checked = false;
|
|
1515
|
+
}
|
|
1516
|
+
updateDisplay();
|
|
1517
|
+
hiddenInput.value = Array.from(selectedValues).join(',');
|
|
1518
|
+
});
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
hiddenInput.value = Array.from(selectedValues).join(',');
|
|
1523
|
+
};
|
|
1524
|
+
|
|
1525
|
+
// Function to close dropdown
|
|
1526
|
+
const closeDropdown = () => {
|
|
1527
|
+
if (dropdown) {
|
|
1528
|
+
dropdown.remove();
|
|
1529
|
+
dropdown = null;
|
|
1530
|
+
}
|
|
1531
|
+
if (dropdownOverlay) {
|
|
1532
|
+
dropdownOverlay.remove();
|
|
1533
|
+
dropdownOverlay = null;
|
|
1534
|
+
}
|
|
1535
|
+
if (container._cleanupHandlers) {
|
|
1536
|
+
container._cleanupHandlers();
|
|
1537
|
+
container._cleanupHandlers = null;
|
|
1538
|
+
}
|
|
1539
|
+
};
|
|
1540
|
+
|
|
1541
|
+
// Function to position dropdown
|
|
1542
|
+
const positionDropdown = () => {
|
|
1543
|
+
if (!dropdown) return;
|
|
1544
|
+
|
|
1545
|
+
const rect = display.getBoundingClientRect();
|
|
1546
|
+
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
1547
|
+
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
|
|
1548
|
+
|
|
1549
|
+
let left = rect.left + scrollLeft;
|
|
1550
|
+
let top = rect.bottom + scrollTop + 4; // 4px gap
|
|
1551
|
+
|
|
1552
|
+
dropdown.style.position = 'absolute';
|
|
1553
|
+
dropdown.style.left = `${left}px`;
|
|
1554
|
+
dropdown.style.top = `${top}px`;
|
|
1555
|
+
dropdown.style.width = `${rect.width}px`;
|
|
1556
|
+
dropdown.style.minWidth = `${rect.width}px`;
|
|
1557
|
+
dropdown.style.boxSizing = 'border-box';
|
|
1558
|
+
dropdown.style.zIndex = '10000';
|
|
1559
|
+
|
|
1560
|
+
// Adjust horizontal position if needed
|
|
1561
|
+
const dropdownRect = dropdown.getBoundingClientRect();
|
|
1562
|
+
const viewportWidth = window.innerWidth;
|
|
1563
|
+
if (dropdownRect.right > viewportWidth) {
|
|
1564
|
+
left = Math.max(10, viewportWidth - dropdownRect.width - 10);
|
|
1565
|
+
dropdown.style.left = `${left}px`;
|
|
1566
|
+
}
|
|
1567
|
+
};
|
|
1568
|
+
|
|
1569
|
+
// Populate options
|
|
1570
|
+
const populateOptions = () => {
|
|
1571
|
+
if (!field.options || !dropdown) return;
|
|
1572
|
+
|
|
1573
|
+
const options = Array.isArray(field.options) ? field.options :
|
|
1574
|
+
Object.entries(field.options).map(([k, v]) => ({Value: k, DisplayText: v}));
|
|
1575
|
+
|
|
1576
|
+
options.forEach(option => {
|
|
1577
|
+
const optValue = option.Value !== undefined ? option.Value :
|
|
1578
|
+
option.value !== undefined ? option.value : option;
|
|
1579
|
+
|
|
1580
|
+
// Skip if value is empty
|
|
1581
|
+
if (optValue == null || optValue === '') {
|
|
1582
|
+
return; // This continues to the next iteration
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
const optText = option.DisplayText || option.text || option;
|
|
1586
|
+
|
|
1587
|
+
const optionDiv = FTableDOMHelper.create('div', {
|
|
1588
|
+
className: 'ftable-multiselect-option',
|
|
1589
|
+
parent: dropdown
|
|
1590
|
+
});
|
|
1591
|
+
|
|
1592
|
+
const checkbox = FTableDOMHelper.create('input', {
|
|
1593
|
+
type: 'checkbox',
|
|
1594
|
+
className: 'ftable-multiselect-checkbox',
|
|
1595
|
+
checked: selectedValues.has(optValue.toString()),
|
|
1596
|
+
parent: optionDiv
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
// Store checkbox reference
|
|
1600
|
+
checkboxMap.set(optValue.toString(), checkbox);
|
|
1601
|
+
|
|
1602
|
+
const label = FTableDOMHelper.create('label', {
|
|
1603
|
+
className: 'ftable-multiselect-label',
|
|
1604
|
+
textContent: optText,
|
|
1605
|
+
parent: optionDiv
|
|
1606
|
+
});
|
|
1607
|
+
|
|
1608
|
+
// Click anywhere on the option to toggle
|
|
1609
|
+
optionDiv.addEventListener('click', (e) => {
|
|
1610
|
+
e.stopPropagation();
|
|
1611
|
+
|
|
1612
|
+
if (selectedValues.has(optValue.toString())) {
|
|
1613
|
+
selectedValues.delete(optValue.toString());
|
|
1614
|
+
checkbox.checked = false;
|
|
1615
|
+
} else {
|
|
1616
|
+
selectedValues.add(optValue.toString());
|
|
1617
|
+
checkbox.checked = true;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
updateDisplay();
|
|
1621
|
+
});
|
|
1622
|
+
});
|
|
1623
|
+
};
|
|
1624
|
+
|
|
1625
|
+
// Toggle dropdown
|
|
1626
|
+
const toggleDropdown = (e) => {
|
|
1627
|
+
if (e) e.stopPropagation();
|
|
1628
|
+
|
|
1629
|
+
if (dropdown) {
|
|
1630
|
+
// Dropdown is open, close it
|
|
1631
|
+
closeDropdown();
|
|
1632
|
+
} else {
|
|
1633
|
+
// Close any other open multiselect dropdowns
|
|
1634
|
+
document.querySelectorAll('.ftable-multiselect-dropdown').forEach(dd => dd.remove());
|
|
1635
|
+
document.querySelectorAll('.ftable-multiselect-overlay').forEach(ov => ov.remove());
|
|
1636
|
+
|
|
1637
|
+
// Create overlay
|
|
1638
|
+
dropdownOverlay = FTableDOMHelper.create('div', {
|
|
1639
|
+
className: 'ftable-multiselect-overlay',
|
|
1640
|
+
parent: document.body
|
|
1641
|
+
});
|
|
1642
|
+
|
|
1643
|
+
// Create dropdown
|
|
1644
|
+
dropdown = FTableDOMHelper.create('div', {
|
|
1645
|
+
className: 'ftable-multiselect-dropdown',
|
|
1646
|
+
parent: document.body
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1649
|
+
// Populate options
|
|
1650
|
+
populateOptions();
|
|
1651
|
+
|
|
1652
|
+
// Position dropdown
|
|
1653
|
+
positionDropdown();
|
|
1654
|
+
|
|
1655
|
+
// Handle clicks outside
|
|
1656
|
+
dropdownOverlay.addEventListener('click', (event) => {
|
|
1657
|
+
if (event.target === dropdownOverlay) {
|
|
1658
|
+
closeDropdown();
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
// Reposition on scroll/resize
|
|
1663
|
+
const repositionHandler = () => positionDropdown();
|
|
1664
|
+
window.addEventListener('scroll', repositionHandler, true);
|
|
1665
|
+
window.addEventListener('resize', repositionHandler);
|
|
1666
|
+
|
|
1667
|
+
// Store cleanup function
|
|
1668
|
+
container._cleanupHandlers = () => {
|
|
1669
|
+
window.removeEventListener('scroll', repositionHandler, true);
|
|
1670
|
+
window.removeEventListener('resize', repositionHandler);
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
};
|
|
1674
|
+
|
|
1675
|
+
display.addEventListener('click', toggleDropdown);
|
|
1676
|
+
toggleBtn.addEventListener('click', toggleDropdown);
|
|
1677
|
+
|
|
1678
|
+
// Clean up when container is removed from DOM
|
|
1679
|
+
const observer = new MutationObserver((mutations) => {
|
|
1680
|
+
mutations.forEach((mutation) => {
|
|
1681
|
+
mutation.removedNodes.forEach((node) => {
|
|
1682
|
+
if (node === container || node.contains && node.contains(container)) {
|
|
1683
|
+
closeDropdown();
|
|
1684
|
+
observer.disconnect();
|
|
1685
|
+
}
|
|
1686
|
+
});
|
|
1687
|
+
});
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
// Start observing once container is in the DOM
|
|
1691
|
+
setTimeout(() => {
|
|
1692
|
+
if (container.parentNode) {
|
|
1693
|
+
observer.observe(container.parentNode, { childList: true, subtree: true });
|
|
1694
|
+
}
|
|
1695
|
+
}, 0);
|
|
1696
|
+
|
|
1697
|
+
// Initialize
|
|
1698
|
+
populateOptions();
|
|
1699
|
+
updateDisplay();
|
|
1700
|
+
|
|
1701
|
+
return container;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1406
1704
|
createRadioGroup(fieldName, field, value) {
|
|
1407
1705
|
const wrapper = FTableDOMHelper.create('div', {
|
|
1408
1706
|
className: 'ftable-radio-group'
|
|
@@ -1722,9 +2020,6 @@ class FTable extends FTableEventEmitter {
|
|
|
1722
2020
|
this.updateSortingHeaders();
|
|
1723
2021
|
this.renderSortingInfo();
|
|
1724
2022
|
|
|
1725
|
-
// Add essential CSS if not already present
|
|
1726
|
-
//this.addEssentialCSS();
|
|
1727
|
-
|
|
1728
2023
|
// now make sure all tables have a % width
|
|
1729
2024
|
this.initColumnWidths();
|
|
1730
2025
|
}
|
|
@@ -1804,40 +2099,6 @@ class FTable extends FTableEventEmitter {
|
|
|
1804
2099
|
return result;
|
|
1805
2100
|
}
|
|
1806
2101
|
|
|
1807
|
-
addEssentialCSS() {
|
|
1808
|
-
// Check if our CSS is already added
|
|
1809
|
-
if (document.querySelector('#ftable-essential-css')) return;
|
|
1810
|
-
|
|
1811
|
-
const css = `
|
|
1812
|
-
.ftable-row-animation {
|
|
1813
|
-
transition: background-color 0.3s ease;
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
.ftable-row-added {
|
|
1817
|
-
background-color: #d4edda !important;
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
.ftable-row-edited {
|
|
1821
|
-
background-color: #d1ecf1 !important;
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
.ftable-row-deleted {
|
|
1825
|
-
opacity: 0;
|
|
1826
|
-
transform: translateY(-10px);
|
|
1827
|
-
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
1828
|
-
}
|
|
1829
|
-
|
|
1830
|
-
.ftable-toolbarsearch {
|
|
1831
|
-
width: 90%;
|
|
1832
|
-
}
|
|
1833
|
-
`;
|
|
1834
|
-
|
|
1835
|
-
const style = document.createElement('style');
|
|
1836
|
-
style.id = 'ftable-essential-css';
|
|
1837
|
-
style.textContent = css;
|
|
1838
|
-
document.head.appendChild(style);
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
2102
|
createPagingUI() {
|
|
1842
2103
|
this.elements.bottomPanel = FTableDOMHelper.create('div', {
|
|
1843
2104
|
className: 'ftable-bottom-panel',
|
|
@@ -2308,12 +2569,19 @@ class FTable extends FTableEventEmitter {
|
|
|
2308
2569
|
container.appendChild(input.datalistElement);
|
|
2309
2570
|
}
|
|
2310
2571
|
|
|
2311
|
-
|
|
2312
|
-
|
|
2572
|
+
// Handle event listeners - check if it's a custom multiselect container
|
|
2573
|
+
let targetElement = input;
|
|
2574
|
+
if (input.classList && input.classList.contains('ftable-multiselect-container') && input.hiddenSelect) {
|
|
2575
|
+
// It's a custom multiselect - attach listener to the hidden select
|
|
2576
|
+
targetElement = input.hiddenSelect;
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
if (targetElement.tagName === 'SELECT') {
|
|
2580
|
+
targetElement.addEventListener('change', (e) => {
|
|
2313
2581
|
this.handleSearchInputChange(e);
|
|
2314
2582
|
});
|
|
2315
2583
|
} else {
|
|
2316
|
-
|
|
2584
|
+
targetElement.addEventListener('input', (e) => {
|
|
2317
2585
|
this.handleSearchInputChange(e);
|
|
2318
2586
|
});
|
|
2319
2587
|
}
|
|
@@ -2378,12 +2646,6 @@ class FTable extends FTableEventEmitter {
|
|
|
2378
2646
|
}
|
|
2379
2647
|
attributes['data-field-name'] = name;
|
|
2380
2648
|
|
|
2381
|
-
const select = FTableDOMHelper.create('select', {
|
|
2382
|
-
attributes: attributes,
|
|
2383
|
-
id: fieldSearchName,
|
|
2384
|
-
className: 'ftable-toolbarsearch'
|
|
2385
|
-
});
|
|
2386
|
-
|
|
2387
2649
|
let optionsSource;
|
|
2388
2650
|
if (isCheckboxValues && field.values) {
|
|
2389
2651
|
optionsSource = Object.entries(field.values).map(([value, displayText]) => ({
|
|
@@ -2394,6 +2656,24 @@ class FTable extends FTableEventEmitter {
|
|
|
2394
2656
|
optionsSource = await this.formBuilder.getFieldOptions(fieldName);
|
|
2395
2657
|
}
|
|
2396
2658
|
|
|
2659
|
+
// If multiple, create custom UI
|
|
2660
|
+
if (hasMultiple) {
|
|
2661
|
+
return this.createCustomMultiSelectForSearch(
|
|
2662
|
+
fieldSearchName,
|
|
2663
|
+
fieldName,
|
|
2664
|
+
field,
|
|
2665
|
+
optionsSource,
|
|
2666
|
+
attributes
|
|
2667
|
+
);
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
// Standard single select
|
|
2671
|
+
const select = FTableDOMHelper.create('select', {
|
|
2672
|
+
attributes: attributes,
|
|
2673
|
+
id: fieldSearchName,
|
|
2674
|
+
className: 'ftable-toolbarsearch'
|
|
2675
|
+
});
|
|
2676
|
+
|
|
2397
2677
|
// Add empty option only if first option is not already empty
|
|
2398
2678
|
const hasEmptyFirst = optionsSource?.length > 0 &&
|
|
2399
2679
|
(optionsSource[0].Value === '' ||
|
|
@@ -2430,6 +2710,320 @@ class FTable extends FTableEventEmitter {
|
|
|
2430
2710
|
return select;
|
|
2431
2711
|
}
|
|
2432
2712
|
|
|
2713
|
+
createCustomMultiSelectForSearch(fieldSearchName, fieldName, field, optionsSource, attributes) {
|
|
2714
|
+
// Create container
|
|
2715
|
+
const container = FTableDOMHelper.create('div', {
|
|
2716
|
+
className: 'ftable-multiselect-container ftable-multiselect-search ftable-toolbarsearch',
|
|
2717
|
+
attributes: { 'data-field-name': attributes['data-field-name'] }
|
|
2718
|
+
});
|
|
2719
|
+
|
|
2720
|
+
// Create hidden select to maintain compatibility with existing search logic
|
|
2721
|
+
const hiddenSelect = FTableDOMHelper.create('select', {
|
|
2722
|
+
id: fieldSearchName,
|
|
2723
|
+
multiple: true,
|
|
2724
|
+
style: 'display: none;',
|
|
2725
|
+
attributes: attributes
|
|
2726
|
+
});
|
|
2727
|
+
container.appendChild(hiddenSelect);
|
|
2728
|
+
|
|
2729
|
+
// Expose hidden select for external access (needed for event listeners and reset)
|
|
2730
|
+
container.hiddenSelect = hiddenSelect;
|
|
2731
|
+
|
|
2732
|
+
// Create display area
|
|
2733
|
+
const display = FTableDOMHelper.create('div', {
|
|
2734
|
+
className: 'ftable-multiselect-display',
|
|
2735
|
+
parent: container
|
|
2736
|
+
});
|
|
2737
|
+
|
|
2738
|
+
const selectedDisplay = FTableDOMHelper.create('div', {
|
|
2739
|
+
className: 'ftable-multiselect-selected',
|
|
2740
|
+
parent: display
|
|
2741
|
+
});
|
|
2742
|
+
|
|
2743
|
+
const placeholderText = field.searchPlaceholder || field.placeholder || this.options.messages.multiSelectPlaceholder || 'Click to select options...';
|
|
2744
|
+
const placeholder = FTableDOMHelper.create('span', {
|
|
2745
|
+
className: 'ftable-multiselect-placeholder',
|
|
2746
|
+
textContent: placeholderText,
|
|
2747
|
+
parent: selectedDisplay
|
|
2748
|
+
});
|
|
2749
|
+
|
|
2750
|
+
// Create dropdown toggle button
|
|
2751
|
+
const toggleBtn = FTableDOMHelper.create('button', {
|
|
2752
|
+
type: 'button',
|
|
2753
|
+
className: 'ftable-multiselect-toggle',
|
|
2754
|
+
innerHTML: '▼',
|
|
2755
|
+
parent: display
|
|
2756
|
+
});
|
|
2757
|
+
|
|
2758
|
+
// Dropdown and overlay will be created on demand and appended to body
|
|
2759
|
+
let dropdown = null;
|
|
2760
|
+
let dropdownOverlay = null;
|
|
2761
|
+
|
|
2762
|
+
// Store selected values and checkbox references
|
|
2763
|
+
const selectedValues = new Set();
|
|
2764
|
+
const checkboxMap = new Map(); // Map of value -> checkbox element
|
|
2765
|
+
|
|
2766
|
+
// Function to update display and hidden select
|
|
2767
|
+
const updateDisplay = () => {
|
|
2768
|
+
selectedDisplay.innerHTML = '';
|
|
2769
|
+
|
|
2770
|
+
// Update hidden select
|
|
2771
|
+
Array.from(hiddenSelect.options).forEach(opt => {
|
|
2772
|
+
opt.selected = selectedValues.has(opt.value);
|
|
2773
|
+
});
|
|
2774
|
+
|
|
2775
|
+
// Trigger change event on hidden select for search functionality
|
|
2776
|
+
hiddenSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
|
2777
|
+
|
|
2778
|
+
if (selectedValues.size === 0) {
|
|
2779
|
+
placeholder.textContent = placeholderText;
|
|
2780
|
+
selectedDisplay.appendChild(placeholder);
|
|
2781
|
+
} else {
|
|
2782
|
+
const selectedArray = Array.from(selectedValues);
|
|
2783
|
+
const optionsMap = new Map();
|
|
2784
|
+
|
|
2785
|
+
// Build options map
|
|
2786
|
+
if (optionsSource && Array.isArray(optionsSource)) {
|
|
2787
|
+
optionsSource.forEach(opt => {
|
|
2788
|
+
const val = opt.Value !== undefined ? opt.Value :
|
|
2789
|
+
opt.value !== undefined ? opt.value : opt;
|
|
2790
|
+
const text = opt.DisplayText || opt.text || opt;
|
|
2791
|
+
optionsMap.set(val.toString(), text);
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
selectedArray.forEach(val => {
|
|
2796
|
+
const tag = FTableDOMHelper.create('span', {
|
|
2797
|
+
className: 'ftable-multiselect-tag',
|
|
2798
|
+
parent: selectedDisplay
|
|
2799
|
+
});
|
|
2800
|
+
|
|
2801
|
+
FTableDOMHelper.create('span', {
|
|
2802
|
+
className: 'ftable-multiselect-tag-text',
|
|
2803
|
+
textContent: optionsMap.get(val.toString()) || val,
|
|
2804
|
+
parent: tag
|
|
2805
|
+
});
|
|
2806
|
+
|
|
2807
|
+
const removeBtn = FTableDOMHelper.create('span', {
|
|
2808
|
+
className: 'ftable-multiselect-tag-remove',
|
|
2809
|
+
innerHTML: '×',
|
|
2810
|
+
parent: tag
|
|
2811
|
+
});
|
|
2812
|
+
|
|
2813
|
+
removeBtn.addEventListener('click', (e) => {
|
|
2814
|
+
e.stopPropagation();
|
|
2815
|
+
selectedValues.delete(val);
|
|
2816
|
+
// Update the checkbox state
|
|
2817
|
+
const checkbox = checkboxMap.get(val.toString());
|
|
2818
|
+
if (checkbox) {
|
|
2819
|
+
checkbox.checked = false;
|
|
2820
|
+
}
|
|
2821
|
+
updateDisplay();
|
|
2822
|
+
});
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2825
|
+
};
|
|
2826
|
+
|
|
2827
|
+
// Function to close dropdown
|
|
2828
|
+
const closeDropdown = () => {
|
|
2829
|
+
if (dropdown) {
|
|
2830
|
+
dropdown.remove();
|
|
2831
|
+
dropdown = null;
|
|
2832
|
+
}
|
|
2833
|
+
if (dropdownOverlay) {
|
|
2834
|
+
dropdownOverlay.remove();
|
|
2835
|
+
dropdownOverlay = null;
|
|
2836
|
+
}
|
|
2837
|
+
if (container._cleanupHandlers) {
|
|
2838
|
+
container._cleanupHandlers();
|
|
2839
|
+
container._cleanupHandlers = null;
|
|
2840
|
+
}
|
|
2841
|
+
};
|
|
2842
|
+
|
|
2843
|
+
// Function to position dropdown
|
|
2844
|
+
const positionDropdown = () => {
|
|
2845
|
+
if (!dropdown) return;
|
|
2846
|
+
|
|
2847
|
+
const rect = display.getBoundingClientRect();
|
|
2848
|
+
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
2849
|
+
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
|
|
2850
|
+
|
|
2851
|
+
let left = rect.left + scrollLeft;
|
|
2852
|
+
let top = rect.bottom + scrollTop + 4; // 4px gap
|
|
2853
|
+
|
|
2854
|
+
dropdown.style.position = 'absolute';
|
|
2855
|
+
dropdown.style.left = `${left}px`;
|
|
2856
|
+
dropdown.style.top = `${top}px`;
|
|
2857
|
+
dropdown.style.width = `${rect.width}px`;
|
|
2858
|
+
dropdown.style.minWidth = `${rect.width}px`;
|
|
2859
|
+
dropdown.style.boxSizing = 'border-box';
|
|
2860
|
+
dropdown.style.zIndex = '10000';
|
|
2861
|
+
|
|
2862
|
+
// Adjust horizontal position if needed
|
|
2863
|
+
const dropdownRect = dropdown.getBoundingClientRect();
|
|
2864
|
+
const viewportWidth = window.innerWidth;
|
|
2865
|
+
if (dropdownRect.right > viewportWidth) {
|
|
2866
|
+
left = Math.max(10, viewportWidth - dropdownRect.width - 10);
|
|
2867
|
+
dropdown.style.left = `${left}px`;
|
|
2868
|
+
}
|
|
2869
|
+
};
|
|
2870
|
+
|
|
2871
|
+
// Populate options in both hidden select and dropdown
|
|
2872
|
+
const populateOptions = () => {
|
|
2873
|
+
if (!optionsSource || !dropdown) return;
|
|
2874
|
+
|
|
2875
|
+
const options = Array.isArray(optionsSource) ? optionsSource :
|
|
2876
|
+
Object.entries(optionsSource).map(([k, v]) => ({Value: k, DisplayText: v}));
|
|
2877
|
+
|
|
2878
|
+
options.forEach(option => {
|
|
2879
|
+
const optValue = option.Value !== undefined ? option.Value :
|
|
2880
|
+
option.value !== undefined ? option.value : option;
|
|
2881
|
+
|
|
2882
|
+
// Skip if value is empty
|
|
2883
|
+
if (optValue == null || optValue === '') {
|
|
2884
|
+
return; // This continues to the next iteration
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
const optText = option.DisplayText || option.text || option;
|
|
2888
|
+
|
|
2889
|
+
// Add to hidden select (only once)
|
|
2890
|
+
if (!hiddenSelect.querySelector(`option[value="${optValue}"]`)) {
|
|
2891
|
+
FTableDOMHelper.create('option', {
|
|
2892
|
+
value: optValue,
|
|
2893
|
+
textContent: optText,
|
|
2894
|
+
parent: hiddenSelect
|
|
2895
|
+
});
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
// Add to visual dropdown
|
|
2899
|
+
const optionDiv = FTableDOMHelper.create('div', {
|
|
2900
|
+
className: 'ftable-multiselect-option',
|
|
2901
|
+
parent: dropdown
|
|
2902
|
+
});
|
|
2903
|
+
|
|
2904
|
+
const checkbox = FTableDOMHelper.create('input', {
|
|
2905
|
+
type: 'checkbox',
|
|
2906
|
+
className: 'ftable-multiselect-checkbox',
|
|
2907
|
+
parent: optionDiv
|
|
2908
|
+
});
|
|
2909
|
+
|
|
2910
|
+
// Set initial checked state
|
|
2911
|
+
checkbox.checked = selectedValues.has(optValue.toString());
|
|
2912
|
+
|
|
2913
|
+
// Store checkbox reference
|
|
2914
|
+
checkboxMap.set(optValue.toString(), checkbox);
|
|
2915
|
+
|
|
2916
|
+
const label = FTableDOMHelper.create('label', {
|
|
2917
|
+
className: 'ftable-multiselect-label',
|
|
2918
|
+
textContent: optText,
|
|
2919
|
+
parent: optionDiv
|
|
2920
|
+
});
|
|
2921
|
+
|
|
2922
|
+
// Click anywhere on the option to toggle
|
|
2923
|
+
optionDiv.addEventListener('click', (e) => {
|
|
2924
|
+
e.stopPropagation();
|
|
2925
|
+
|
|
2926
|
+
if (selectedValues.has(optValue.toString())) {
|
|
2927
|
+
selectedValues.delete(optValue.toString());
|
|
2928
|
+
checkbox.checked = false;
|
|
2929
|
+
} else {
|
|
2930
|
+
selectedValues.add(optValue.toString());
|
|
2931
|
+
checkbox.checked = true;
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
updateDisplay();
|
|
2935
|
+
});
|
|
2936
|
+
});
|
|
2937
|
+
};
|
|
2938
|
+
|
|
2939
|
+
// Toggle dropdown
|
|
2940
|
+
const toggleDropdown = (e) => {
|
|
2941
|
+
if (e) e.stopPropagation();
|
|
2942
|
+
|
|
2943
|
+
if (dropdown) {
|
|
2944
|
+
// Dropdown is open, close it
|
|
2945
|
+
closeDropdown();
|
|
2946
|
+
} else {
|
|
2947
|
+
// Close any other open multiselect dropdowns
|
|
2948
|
+
document.querySelectorAll('.ftable-multiselect-dropdown').forEach(dd => dd.remove());
|
|
2949
|
+
document.querySelectorAll('.ftable-multiselect-overlay').forEach(ov => ov.remove());
|
|
2950
|
+
|
|
2951
|
+
// Create overlay
|
|
2952
|
+
dropdownOverlay = FTableDOMHelper.create('div', {
|
|
2953
|
+
className: 'ftable-multiselect-overlay',
|
|
2954
|
+
parent: document.body
|
|
2955
|
+
});
|
|
2956
|
+
|
|
2957
|
+
// Create dropdown
|
|
2958
|
+
dropdown = FTableDOMHelper.create('div', {
|
|
2959
|
+
className: 'ftable-multiselect-dropdown',
|
|
2960
|
+
parent: document.body
|
|
2961
|
+
});
|
|
2962
|
+
|
|
2963
|
+
// Populate options
|
|
2964
|
+
populateOptions();
|
|
2965
|
+
|
|
2966
|
+
// Position dropdown
|
|
2967
|
+
positionDropdown();
|
|
2968
|
+
|
|
2969
|
+
// Handle clicks outside
|
|
2970
|
+
dropdownOverlay.addEventListener('click', (event) => {
|
|
2971
|
+
if (event.target === dropdownOverlay) {
|
|
2972
|
+
closeDropdown();
|
|
2973
|
+
}
|
|
2974
|
+
});
|
|
2975
|
+
|
|
2976
|
+
// Reposition on scroll/resize
|
|
2977
|
+
const repositionHandler = () => positionDropdown();
|
|
2978
|
+
window.addEventListener('scroll', repositionHandler, true);
|
|
2979
|
+
window.addEventListener('resize', repositionHandler);
|
|
2980
|
+
|
|
2981
|
+
// Store cleanup function
|
|
2982
|
+
container._cleanupHandlers = () => {
|
|
2983
|
+
window.removeEventListener('scroll', repositionHandler, true);
|
|
2984
|
+
window.removeEventListener('resize', repositionHandler);
|
|
2985
|
+
};
|
|
2986
|
+
}
|
|
2987
|
+
};
|
|
2988
|
+
|
|
2989
|
+
display.addEventListener('click', toggleDropdown);
|
|
2990
|
+
toggleBtn.addEventListener('click', toggleDropdown);
|
|
2991
|
+
|
|
2992
|
+
// Add reset method to container
|
|
2993
|
+
container.resetMultiSelect = () => {
|
|
2994
|
+
selectedValues.clear();
|
|
2995
|
+
checkboxMap.forEach(checkbox => {
|
|
2996
|
+
checkbox.checked = false;
|
|
2997
|
+
});
|
|
2998
|
+
closeDropdown();
|
|
2999
|
+
updateDisplay();
|
|
3000
|
+
};
|
|
3001
|
+
|
|
3002
|
+
// Clean up when container is removed from DOM
|
|
3003
|
+
const observer = new MutationObserver((mutations) => {
|
|
3004
|
+
mutations.forEach((mutation) => {
|
|
3005
|
+
mutation.removedNodes.forEach((node) => {
|
|
3006
|
+
if (node === container || node.contains && node.contains(container)) {
|
|
3007
|
+
closeDropdown();
|
|
3008
|
+
observer.disconnect();
|
|
3009
|
+
}
|
|
3010
|
+
});
|
|
3011
|
+
});
|
|
3012
|
+
});
|
|
3013
|
+
|
|
3014
|
+
// Start observing once container is in the DOM
|
|
3015
|
+
setTimeout(() => {
|
|
3016
|
+
if (container.parentNode) {
|
|
3017
|
+
observer.observe(container.parentNode, { childList: true, subtree: true });
|
|
3018
|
+
}
|
|
3019
|
+
}, 0);
|
|
3020
|
+
|
|
3021
|
+
// Initialize
|
|
3022
|
+
updateDisplay();
|
|
3023
|
+
|
|
3024
|
+
return container;
|
|
3025
|
+
}
|
|
3026
|
+
|
|
2433
3027
|
async createDatalistForSearch(fieldName, field) {
|
|
2434
3028
|
const fieldSearchName = 'ftable-toolbarsearch-' + fieldName;
|
|
2435
3029
|
|
|
@@ -2520,6 +3114,14 @@ class FTable extends FTableEventEmitter {
|
|
|
2520
3114
|
}
|
|
2521
3115
|
});
|
|
2522
3116
|
|
|
3117
|
+
// Clear custom multiselect containers
|
|
3118
|
+
const multiSelectContainers = this.elements.table.querySelectorAll('.ftable-multiselect-container');
|
|
3119
|
+
multiSelectContainers.forEach(container => {
|
|
3120
|
+
if (typeof container.resetMultiSelect === 'function') {
|
|
3121
|
+
container.resetMultiSelect();
|
|
3122
|
+
}
|
|
3123
|
+
});
|
|
3124
|
+
|
|
2523
3125
|
// Reload data without search parameters
|
|
2524
3126
|
this.load();
|
|
2525
3127
|
}
|