Skip to content

Commit 17abb54

Browse files
committed
Eliminating Jquery positioning of menu
1 parent 70f64e7 commit 17abb54

1 file changed

Lines changed: 99 additions & 40 deletions

File tree

lib/editors/KeyValueEditor.js

Lines changed: 99 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
import $ from 'jquery';
21
import Handsontable from 'handsontable';
32
import { MULTIVALUED_DELIMITER} from '../utils/fields';
43
import { isEmptyUnitVal } from '../utils/general';
54

65
// Derived from: https://jsfiddle.net/handsoncode/f0b41jug/
76

87
/**
9-
* The cell type adds supports for displaing the label value except the key in the key-value
8+
* The cell type adds supports for displaying the label value except the key in the key-value
109
* dropdown editor type.
10+
*
11+
* NOTE on filtering: The hiddenRows plugin is used to filter while keeping all source data
12+
* in the inner HOT. If filtering stops working after an updateSettings call, call
13+
* dropdownHotInstance.getPlugin('hiddenRows').enablePlugin() to re-enable the plugin.
1114
*/
1215
export default class KeyValueListEditor extends Handsontable.editors
1316
.HandsontableEditor {
@@ -26,38 +29,44 @@ export default class KeyValueListEditor extends Handsontable.editors
2629
prepare(row, col, prop, td, value, cellProperties) {
2730
super.prepare(row, col, prop, td, value, cellProperties);
2831
let self = this;
29-
this.MENU_HEIGHT = 200;
3032

3133
function filter(event) {
32-
const text = event.srcElement.value.toLowerCase();// word typed so far.
34+
const text = (event.target || event.srcElement).value.toLowerCase();
3335
const hide = [];
3436
const show = [];
35-
let count = 0;
3637
self.htOptions.data.forEach((row, index) => {
37-
count += 1;
38-
if (row.label.toLowerCase().includes(text)) // && count < 13
38+
if (row.label.toLowerCase().includes(text))
3939
show.push(index);
4040
else
4141
hide.push(index);
4242
});
4343
self.hiddenRowsPlugin.showRows(show);
4444
self.hiddenRowsPlugin.hideRows(hide);
45-
self.dropdownHotInstance.render();
46-
}
4745

48-
// Setting for pulldown menu display
46+
if (self._flipAbove && self.htContainer) {
47+
const visibleCount = show.length;
48+
const newH = Math.max(23, Math.min(visibleCount * 23, self._dropdownMaxH));
49+
self.dropdownHotInstance.updateSettings({ height: newH });
50+
// Re-apply hidden rows in case updateSettings reset the plugin state
51+
self.hiddenRowsPlugin.showRows(show);
52+
self.hiddenRowsPlugin.hideRows(hide);
53+
self.dropdownHotInstance.render();
54+
self.htContainer.style.top = `${self._cellTop - newH}px`;
55+
} else {
56+
self.dropdownHotInstance.render();
57+
}
58+
}
4959

50-
// Adding dynamic filter. DOM element textarea.handsontableInput. This is
51-
// relative to event's TEXTAREA since there are other textareas around
52-
// Done as an onkeyup() since its reset each time user visits a table cell.
53-
this.TEXTAREA.onkeyup = filter;
60+
// Adding dynamic filter. Done as oninput since it only fires when the value actually
61+
// changes (not on arrow/modifier keys), and is reset each time user visits a table cell.
62+
this.TEXTAREA.oninput = filter;
5463

5564
Object.assign(this.htOptions, {
5665
licenseKey: 'non-commercial-and-evaluation',
5766
data: this.cellProperties.source,
5867
rowHeaders: false,
59-
colWidths: 250,
60-
height: this.MENU_HEIGHT,
68+
colWidths: 250,
69+
height: 200, // Initial height; overridden by open() based on available space.
6170
columns: [{data: '_id'},{data: 'label'}],
6271
hiddenColumns: {columns: [0]},
6372
hiddenRows: {rows: []},
@@ -90,34 +99,84 @@ export default class KeyValueListEditor extends Handsontable.editors
9099

91100
}
92101

93-
// Done once each time user clicks on cell and menu is displayed.
94-
focus() {
102+
/**
103+
* Opens the dropdown, computing height and position based on available viewport space.
104+
* Phase 1: sets htOptions.height before super.open() constructs the inner HOT instance.
105+
* Phase 2: after render, flips above the cell using position:fixed if space below is tight.
106+
*/
107+
open() {
108+
const ROW_HEIGHT = 23;
109+
const MAX_ROWS = 10;
110+
111+
// Reset previous state
112+
this._flipAbove = false;
113+
this._cellTop = 0;
114+
this._dropdownMaxH = 0;
115+
if (this.htContainer) {
116+
this.htContainer.style.transform = '';
117+
this.htContainer.style.position = '';
118+
this.htContainer.style.top = '';
119+
this.htContainer.style.left = '';
120+
this.htContainer.style.width = '';
121+
this.htContainer.style.zIndex = '';
122+
}
123+
124+
// Phase 1: compute height before inner HOT is constructed by super.open()
125+
if (this.TD && this.cellProperties) {
126+
const cellRect = this.TD.getBoundingClientRect();
127+
const viewH = window.innerHeight || document.documentElement.clientHeight;
128+
const hotBottom = this.hot.rootElement
129+
? this.hot.rootElement.getBoundingClientRect().bottom : viewH;
130+
const effectiveBottom = Math.min(viewH, hotBottom);
131+
const spaceBelow = effectiveBottom - cellRect.bottom;
132+
const spaceAbove = cellRect.top;
133+
const wantedH = Math.min(
134+
(this.cellProperties.source || []).length, MAX_ROWS
135+
) * ROW_HEIGHT;
136+
const willOpenAbove = spaceBelow < wantedH && spaceAbove > spaceBelow;
137+
const available = willOpenAbove ? spaceAbove : spaceBelow;
138+
const dropdownH = Math.max(ROW_HEIGHT, Math.min(wantedH, available - 4));
139+
this._dropdownMaxH = dropdownH;
140+
this.htOptions.height = dropdownH;
141+
}
95142

143+
super.open();
144+
145+
// Phase 2: flip above cell with position:fixed if space below is insufficient.
146+
// Use actual rendered height from getBoundingClientRect() (forces a synchronous layout
147+
// reflow) rather than the Phase 1 estimate, so positioning is accurate on first paint.
148+
if (this.TD && this.htContainer) {
149+
const actualH = this.htContainer.getBoundingClientRect().height;
150+
if (actualH > 0) {
151+
const cellRect = this.TD.getBoundingClientRect();
152+
const viewH = window.innerHeight || document.documentElement.clientHeight;
153+
const hotBottom = this.hot.rootElement
154+
? this.hot.rootElement.getBoundingClientRect().bottom : viewH;
155+
const effectiveBottom = Math.min(viewH, hotBottom);
156+
const spaceBelow = effectiveBottom - cellRect.bottom;
157+
158+
if (spaceBelow < actualH && cellRect.top > spaceBelow) {
159+
this._flipAbove = true;
160+
this._cellTop = cellRect.top;
161+
this._dropdownMaxH = actualH; // update to actual for filter resize path
162+
const w = this.htContainer.offsetWidth || 250;
163+
this.htContainer.style.position = 'fixed';
164+
this.htContainer.style.zIndex = '99999';
165+
this.htContainer.style.width = `${w}px`;
166+
this.htContainer.style.left = `${cellRect.left}px`;
167+
this.htContainer.style.top = `${cellRect.top - actualH}px`;
168+
}
169+
}
170+
}
171+
}
172+
173+
// Done once each time user clicks on cell and menu is displayed.
174+
focus() {
96175
super.focus();
97176

98-
// Helpers for autocomplete .filter() show/hide of rows:
177+
// Helpers for filter() show/hide of rows via hiddenRows plugin:
99178
this.dropdownHotInstance = this.hot.getActiveEditor().htEditor;
100179
this.hiddenRowsPlugin = this.dropdownHotInstance.getPlugin('hiddenRows');
101-
102-
// This section is to fix a handsontable bug where autocomplete/ dropdown
103-
// menus don't display if at bottom of a menu.
104-
this.menu = this.TEXTAREA.nextElementSibling;
105-
const menu_height = $(this.menu).height();
106-
107-
this.input_top = $(this.TEXTAREA).offset().top;
108-
this.input_height = $(this.TEXTAREA).height();
109-
const dh_height = $('#data-harmonizer-grid').height();
110-
const dh_top = $('#data-harmonizer-grid').offset().top;
111-
// Flip menu to top of input area if it would otherwise extend below DH table area.
112-
this.menu_above = (dh_height + dh_top) < (this.input_top + menu_height);
113-
// This is the starting height of the menu.
114-
if (this.menu_above)
115-
$('div.handsontableEditor').css('top', '-' + menu_height + 'px');
116-
else
117-
$('div.handsontableEditor').css('top', '0px');
118-
// Imposes table.htCoreposition: fixed in data-harmonizer.css :
119-
$(this.menu).toggleClass('menu-above', this.menu_above);
120-
121180
}
122181

123182
/**
@@ -223,7 +282,7 @@ export const multiKeyValueListRenderer = function (hot, TD, row, col, prop, valu
223282
.join(MULTIVALUED_DELIMITER);
224283
}
225284

226-
// This directly sets what is displayed in the cell on render()
285+
// This directly sets what is displayed in the cell on render()
227286
// Uses the label as the display value but keep the _id as the stored value
228287
TD.innerHTML = `<div class="htAutocompleteArrow">▼</div>${label}`;
229288
//}

0 commit comments

Comments
 (0)