UNPKG

17.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 Element = require( './Element.js' ) ;
32const Container = require( './Container.js' ) ;
33
34const Promise = require( 'seventh' ) ;
35
36
37
38function Document( options ) {
39 // Clone options if necessary
40 options = ! options ? {} : options.internal ? options : Object.create( options ) ;
41 options.internal = true ;
42
43 if ( ! options.inlineTerm ) {
44 options.outputX = 1 ;
45 options.outputY = 1 ;
46 options.outputWidth = options.outputDst.width ;
47 options.outputHeight = options.outputDst.height ;
48 }
49
50 // Bypass the Element rule for strictInline, this mode should only be used for inline static Element
51 this.strictInlineSupport = !! options.strictInline ;
52
53 Container.call( this , options ) ;
54
55 // A document do not have parent
56 this.parent = null ;
57
58 // The document of a document is itself
59 this.document = this ;
60
61 // Being the top-level element before the Terminal object, this must use delta-drawing (except for strictInline mode)
62 this.deltaDraw = ! this.strictInline ;
63
64 this.id = '_document' + '_' + ( nextId ++ ) ;
65 this.eventSource = options.eventSource ;
66 this.focusElement = null ;
67 this.hoverElement = null ;
68 this.clickOutCandidates = new Set() ;
69 this.elements = {} ;
70 this.onEventSourceKey = this.onEventSourceKey.bind( this ) ;
71 this.onEventSourceMouse = this.onEventSourceMouse.bind( this ) ;
72 this.onEventSourceResize = this.onEventSourceResize.bind( this ) ;
73
74 if ( ! this.strictInline ) {
75 // Do not change turn on/change input grabbing mode in strictInline mode
76 this.eventSource.grabInput( { mouse: 'motion' } ) ;
77 //this.eventSource.grabInput( { mouse: 'button' } ) ;
78 }
79
80 if ( options.keyBindings ) { this.keyBindings = options.keyBindings ; }
81 this.elementByShortcut = {} ;
82
83 this.setClipboard = Promise.debounceUpdate( async ( str , source ) => {
84 if ( ! this.outputDst.setClipboard ) { return ; }
85 await this.outputDst.setClipboard( str , source ) ;
86
87 // Avoid running too much xclip shell command
88 await Promise.resolveTimeout( 500 ) ;
89 } ) ;
90
91 this.getClipboard = Promise.debounceDelay( 500 , async ( source ) => {
92 if ( ! this.outputDst.getClipboard ) { return '' ; }
93 return this.outputDst.getClipboard( source ) ;
94 } ) ;
95
96 this.eventSource.on( 'key' , this.onEventSourceKey ) ;
97 this.eventSource.on( 'mouse' , this.onEventSourceMouse ) ;
98 this.eventSource.on( 'resize' , this.onEventSourceResize ) ;
99
100 // Only draw if we are not a superclass of the object
101 if ( this.elementType === 'Document' && ! options.noDraw ) { this.draw() ; }
102}
103
104module.exports = Document ;
105
106//Document.prototype = Object.create( Element.prototype ) ;
107Document.prototype = Object.create( Container.prototype ) ;
108Document.prototype.constructor = Document ;
109Document.prototype.elementType = 'Document' ;
110
111
112
113Document.prototype.destroy = function( isSubDestroy ) {
114 this.eventSource.off( 'key' , this.onEventSourceKey ) ;
115 this.eventSource.off( 'mouse' , this.onEventSourceMouse ) ;
116 this.eventSource.off( 'resize' , this.onEventSourceResize ) ;
117
118 Element.prototype.destroy.call( this , isSubDestroy ) ;
119} ;
120
121
122
123Document.prototype.keyBindings = {
124 TAB: 'focusNext' ,
125 SHIFT_TAB: 'focusPrevious'
126} ;
127
128
129
130// Next element ID
131var nextId = 0 ;
132
133Document.prototype.assignId = function( element , id ) {
134 if ( ! id || typeof id !== 'string' || id[ 0 ] === '_' || this.elements[ id ] ) {
135 id = '_' + element.elementType + '_' + ( nextId ++ ) ;
136 }
137
138 element.id = id ;
139 this.elements[ id ] = element ;
140} ;
141
142
143
144Document.prototype.unassignId = function( element , id ) {
145 element.id = null ;
146 delete this.elements[ id ] ;
147} ;
148
149
150
151Document.prototype.giveFocusTo = function( element , type ) {
152 if ( ! ( element instanceof Element ) ) { throw new TypeError( '' + element + ' is not an instance of Element.' ) ; }
153 if ( ! type ) { type = 'direct' ; }
154 if ( this.isAncestorOf( element ) ) { return this.giveFocusTo_( element , type ) ; }
155} ;
156
157
158
159Document.prototype.giveFocusTo_ = function( element , type ) {
160 var ancestor , focusAware ;
161
162 if ( this.focusElement !== element ) {
163 if ( this.focusElement ) { this.focusElement.emit( 'focus' , false , type , this.focusElement ) ; }
164 this.focusElement = element ;
165 this.focusElement.emit( 'focus' , true , type , this.focusElement ) ;
166 }
167
168 // Return false if the focus was given to an element that does not care about focus and key event
169 focusAware = ! this.focusElement.disabled && ( this.focusElement.listenerCount( 'focus' ) || this.focusElement.listenerCount( 'key' ) ) ;
170
171 if ( focusAware ) {
172 ancestor = this.focusElement ;
173
174 while ( ancestor ) {
175 if ( ancestor.listenerCount( 'clickOut' ) ) { this.clickOutCandidates.add( ancestor ) ; }
176 ancestor = ancestor.parent ;
177 }
178 }
179
180 return focusAware ;
181} ;
182
183
184
185Document.prototype.focusNext = function() {
186 var index , startingElement , currentElement , focusAware ;
187
188 if ( ! this.focusElement || ! this.isAncestorOf( this.focusElement ) ) { currentElement = this ; }
189 else { currentElement = this.focusElement ; }
190
191 if ( currentElement === this && ! this.children.length ) { return ; }
192
193 startingElement = currentElement ;
194
195 for ( ;; ) {
196 if ( currentElement.children.length && ! currentElement.noChildFocus ) {
197 // Give focus to the first child of the element
198 currentElement = currentElement.children[ 0 ] ;
199 if ( ! currentElement.hidden ) { focusAware = this.giveFocusTo_( currentElement , 'cycle' ) ; }
200 }
201 else if ( currentElement.parent ) {
202 for ( ;; ) {
203 index = currentElement.parent.children.indexOf( currentElement ) ;
204
205 if ( index + 1 < currentElement.parent.children.length ) {
206 // Give focus to the next sibling
207 currentElement = currentElement.parent.children[ index + 1 ] ;
208 if ( ! currentElement.hidden ) {
209 focusAware = this.giveFocusTo_( currentElement , 'cycle' ) ;
210 break ;
211 }
212 }
213 else if ( currentElement.parent.parent ) {
214 currentElement = currentElement.parent ;
215 }
216 else {
217 // We are at the top-level, just below the document, so cycle again at the first-top-level child
218
219 // This check fixes infinite loop
220 if ( startingElement === currentElement.parent ) { return ; }
221
222 currentElement = currentElement.parent.children[ 0 ] ;
223 if ( ! currentElement.hidden ) {
224 focusAware = this.giveFocusTo_( currentElement , 'cycle' ) ;
225 break ;
226 }
227 }
228 }
229 }
230 else {
231 // Nothing to do: no children, no parent, nothing...
232 return ;
233 }
234
235 // Exit if the focus was given to a focus-aware element or if we have done a full loop already
236 //console.error( 'end of loop: ' , focusAware , startingElement.content , currentElement.content ) ;
237 if ( startingElement === currentElement || ( ! currentElement.hidden && focusAware ) ) { break ; }
238 }
239} ;
240
241
242
243Document.prototype.focusPrevious = function() {
244 var index , startingElement , currentElement , focusAware ;
245
246 if ( ! this.focusElement || ! this.isAncestorOf( this.focusElement ) ) { currentElement = this ; }
247 else { currentElement = this.focusElement ; }
248
249 startingElement = currentElement ;
250
251 for ( ;; ) {
252 if ( currentElement.parent ) {
253 index = currentElement.parent.children.indexOf( currentElement ) ;
254
255 if ( index - 1 >= 0 ) {
256 // Give focus to the last child of the last child of the ... of the previous sibling
257 currentElement = currentElement.parent.children[ index - 1 ] ;
258
259 while ( currentElement.children.length && ! currentElement.noChildFocus ) {
260 currentElement = currentElement.children[ currentElement.children.length - 1 ] ;
261 }
262
263 if ( ! currentElement.hidden ) { focusAware = this.giveFocusTo_( currentElement , 'cycle' ) ; }
264 }
265 else if ( currentElement.parent.parent ) {
266 currentElement = currentElement.parent ;
267 if ( ! currentElement.hidden ) { focusAware = this.giveFocusTo_( currentElement , 'cycle' ) ; }
268 }
269 else {
270 // We are at the top-level, just below the document, so cycle again to the last child of the last child
271 // of the ... of the last-top-level child
272
273 // This check fixes infinite loop
274 if ( startingElement === currentElement.parent ) { return ; }
275
276 currentElement = currentElement.parent.children[ currentElement.parent.children.length - 1 ] ;
277
278 while ( currentElement.children.length && ! currentElement.noChildFocus ) {
279 currentElement = currentElement.children[ currentElement.children.length - 1 ] ;
280 }
281
282 if ( ! currentElement.hidden ) { focusAware = this.giveFocusTo_( currentElement , 'cycle' ) ; }
283 }
284 }
285 else if ( currentElement.children.length ) {
286 // Give focus to the last child of the element
287 currentElement = currentElement.children[ currentElement.children.length - 1 ] ;
288
289 while ( currentElement.children.length && ! currentElement.noChildFocus ) {
290 currentElement = currentElement.children[ currentElement.children.length - 1 ] ;
291 }
292
293 if ( ! currentElement.hidden ) { focusAware = this.giveFocusTo_( currentElement , 'cycle' ) ; }
294 }
295 else {
296 // Nothing to do: no children, no parent, nothing...
297 return ;
298 }
299
300 // Exit if the focus was given to a focus-aware element or if we have done a full loop already
301 //console.error( 'end of loop: ' , focusAware , startingElement.content , currentElement.content ) ;
302 if ( startingElement === currentElement || ( ! currentElement.hidden && focusAware ) ) { break ; }
303 }
304} ;
305
306
307
308Document.prototype.onEventSourceKey = function( key , altKeys , data ) {
309 if ( this.focusElement ) {
310 this.bubblingEvent( this.focusElement , key , altKeys , data ) ;
311 }
312 else {
313 this.defaultKeyHandling( key , altKeys , data ) ;
314 }
315} ;
316
317
318
319Document.prototype.bubblingEvent = function( element , key , altKeys , data ) {
320 if ( element !== this ) {
321 element.emit( 'key' , key , altKeys , data , ( interruption , event ) => {
322 // Interruption means: the child consume the event, it does not want bubbling
323 if ( ! interruption ) {
324 if ( element.parent ) { this.bubblingEvent( element.parent , key , altKeys , data ) ; }
325 else { this.defaultKeyHandling( key , altKeys , data ) ; }
326 }
327 } ) ;
328 }
329 else {
330 this.defaultKeyHandling( key , altKeys , data ) ;
331 }
332} ;
333
334
335
336Document.prototype.defaultKeyHandling = function( key , altKeys , data ) {
337 switch ( this.keyBindings[ key ] ) {
338 case 'focusNext' :
339 this.focusNext() ;
340 break ;
341 case 'focusPrevious' :
342 this.focusPrevious() ;
343 break ;
344 default :
345 if ( this.elementByShortcut[ key ] && this.elementByShortcut[ key ].document === this ) {
346 this.elementByShortcut[ key ].emit( 'shortcut' , key , altKeys , data ) ;
347 }
348 else {
349 this.emit( 'key' , key , altKeys , data ) ;
350 }
351 break ;
352 }
353} ;
354
355
356
357Document.prototype.createShortcuts = function( element , ... keys ) {
358 if ( element.document !== this ) { return ; }
359 keys.forEach( key => this.elementByShortcut[ key ] = element ) ;
360} ;
361
362
363
364Document.prototype.removeElementShortcuts = function( element ) {
365 for ( let key in this.elementByShortcut ) {
366 if ( this.elementByShortcut[ key ] === element ) { this.elementByShortcut[ key ] = null ; }
367 }
368} ;
369
370
371
372Document.prototype.onEventSourceMouse = function( name , data ) {
373 var matches ;
374
375 switch ( name ) {
376 case 'MOUSE_LEFT_BUTTON_PRESSED' :
377 this.mouseClick( data ) ;
378 break ;
379
380 case 'MOUSE_MOTION' :
381 this.mouseMotion( data ) ;
382 break ;
383
384 case 'MOUSE_DRAG' :
385 this.mouseDrag( data ) ;
386 break ;
387
388 case 'MOUSE_MIDDLE_BUTTON_PRESSED' :
389 this.mouseMiddleClick( data ) ;
390 break ;
391
392 case 'MOUSE_WHEEL_UP' :
393 data.yDirection = -1 ;
394 this.mouseWheel( data ) ;
395 break ;
396
397 case 'MOUSE_WHEEL_DOWN' :
398 data.yDirection = 1 ;
399 this.mouseWheel( data ) ;
400 break ;
401 }
402} ;
403
404
405
406/*
407 /!\ Not sure if it's the correct way to do that /!\
408 E.g: Does an element that listen to 'hover' intercept 'click'?
409 It is already proven to be bad for the mouse wheel, for ColumnMenu, it would prevent the menu to scroll on mouse wheel
410 because the buttons (children) catch the event without doing anything at all with it.
411
412 Mouse event must have event bubbling too.
413*/
414const COMMON_MOUSE_AWARE_FILTER = element =>
415 element.listenerCount( 'click' ) || element.listenerCount( 'clickOut' ) ||
416 element.listenerCount( 'middleClick' ) || //element.listenerCount( 'wheel' ) ||
417 element.listenerCount( 'drag' ) ||
418 element.listenerCount( 'hover' ) || element.listenerCount( 'leave' ) || element.listenerCount( 'enter' ) ;
419
420
421
422Document.prototype.mouseClick = function( data ) {
423 var matches = this.childrenAt( data.x - this.outputX , data.y - this.outputY , COMMON_MOUSE_AWARE_FILTER ) ;
424 //console.error( "\n\n\n\n" , matches ) ;
425
426 if ( ! matches.length ) {
427 if ( this.clickOutCandidates.size ) {
428 for ( let candidate of this.clickOutCandidates ) {
429 // Check that the candidate is still attached
430 if ( candidate.document === this ) {
431 candidate.emit( 'clickOut' ) ;
432 }
433 }
434 this.clickOutCandidates.clear() ;
435 }
436
437 return ;
438 }
439
440 if ( this.clickOutCandidates.size ) {
441 for ( let candidate of this.clickOutCandidates ) {
442 // Check that the candidate is still attached and is not on the click's tree branch
443 if ( candidate.document === this && candidate !== matches[ 0 ].element && ! candidate.isAncestorOf( matches[ 0 ].element ) ) {
444 candidate.emit( 'clickOut' ) ;
445 }
446 }
447
448 this.clickOutCandidates.clear() ;
449 }
450
451 matches[ 0 ].element.emit( 'click' , { x: matches[ 0 ].x , y: matches[ 0 ].y } , matches[ 0 ].element ) ;
452} ;
453
454
455
456Document.prototype.mouseMotion = function( data ) {
457 var matches = this.childrenAt( data.x - this.outputX , data.y - this.outputY , COMMON_MOUSE_AWARE_FILTER ) ;
458 //console.error( "\n\n\n\n" , matches ) ;
459
460 if ( ! matches.length ) {
461 if ( this.hoverElement ) {
462 this.hoverElement.emit( 'leave' ) ;
463 this.hoverElement = null ;
464 }
465
466 return ;
467 }
468
469 matches[ 0 ].element.emit( 'hover' , { x: matches[ 0 ].x , y: matches[ 0 ].y } , matches[ 0 ].element ) ;
470
471 matches.forEach( match => {
472 if ( match.element.listenerCount( 'clickOut' ) ) {
473 this.clickOutCandidates.add( match.element ) ;
474 }
475 } ) ;
476
477 if ( matches[ 0 ].element !== this.hoverElement ) {
478 if ( this.hoverElement ) { this.hoverElement.emit( 'leave' ) ; }
479
480 this.hoverElement = matches[ 0 ].element ;
481 this.hoverElement.emit( 'enter' ) ;
482 }
483} ;
484
485
486
487Document.prototype.mouseDrag = function( data ) {
488 var matches = this.childrenAt( data.x - this.outputX , data.y - this.outputY , COMMON_MOUSE_AWARE_FILTER ) ;
489 //console.error( "\n\n\n\n" , matches ) ;
490
491 if ( ! matches.length ) {
492 if ( this.hoverElement ) {
493 this.hoverElement.emit( 'leave' ) ;
494 this.hoverElement = null ;
495 }
496
497 return ;
498 }
499
500 // Elements that match the starting position of the drag
501 var fromMatches = this.childrenAt( data.xFrom - this.outputX , data.yFrom - this.outputY , COMMON_MOUSE_AWARE_FILTER ) ;
502
503 // To send a 'drag' event, the origin of the drag should be on the same element
504 if ( fromMatches.length && fromMatches[ 0 ].element === matches[ 0 ].element ) {
505 // True drag
506 matches[ 0 ].element.emit(
507 'drag' ,
508 {
509 xFrom: fromMatches[ 0 ].x ,
510 yFrom: fromMatches[ 0 ].y ,
511 x: matches[ 0 ].x ,
512 y: matches[ 0 ].y
513 } ,
514 matches[ 0 ].element
515 ) ;
516 }
517 else {
518 // Starting and current element mismatch, convert it to a 'hover' event
519 matches[ 0 ].element.emit( 'hover' , { x: matches[ 0 ].x , y: matches[ 0 ].y } , matches[ 0 ].element ) ;
520 }
521
522 matches.forEach( match => {
523 if ( match.element.listenerCount( 'clickOut' ) ) {
524 this.clickOutCandidates.add( match.element ) ;
525 }
526 } ) ;
527
528 if ( matches[ 0 ].element !== this.hoverElement ) {
529 if ( this.hoverElement ) { this.hoverElement.emit( 'leave' ) ; }
530
531 this.hoverElement = matches[ 0 ].element ;
532 this.hoverElement.emit( 'enter' ) ;
533 }
534} ;
535
536
537
538Document.prototype.mouseMiddleClick = function( data ) {
539 var matches = this.childrenAt( data.x - this.outputX , data.y - this.outputY , COMMON_MOUSE_AWARE_FILTER ) ;
540 if ( ! matches.length ) { return ; }
541 matches[ 0 ].element.emit( 'middleClick' , { x: matches[ 0 ].x , y: matches[ 0 ].y } , matches[ 0 ].element ) ;
542} ;
543
544
545
546Document.prototype.mouseWheel = function( data ) {
547 //var matches = this.childrenAt( data.x - this.outputX , data.y - this.outputY , COMMON_MOUSE_AWARE_FILTER ) ;
548 var matches = this.childrenAt( data.x - this.outputX , data.y - this.outputY , element => element.listenerCount( 'wheel' ) ) ;
549 if ( ! matches.length ) { return ; }
550 matches[ 0 ].element.emit( 'wheel' , { x: matches[ 0 ].x , y: matches[ 0 ].y , yDirection: data.yDirection } , matches[ 0 ].element ) ;
551} ;
552
553
554
555Document.prototype.onEventSourceResize = function( width , height ) {
556 //console.error( "Document#onEventSourceResize() " , width , height ) ;
557
558 // Always resize inputDst to match outputDst (Terminal)
559 this.resizeInput( {
560 x: 0 ,
561 y: 0 ,
562 width: width ,
563 height: height
564 } ) ;
565
566 //this.inputDst.clear() ;
567 //this.postDrawSelf() ;
568
569 this.draw() ;
570} ;
571