Skip to content

Commit 7a59215

Browse files
committed
fixes multiselect menu issue
1 parent 35e2342 commit 7a59215

2 files changed

Lines changed: 940 additions & 52 deletions

File tree

lib/DataHarmonizer.js

Lines changed: 37 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,26 @@ class DataHarmonizer {
643643

644644
//console.log("afterSelectionEnd",self.schema, row, column, row2, column2, selectionLayerLevel)
645645

646-
// This is getting triggered twice? Trap and exit
646+
/* This is getting triggered twice. Trap and exit.
647+
* Claude explanation: Not DOM event bubbling — Handsontable's hook system is internal, entirely separate from the DOM event system. The double-fire is caused by Handsontable's own plugin system re-triggering the selection hook as a side effect of the work done inside the handler.
648+
649+
The causal chain:
650+
1. User clicks a cell → Handsontable fires afterSelectionEnd
651+
2. Handler detects a row change → calls crudCalculateDependentKeys
652+
3. That leads (through refreshTabDisplay) to tabFilter on dependent DH instances
653+
4. tabFilter calls, in sequence:
654+
- filtersPlugin.clearConditions() — clears any active filter conditions
655+
- hiddenRowsPlugin.showRows(shown) / hideRows(hidden) — changes the visible row set
656+
- dh.hot.resumeExecution() — flushes all queued plugin operations at once
657+
- dh.render() — redraws the table
658+
The resumeExecution() flush is what causes it. When Handsontable flushes queued operations after hiding/showing rows, it internally re-validates and re-applies the current selection
659+
to account for the fact that visual row indices may have shifted. That re-validation fires afterSelectionEnd again on the same instance with identical coordinates — same row, column,row2, column2.
660+
The suspendExecution() / resumeExecution() pattern is recommended by Handsontable for batching plugin operations, but it comes with the side effect that the selection hooks can re-fire during the flush. It is specifically not the filtersPlugin.clearConditions() call alone, nor the hide/show calls alone — it's the combination flushed together via
661+
resumeExecution().
662+
663+
The deduplication guard in the handler (current_selection coordinate comparison → early return) is the correct way to handle this. It's a well-known Handsontable quirk with the
664+
HiddenRows + Filters plugin combination.
665+
*/
647666
if (row === self.current_selection[0]
648667
&& column === self.current_selection[1]
649668
&& row2 === self.current_selection[2]
@@ -1597,29 +1616,6 @@ class DataHarmonizer {
15971616
let meta = dh.getColumnMeta(col);
15981617
if (meta.source && meta.meta?.datatype === 'multivalued') {
15991618

1600-
/*
1601-
const value = dh.getDataAtCell(row, col);
1602-
const selections = parseMultivaluedValue(value);
1603-
const formattedValue = formatMultivaluedValue(selections);
1604-
1605-
// Cleanup of empty values that can occur with user editing in popup leading/trailing or double ";"
1606-
if (value !== formattedValue) {
1607-
dh.setDataAtCell(row, col, formattedValue, 'prevalidate');
1608-
}
1609-
let content = '';
1610-
Object.values(meta.source).forEach(choice => {
1611-
const {label, value} = choice; // unpacking
1612-
let selected = selections.includes(value)
1613-
? 'selected="selected"'
1614-
: '';
1615-
content += `<option value="${value}" ${selected}'>${label}</option>`;
1616-
});
1617-
1618-
$('#field-description-text').html(
1619-
`<span>${self.slots[col].title}</span>
1620-
<select multiple class="multiselect" rows="15">${content}</select>`
1621-
);
1622-
*/
16231619
$('#multiselect-text').html(
16241620
`<span>${self.slots[col].title}</span>
16251621
<select multiple class="multiselect" rows="15"></select>`
@@ -1635,15 +1631,11 @@ class DataHarmonizer {
16351631
items: dh.getDataAtCell(row, col)?.split(MULTIVALUED_DELIMITER) || [],
16361632

16371633
hideSelected: false,
1634+
// Required for cursor to appear after selection, otherwise user is left
1635+
// unable to click within selected items to remove one or more.
16381636
onChange: function() {
1639-
this.focus(); // required for cursor
1640-
},
1641-
/*
1642-
onBlur: function(e, dest) {alert('blurring')}, // works
1643-
onDelete: function(values) { // works
1644-
return confirm(values = array of deleted items);
1637+
this.focus();
16451638
},
1646-
*/
16471639
render: {
16481640
// This is the label shown for the selected item in the popup
16491641
// input control.
@@ -1663,30 +1655,23 @@ class DataHarmonizer {
16631655
},
16641656
}) // must be rendered when html is visible
16651657
// See https://selectize.dev/docs/events
1666-
/*.on('destroy', ...) not working because .destroy() never fired.
1667-
1668-
// Problem: change fires 'beforeChange' on each setDataAtCell
1669-
.on('change', function () {
1670-
let newValCsv = formatMultivaluedValue(
1671-
$('#field-description-text .multiselect').val()
1672-
);
1673-
dh.setDataAtCell(row, col, newValCsv, 'multiselect_add');
1674-
});
1675-
*/
16761658

1677-
// HACK TO GET A SINGLE beforeChange event on "OK" of .selectize
1678-
// so that validation happens only once on updated multiselect items.
1679-
// This is key to saving selected content.
1680-
$('#multiselect-modal button[data-dismiss]').off().on('click', function () {
1681-
let newValCsv = formatMultivaluedValue(
1682-
$('#multiselect-text .multiselect').val()
1683-
);
1684-
dh.setDataAtCell(row, col, newValCsv, 'multiselect_change');
1685-
})
1659+
// HACK TO GET A SINGLE beforeChange event on "OK" of .selectize
1660+
// so that validation happens only once on updated multiselect items.
1661+
// This is key to saving selected content.
1662+
$('#multiselect-modal button[data-dismiss]').off().on('click', function () {
1663+
let newValCsv = formatMultivaluedValue(
1664+
$('#multiselect-text .multiselect').val()
1665+
);
1666+
dh.setDataAtCell(row, col, newValCsv, 'multiselect_change');
1667+
})
16861668

16871669
$('#multiselect-modal').modal('show');
1688-
// Automatically opens menu below input box
1689-
$('#multiselect-text .multiselect')[0].selectize.focus();
1670+
$('#multiselect-text select')[0].selectize.open();
1671+
// hide().slideDown() briefly removes the dropdown from interaction,
1672+
// preventing coincidental item selection if the cursor is already over
1673+
// the area where the dropdown renders.
1674+
$('#multiselect-text div.selectize-dropdown').hide().slideDown("fast");
16901675
}
16911676
},
16921677
});

0 commit comments

Comments
 (0)