UNPKG

14.5 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 TextBox = require( './TextBox.js' ) ;
33const boxesChars = require( '../spChars.js' ).box ;
34
35
36
37function TextTable( options ) {
38 // Clone options if necessary
39 options = ! options ? {} : options.internal ? options : Object.create( options ) ;
40 options.internal = true ;
41
42 Element.call( this , options ) ;
43
44 this.cellContents = options.cellContents ; // Should be an array of array of text
45 this.contentHasMarkup = options.contentHasMarkup ;
46
47 this.textBoxes = null ; // Same format: array of array of textBoxes
48 this.rowCount = 0 ;
49 this.columnCount = 0 ;
50
51 this.rowHeights = [] ;
52 this.columnWidths = [] ;
53
54 this.textAttr = options.textAttr || { bgColor: 'default' } ;
55 this.voidAttr = options.voidAttr || options.emptyAttr || null ;
56
57 this.firstRowTextAttr = options.firstRowTextAttr || null ;
58 this.firstRowVoidAttr = options.firstRowVoidAttr || null ;
59 this.evenRowTextAttr = options.evenRowTextAttr || null ;
60 this.evenRowVoidAttr = options.evenRowVoidAttr || null ;
61
62 this.firstColumnTextAttr = options.firstColumnTextAttr || null ;
63 this.firstColumnVoidAttr = options.firstColumnVoidAttr || null ;
64 this.evenColumnTextAttr = options.evenColumnTextAttr || null ;
65 this.evenColumnVoidAttr = options.evenColumnVoidAttr || null ;
66
67 this.firstCellTextAttr = options.firstCellTextAttr || null ;
68 this.firstCellVoidAttr = options.firstCellVoidAttr || null ;
69
70 // When rowNumber AND columnNumber are both even
71 this.evenCellTextAttr = options.evenCellTextAttr || null ;
72 this.evenCellVoidAttr = options.evenCellVoidAttr || null ;
73
74 // Checker-like: when rowNumber + columnNumber is even
75 this.checkerEvenCellTextAttr = options.checkerEvenCellTextAttr || null ;
76 this.checkerEvenCellVoidAttr = options.checkerEvenCellVoidAttr || null ;
77
78 this.expandToWidth = options.expandToWidth !== undefined ? !! options.expandToWidth : !! options.fit ;
79 this.shrinkToWidth = options.shrinkToWidth !== undefined ? !! options.shrinkToWidth : !! options.fit ;
80 this.expandToHeight =
81 options.expandToHeight !== undefined ? !! options.expandToHeight :
82 ! options.height ? false :
83 !! options.fit ;
84 this.shrinkToHeight =
85 options.shrinkToHeight !== undefined ? !! options.shrinkToHeight :
86 ! options.height ? false :
87 !! options.fit ;
88 this.wordWrap = options.wordWrap !== undefined || options.wordwrap !== undefined ?
89 !! ( options.wordWrap || options.wordwrap ) : !! options.fit ;
90 this.lineWrap = this.wordWrap || ( options.lineWrap !== undefined ? !! options.lineWrap : !! options.fit ) ;
91
92 this.hasBorder = options.hasBorder !== undefined ? !! options.hasBorder : true ; // Default to true
93 this.borderAttr = options.borderAttr || this.textAttr ;
94 this.borderChars = options.borderChars || boxesChars.light ;
95
96 if ( options.borderChars ) {
97 if ( typeof options.borderChars === 'object' ) {
98 this.borderChars = options.borderChars ;
99 }
100 else if ( typeof options.borderChars === 'string' && boxesChars[ options.borderChars ] ) {
101 this.borderChars = boxesChars[ options.borderChars ] ;
102 }
103 }
104
105 if ( options.textBoxKeyBindings ) { this.textBoxKeyBindings = options.textBoxKeyBindings ; }
106
107 this.initChildren() ;
108 this.computeCells() ;
109
110 if ( ! options.width ) { this.outputWidth = this.contentWidth ; }
111 if ( ! options.height ) { this.outputHeight = this.contentHeight ; }
112
113 // Only draw if we are not a superclass of the object
114 if ( this.elementType === 'TextTable' && ! options.noDraw ) { this.draw() ; }
115}
116
117module.exports = TextTable ;
118
119TextTable.prototype = Object.create( Element.prototype ) ;
120TextTable.prototype.constructor = TextTable ;
121TextTable.prototype.elementType = 'TextTable' ;
122
123
124
125// Support for strictInline mode
126TextTable.prototype.strictInlineSupport = true ;
127
128
129
130TextTable.prototype.destroy = function( isSubDestroy ) {
131 Element.prototype.destroy.call( this , isSubDestroy ) ;
132} ;
133
134
135
136TextTable.prototype.textBoxKeyBindings = TextBox.prototype.keyBindings ;
137
138
139
140TextTable.prototype.initChildren = function() {
141 var row , cellContent , textAttr , voidAttr ;
142
143 this.rowCount = this.cellContents.length ;
144 this.columnCount = 0 ;
145 this.textBoxes = [] ;
146
147 var x = 0 , y = 0 ;
148
149 for ( row of this.cellContents ) {
150 this.textBoxes[ y ] = [] ;
151 x = 0 ;
152
153 for ( cellContent of row ) {
154 if ( x >= this.columnCount ) { this.columnCount = x + 1 ; }
155
156 textAttr =
157 this.firstCellTextAttr && ! x && ! y ? this.firstCellTextAttr :
158 this.firstRowTextAttr && ! y ? this.firstRowTextAttr :
159 this.firstColumnTextAttr && ! x ? this.firstColumnTextAttr :
160 this.evenCellTextAttr && ! ( x % 2 ) && ! ( y % 2 ) ? this.evenCellTextAttr :
161 this.checkerEvenCellTextAttr && ! ( ( x + y ) % 2 ) ? this.checkerEvenCellTextAttr :
162 this.evenRowTextAttr && ! ( y % 2 ) ? this.evenRowTextAttr :
163 this.evenColumnTextAttr && ! ( y % 2 ) ? this.evenColumnTextAttr :
164 this.textAttr ;
165
166 voidAttr =
167 this.firstCellVoidAttr && ! x && ! y ? this.firstCellVoidAttr :
168 this.firstRowVoidAttr && ! y ? this.firstRowVoidAttr :
169 this.firstColumnVoidAttr && ! x ? this.firstColumnVoidAttr :
170 this.evenCellVoidAttr && ! ( x % 2 ) && ! ( y % 2 ) ? this.evenCellVoidAttr :
171 this.checkerEvenCellVoidAttr && ! ( ( x + y ) % 2 ) ? this.checkerEvenCellVoidAttr :
172 this.evenRowVoidAttr && ! ( y % 2 ) ? this.evenRowVoidAttr :
173 this.evenColumnVoidAttr && ! ( y % 2 ) ? this.evenColumnVoidAttr :
174 this.voidAttr || textAttr ;
175
176 this.textBoxes[ y ][ x ] = new TextBox( {
177 internal: true ,
178 parent: this ,
179 content: cellContent ,
180 contentHasMarkup: this.contentHasMarkup ,
181 //value: cellContent ,
182 x: this.outputX ,
183 y: this.outputY ,
184 width: this.outputWidth ,
185 height: this.outputHeight ,
186 lineWrap: this.lineWrap ,
187 wordWrap: this.wordWrap ,
188 //scrollable: !! this.scrollable ,
189 //vScrollBar: !! this.vScrollBar ,
190 //hScrollBar: !! this.hScrollBar ,
191 //hiddenContent: this.hiddenContent ,
192 textAttr ,
193 voidAttr ,
194 keyBindings: this.textBoxKeyBindings ,
195 noDraw: true
196 } ) ;
197
198 x ++ ;
199 }
200
201 y ++ ;
202 }
203} ;
204
205
206
207TextTable.prototype.computeCells = function() {
208 var shrinked = this.computeColumnWidths() ;
209
210 if ( shrinked ) {
211 this.textBoxesWordWrap() ;
212 //this.computeColumnWidths() ;
213 }
214
215 this.computeRowHeights() ;
216 this.textBoxesSizeAndPosition() ;
217} ;
218
219
220
221TextTable.prototype.computeColumnWidths = function() {
222 var x , y , textBox , max , width ;
223
224 this.contentWidth = + this.hasBorder ; // +true = 1
225
226 for ( x = 0 ; x < this.columnCount ; x ++ ) {
227 max = 0 ;
228 for ( y = 0 ; y < this.rowCount ; y ++ ) {
229 textBox = this.textBoxes[ y ][ x ] ;
230 if ( ! textBox ) { continue ; }
231 width = textBox.getContentSize().width || 1 ;
232 if ( width > max ) { max = width ; }
233 }
234 this.columnWidths[ x ] = max ;
235 this.contentWidth += max + this.hasBorder ; // +true = 1
236 }
237
238 if ( this.expandToWidth && this.contentWidth < this.outputWidth ) {
239 this.expand( this.contentWidth , this.outputWidth , this.columnWidths ) ;
240 }
241 else if ( this.shrinkToWidth && this.contentWidth > this.outputWidth ) {
242 this.shrink( this.contentWidth , this.outputWidth , this.columnWidths ) ;
243 return true ;
244 }
245
246 return false ;
247} ;
248
249
250
251TextTable.prototype.computeRowHeights = function() {
252 var x , y , textBox , max , height ;
253
254 this.contentHeight = + this.hasBorder ; // +true = 1
255 for ( y = 0 ; y < this.rowCount ; y ++ ) {
256 max = 0 ;
257 for ( x = 0 ; x < this.columnCount ; x ++ ) {
258 textBox = this.textBoxes[ y ][ x ] ;
259 if ( ! textBox ) { continue ; }
260 height = textBox.getContentSize().height || 1 ;
261 if ( height > max ) { max = height ; }
262 }
263 this.rowHeights[ y ] = max ;
264 this.contentHeight += max + this.hasBorder ; // +true = 1
265 }
266
267 if ( this.expandToHeight && this.contentHeight < this.outputHeight ) {
268 this.expand( this.contentHeight , this.outputHeight , this.rowHeights ) ;
269 }
270 else if ( this.shrinkToHeight && this.contentHeight > this.outputHeight ) {
271 this.shrink( this.contentHeight , this.outputHeight , this.rowHeights ) ;
272 return true ;
273 }
274} ;
275
276
277
278// Expand an array of size, using proportional expansion
279TextTable.prototype.expand = function( contentSize , outputSize , sizeArray ) {
280 var x ,
281 floatSize = 0 ,
282 remainder = 0 ,
283 count = sizeArray.length ,
284 noBorderWantedSize = outputSize - ( this.hasBorder ? count + 1 : 0 ) ;
285
286 if ( noBorderWantedSize <= 0 ) { return ; }
287
288 var noBorderSize = contentSize - ( this.hasBorder ? count + 1 : 0 ) ,
289 rate = noBorderWantedSize / noBorderSize ;
290
291 // Adjust from left to right
292 for ( x = 0 ; x < count ; x ++ ) {
293 floatSize = sizeArray[ x ] * rate + remainder ;
294 sizeArray[ x ] = Math.max( 1 , Math.round( floatSize ) ) ;
295 remainder = floatSize - sizeArray[ x ] ;
296 }
297} ;
298
299
300
301// Shrink an array of size, larger are shrinked first
302TextTable.prototype.shrink = function( contentSize , outputSize , sizeArray ) {
303 var x , max ,
304 secondMax = 0 ,
305 maxIndexes = [] ,
306 count = sizeArray.length ,
307 floatColumnDelta , columnDelta , partialColumn ,
308 delta = contentSize - outputSize ;
309
310 //console.log( contentSize , outputSize , delta ) ;
311
312 while ( delta > 0 ) {
313 max = 0 ;
314 secondMax = 0 ;
315 maxIndexes.length = 0 ;
316
317 for ( x = 0 ; x < count ; x ++ ) {
318 if ( sizeArray[ x ] > max ) {
319 secondMax = max ;
320 max = sizeArray[ x ] ;
321 maxIndexes.length = 0 ;
322 maxIndexes.push( x ) ;
323 }
324 else if ( sizeArray[ x ] === max ) {
325 maxIndexes.push( x ) ;
326 }
327 else if ( sizeArray[ x ] > secondMax ) {
328 secondMax = sizeArray[ x ] ;
329 }
330 }
331
332 // We can't shrink anymore
333 // /!\ we should probably test that before entering the loop!
334 if ( ! max ) { return ; }
335
336 floatColumnDelta = Math.min( max - secondMax , delta / maxIndexes.length ) ;
337 columnDelta = Math.floor( floatColumnDelta ) ;
338
339 if ( columnDelta >= 0 ) {
340 for ( let index of maxIndexes ) {
341 sizeArray[ index ] -= columnDelta ;
342 delta -= columnDelta ;
343 }
344 }
345
346 if ( columnDelta !== floatColumnDelta ) {
347 partialColumn = delta % maxIndexes.length ;
348 for ( let i = 0 ; i < maxIndexes.length && i < partialColumn ; i ++ ) {
349 sizeArray[ maxIndexes[ i ] ] -- ;
350 }
351 delta -= partialColumn ;
352 }
353 }
354} ;
355
356
357
358TextTable.prototype.textBoxesWordWrap = function() {
359 var x , y , textBox ;
360
361 for ( y = 0 ; y < this.rowCount ; y ++ ) {
362 for ( x = 0 ; x < this.columnCount ; x ++ ) {
363 textBox = this.textBoxes[ y ][ x ] ;
364
365 if ( textBox ) {
366 textBox.setSizeAndPosition( {
367 outputX: this.outputX ,
368 outputY: this.outputY ,
369 outputWidth: this.columnWidths[ x ] ,
370 outputHeight: this.outputHeight
371 } ) ;
372 }
373 }
374 }
375} ;
376
377
378
379TextTable.prototype.textBoxesSizeAndPosition = function() {
380 var x , y , outputX , outputY , textBox ;
381
382 outputY = this.outputY + this.hasBorder ;
383
384 for ( y = 0 ; y < this.rowCount ; y ++ ) {
385 outputX = this.outputX + this.hasBorder ;
386
387 for ( x = 0 ; x < this.columnCount ; x ++ ) {
388 textBox = this.textBoxes[ y ][ x ] ;
389
390 if ( textBox ) {
391 textBox.setSizeAndPosition( {
392 outputX ,
393 outputY ,
394 outputWidth: this.columnWidths[ x ] ,
395 outputHeight: this.rowHeights[ y ]
396 } ) ;
397 }
398
399 outputX += this.columnWidths[ x ] + this.hasBorder ;
400 }
401
402 outputY += this.rowHeights[ y ] + this.hasBorder ;
403 }
404} ;
405
406
407
408TextTable.prototype.preDrawSelf = function() {
409 // This only draw the frame/border
410 if ( ! this.hasBorder ) { return ; }
411
412 var i , j , x , y ;
413
414 //console.log( this.columnWidths , this.rowHeights , this.columnCount , this.rowCount ) ;
415
416 y = this.outputY ;
417
418 for ( j = 0 ; j < this.rowHeights.length ; j ++ ) {
419 x = this.outputX ;
420
421 for ( i = 0 ; i < this.columnWidths.length ; i ++ ) {
422 // For each cell...
423
424 // ... draw the top-left corner
425 this.outputDst.put( { x , y , attr: this.borderAttr } ,
426 j ?
427 ( i ? this.borderChars.cross : this.borderChars.leftTee ) :
428 ( i ? this.borderChars.topTee : this.borderChars.topLeft )
429 ) ;
430
431 // ... draw the left border
432 this.outputDst.put( {
433 x , y: y + 1 , direction: 'down' , attr: this.borderAttr
434 } , this.borderChars.vertical.repeat( this.rowHeights[ j ] ) ) ;
435 x ++ ;
436
437 // ... draw the top border
438 this.outputDst.put( { x , y , attr: this.borderAttr } , this.borderChars.horizontal.repeat( this.columnWidths[ i ] ) ) ;
439 x += this.columnWidths[ i ] ;
440 }
441
442 // Draw the top-right corner only for the last cell
443 this.outputDst.put( { x , y , attr: this.borderAttr } , j ? this.borderChars.rightTee : this.borderChars.topRight ) ;
444
445 // Draw the right border only for the last cell
446 this.outputDst.put( {
447 x , y: y + 1 , direction: 'down' , attr: this.borderAttr
448 } , this.borderChars.vertical.repeat( this.rowHeights[ j ] ) ) ;
449 y += this.rowHeights[ j ] + 1 ;
450 }
451
452
453 // Only for the last row, we have to draw the bottom border and corners
454 x = this.outputX ;
455
456 for ( i = 0 ; i < this.columnWidths.length ; i ++ ) {
457 // For each bottom cells...
458
459 // ... draw the bottom-left corner
460 this.outputDst.put( { x , y , attr: this.borderAttr } , i ? this.borderChars.bottomTee : this.borderChars.bottomLeft ) ;
461 x ++ ;
462
463 // ... draw the bottom border
464 this.outputDst.put( { x , y , attr: this.borderAttr } , this.borderChars.horizontal.repeat( this.columnWidths[ i ] ) ) ;
465 x += this.columnWidths[ i ] ;
466 }
467
468 // Draw the bottom-right corner only for the last cell of the last row
469 this.outputDst.put( { x , y , attr: this.borderAttr } , this.borderChars.bottomRight ) ;
470} ;
471