1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 | "use strict" ;
|
28 |
|
29 |
|
30 |
|
31 | var Promise = require( 'seventh' ) ;
|
32 |
|
33 |
|
34 |
|
35 | var defaultKeyBindings = {
|
36 | ENTER: 'submit' ,
|
37 | KP_ENTER: 'submit' ,
|
38 |
|
39 |
|
40 |
|
41 |
|
42 | UP: 'previous' ,
|
43 | DOWN: 'next' ,
|
44 | LEFT: 'previousColumn' ,
|
45 | RIGHT: 'nextColumn' ,
|
46 | TAB: 'cycleNext' ,
|
47 | SHIFT_TAB: 'cyclePrevious' ,
|
48 | HOME: 'first' ,
|
49 | END: 'last'
|
50 | } ;
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 | module.exports = function gridMenu( menuItems_ , options , callback ) {
|
81 | if ( arguments.length < 1 ) { throw new Error( '[terminal] gridMenu() needs at least an array of menuItems argument' ) ; }
|
82 |
|
83 | if ( ! Array.isArray( menuItems_ ) || ! menuItems_.length ) { throw new TypeError( '[terminal] gridMenu(): argument #0 should be a non-empty array' ) ; }
|
84 |
|
85 | if ( typeof options === 'function' ) { callback = options ; options = {} ; }
|
86 | else if ( ! options || typeof options !== 'object' ) { options = {} ; }
|
87 |
|
88 | if ( ! options.style ) { options.style = this ; }
|
89 | if ( ! options.selectedStyle ) { options.selectedStyle = this.inverse ; }
|
90 |
|
91 | if ( options.leftPadding === undefined ) { options.leftPadding = ' ' ; }
|
92 | if ( options.selectedLeftPadding === undefined ) { options.selectedLeftPadding = ' ' ; }
|
93 | if ( options.rightPadding === undefined ) { options.rightPadding = ' ' ; }
|
94 | if ( options.selectedRightPadding === undefined ) { options.selectedRightPadding = ' ' ; }
|
95 |
|
96 | if ( ! options.x ) { options.x = 1 ; }
|
97 |
|
98 | if ( ! options.y ) { this( '\n' ) ; }
|
99 | else { this.moveTo( options.x , options.y ) ; }
|
100 |
|
101 | if ( ! options.width ) { options.width = this.width - options.x + 1 ; }
|
102 |
|
103 |
|
104 | if ( ! options.itemMaxWidth ) { options.itemMaxWidth = Math.floor( ( options.width - 1 ) / 3 ) ; }
|
105 |
|
106 | var keyBindings = options.keyBindings || defaultKeyBindings ;
|
107 |
|
108 | if ( ! this.grabbing ) { this.grabInput() ; }
|
109 |
|
110 |
|
111 | var start = {} , selectedIndex = 0 , finished = false , alreadyCleanedUp = false ,
|
112 | itemInnerWidth = 0 , itemOuterWidth = 0 ,
|
113 | menuItems , columns , rows , padLength ;
|
114 |
|
115 | padLength = Math.max( options.leftPadding.length , options.selectedLeftPadding.length ) +
|
116 | Math.max( options.rightPadding.length , options.selectedRightPadding.length ) ;
|
117 |
|
118 | menuItems_ = menuItems_.map( element => {
|
119 | if ( typeof element !== 'string' ) { element = '' + element ; }
|
120 | itemInnerWidth = Math.max( itemInnerWidth , element.length ) ;
|
121 | return element ;
|
122 | } ) ;
|
123 |
|
124 | itemInnerWidth = Math.min( itemInnerWidth , options.itemMaxWidth - padLength ) ;
|
125 | itemOuterWidth = itemInnerWidth + padLength ;
|
126 | columns = Math.floor( options.width / itemOuterWidth ) ;
|
127 | rows = Math.ceil( menuItems_.length / columns ) ;
|
128 |
|
129 | menuItems = menuItems_.map( ( element , index ) => ( {
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 | offsetY: index % rows ,
|
136 | offsetX: options.x - 1 + Math.floor( index / rows ) * itemOuterWidth ,
|
137 |
|
138 | index: index ,
|
139 | text: element ,
|
140 | displayText: element.length > itemInnerWidth ?
|
141 | element.slice( 0 , itemInnerWidth - 1 ) + '…' :
|
142 | element + ' '.repeat( itemInnerWidth - element.length )
|
143 | } ) ) ;
|
144 |
|
145 |
|
146 |
|
147 |
|
148 | var cleanup = ( error , data ) => {
|
149 | if ( alreadyCleanedUp ) { return ; }
|
150 | alreadyCleanedUp = true ;
|
151 |
|
152 | finished = true ;
|
153 | this.removeListener( 'key' , onKey ) ;
|
154 | this.removeListener( 'mouse' , onMouse ) ;
|
155 | this.moveTo( 1 , start.y + rows ) ;
|
156 |
|
157 | if ( error ) {
|
158 | if ( callback ) { callback( error ) ; }
|
159 | else { controller.promise.reject( error ) ; }
|
160 | return ;
|
161 | }
|
162 |
|
163 | var value = data !== undefined ? data : {
|
164 | selectedIndex: selectedIndex ,
|
165 | selectedText: menuItems[ selectedIndex ].text ,
|
166 | x: 1 + menuItems[ selectedIndex ].offsetX ,
|
167 | y: start.y + menuItems[ selectedIndex ].offsetY
|
168 | } ;
|
169 |
|
170 | if ( callback ) { callback( undefined , value ) ; }
|
171 | else { controller.promise.resolve( value ) ; }
|
172 | } ;
|
173 |
|
174 |
|
175 | var redraw = () => {
|
176 | for ( var i = 0 ; i < menuItems.length ; i ++ ) { redrawItem( i ) ; }
|
177 | redrawCursor() ;
|
178 | } ;
|
179 |
|
180 | var redrawItem = ( index ) => {
|
181 | var item = menuItems[ index ] ;
|
182 |
|
183 | this.moveTo( 1 + item.offsetX , start.y + item.offsetY ) ;
|
184 |
|
185 | if ( index === selectedIndex ) {
|
186 | options.selectedStyle.noFormat( options.selectedLeftPadding ) ;
|
187 | options.selectedStyle.noFormat( item.displayText ) ;
|
188 | options.selectedStyle.noFormat( options.selectedRightPadding ) ;
|
189 | }
|
190 | else {
|
191 | options.style.noFormat( options.leftPadding ) ;
|
192 | options.style.noFormat( item.displayText ) ;
|
193 | options.style.noFormat( options.rightPadding ) ;
|
194 | }
|
195 | } ;
|
196 |
|
197 | var redrawCursor = () => {
|
198 | this.moveTo( 1 + menuItems[ selectedIndex ].offsetX , start.y + menuItems[ selectedIndex ].offsetY ) ;
|
199 | } ;
|
200 |
|
201 |
|
202 | var onKey = ( key , trash , data ) => {
|
203 | if ( finished ) { return ; }
|
204 |
|
205 | var oldSelectedIndex = selectedIndex ;
|
206 |
|
207 | switch( keyBindings[ key ] ) {
|
208 | case 'submit' :
|
209 | cleanup() ;
|
210 | break ;
|
211 |
|
212 | case 'previous' :
|
213 | if ( selectedIndex > 0 ) {
|
214 | selectedIndex -- ;
|
215 | redrawItem( selectedIndex ) ;
|
216 | redrawItem( selectedIndex + 1 ) ;
|
217 | redrawCursor() ;
|
218 |
|
219 | }
|
220 | break ;
|
221 |
|
222 | case 'next' :
|
223 | if ( selectedIndex < menuItems.length - 1 ) {
|
224 | selectedIndex ++ ;
|
225 | redrawItem( selectedIndex - 1 ) ;
|
226 | redrawItem( selectedIndex ) ;
|
227 | redrawCursor() ;
|
228 |
|
229 | }
|
230 | break ;
|
231 | |
232 |
|
233 |
|
234 |
|
235 |
|
236 |
|
237 |
|
238 |
|
239 |
|
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
245 |
|
246 |
|
247 |
|
248 |
|
249 |
|
250 |
|
251 |
|
252 |
|
253 |
|
254 | case 'previousColumn' :
|
255 | if ( selectedIndex >= rows ) {
|
256 | selectedIndex -= rows ;
|
257 | redrawItem( oldSelectedIndex ) ;
|
258 | redrawItem( selectedIndex ) ;
|
259 | redrawCursor() ;
|
260 |
|
261 | }
|
262 | break ;
|
263 |
|
264 | case 'nextColumn' :
|
265 | if ( selectedIndex < menuItems.length - rows ) {
|
266 | selectedIndex += rows ;
|
267 | redrawItem( oldSelectedIndex ) ;
|
268 | redrawItem( selectedIndex ) ;
|
269 | redrawCursor() ;
|
270 |
|
271 | }
|
272 | break ;
|
273 |
|
274 | case 'cyclePrevious' :
|
275 | selectedIndex -- ;
|
276 |
|
277 | if ( selectedIndex < 0 ) { selectedIndex = menuItems.length - 1 ; }
|
278 |
|
279 | redrawItem( oldSelectedIndex ) ;
|
280 | redrawItem( selectedIndex ) ;
|
281 | redrawCursor() ;
|
282 |
|
283 | break ;
|
284 |
|
285 | case 'cycleNext' :
|
286 | selectedIndex ++ ;
|
287 |
|
288 | if ( selectedIndex >= menuItems.length ) { selectedIndex = 0 ; }
|
289 |
|
290 | redrawItem( oldSelectedIndex ) ;
|
291 | redrawItem( selectedIndex ) ;
|
292 | redrawCursor() ;
|
293 |
|
294 | break ;
|
295 |
|
296 | case 'first' :
|
297 | if ( selectedIndex !== 0 ) {
|
298 | selectedIndex = 0 ;
|
299 | redrawItem( oldSelectedIndex ) ;
|
300 | redrawItem( selectedIndex ) ;
|
301 | redrawCursor() ;
|
302 |
|
303 | }
|
304 | break ;
|
305 |
|
306 | case 'last' :
|
307 | if ( selectedIndex !== menuItems.length - 1 ) {
|
308 | selectedIndex = menuItems.length - 1 ;
|
309 | redrawItem( oldSelectedIndex ) ;
|
310 | redrawItem( selectedIndex ) ;
|
311 | redrawCursor() ;
|
312 |
|
313 | }
|
314 | break ;
|
315 |
|
316 | default :
|
317 | if ( options.exitOnUnexpectedKey ) {
|
318 | cleanup( undefined , { unexpectedKey: key , unexpectedKeyData: data } ) ;
|
319 | }
|
320 | break ;
|
321 | }
|
322 | } ;
|
323 |
|
324 |
|
325 | var onMouse = ( name , data ) => {
|
326 |
|
327 | if ( finished ) { return ; }
|
328 |
|
329 |
|
330 | if ( data.y < start.y || data.y >= start.y + rows ) { return ; }
|
331 |
|
332 | var i , inBounds = false ,
|
333 | oldSelectedIndex = selectedIndex ;
|
334 |
|
335 | for ( i = 0 ; i < menuItems.length ; i ++ ) {
|
336 | if (
|
337 | data.y === start.y + menuItems[ i ].offsetY &&
|
338 | data.x >= 1 + menuItems[ i ].offsetX &&
|
339 | data.x < 1 + menuItems[ i ].offsetX + itemOuterWidth
|
340 | ) {
|
341 | inBounds = true ;
|
342 |
|
343 | if ( selectedIndex !== i ) {
|
344 | selectedIndex = i ;
|
345 | redrawItem( oldSelectedIndex ) ;
|
346 | redrawItem( selectedIndex ) ;
|
347 | redrawCursor() ;
|
348 | }
|
349 |
|
350 | break ;
|
351 | }
|
352 | }
|
353 |
|
354 | if ( inBounds && name === 'MOUSE_LEFT_BUTTON_PRESSED' ) {
|
355 | cleanup() ;
|
356 | }
|
357 | } ;
|
358 |
|
359 |
|
360 | this.getCursorLocation( ( error , x , y ) => {
|
361 | if ( error ) {
|
362 |
|
363 |
|
364 |
|
365 | this.row.eraseLineAfter( this.height )( '\n' ) ;
|
366 | x = 1 ;
|
367 | y = this.height ;
|
368 | }
|
369 |
|
370 | start.x = x ;
|
371 | start.y = y ;
|
372 |
|
373 | var extra = start.y + rows - this.height ;
|
374 |
|
375 | if ( extra > 0 ) {
|
376 |
|
377 | this( '\n'.repeat( extra ) ) ;
|
378 | start.y -= extra ;
|
379 | }
|
380 |
|
381 | redraw() ;
|
382 |
|
383 | this.on( 'key' , onKey ) ;
|
384 | if ( this.mouseGrabbing ) { this.on( 'mouse' , onMouse ) ; }
|
385 | } ) ;
|
386 |
|
387 |
|
388 | var controller = {} ;
|
389 |
|
390 | controller.promise = new Promise() ;
|
391 |
|
392 | return controller ;
|
393 | } ;
|
394 |
|