UNPKG

15.3 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 Element = require( './Element.js' ) ;
32const Slider = require( './Slider.js' ) ;
33
34const ScreenBuffer = require( '../ScreenBuffer.js' ) ;
35const TextBuffer = require( '../TextBuffer.js' ) ;
36const Rect = require( '../Rect.js' ) ;
37
38const string = require( 'string-kit' ) ;
39
40
41
42function TextBox( options ) {
43 // Clone options if necessary
44 options = ! options ? {} : options.internal ? options : Object.create( options ) ;
45 options.internal = true ;
46
47 Element.call( this , options ) ;
48
49 this.onKey = this.onKey.bind( this ) ;
50 this.onClick = this.onClick.bind( this ) ;
51 this.onDrag = this.onDrag.bind( this ) ;
52 this.onWheel = this.onWheel.bind( this ) ;
53
54 if ( options.keyBindings ) { this.keyBindings = options.keyBindings ; }
55
56 this.textAttr = options.textAttr || options.attr || { bgColor: 'default' } ;
57 this.altTextAttr = options.altTextAttr || Object.assign( {} , this.textAttr , { color: 'gray' , italic: true } ) ;
58 this.voidAttr = options.voidAttr || options.emptyAttr || options.attr || { bgColor: 'default' } ;
59
60 this.scrollable = !! options.scrollable ;
61 this.hasVScrollBar = this.scrollable && !! options.vScrollBar ;
62 this.hasHScrollBar = this.scrollable && !! options.hScrollBar ;
63 this.scrollX = options.scrollX || 0 ;
64 this.scrollY = options.scrollY || 0 ;
65
66 // false: scroll down to the bottom of the content, both content bottom and textBox bottom on the same cell
67 // true: scroll down until the bottom of the content reaches the top of the textBox
68 this.extraScrolling = !! options.extraScrolling ;
69
70 // Right shift of the first-line, may be useful for prompt, or continuing another box in the flow
71 this.firstLineRightShift = options.firstLineRightShift || 0 ;
72
73 this.wordWrap = !! ( options.wordWrap || options.wordwrap ) ;
74 this.lineWrap = !! ( options.lineWrap || this.wordWrap ) ;
75
76 this.hiddenContent = options.hiddenContent ;
77
78 this.stateMachine = options.stateMachine ;
79
80 this.textAreaWidth = this.hasVScrollBar ? this.outputWidth - 1 : this.outputWidth ;
81 this.textAreaHeight = this.hasHScrollBar ? this.outputHeight - 1 : this.outputHeight ;
82
83 this.textBuffer = null ;
84 this.altTextBuffer = null ;
85 this.vScrollBarSlider = null ;
86 this.hScrollBarSlider = null ;
87
88 this.on( 'key' , this.onKey ) ;
89 this.on( 'click' , this.onClick ) ;
90 this.on( 'drag' , this.onDrag ) ;
91 this.on( 'wheel' , this.onWheel ) ;
92
93 this.initChildren() ;
94
95 if ( this.setContent === TextBox.prototype.setContent ) {
96 this.setContent( options.content , options.contentHasMarkup , true ) ;
97 }
98
99 // Only draw if we are not a superclass of the object
100 if ( this.elementType === 'TextBox' && ! options.noDraw ) { this.draw() ; }
101}
102
103module.exports = TextBox ;
104
105TextBox.prototype = Object.create( Element.prototype ) ;
106TextBox.prototype.constructor = TextBox ;
107TextBox.prototype.elementType = 'TextBox' ;
108
109
110
111// Support for strictInline mode
112TextBox.prototype.strictInlineSupport = true ;
113
114
115
116TextBox.prototype.destroy = function( isSubDestroy ) {
117 this.off( 'key' , this.onKey ) ;
118 this.off( 'click' , this.onClick ) ;
119 this.off( 'drag' , this.onDrag ) ;
120 this.off( 'wheel' , this.onWheel ) ;
121
122 Element.prototype.destroy.call( this , isSubDestroy ) ;
123} ;
124
125
126
127TextBox.prototype.initChildren = function() {
128 this.textBuffer = new TextBuffer( {
129 dst: this.outputDst ,
130 //palette: this.document.palette ,
131 x: this.outputX ,
132 y: this.outputY ,
133 //width: this.textAreaWidth ,
134 //height: this.textAreaHeight
135 firstLineRightShift: this.firstLineRightShift ,
136 lineWrapWidth: this.lineWrap ? this.textAreaWidth : null ,
137 wordWrap: this.wordWrap ,
138 dstClipRect: {
139 x: this.outputX ,
140 y: this.outputY ,
141 width: this.textAreaWidth ,
142 height: this.textAreaHeight
143 } ,
144 hidden: this.hiddenContent ,
145 forceInBound: true ,
146 stateMachine: this.stateMachine
147 } ) ;
148
149 this.textBuffer.setDefaultAttr( this.textAttr ) ;
150 this.textBuffer.setVoidAttr( this.voidAttr ) ;
151
152
153 if ( this.useAltTextBuffer ) {
154 this.altTextBuffer = new TextBuffer( {
155 firstLineRightShift: this.firstLineRightShift ,
156 lineWrapWidth: this.lineWrap ? this.textAreaWidth : null ,
157 wordWrap: this.wordWrap ,
158 dstClipRect: {
159 x: this.outputX ,
160 y: this.outputY ,
161 width: this.textAreaWidth ,
162 height: this.textAreaHeight
163 }
164 //, stateMachine: this.stateMachine
165 } ) ;
166
167 this.altTextBuffer.setDefaultAttr( this.altTextAttr ) ;
168 this.altTextBuffer.setVoidAttr( this.voidAttr ) ;
169 this.textBuffer.setVoidTextBuffer( this.altTextBuffer ) ;
170 }
171
172
173 if ( this.hasVScrollBar ) {
174 this.vScrollBarSlider = new Slider( {
175 internal: true ,
176 parent: this ,
177 x: this.outputX + this.outputWidth - 1 ,
178 y: this.outputY ,
179 height: this.outputHeight ,
180 isVertical: true ,
181 valueToRate: scrollY => -scrollY / Math.max( 1 , this.textBuffer.buffer.length - this.textAreaHeight ) ,
182 rateToValue: rate => -rate * Math.max( 1 , this.textBuffer.buffer.length - this.textAreaHeight ) ,
183 noDraw: true
184 } ) ;
185
186 this.vScrollBarSlider.on( 'slideStep' , d => this.scroll( 0 , -d * Math.ceil( this.textAreaHeight / 2 ) ) ) ;
187 this.vScrollBarSlider.on( 'slide' , value => {
188 this.scrollTo( 0 , value ) ;
189 this.draw() ;
190 } ) ;
191 }
192
193 if ( this.hasHScrollBar ) {
194 this.hScrollBarSlider = new Slider( {
195 internal: true ,
196 parent: this ,
197 x: this.outputX ,
198 y: this.outputY + this.outputHeight - 1 ,
199 width: this.hasVScrollBar ? this.outputWidth - 1 : this.outputWidth ,
200 valueToRate: scrollX => {
201 var lineWidth = this.textBuffer.getContentSize().width ;
202 return -scrollX / Math.max( 1 , lineWidth - this.textAreaWidth ) ;
203 } ,
204 rateToValue: rate => {
205 var lineWidth = this.textBuffer.getContentSize().width ;
206 return -rate * Math.max( 1 , lineWidth - this.textAreaWidth ) ;
207 } ,
208 noDraw: true
209 } ) ;
210
211 this.hScrollBarSlider.on( 'slideStep' , d => this.scroll( -d * Math.ceil( this.textAreaWidth / 2 ) , 0 ) ) ;
212 this.hScrollBarSlider.on( 'slide' , value => {
213 this.scrollTo( value , 0 ) ;
214 this.draw() ;
215 } ) ;
216 }
217} ;
218
219
220
221TextBox.prototype.setSizeAndPosition = function( options ) {
222 this.outputX = options.outputX || options.x || this.outputX || 0 ;
223 this.outputY = options.outputY || options.y || this.outputY || 0 ;
224 this.outputWidth = options.outputWidth || options.width || this.outputWidth || 1 ;
225 this.outputHeight = options.outputHeight || options.height || this.outputHeight || 1 ;
226
227 this.textAreaWidth = this.hasVScrollBar ? this.outputWidth - 1 : this.outputWidth ;
228 this.textAreaHeight = this.hasHScrollBar ? this.outputHeight - 1 : this.outputHeight ;
229
230 this.textBuffer.lineWrapWidth = this.lineWrap ? this.textAreaWidth : null ;
231 if ( this.altTextBuffer ) { this.altTextBuffer.lineWrapWidth = this.lineWrap ? this.textAreaWidth : null ; }
232
233 this.textBuffer.x = this.outputX ;
234 this.textBuffer.y = this.outputY ;
235
236 this.textBuffer.dstClipRect = new Rect( {
237 x: this.outputX ,
238 y: this.outputY ,
239 width: this.textAreaWidth ,
240 height: this.textAreaHeight
241 } ) ;
242
243 // Update word-wrap
244 if ( this.lineWrap ) {
245 this.textBuffer.wrapAllLines() ;
246 if ( this.altTextBuffer ) { this.altTextBuffer.wrapAllLines() ; }
247 }
248
249 if ( this.vScrollBarSlider ) {
250 this.vScrollBarSlider.x = this.outputX + this.outputWidth - 1 ;
251 this.vScrollBarSlider.y = this.outputY ;
252 this.vScrollBarSlider.height = this.outputHeight ;
253 }
254
255 if ( this.hScrollBarSlider ) {
256 this.hScrollBarSlider.x = this.outputX ;
257 this.hScrollBarSlider.y = this.outputY + this.outputHeight - 1 ;
258 this.hScrollBarSlider.width = this.hasVScrollBar ? this.outputWidth - 1 : this.outputWidth ;
259 }
260} ;
261
262
263
264TextBox.prototype.keyBindings = {
265 UP: 'tinyScrollUp' ,
266 DOWN: 'tinyScrollDown' ,
267 PAGE_UP: 'scrollUp' ,
268 PAGE_DOWN: 'scrollDown' ,
269 ' ': 'scrollDown' ,
270 HOME: 'scrollTop' ,
271 END: 'scrollBottom' ,
272 LEFT: 'scrollLeft' ,
273 RIGHT: 'scrollRight' ,
274 CTRL_O: 'copyClipboard'
275} ;
276
277
278
279TextBox.prototype.preDrawSelf = function() {
280 // It's best to force the dst now, because it avoids to set textBuffer.dst everytime it changes,
281 // and it could be changed by userland (so hard to keep it in sync without setters/getters)
282 this.textBuffer.draw( { dst: this.outputDst } ) ;
283} ;
284
285
286
287TextBox.prototype.scroll = function( dx , dy ) {
288 return this.scrollTo( dx ? this.scrollX + dx : null , dy ? this.scrollY + dy : null ) ;
289} ;
290
291
292
293TextBox.prototype.scrollTo = function( x , y , internalAndNoDraw = false ) {
294 if ( ! this.scrollable ) { return ; }
295
296 if ( x !== undefined && x !== null ) {
297 // Got a +1 after content size because of the word-wrap thing and eventual invisible \n
298 this.scrollX = Math.min( 0 , Math.max( Math.round( x ) ,
299 ( this.extraScrolling ? 1 : this.textAreaWidth ) - this.textBuffer.getContentSize().width + 1 )
300 ) ;
301
302 this.textBuffer.x = this.outputX + this.scrollX ;
303 }
304
305 if ( y !== undefined && y !== null ) {
306 this.scrollY = Math.min( 0 , Math.max( Math.round( y ) ,
307 ( this.extraScrolling ? 1 : this.textAreaHeight ) - this.textBuffer.buffer.length )
308 ) ;
309
310 this.textBuffer.y = this.outputY + this.scrollY ;
311 }
312
313 if ( ! internalAndNoDraw ) {
314 if ( this.vScrollBarSlider ) {
315 this.vScrollBarSlider.setValue( this.scrollY , true ) ;
316 }
317
318 if ( this.hScrollBarSlider ) {
319 this.hScrollBarSlider.setValue( this.scrollX , true ) ;
320 }
321
322 this.draw() ;
323 }
324} ;
325
326
327
328TextBox.prototype.autoScrollAndDraw = function( onlyDrawCursor = false ) {
329 var x , y ;
330
331 // We use cx-1 because at least we want to see the char just before the cursor (backspace, etc...)
332 // But do nothing if there is no scrolling yet (do not set x to 0 if it's unnecessary)
333 if ( this.textBuffer.cx - 1 < -this.scrollX && this.scrollX !== 0 ) {
334 x = -Math.max( 0 , this.textBuffer.cx - 1 ) ;
335 }
336 else if ( this.textBuffer.cx > this.textAreaWidth - this.scrollX - 1 ) {
337 x = this.textAreaWidth - 1 - this.textBuffer.cx ;
338 }
339
340 if ( this.textBuffer.cy < -this.scrollY ) {
341 y = -this.textBuffer.cy ;
342 }
343 else if ( this.textBuffer.cy > this.textAreaHeight - this.scrollY - 1 ) {
344 y = this.textAreaHeight - 1 - this.textBuffer.cy ;
345 }
346
347 if ( x !== undefined || y !== undefined ) {
348 // .scrollTo() call .draw(), so no need to do that here...
349 this.scrollTo( x , y ) ;
350 }
351 else if ( ! onlyDrawCursor ) {
352 this.draw() ;
353 }
354 else {
355 this.drawCursor() ;
356 }
357} ;
358
359
360
361TextBox.prototype.autoScrollAndDrawCursor = function() {
362 return this.autoScrollAndDraw( true ) ;
363} ;
364
365
366
367TextBox.prototype.getContentSize = function() {
368 return this.textBuffer.getContentSize() ;
369} ;
370
371
372
373TextBox.prototype.getContent = function() {
374 return this.textBuffer.getText() ;
375} ;
376
377
378
379TextBox.prototype.setContent = function( content , hasMarkup , dontDraw ) {
380 var contentSize ;
381
382 if ( typeof content !== 'string' ) {
383 if ( content === null || content === undefined ) { content = '' ; }
384 else { content = '' + content ; }
385 }
386
387 this.content = content ;
388 this.contentHasMarkup = !! hasMarkup ;
389
390 this.textBuffer.setText( this.content , this.contentHasMarkup , this.textAttr ) ;
391
392 if ( this.stateMachine ) {
393 this.textBuffer.runStateMachine() ;
394 }
395
396 // Move the cursor at the end of the input
397 this.textBuffer.moveToEndOfLine() ;
398
399 if ( ! dontDraw ) {
400 this.drawCursor() ;
401 this.redraw() ;
402 }
403} ;
404
405
406
407// Get content for alternate textBuffer
408TextBox.prototype.getAltContent = function() {
409 if ( ! this.altTextBuffer ) { return null ; }
410 return this.altTextBuffer.getText() ;
411} ;
412
413
414
415// Set content for alternate textBuffer
416TextBox.prototype.setAltContent = function( content , hasMarkup , dontDraw ) {
417 if ( ! this.altTextBuffer ) { return ; }
418
419 var contentSize ;
420
421 if ( typeof content !== 'string' ) {
422 if ( content === null || content === undefined ) { content = '' ; }
423 else { content = '' + content ; }
424 }
425
426 //this.content = content ;
427 //this.contentHasMarkup = !! hasMarkup ;
428
429 this.altTextBuffer.setText( content , hasMarkup , this.altTextAttr ) ;
430
431 //if ( this.stateMachine ) { this.altTextBuffer.runStateMachine() ; }
432
433 if ( ! dontDraw ) {
434 this.drawCursor() ;
435 this.redraw() ;
436 }
437} ;
438
439
440
441TextBox.prototype.onKey = function( key , trash , data ) {
442 switch( this.keyBindings[ key ] ) {
443 case 'tinyScrollUp' :
444 this.scroll( 0 , Math.ceil( this.textAreaHeight / 5 ) ) ;
445 break ;
446
447 case 'tinyScrollDown' :
448 this.scroll( 0 , -Math.ceil( this.textAreaHeight / 5 ) ) ;
449 break ;
450
451 case 'scrollUp' :
452 this.scroll( 0 , Math.ceil( this.textAreaHeight / 2 ) ) ;
453 break ;
454
455 case 'scrollDown' :
456 this.scroll( 0 , -Math.ceil( this.textAreaHeight / 2 ) ) ;
457 break ;
458
459 case 'scrollLeft' :
460 this.scroll( Math.ceil( this.textAreaWidth / 2 ) , 0 ) ;
461 break ;
462
463 case 'scrollRight' :
464 this.scroll( -Math.ceil( this.textAreaWidth / 2 ) , 0 ) ;
465 break ;
466
467 case 'scrollTop' :
468 this.scroll( 0 , 0 ) ;
469 break ;
470
471 case 'scrollBottom' :
472 // Ignore extra scrolling here
473 this.scroll( 0 , this.textAreaHeight - this.textBuffer.buffer.length ) ;
474 break ;
475
476 case 'copyClipboard' :
477 if ( this.document ) {
478 this.document.setClipboard( this.textBuffer.getSelectionText() ).catch( () => undefined ) ;
479 }
480 break ;
481
482 default :
483 return ; // Bubble up
484 }
485
486 return true ; // Do not bubble up
487} ;
488
489
490
491TextBox.prototype.onClick = function( data ) {
492 // It is susceptible to click event only when it is scrollable
493 if ( this.scrollable && ! this.hasFocus ) {
494 this.document.giveFocusTo( this , 'select' ) ;
495 }
496} ;
497
498
499
500TextBox.prototype.onWheel = function( data ) {
501 // It's a "tiny" scroll
502 if ( ! this.hasFocus ) {
503 this.document.giveFocusTo( this , 'select' ) ;
504 }
505
506 if ( this.scrollable ) {
507 this.scroll( 0 , -data.yDirection * Math.ceil( this.textAreaHeight / 5 ) ) ;
508 }
509} ;
510
511
512
513TextBox.prototype.onDrag = function( data ) {
514 var xmin , ymin , xmax , ymax ;
515
516 if ( ! this.hasFocus ) {
517 this.document.giveFocusTo( this , 'select' ) ;
518 }
519
520 if ( data.yFrom < data.y || ( data.yFrom === data.y && data.xFrom <= data.x ) ) {
521 ymin = data.yFrom ;
522 ymax = data.y ;
523 xmin = data.xFrom ;
524 xmax = data.x ;
525 }
526 else {
527 ymin = data.y ;
528 ymax = data.yFrom ;
529 xmin = data.x ;
530 xmax = data.xFrom ;
531 }
532
533 this.textBuffer.setSelectionRegion( {
534 xmin: xmin - this.scrollX ,
535 xmax: xmax - this.scrollX ,
536 ymin: ymin - this.scrollY ,
537 ymax: ymax - this.scrollY
538 } ) ;
539
540 if ( this.document ) {
541 this.document.setClipboard( this.textBuffer.getSelectionText() , 'primary' ).catch( () => undefined ) ;
542 }
543
544 this.draw() ;
545} ;
546