UNPKG

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