UNPKG

20.8 kBJavaScriptView Raw
1/*
2 Terminal Kit
3
4 Copyright (c) 2009 - 2020 Cédric Ronvel
5
6 The MIT License (MIT)
7
8 Permission is hereby granted, free of charge, to any person obtaining a copy
9 of this software and associated documentation files (the "Software"), to deal
10 in the Software without restriction, including without limitation the rights
11 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 copies of the Software, and to permit persons to whom the Software is
13 furnished to do so, subject to the following conditions:
14
15 The above copyright notice and this permission notice shall be included in all
16 copies or substantial portions of the Software.
17
18 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 SOFTWARE.
25*/
26
27"use strict" ;
28
29
30
31const termkit = require( './termkit' ) ;
32const stringWidth = termkit.stringWidth ;
33const string = require( 'string-kit' ) ;
34const NextGenEvents = require( 'nextgen-events' ) ;
35const Promise = require( 'seventh' ) ;
36
37
38
39const defaultKeyBindings = {
40 ENTER: 'submit' ,
41 KP_ENTER: 'submit' ,
42 UP: 'previous' ,
43 DOWN: 'next' ,
44 TAB: 'cycleNext' ,
45 SHIFT_TAB: 'cyclePrevious' ,
46 HOME: 'first' ,
47 END: 'last' ,
48 BACKSPACE: 'cancel' ,
49 DELETE: 'cancel' ,
50 ESCAPE: 'escape'
51} ;
52
53
54
55/*
56 singleColumnMenu( menuItems , [options] , callback )
57 * menuItems `array` of menu item text
58 * options `object` of options, where:
59 * y `number` the line where the menu will be displayed, default to the next line
60 * style `function` the style of unselected items, default to `term`
61 * selectedStyle `function` the style of the selected item, default to `term.inverse`
62 * submittedStyle `function` the style of the submitted item, default to `term.bgGray.bold`
63 * disabledStyle `function` the style of unselected items **when the menu is paused/disabled**,
64 default to `term.dim`
65 * disabledSelectedStyle `function` the style of the selected item **when the menu is paused/disabled**,
66 default to `term.bgGray.dim`
67 * disabledSubmittedStyle `function` the style of the submitted item **when the menu is paused/disabled**,
68 default to `term.bgGray`
69 * leftPadding `string` the text to put before a menu item, default to ' '
70 * selectedLeftPadding `string` the text to put before a selected menu item, default to ' '
71 * submittedLeftPadding `string` the text to put before a submitted menu item, default to ' '
72 * extraLines `number` ensure that many lines after the bottom of the menu
73 * oneLineItem `boolean` if true (default: false), big items do not span multiple lines, instead they are truncated
74 and ended with an ellipsis char
75 * itemMaxWidth `number` the max width for an item, default to the terminal width
76 * continueOnSubmit `boolean` if true, the submit action does not end the menu, the callback argument is ignored.
77 The 'submit' event should be listened instead.
78 * selectedIndex `number` selected index at initialization (default: 0)
79 * unsubmittableIndexes `Array` of `boolean` indexes that are not submittable (default: [])
80 * submitted `boolean` if true, selected index is already submitted at initialization (default: false)
81 * paused `boolean` (default: false) true if the menu start in paused/disabled mode
82 * scrollRegionBottom `number` if set, it indicates the bottom line of the current scroll region
83 * keyBindings `Object` overide default key bindings
84 * cancelable `boolean` if ESCAPE is pressed, it exits, calling the callback with undefined values
85 * exitOnUnexpectedKey `boolean` if an unexpected key is pressed, it exits, calling the callback with undefined values
86 * callback( error , response ), where:
87 * error
88 * response `Object` where:
89 * selectedIndex `number` the user-selected menu item index
90 * selectedText `string` the user-selected menu item text
91 * x `number` the x coordinate of the selected menu item (the first character)
92 * y `number` the y coordinate of the selected menu item
93 * unexpectedKey `string` when 'exitOnUnexpectedKey' option is set, this contains the key that produced the exit
94 * canceled `bool` when 'cancelable' option is set, this is set to true
95*/
96module.exports = function singleColumnMenu( menuItemsArg , options , callback ) {
97 if ( arguments.length < 1 ) { throw new Error( '[terminal] singleColumnMenu() needs at least an array of menuItems' ) ; }
98 if ( ! Array.isArray( menuItemsArg ) || ! menuItemsArg.length ) { throw new TypeError( '[terminal] singleColumnMenu(): argument #0 should be a non-empty array' ) ; }
99
100 if ( typeof options === 'function' ) { callback = options ; options = {} ; }
101 else if ( ! options || typeof options !== 'object' ) { options = {} ; }
102
103 if ( ! options.style ) { options.style = this ; }
104 if ( ! options.selectedStyle ) { options.selectedStyle = this.inverse ; }
105 if ( ! options.submittedStyle ) { options.submittedStyle = this.bgGray.bold ; }
106 if ( ! options.disabledStyle ) { options.disabledStyle = this.dim ; }
107 if ( ! options.disabledSelectedStyle ) { options.disabledSelectedStyle = this.bgGray.dim ; }
108 if ( ! options.disabledSubmittedStyle ) { options.disabledSubmittedStyle = this.bgGray ; }
109
110 if ( options.leftPadding === undefined ) { options.leftPadding = ' ' ; }
111 if ( options.selectedLeftPadding === undefined ) { options.selectedLeftPadding = options.leftPadding ; }
112 if ( options.submittedLeftPadding === undefined ) { options.submittedLeftPadding = options.leftPadding ; }
113
114 if ( typeof options.extraLines !== 'number' || options.extraLines < 0 ) { options.extraLines = 1 ; }
115
116 if ( ! options.itemMaxWidth ) { options.itemMaxWidth = this.width - 1 ; }
117
118 if ( ! options.unsubmittableIndexes ) { options.unsubmittableIndexes = [] ; }
119
120 var selectedIndex = options.selectedIndex || 0 ;
121 var submittedIndex = options.submitted ? options.selectedIndex : null ;
122 var paused = !! options.paused ;
123
124 var keyBindings = options.keyBindings || defaultKeyBindings ;
125
126 if ( ! this.grabbing ) { this.grabInput() ; }
127
128
129 var start = {} , end = {} , textWidth , outerWidth , paddingLength ,
130 menuItems , offsetY = 0 , lineCount = 0 , scrollLines = 0 ,
131 controller , finished = false , alreadyCleanedUp = false ;
132
133
134
135 // Functions...
136
137
138
139 var init = () => {
140 computeItems( menuItemsArg ) ;
141
142 if ( options.y !== undefined ) {
143 this.moveTo( 1 , options.y ) ;
144 finishInit( 1 , options.y ) ;
145 }
146 else {
147 this( '\n' ) ;
148 this.getCursorLocation( ( error , x , y ) => {
149 if ( error ) {
150 // Some bad terminals (windows...) doesn't support cursor location request, we should fallback to a decent behavior.
151 // So we just move to the last line and create a new line.
152 //cleanup( error ) ; return ;
153 this.row.eraseLineAfter( this.height )( '\n' ) ;
154 x = 1 ;
155 y = this.height ;
156 }
157
158 finishInit( x , y ) ;
159 } ) ;
160 }
161 } ;
162
163
164
165 var computeItems = ( menuItems_ ) => {
166 textWidth = 0 ;
167
168 paddingLength = Math.max( stringWidth( options.leftPadding ) , stringWidth( options.selectedLeftPadding ) ) ;
169
170 menuItems_ = menuItems_.map( element => {
171 if ( typeof element !== 'string' ) { element = '' + element ; }
172 textWidth = Math.max( textWidth , stringWidth( element ) ) ;
173 return element ;
174 } ) ;
175
176 if ( ! options.oneLineItem && textWidth > options.itemMaxWidth - paddingLength ) {
177 outerWidth = Math.min( textWidth + paddingLength , this.width ) ;
178
179 menuItems = menuItems_.map( ( element , index ) => {
180
181 var item , lines ,
182 lineLength = options.itemMaxWidth - paddingLength ;
183
184 lines = string.wordwrap( element , {
185 width: lineLength ,
186 noJoin: true ,
187 fill: true ,
188 skipFn: termkit.escapeSequenceSkipFn
189 } ) ;
190
191 item = {
192 offsetY: offsetY ,
193 index: index ,
194 text: element ,
195 displayText: lines
196 } ;
197
198 offsetY += lines.length ;
199
200 return item ;
201 } ) ;
202
203 lineCount = offsetY ;
204 }
205 else {
206 textWidth = Math.min( textWidth , options.itemMaxWidth - paddingLength ) ;
207 outerWidth = Math.min( textWidth + paddingLength , this.width ) ;
208
209 menuItems = menuItems_.map( ( element , index ) => {
210 var elementWidth = stringWidth( element ) ;
211
212 return {
213 offsetY: index ,
214 index: index ,
215 text: element ,
216 displayText: [ elementWidth > textWidth ?
217 element.slice( 0 , textWidth - 1 ) + '…' :
218 element + ' '.repeat( textWidth - elementWidth ) ]
219 } ;
220 } ) ;
221
222 lineCount = menuItems.length ;
223 }
224 } ;
225
226
227
228 var finishInit = ( x , y ) => {
229 // It is possible for userland to end the menu immediately
230 if ( finished ) { return ; }
231
232 prepareArea( x , y ) ;
233 redraw() ;
234
235 this.on( 'key' , onKey ) ;
236 if ( this.mouseGrabbing ) { this.on( 'mouse' , onMouse ) ; }
237
238 controller.emit( 'ready' ) ;
239 emitHighlight() ;
240 } ;
241
242
243
244 var emitHighlight = () => {
245 var item = menuItems[ selectedIndex ] ;
246
247 controller.emit( 'highlight' , {
248 highlightedIndex: item.index ,
249 highlightedText: item.text ,
250 submitted: submittedIndex !== null ,
251 x: 1 ,
252 y: start.y + item.offsetY
253 } ) ;
254
255 } ;
256
257
258
259 var prepareArea = ( x , y ) => {
260 start.x = x ;
261 start.y = y ;
262
263 end.x = 1 ;
264 end.y = y + lineCount ;
265
266 scrollLines = start.y + lineCount - ( options.scrollRegionBottom || this.height ) - 1 + options.extraLines ;
267
268 if ( scrollLines > 0 ) {
269 // create extra lines
270 this( '\n'.repeat( scrollLines ) ) ;
271 start.y -= scrollLines ;
272 end.y -= scrollLines ;
273 }
274 } ;
275
276
277
278 var cleanup = ( error , data , eraseMenu ) => {
279 if ( alreadyCleanedUp ) { return ; }
280 alreadyCleanedUp = true ;
281
282 finished = true ;
283 this.removeListener( 'key' , onKey ) ;
284 this.removeListener( 'mouse' , onMouse ) ;
285
286 if ( error === 'abort' ) { return ; }
287
288 if ( controller.hasState( 'ready' ) ) {
289 if ( eraseMenu ) { erase() ; }
290 else { this.moveTo( 1 , end.y ) ; }
291 }
292
293 if ( error ) {
294 if ( callback ) { callback( error ) ; }
295 else { controller.promise.reject( error ) ; }
296 return ;
297 }
298
299 var value = data !== undefined ? data : {
300 selectedIndex: selectedIndex ,
301 selectedText: menuItems[ selectedIndex ].text ,
302 submitted: submittedIndex !== null ,
303 x: 1 ,
304 y: start.y + menuItems[ selectedIndex ].offsetY
305 } ;
306
307 if ( callback ) { callback( undefined , value ) ; }
308 else { controller.promise.resolve( value ) ; }
309 } ;
310
311
312
313 var erase = () => {
314 if ( ! controller.hasState( 'ready' ) ) { controller.once( 'ready' , erase ) ; return ; }
315
316 var i , j ;
317
318 for ( i = start.x , j = start.y ; j <= end.y ; i = 1 , j ++ ) {
319 this.moveTo.eraseLineAfter( i , j ) ;
320 }
321
322 this.moveTo( 1 , start.y ) ;
323 } ;
324
325
326
327 // Compute the coordinate of the end of a string, given a start coordinate
328 var redraw = () => {
329 for ( var i = 0 ; i < menuItems.length ; i ++ ) { redrawItem( i ) ; }
330 redrawCursor() ;
331 } ;
332
333
334
335 var redrawItem = ( index ) => {
336
337 // Called by finishInit before emitting 'ready'
338 //if ( ! controller.hasState( 'ready' ) ) { controller.once( 'ready' , redrawItem.bind( undefined , index ) ) ; return ; }
339
340 var item = menuItems[ index ] ;
341
342 item.displayText.forEach( ( text , line ) => {
343
344 this.moveTo( 1 , start.y + item.offsetY + line ) ;
345
346 if ( paused || options.unsubmittableIndexes[ index ] ) {
347 if ( index === submittedIndex ) {
348 if ( line ) { options.disabledSubmittedStyle.forceStyleOnReset.noFormat( options.leftPadding ) ; }
349 else { options.disabledSubmittedStyle.forceStyleOnReset.noFormat( options.submittedLeftPadding ) ; }
350
351 options.disabledSubmittedStyle.forceStyleOnReset.noFormat( text ) ;
352 }
353 else if ( index === selectedIndex ) {
354 if ( line ) { options.disabledSelectedStyle.forceStyleOnReset.noFormat( options.leftPadding ) ; }
355 else { options.disabledSelectedStyle.forceStyleOnReset.noFormat( options.selectedLeftPadding ) ; }
356
357 options.disabledSelectedStyle.forceStyleOnReset.noFormat( text ) ;
358 }
359 else {
360 options.disabledStyle.forceStyleOnReset.noFormat( options.leftPadding ) ;
361 options.disabledStyle.forceStyleOnReset.noFormat( text ) ;
362 }
363 }
364 else if ( index === submittedIndex ) {
365 if ( line ) { options.submittedStyle.forceStyleOnReset.noFormat( options.leftPadding ) ; }
366 else { options.submittedStyle.forceStyleOnReset.noFormat( options.submittedLeftPadding ) ; }
367
368 options.submittedStyle.forceStyleOnReset.noFormat( text ) ;
369 }
370 else if ( index === selectedIndex ) {
371 if ( line ) { options.selectedStyle.forceStyleOnReset.noFormat( options.leftPadding ) ; }
372 else { options.selectedStyle.forceStyleOnReset.noFormat( options.selectedLeftPadding ) ; }
373
374 options.selectedStyle.forceStyleOnReset.noFormat( text ) ;
375 }
376 else {
377 options.style.forceStyleOnReset.noFormat( options.leftPadding ) ;
378 options.style.forceStyleOnReset.noFormat( text ) ;
379 }
380 } ) ;
381 } ;
382
383
384
385 var redrawCursor = () => {
386 // Called by finishInit before emitting 'ready'
387 //if ( ! controller.hasState( 'ready' ) ) { controller.once( 'ready' , redrawCursor ) ; return ; }
388 this.moveTo( 1 , start.y + menuItems[ selectedIndex ].offsetY ) ;
389 } ;
390
391
392
393 var select = ( index ) => {
394 var oldSelectedIndex = selectedIndex ;
395
396 if ( selectedIndex !== index && index >= 0 && index < menuItems.length ) {
397 selectedIndex = index ;
398
399 // Don't redraw now if not ready, it will be drawn once ready (avoid double-draw)
400 if ( controller.hasState( 'ready' ) ) {
401 redrawItem( oldSelectedIndex ) ;
402 redrawItem( selectedIndex ) ;
403 redrawCursor() ;
404 emitHighlight() ;
405 }
406 }
407 } ;
408
409
410
411 var submit = () => {
412 if ( submittedIndex !== null || options.unsubmittableIndexes[ selectedIndex ] ) { return ; }
413 submittedIndex = selectedIndex ;
414
415 // Don't redraw now if not ready, it will be drawn once ready (avoid double-draw)
416 if ( controller.hasState( 'ready' ) ) {
417 redrawItem( submittedIndex ) ;
418 redrawCursor() ;
419 }
420
421 controller.emit( 'submit' , {
422 selectedIndex: submittedIndex ,
423 selectedText: menuItems[ submittedIndex ].text ,
424 submitted: true ,
425 x: 1 ,
426 y: start.y + menuItems[ submittedIndex ].offsetY
427 } ) ;
428
429 if ( ! options.continueOnSubmit ) { cleanup() ; }
430 } ;
431
432
433
434 var cancel = () => {
435 var oldSelectedIndex = submittedIndex ;
436
437 if ( submittedIndex === null ) { return ; }
438 submittedIndex = null ;
439 redrawItem( oldSelectedIndex ) ;
440 redrawCursor() ;
441 controller.emit( 'cancel' ) ;
442 } ;
443
444
445
446 var pause = () => {
447 if ( paused ) { return ; }
448 paused = true ;
449
450 // Don't redraw now if not ready, it will be drawn once ready (avoid double-draw)
451 if ( controller.hasState( 'ready' ) ) { redraw() ; }
452 } ;
453
454
455
456 var resume = () => {
457 if ( ! paused ) { return ; }
458 paused = false ;
459
460 // Don't redraw now if not ready, it will be drawn once ready (avoid double-draw)
461 if ( controller.hasState( 'ready' ) ) { redraw() ; }
462 } ;
463
464
465
466 var onKey = ( key , trash , data ) => {
467
468 if ( finished || paused ) { return ; }
469
470 var oldSelectedIndex = selectedIndex ;
471
472 switch ( keyBindings[ key ] ) {
473 case 'submit' :
474 submit() ;
475 break ;
476
477 case 'previous' :
478 if ( submittedIndex !== null ) { return ; }
479 if ( selectedIndex > 0 ) {
480 selectedIndex -- ;
481 redrawItem( selectedIndex ) ;
482 redrawItem( selectedIndex + 1 ) ;
483 redrawCursor() ;
484 //redraw() ;
485 emitHighlight() ;
486 }
487 break ;
488
489 case 'next' :
490 if ( submittedIndex !== null ) { return ; }
491 if ( selectedIndex < menuItems.length - 1 ) {
492 selectedIndex ++ ;
493 redrawItem( selectedIndex - 1 ) ;
494 redrawItem( selectedIndex ) ;
495 redrawCursor() ;
496 //redraw() ;
497 emitHighlight() ;
498 }
499 break ;
500
501 case 'cyclePrevious' :
502 if ( submittedIndex !== null ) { return ; }
503 selectedIndex -- ;
504
505 if ( selectedIndex < 0 ) { selectedIndex = menuItems.length - 1 ; }
506
507 redrawItem( oldSelectedIndex ) ;
508 redrawItem( selectedIndex ) ;
509 redrawCursor() ;
510 //redraw() ;
511 emitHighlight() ;
512 break ;
513
514 case 'cycleNext' :
515 if ( submittedIndex !== null ) { return ; }
516 selectedIndex ++ ;
517
518 if ( selectedIndex >= menuItems.length ) { selectedIndex = 0 ; }
519
520 redrawItem( oldSelectedIndex ) ;
521 redrawItem( selectedIndex ) ;
522 redrawCursor() ;
523 //redraw() ;
524 emitHighlight() ;
525 break ;
526
527 case 'first' :
528 if ( submittedIndex !== null ) { return ; }
529 if ( selectedIndex !== 0 ) {
530 selectedIndex = 0 ;
531 redrawItem( oldSelectedIndex ) ;
532 redrawItem( selectedIndex ) ;
533 redrawCursor() ;
534 //redraw() ;
535 emitHighlight() ;
536 }
537 break ;
538
539 case 'last' :
540 if ( submittedIndex !== null ) { return ; }
541 if ( selectedIndex !== menuItems.length - 1 ) {
542 selectedIndex = menuItems.length - 1 ;
543 redrawItem( oldSelectedIndex ) ;
544 redrawItem( selectedIndex ) ;
545 redrawCursor() ;
546 //redraw() ;
547 emitHighlight() ;
548 }
549 break ;
550
551 case 'cancel' :
552 cancel() ;
553 break ;
554
555 case 'escape' :
556 if ( options.cancelable ) {
557 cleanup( undefined , { canceled: true } ) ;
558 }
559 if ( options.exitOnUnexpectedKey ) {
560 cleanup( undefined , { unexpectedKey: key , unexpectedKeyData: data } ) ;
561 }
562 break ;
563
564 default :
565 if ( options.exitOnUnexpectedKey ) {
566 cleanup( undefined , { unexpectedKey: key , unexpectedKeyData: data } ) ;
567 }
568 break ;
569 }
570 } ;
571
572
573
574 var onMouse = ( name , data ) => {
575
576 if ( finished || paused || submittedIndex !== null ) { return ; }
577
578 // If out of bounds, exit now!
579 if ( data.y < start.y || data.y >= end.y ) { return ; }
580
581 var i , yMin , yMax ,
582 inBounds = false ;
583
584 for ( i = 0 ; i < menuItems.length ; i ++ ) {
585 yMin = start.y + menuItems[ i ].offsetY ;
586 yMax = start.y + menuItems[ i ].offsetY + menuItems[ i ].displayText.length - 1 ;
587
588 if ( data.y >= yMin && data.y <= yMax && data.x < 1 + outerWidth ) {
589 inBounds = true ;
590 select( i ) ;
591 break ;
592 }
593 }
594
595 if ( inBounds && name === 'MOUSE_LEFT_BUTTON_PRESSED' ) {
596 submit() ;
597 }
598 } ;
599
600
601
602 // Return a controller for the menu
603
604 controller = Object.create( NextGenEvents.prototype ) ;
605
606 controller.defineStates( 'ready' ) ;
607
608 // Stop everything and do not even call the callback
609 controller.abort = () => {
610 if ( finished ) { return ; }
611 cleanup( 'abort' ) ;
612 } ;
613
614 // Stop and call the completion callback with no item
615 controller.stop = ( eraseMenu ) => {
616 if ( finished ) { return ; }
617 cleanup( undefined , undefined , eraseMenu ) ;
618 } ;
619
620 controller.select = select ;
621 controller.submit = submit ;
622 controller.cancel = cancel ;
623 controller.erase = erase ;
624
625 // Pause and resume: the menu will not respond to event when paused
626 controller.pause = pause ;
627 controller.resume = resume ;
628 controller.focus = ( value ) => {
629 if ( value ) { resume() ; }
630 else { pause() ; }
631 } ;
632
633 // Get the current state
634 controller.getState = () => ( {
635 selectedIndex: selectedIndex ,
636 selectedText: menuItems[ selectedIndex ].text ,
637 submitted: submittedIndex !== null ,
638 start: start ,
639 end: end ,
640 x: 1 ,
641 y: start.y + menuItems[ selectedIndex ].offsetY
642 //scrollLines: scrollLines
643 } ) ;
644
645 // Get the current position
646 controller.getPosition = () => ( { x: start.x , y: start.y } ) ;
647
648 // Hide the menu
649 controller.hide = () => {
650 if ( ! controller.hasState( 'ready' ) ) { controller.once( 'ready' , controller.hide ) ; return ; }
651 erase() ;
652 } ;
653
654 // Show the menu
655 controller.show = () => {
656 if ( ! controller.hasState( 'ready' ) ) { controller.once( 'ready' , controller.show ) ; return ; }
657 redraw() ;
658 } ;
659
660 // Redraw the menu
661 controller.redraw = () => {
662 if ( ! controller.hasState( 'ready' ) ) { controller.once( 'ready' , controller.redraw ) ; return ; }
663 redraw() ;
664 } ;
665
666 // Redraw the cursor
667 controller.redrawCursor = () => {
668 if ( ! controller.hasState( 'ready' ) ) { controller.once( 'ready' , controller.redrawCursor ) ; return ; }
669 redrawCursor() ;
670 } ;
671
672 // Rebase the menu where the cursor is
673 controller.rebase = () => {
674 if ( ! controller.hasState( 'ready' ) ) { controller.once( 'ready' , controller.rebase ) ; return ; }
675
676 // First, disable the menu: getCursorLocation is async!
677 var wasPaused = paused ;
678 paused = true ;
679
680 this.getCursorLocation( ( error , x , y ) => {
681 if ( error ) {
682 // Some bad terminals (windows...) doesn't support cursor location request, we should fallback to a decent behavior.
683 // Here we just ignore the rebase.
684 //cleanup( error ) ;
685 return ;
686 }
687
688 paused = wasPaused ;
689 prepareArea( x , y ) ;
690 redraw() ;
691 controller.emit( 'rebased' ) ;
692 } ) ;
693 } ;
694
695 controller.promise = new Promise() ;
696
697 // Init the menu
698 init() ;
699
700 return controller ;
701} ;
702