UNPKG

14.4 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 BaseMenu = require( './BaseMenu.js' ) ;
33const Button = require( './Button.js' ) ;
34const ToggleButton = require( './ToggleButton.js' ) ;
35
36
37
38// Inherit from BaseMenu for common methods
39
40function ColumnMenu( options ) {
41 // Clone options if necessary
42 options = ! options ? {} : options.internal ? options : Object.create( options ) ;
43 options.internal = true ;
44
45 if ( ! options.outputWidth && ! options.width ) {
46 if ( options.parent ) {
47 options.outputWidth = Math.min( options.parent.inputWidth , options.parent.outputWidth ) ;
48 }
49 else if ( options.inlineTerm ) {
50 options.outputWidth = options.inlineTerm.width ;
51 }
52 }
53
54 this.buttonsMaxWidth = 0 ;
55 this.buttonPaddingWidth = 0 ;
56 this.buttonSymbolWidth = 0 ;
57 this.pageMaxHeight = options.pageMaxHeight || null ;
58 this.pageHeight = 0 ; // current page height, computed
59 this.pageItemsDef = [] ;
60 //this.masterItem = options.masterItem || null ;
61
62 if ( ! options.multiLineItems ) {
63 options.height = options.items && options.items.length ;
64 }
65
66 BaseMenu.call( this , options ) ;
67
68 this.multiLineItems = !! options.multiLineItems ;
69
70 this.initChildren() ;
71
72 // Only draw if we are not a superclass of the object
73 if ( this.elementType === 'ColumnMenu' && ! options.noDraw ) { this.draw() ; }
74}
75
76module.exports = ColumnMenu ;
77
78ColumnMenu.prototype = Object.create( BaseMenu.prototype ) ;
79ColumnMenu.prototype.constructor = ColumnMenu ;
80ColumnMenu.prototype.elementType = 'ColumnMenu' ;
81
82
83
84ColumnMenu.prototype.inlineNewLine = true ;
85ColumnMenu.prototype.ButtonClass = Button ;
86
87
88
89ColumnMenu.prototype.defaultOptions = {
90 buttonBlurAttr: { bgColor: 'black' , color: 'white' , bold: true } ,
91 buttonEvenBlurAttr: null ,
92 buttonFocusAttr: { bgColor: 'white' , color: 'black' , bold: true } ,
93 buttonDisabledAttr: { bgColor: 'black' , color: 'gray' , bold: true } ,
94 buttonSubmittedAttr: { bgColor: 'gray' , color: 'brightWhite' , bold: true } ,
95 turnedOnBlurAttr: { bgColor: 'cyan' } ,
96 turnedOnFocusAttr: { bgColor: 'brightCyan' , bold: true } ,
97 turnedOffBlurAttr: { bgColor: 'gray' , dim: true } ,
98 turnedOffFocusAttr: { bgColor: 'white' , color: 'black' , bold: true }
99} ;
100
101
102
103ColumnMenu.prototype.destroy = function( isSubDestroy ) {
104 this.off( 'key' , this.onKey ) ;
105 this.off( 'focus' , this.onFocus ) ;
106
107 Element.prototype.destroy.call( this , isSubDestroy ) ;
108} ;
109
110
111
112ColumnMenu.prototype.keyBindings = {
113 UP: 'previous' ,
114 DOWN: 'next' ,
115 PAGE_UP: 'previousPage' ,
116 PAGE_DOWN: 'nextPage' ,
117 HOME: 'firstPage' ,
118 END: 'lastPage' ,
119 // ENTER: 'submit' ,
120 // KP_ENTER: 'submit' ,
121 ALT_ENTER: 'submit'
122} ;
123
124ColumnMenu.prototype.buttonKeyBindings = {
125 ENTER: 'submit' ,
126 KP_ENTER: 'submit'
127} ;
128
129ColumnMenu.prototype.toggleButtonKeyBindings = {
130 ENTER: 'toggle' ,
131 KP_ENTER: 'toggle'
132} ;
133
134
135
136// Pre-compute page and eventually create Buttons automatically
137ColumnMenu.prototype.initChildren = function() {
138 // Do not exit now: maybe there are masterDef and separatorDef (SelectList*)
139 //if ( ! this.itemsDef.length ) { return ; }
140
141 this.buttonPaddingWidth =
142 Math.max(
143 Element.computeContentWidth( this.blurLeftPadding , this.paddingHasMarkup ) ,
144 Element.computeContentWidth( this.focusLeftPadding , this.paddingHasMarkup ) ,
145 Element.computeContentWidth( this.disabledLeftPadding , this.paddingHasMarkup ) ,
146 Element.computeContentWidth( this.submittedLeftPadding , this.paddingHasMarkup ) ,
147 Element.computeContentWidth( this.turnedOnFocusLeftPadding , this.paddingHasMarkup ) ,
148 Element.computeContentWidth( this.turnedOffFocusLeftPadding , this.paddingHasMarkup ) ,
149 Element.computeContentWidth( this.turnedOnBlurLeftPadding , this.paddingHasMarkup ) ,
150 Element.computeContentWidth( this.turnedOffBlurLeftPadding , this.paddingHasMarkup )
151 ) + Math.max(
152 Element.computeContentWidth( this.blurRightPadding , this.paddingHasMarkup ) ,
153 Element.computeContentWidth( this.focusRightPadding , this.paddingHasMarkup ) ,
154 Element.computeContentWidth( this.disabledRightPadding , this.paddingHasMarkup ) ,
155 Element.computeContentWidth( this.submittedRightPadding , this.paddingHasMarkup ) ,
156 Element.computeContentWidth( this.turnedOnFocusRightPadding , this.paddingHasMarkup ) ,
157 Element.computeContentWidth( this.turnedOffFocusRightPadding , this.paddingHasMarkup ) ,
158 Element.computeContentWidth( this.turnedOnBlurRightPadding , this.paddingHasMarkup ) ,
159 Element.computeContentWidth( this.turnedOffBlurRightPadding , this.paddingHasMarkup )
160 ) ;
161
162 if ( this.buttonPaddingWidth > this.outputWidth ) {
163 // The padding itself is bigger than the width... so what should we do?
164 return ;
165 }
166
167 var ellipsisWidth = Element.computeContentWidth( this.contentEllipsis , false ) ;
168
169
170 this.previousPageDef = Object.assign( { content: '▲' , internalRole: 'previousPage' } , this.previousPageDef ) ;
171 this.previousPageDef.contentHasMarkup = this.previousPageDef.contentHasMarkup || this.previousPageDef.markup ;
172 this.previousPageDef.width = this.buttonPaddingWidth + Element.computeContentWidth( this.previousPageDef.content , this.previousPageDef.contentHasMarkup ) ;
173 this.previousPageDef.buttonContent = this.previousPageDef.content ;
174
175 this.nextPageDef = Object.assign( { content: '▼' , internalRole: 'nextPage' } , this.nextPageDef ) ;
176 this.nextPageDef.contentHasMarkup = this.nextPageDef.contentHasMarkup || this.nextPageDef.markup ;
177 this.nextPageDef.width = this.buttonPaddingWidth + Element.computeContentWidth( this.nextPageDef.content , this.nextPageDef.contentHasMarkup ) ;
178 this.nextPageDef.buttonContent = this.nextPageDef.content ;
179
180 if ( this.masterDef ) {
181 this.masterDef = Object.assign( { content: 'column-menu' , internalRole: 'master' } , this.masterDef ) ;
182 this.masterDef.contentHasMarkup = this.masterDef.contentHasMarkup || this.masterDef.markup ;
183
184 this.masterDef.buttonContent = this.masterDef.content ;
185
186 if ( this.masterDef.symbol ) {
187 this.buttonSymbolWidth = 1 + Element.computeContentWidth( this.masterDef.symbol ) ;
188 this.masterDef.buttonContent += ' ' + this.masterDef.symbol ;
189 }
190
191 this.masterDef.width = this.buttonPaddingWidth + Element.computeContentWidth( this.masterDef.buttonContent , this.masterDef.contentHasMarkup ) ;
192 }
193
194 this.buttonsMaxWidth = Math.max( this.buttonsMaxWidth , this.previousPageDef.width , this.nextPageDef.width , this.masterDef ? this.masterDef.width : 0 ) ;
195
196
197 var page = 0 , pageHeight = 0 ;
198
199 this.itemsDef.forEach( ( def , index ) => {
200 def.buttonContent = def.content ;
201 def.contentHasMarkup = def.contentHasMarkup || def.markup ;
202
203 var contentWidth = Element.computeContentWidth( def.content , def.contentHasMarkup ) ,
204 currentHeight = 1 ,
205 isLastItem = index === this.itemsDef.length - 1 ,
206 currentPageMaxHeight = this.pageMaxHeight && ! isLastItem ? this.pageMaxHeight - 1 : this.pageMaxHeight ,
207 overflow = this.buttonPaddingWidth + contentWidth - this.outputWidth ;
208
209 if ( overflow > 0 ) {
210 if ( this.multiLineItems ) {
211 def.buttonContent = Element.wordWrapContent( def.content , this.outputWidth - this.buttonPaddingWidth , def.contentHasMarkup ) ;
212 contentWidth = this.outputWidth - this.buttonPaddingWidth ;
213 currentHeight = def.buttonContent.length ;
214 }
215 else {
216 def.buttonContent = Element.truncateContent( def.content , contentWidth - overflow - ellipsisWidth , def.contentHasMarkup ) + this.contentEllipsis ;
217 contentWidth = Element.computeContentWidth( def.buttonContent , def.contentHasMarkup ) ;
218 }
219 }
220
221 if ( this.pageMaxHeight && pageHeight && pageHeight + currentHeight > currentPageMaxHeight ) {
222 page ++ ;
223 pageHeight = 1 + currentHeight ;
224 }
225 else {
226 pageHeight += currentHeight ;
227 }
228
229 def.width = this.buttonPaddingWidth + contentWidth ;
230 def.page = page ;
231
232 if ( def.width + this.buttonSymbolWidth > this.buttonsMaxWidth ) {
233 this.buttonsMaxWidth = def.width + this.buttonSymbolWidth ;
234 }
235
236 if ( ! this.pageItemsDef[ page ] ) { this.pageItemsDef[ page ] = [] ; }
237 this.pageItemsDef[ page ].push( def ) ;
238 } ) ;
239
240 this.maxPage = page ;
241
242 if ( this.separatorDef ) {
243 this.separatorDef = Object.assign( { content: '-' , disabled: true , internalRole: 'separator' } , this.separatorDef ) ;
244 this.separatorDef.width = Element.computeContentWidth( this.separatorDef.content , this.separatorDef.contentHasMarkup ) ;
245
246 if ( this.separatorDef.contentRepeat && this.separatorDef.width < this.buttonsMaxWidth - this.buttonPaddingWidth ) {
247 this.separatorDef.content = this.separatorDef.content.repeat( Math.floor( ( this.buttonsMaxWidth - this.buttonPaddingWidth ) / this.separatorDef.width ) ) ;
248 this.separatorDef.width = Element.computeContentWidth( this.separatorDef.content , this.separatorDef.contentHasMarkup ) ;
249 }
250
251 this.separatorDef.width += this.buttonPaddingWidth ;
252 this.separatorDef.buttonContent = this.separatorDef.content ;
253 }
254
255 if ( this.masterDef && this.masterDef.width < this.buttonsMaxWidth ) {
256 this.masterDef.buttonContent = this.masterDef.content + ' ' + ' '.repeat( this.buttonsMaxWidth - this.masterDef.width ) + this.masterDef.symbol ;
257 this.masterDef.width = this.buttonsMaxWidth ;
258 }
259
260
261 // Force at least an empty page
262 if ( ! this.pageItemsDef.length ) { this.pageItemsDef.push( [] ) ; }
263
264 this.pageItemsDef.forEach( ( pageDef , index ) => {
265 if ( index ) { pageDef.unshift( this.previousPageDef ) ; }
266 if ( index < this.pageItemsDef.length - 1 ) { pageDef.push( this.nextPageDef ) ; }
267 if ( this.separatorDef ) { pageDef.unshift( this.separatorDef ) ; }
268 if ( this.masterDef ) { pageDef.unshift( this.masterDef ) ; }
269 } ) ;
270
271 // /!\ Adjust the output width? /!\
272 if ( this.outputWidth > this.buttonsMaxWidth ) {
273 this.outputWidth = this.buttonsMaxWidth ;
274 }
275
276 // Only initPage if we are not a superclass of the object
277 if ( this.elementType === 'ColumnMenu' ) { this.initPage() ; }
278} ;
279
280
281
282ColumnMenu.prototype.initPage = function( page = this.page ) {
283 var buttonOffsetX = 0 , buttonOffsetY = 0 ;
284
285 if ( ! this.pageItemsDef[ page ] ) { return ; }
286
287 this.buttons.forEach( button => button.destroy( false , true ) ) ;
288 this.buttons.length = 0 ;
289
290 this.pageItemsDef[ page ].forEach( ( def , index ) => {
291 var ButtonConstructor , isToggle , key , value , blurAttr ;
292
293 if ( ! Array.isArray( def.buttonContent ) ) {
294 def.buttonContent = [ def.buttonContent + ' '.repeat( this.buttonsMaxWidth - def.width ) ] ;
295 }
296
297 ButtonConstructor = def.internalRole ? Button : this.ButtonClass ;
298 isToggle = ButtonConstructor === ToggleButton || ButtonConstructor.prototype instanceof ToggleButton ;
299
300 key = def.key ; // For ToggleButton
301 value = this.childUseParentKeyValue && key && this.value && typeof this.value === 'object' ? this.value[ key ] : def.value ;
302
303 if ( index % 2 ) {
304 // Odd
305 blurAttr = def.blurAttr || this.buttonBlurAttr ;
306 }
307 else {
308 // Even
309 blurAttr = def.evenBlurAttr || def.blurAttr || this.buttonEvenBlurAttr || this.buttonBlurAttr ;
310 }
311
312 this.buttons[ index ] = new ButtonConstructor( {
313 internal: true ,
314 parent: this ,
315 childId: index ,
316 internalRole: def.internalRole ,
317 content: def.buttonContent ,
318 contentHasMarkup: def.contentHasMarkup ,
319 disabled: def.disabled ,
320 def ,
321 key ,
322 value ,
323 outputX: this.outputX + buttonOffsetX ,
324 outputY: this.outputY + buttonOffsetY ,
325
326 blurAttr ,
327 focusAttr: def.focusAttr || this.buttonFocusAttr ,
328 disabledAttr: def.disabledAttr || this.buttonDisabledAttr ,
329 submittedAttr: def.submittedAttr || this.buttonSubmittedAttr ,
330 turnedOnFocusAttr: def.turnedOnFocusAttr || this.turnedOnFocusAttr ,
331 turnedOffFocusAttr: def.turnedOffFocusAttr || this.turnedOffFocusAttr ,
332 turnedOnBlurAttr: def.turnedOnBlurAttr || this.turnedOnBlurAttr ,
333 turnedOffBlurAttr: def.turnedOffBlurAttr || this.turnedOffBlurAttr ,
334
335 blurLeftPadding: this.blurLeftPadding ,
336 blurRightPadding: this.blurRightPadding ,
337 focusLeftPadding: this.focusLeftPadding ,
338 focusRightPadding: this.focusRightPadding ,
339 disabledLeftPadding: this.disabledLeftPadding ,
340 disabledRightPadding: this.disabledRightPadding ,
341 submittedLeftPadding: this.submittedLeftPadding ,
342 submittedRightPadding: this.submittedRightPadding ,
343
344 turnedOnFocusLeftPadding: this.turnedOnFocusLeftPadding ,
345 turnedOnFocusRightPadding: this.turnedOnFocusRightPadding ,
346 turnedOffFocusLeftPadding: this.turnedOffFocusLeftPadding ,
347 turnedOffFocusRightPadding: this.turnedOffFocusRightPadding ,
348 turnedOnBlurLeftPadding: this.turnedOnBlurLeftPadding ,
349 turnedOnBlurRightPadding: this.turnedOnBlurRightPadding ,
350 turnedOffBlurLeftPadding: this.turnedOffBlurLeftPadding ,
351 turnedOffBlurRightPadding: this.turnedOffBlurRightPadding ,
352
353 paddingHasMarkup: this.paddingHasMarkup ,
354
355 keyBindings: isToggle ? this.toggleButtonKeyBindings : this.buttonKeyBindings ,
356 actionKeyBindings: isToggle ? this.toggleButtonActionKeyBindings : this.buttonActionKeyBindings ,
357 shortcuts: def.shortcuts ,
358
359 noDraw: true
360 } ) ;
361
362 this.buttons[ index ].on( 'submit' , this.onButtonSubmit ) ;
363
364 if ( isToggle ) {
365 this.buttons[ index ].on( 'toggle' , this.onButtonToggle ) ;
366 }
367
368 buttonOffsetY += this.buttons[ index ].outputHeight ;
369 } ) ;
370
371 // Set outputHeight to the correct value
372 this.pageHeight = this.outputHeight = buttonOffsetY ;
373} ;
374