UNPKG

16 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 Promise = require( 'seventh' ) ;
32const TextBox = require( './TextBox.js' ) ;
33const EditableTextBox = require( './EditableTextBox.js' ) ;
34const RowMenu = require( './RowMenu.js' ) ;
35const string = require( 'string-kit' ) ;
36const computeAutoCompleteArray = require( '../autoComplete.js' ) ;
37
38
39
40/*
41 This is the Document-model version of .inputField().
42 Like an EditableTextBox, with a one-line hard-line-wrap TextBuffer, outputHeight start at 1 but can grow when the user
43 type a lot of thing, can auto-complete with or without menu, have history, and so on...
44*/
45
46/*
47 Check-list of things that .inputField() has and InlineInput still don't:
48 * Inline mode, capable of adding a new line at the end of the screen when it is needed
49 * editing actions: deleteAllBefore, deleteAllAfter, deletePreviousWord, deleteNextWord, startOfInput, endOfInput
50 * meta key for more keyboard commands, e.g.: maybe something like CTRL_K <key> --> META_<key>
51 * allow placeholder to be used as default (submitting without actually entering anything) when appropriate
52 * disable echoing (no output and no cursor movements)
53 * setting the cursor "offset" position beforehand
54 * min/max length
55 * Maybe (very low priority): support for the .inputField()'s token feature (tokenHook, tokenResetHook, tokenRegExp).
56*/
57
58function InlineInput( options ) {
59 // Clone options if necessary
60 options = ! options ? {} : options.internal ? options : Object.create( options ) ;
61 options.internal = true ;
62
63 if ( options.value ) { options.content = options.value ; }
64
65 // It is always 1 at the begining
66 options.outputHeight = 1 ;
67
68 // No scrolling
69 options.scrollable = options.hasVScrollBar = options.hasHScrollBar = options.extraScrolling = false ;
70 options.scrollX = options.scrollY = 0 ;
71
72 // It always have line-wrapping on
73 options.lineWrap = true ;
74
75 this.onAutoCompleteMenuSubmit = this.onAutoCompleteMenuSubmit.bind( this ) ;
76 this.onAutoCompleteMenuCancel = this.onAutoCompleteMenuCancel.bind( this ) ;
77
78 this.promptTextBox = null ;
79
80 if ( options.prompt ) {
81 this.promptTextBox = new TextBox( Object.assign(
82 {
83 textAttr: options.textAttr
84 } ,
85 options.prompt ,
86 {
87 internal: true ,
88 //parent: this ,
89 outputX: options.outputX || options.x ,
90 outputY: options.outputY || options.y ,
91 outputWidth: options.outputWidth || options.width ,
92 outputHeight: options.outputHeight || options.height ,
93 lineWrap: options.lineWrap ,
94 wordWrap: options.wordWrap || options.wordwrap
95 }
96 ) ) ;
97
98 // Drop void cells
99 this.promptTextBox.textBuffer.setVoidAttr( null ) ;
100
101 let size = this.promptTextBox.getContentSize() ;
102 this.promptTextBox.setSizeAndPosition( size ) ;
103
104 if ( size.height > 1 ) {
105 options.outputY = ( options.outputY || options.y ) + size.height - 1 ;
106 options.firstLineRightShift = this.promptTextBox.textBuffer.buffer[ this.promptTextBox.textBuffer.buffer.length - 1 ].length ;
107 }
108 else {
109 options.firstLineRightShift = size.width ;
110 }
111 }
112
113 EditableTextBox.call( this , options ) ;
114
115
116 this.history = options.history ;
117 this.contentArray = options.history ? [ ... options.history , this.content ] : [ this.content ] ;
118 this.contentIndex = this.contentArray.length - 1 ;
119
120 this.disabled = !! options.disabled ;
121 this.submitted = !! options.submitted ;
122 this.cancelable = !! options.cancelable ;
123 this.canceled = !! options.canceled ;
124
125 this.autoComplete = options.autoComplete ;
126 this.useAutoCompleteHint = !! ( this.autoComplete && ( options.useAutoCompleteHint || options.autoCompleteHint ) ) ;
127 this.useAutoCompleteMenu = !! ( this.autoComplete && ( options.useAutoCompleteMenu || options.autoCompleteMenu ) ) ;
128 this.autoCompleteMenu = null ;
129 this.autoCompleteLeftPart = null ;
130 this.autoCompleteRightPart = null ;
131
132 this.menuOptions = Object.assign( {} , this.defaultMenuOptions , options.menu ) ;
133
134 this.placeholder = options.placeholder ;
135 this.placeholderHasMarkup = !! options.placeholderHasMarkup ;
136
137 if ( this.placeholder ) {
138 this.setAltContent( this.placeholder , this.placeholderHasMarkup ) ;
139 }
140
141 if ( this.promptTextBox ) {
142 this.attach( this.promptTextBox ) ;
143 }
144
145 // Only draw if we are not a superclass of the object
146 if ( this.elementType === 'InlineInput' && ! options.noDraw ) { this.draw() ; }
147}
148
149module.exports = InlineInput ;
150
151InlineInput.prototype = Object.create( EditableTextBox.prototype ) ;
152InlineInput.prototype.constructor = InlineInput ;
153InlineInput.prototype.elementType = 'InlineInput' ;
154
155// Has a fallback textBuffer for hint/placeholder
156InlineInput.prototype.useAltTextBuffer = true ;
157
158
159
160InlineInput.prototype.defaultMenuOptions = {
161 buttonBlurAttr: { bgColor: 'default' , color: 'default' } ,
162 buttonFocusAttr: { bgColor: 'green' , color: 'blue' , dim: true } ,
163 buttonDisabledAttr: { bgColor: 'white' , color: 'brightBlack' } ,
164 buttonSubmittedAttr: { bgColor: 'brightWhite' , color: 'brightBlack' } ,
165 buttonSeparatorAttr: { bgColor: 'default' } ,
166 backgroundAttr: { bgColor: 'default' } ,
167 //leftPadding: ' ' , rightPadding: ' ' ,
168 justify: true ,
169 keyBindings: Object.assign( {} , RowMenu.prototype.keyBindings , {
170 TAB: 'next' ,
171 SHIFT_TAB: 'previous'
172 } )
173} ;
174
175
176
177InlineInput.prototype.destroy = function( isSubDestroy ) {
178 EditableTextBox.prototype.destroy.call( this , isSubDestroy ) ;
179} ;
180
181
182
183InlineInput.prototype.keyBindings = {
184 ENTER: 'submit' ,
185 KP_ENTER: 'submit' ,
186 ESCAPE: 'cancel' ,
187 TAB: 'autoComplete' ,
188 CTRL_R: 'historyAutoComplete' ,
189 UP: 'historyPrevious' ,
190 DOWN: 'historyNext' ,
191 BACKSPACE: 'backDelete' ,
192 DELETE: 'delete' ,
193 LEFT: 'backward' ,
194 RIGHT: 'forward' ,
195 CTRL_LEFT: 'startOfWord' ,
196 CTRL_RIGHT: 'endOfWord' ,
197 HOME: 'startOfLine' ,
198 END: 'endOfLine' ,
199 CTRL_O: 'copyClipboard' ,
200 CTRL_P: 'pasteClipboard'
201} ;
202
203
204
205InlineInput.prototype.preDrawSelf = function() {
206 if ( this.promptTextBuffer ) {
207 // It's best to force the dst now, because it avoids to set textBuffer.dst everytime it changes,
208 // and it could be changed by userland (so hard to keep it in sync without setters/getters)
209 this.promptTextBuffer.draw( { dst: this.outputDst } ) ;
210 }
211
212 EditableTextBox.prototype.preDrawSelf.call( this ) ;
213} ;
214
215
216
217InlineInput.prototype.autoResizeAndDraw = function( onlyDrawCursor = false ) {
218 var height = Math.max( this.textBuffer.buffer.length , ( this.altTextBuffer && this.altTextBuffer.buffer.length ) || 0 ) ;
219
220 if ( height > this.outputHeight ) {
221 this.setSizeAndPosition( { outputHeight: height } ) ;
222 }
223
224 if ( ! onlyDrawCursor ) {
225 this.draw() ;
226 }
227 else {
228 this.drawCursor() ;
229 }
230} ;
231
232
233
234InlineInput.prototype.autoResizeAndDrawCursor = function() {
235 return this.autoResizeAndDraw( true ) ;
236} ;
237
238
239
240InlineInput.prototype.runAutoCompleteHint = async function( autoComplete ) {
241 //console.error( "bob, please")
242 var autoCompleted ;
243
244 var [ leftPart , rightPart ] = this.textBuffer.getCursorSplittedText() ;
245
246 if ( rightPart ) {
247 this.altTextBuffer.setText( '' ) ;
248 }
249 else {
250 if ( Array.isArray( autoComplete ) ) {
251 autoCompleted = computeAutoCompleteArray( autoComplete , leftPart , false ) ;
252 }
253 else if ( typeof autoComplete === 'function' ) {
254 autoCompleted = await autoComplete( leftPart , false ) ;
255 }
256 else {
257 return ;
258 }
259
260 if ( Array.isArray( autoCompleted ) ) {
261 if ( ! autoCompleted.length ) { return ; }
262 autoCompleted = autoCompleted[ 0 ] ;
263 }
264
265 if ( autoCompleted === leftPart ) {
266 this.altTextBuffer.setText( '' ) ;
267 }
268 else {
269 this.altTextBuffer.setText( autoCompleted ) ;
270 //this.altTextBuffer.runStateMachine() ;
271 }
272 }
273
274 this.autoResizeAndDraw() ;
275} ;
276
277
278
279InlineInput.prototype.runAutoComplete = async function( autoComplete ) {
280 var autoCompleted ;
281
282 [ this.autoCompleteLeftPart , this.autoCompleteRightPart ] = this.textBuffer.getCursorSplittedText() ;
283
284 if ( Array.isArray( autoComplete ) ) {
285 autoCompleted = computeAutoCompleteArray( autoComplete , this.autoCompleteLeftPart , this.useAutoCompleteMenu ) ;
286 }
287 else if ( typeof autoComplete === 'function' ) {
288 autoCompleted = await autoComplete( this.autoCompleteLeftPart , this.useAutoCompleteMenu ) ;
289 }
290 else {
291 return ;
292 }
293
294 if ( Array.isArray( autoCompleted ) ) {
295 if ( ! autoCompleted.length ) { return ; }
296
297 if ( this.useAutoCompleteMenu ) {
298 this.runAutoCompleteMenu( autoCompleted ) ;
299 return ;
300 }
301
302 autoCompleted = autoCompleted[ 0 ] ;
303 }
304
305 this.runAutoCompleted( autoCompleted ) ;
306} ;
307
308
309
310InlineInput.prototype.runAutoCompleted = async function( autoCompleted ) {
311 this.textBuffer.setText( autoCompleted + this.autoCompleteRightPart ) ;
312 this.textBuffer.setCursorOffset( autoCompleted.length ) ;
313 this.textBuffer.runStateMachine() ;
314 this.autoResizeAndDraw() ;
315} ;
316
317
318
319InlineInput.prototype.runAutoCompleteMenu = async function( items ) {
320 // No items, leave now...
321 if ( ! items || ! items.length ) { return ; }
322
323 if ( this.autoCompleteMenu ) {
324 // Should never happen, but just in case...
325 this.autoCompleteMenu.destroy() ;
326 }
327
328 // Make the ColumnMenu a child of the button, so focus cycle will work as expected
329 this.autoCompleteMenu = new RowMenu( Object.assign( {} , this.menuOptions , {
330 internal: true ,
331 parent: this ,
332 x: this.outputX ,
333 y: this.outputY + this.outputHeight ,
334 outputWidth: this.outputWidth ,
335 items: items.map( item => ( { value: item , content: item } ) )
336 } ) ) ;
337
338 this.document.giveFocusTo( this.autoCompleteMenu ) ;
339
340 this.autoCompleteMenu.once( 'submit' , this.onAutoCompleteMenuSubmit ) ;
341 this.autoCompleteMenu.once( 'cancel' , this.onAutoCompleteMenuCancel ) ;
342} ;
343
344
345
346InlineInput.prototype.onAutoCompleteMenuSubmit = function( selectedText ) {
347 this.autoCompleteMenu.destroy() ;
348 this.autoCompleteMenu = null ;
349 this.document.giveFocusTo( this ) ;
350 this.runAutoCompleted( selectedText ) ;
351} ;
352
353
354
355InlineInput.prototype.onAutoCompleteMenuCancel = function() {
356 this.autoCompleteMenu.destroy() ;
357 this.autoCompleteMenu = null ;
358 this.document.giveFocusTo( this ) ;
359} ;
360
361
362
363InlineInput.prototype.onKey = function( key , trash , data ) {
364 if ( this.autoCompleteMenu ) {
365 // If the autoCompleteMenu is on, force a cancel
366 this.autoCompleteMenu.emit( 'cancel' ) ;
367 }
368
369 if ( data && data.isCharacter ) {
370 if ( this.placeholder ) {
371 // Remove the placeholder on the first input
372 this.placeholder = null ;
373 this.setAltContent( '' , false , true ) ;
374 }
375
376 this.textBuffer.insert( key , this.textAttr ) ;
377 this.textBuffer.runStateMachine() ;
378
379 if ( this.useAutoCompleteHint ) { this.runAutoCompleteHint( this.autoComplete ) ; }
380 else { this.autoResizeAndDraw() ; }
381 }
382 else {
383 // Here we have a special key
384
385 switch( this.keyBindings[ key ] ) {
386 case 'submit' :
387 if ( this.disabled || this.submitted || this.canceled ) { break ; }
388 //this.submitted = true ;
389 this.emit( 'submit' , this.getValue() , undefined , this ) ;
390 break ;
391
392 case 'cancel' :
393 if ( ! this.cancelable || this.disabled || this.canceled ) { break ; }
394 //this.canceled = true ;
395 this.emit( 'cancel' , this ) ;
396 break ;
397
398 case 'autoComplete' :
399 if ( ! this.autoComplete ) { break ; }
400 this.runAutoComplete( this.autoComplete ) ;
401 break ;
402
403 case 'historyAutoComplete' :
404 if ( ! this.autoComplete ) { break ; }
405 this.runAutoComplete( this.history ) ;
406 break ;
407
408 case 'historyPrevious' :
409 if ( this.contentIndex <= 0 ) { break ; }
410 this.contentArray[ this.contentIndex ] = this.getContent() ;
411 this.contentIndex -- ;
412 this.setContent( this.contentArray[ this.contentIndex ] ) ;
413 this.textBuffer.runStateMachine() ;
414 this.autoResizeAndDraw() ;
415 break ;
416
417 case 'historyNext' :
418 if ( this.contentIndex >= this.contentArray.length - 1 ) { break ; }
419 this.contentArray[ this.contentIndex ] = this.getContent() ;
420 this.contentIndex ++ ;
421 this.setContent( this.contentArray[ this.contentIndex ] ) ;
422 this.textBuffer.runStateMachine() ;
423 this.autoResizeAndDraw() ;
424 break ;
425
426 case 'backDelete' :
427 this.textBuffer.backDelete() ;
428 this.textBuffer.runStateMachine() ;
429 if ( this.useAutoCompleteHint ) { this.runAutoCompleteHint( this.autoComplete ) ; }
430 else { this.autoResizeAndDraw() ; }
431 break ;
432
433 case 'delete' :
434 this.textBuffer.delete() ;
435 this.textBuffer.runStateMachine() ;
436 if ( this.useAutoCompleteHint ) { this.runAutoCompleteHint( this.autoComplete ) ; }
437 else { this.autoResizeAndDraw() ; }
438 break ;
439
440 case 'backward' :
441 this.textBuffer.moveBackward() ;
442 this.autoResizeAndDrawCursor() ;
443 break ;
444
445 case 'forward' :
446 this.textBuffer.moveForward() ;
447 this.autoResizeAndDrawCursor() ;
448 break ;
449
450 case 'startOfWord' :
451 this.textBuffer.moveToStartOfWord() ;
452 this.autoResizeAndDrawCursor() ;
453 break ;
454
455 case 'endOfWord' :
456 this.textBuffer.moveToEndOfWord() ;
457 this.autoResizeAndDrawCursor() ;
458 break ;
459
460 case 'startOfLine' :
461 this.textBuffer.moveToColumn( 0 ) ;
462 this.autoResizeAndDrawCursor() ;
463 break ;
464
465 case 'endOfLine' :
466 this.textBuffer.moveToEndOfLine() ;
467 this.autoResizeAndDrawCursor() ;
468 break ;
469
470 case 'left' :
471 this.textBuffer.moveLeft() ;
472 this.autoResizeAndDrawCursor() ;
473 break ;
474
475 case 'right' :
476 this.textBuffer.moveRight() ;
477 this.autoResizeAndDrawCursor() ;
478 break ;
479
480 case 'pasteClipboard' :
481 if ( this.document ) {
482 this.document.getClipboard().then( str => {
483 if ( str ) {
484 this.textBuffer.insert( str , this.textAttr ) ;
485 this.textBuffer.runStateMachine() ;
486 if ( this.useAutoCompleteHint ) { this.runAutoCompleteHint( this.autoComplete ) ; }
487 else { this.autoResizeAndDraw() ; }
488 }
489 } )
490 .catch( () => undefined ) ;
491 }
492 break ;
493
494 case 'copyClipboard' :
495 if ( this.document ) {
496 this.document.setClipboard( this.textBuffer.getSelectionText() ).catch( () => undefined ) ;
497 }
498 break ;
499
500 default :
501 return ; // Bubble up
502 }
503 }
504
505 return true ; // Do not bubble up
506} ;
507
508
509
510/*
511InlineInput.prototype.onFocus = function( focus , type ) {
512 this.hasFocus = focus ;
513 this.updateStatus() ;
514 this.draw() ;
515} ;
516*/
517
518
519/*
520InlineInput.prototype.onClick = function( data ) {
521 if ( ! this.hasFocus ) {
522 this.document.giveFocusTo( this , 'select' ) ;
523 }
524 else {
525 this.textBuffer.moveTo( data.x - this.scrollX , data.y - this.scrollY ) ;
526 this.drawCursor() ;
527 }
528} ;
529*/
530
531/*
532InlineInput.prototype.onMiddleClick = function( data ) {
533 if ( ! this.hasFocus ) {
534 this.document.giveFocusTo( this , 'select' ) ;
535 }
536
537 // Do not moveTo, it's quite boring
538 //this.textBuffer.moveTo( data.x , data.y ) ;
539
540 if ( this.document ) {
541 this.document.getClipboard( 'primary' ).then( str => {
542 if ( str ) {
543 this.textBuffer.insert( str , this.textAttr ) ;
544 this.textBuffer.runStateMachine() ;
545 this.autoResizeAndDraw() ;
546 }
547 //else { this.drawCursor() ; }
548 } )
549 .catch( () => undefined ) ;
550 }
551 //else { this.drawCursor() ; }
552} ;
553*/
554
555
556// There isn't much to do ATM
557//InlineInput.prototype.updateStatus = function() {} ;
558