1- import $ from 'jquery' ;
21import Handsontable from 'handsontable' ;
32import { MULTIVALUED_DELIMITER } from '../utils/fields' ;
43import { 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 */
1215export 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