@@ -58,6 +58,41 @@ const symbol = (state: State) => {
5858 }
5959} ;
6060
61+ interface LimitOptionsParams < TOption > {
62+ options : TOption [ ] ;
63+ maxItems : number | undefined ;
64+ cursor : number ;
65+ style : ( option : TOption , active : boolean ) => string ;
66+ }
67+
68+ const limitOptions = < TOption > ( params : LimitOptionsParams < TOption > ) : string [ ] => {
69+ const { cursor, options, style } = params ;
70+
71+ // We clamp to minimum 5 because anything less doesn't make sense UX wise
72+ const maxItems = params . maxItems === undefined ? Infinity : Math . max ( params . maxItems , 5 ) ;
73+ let slidingWindowLocation = 0 ;
74+
75+ if ( cursor >= slidingWindowLocation + maxItems - 3 ) {
76+ slidingWindowLocation = Math . max ( Math . min ( cursor - maxItems + 3 , options . length - maxItems ) , 0 ) ;
77+ } else if ( cursor < slidingWindowLocation + 2 ) {
78+ slidingWindowLocation = Math . max ( cursor - 2 , 0 ) ;
79+ }
80+
81+ const shouldRenderTopEllipsis = maxItems < options . length && slidingWindowLocation > 0 ;
82+ const shouldRenderBottomEllipsis =
83+ maxItems < options . length && slidingWindowLocation + maxItems < options . length ;
84+
85+ return options
86+ . slice ( slidingWindowLocation , slidingWindowLocation + maxItems )
87+ . map ( ( option , i , arr ) => {
88+ const isTopLimit = i === 0 && shouldRenderTopEllipsis ;
89+ const isBottomLimit = i === arr . length - 1 && shouldRenderBottomEllipsis ;
90+ return isTopLimit || isBottomLimit
91+ ? color . dim ( '...' )
92+ : style ( option , i + slidingWindowLocation === cursor ) ;
93+ } ) ;
94+ } ;
95+
6196export interface TextOptions {
6297 message : string ;
6398 placeholder ?: string ;
@@ -184,20 +219,20 @@ export interface SelectOptions<Value> {
184219export const select = < Value > ( opts : SelectOptions < Value > ) => {
185220 const opt = ( option : Option < Value > , state : 'inactive' | 'active' | 'selected' | 'cancelled' ) => {
186221 const label = option . label ?? String ( option . value ) ;
187- if ( state === 'active' ) {
188- return `${ color . green ( S_RADIO_ACTIVE ) } ${ label } ${
189- option . hint ? color . dim ( `(${ option . hint } )` ) : ''
190- } `;
191- } else if ( state === 'selected' ) {
192- return `${ color . dim ( label ) } ` ;
193- } else if ( state === 'cancelled' ) {
194- return `${ color . strikethrough ( color . dim ( label ) ) } ` ;
222+ switch ( state ) {
223+ case 'selected' :
224+ return `${ color . dim ( label ) } ` ;
225+ case 'active' :
226+ return `${ color . green ( S_RADIO_ACTIVE ) } ${ label } ${
227+ option . hint ? color . dim ( `(${ option . hint } )` ) : ''
228+ } `;
229+ case 'cancelled' :
230+ return `${ color . strikethrough ( color . dim ( label ) ) } ` ;
231+ default :
232+ return `${ color . dim ( S_RADIO_INACTIVE ) } ${ color . dim ( label ) } ` ;
195233 }
196- return `${ color . dim ( S_RADIO_INACTIVE ) } ${ color . dim ( label ) } ` ;
197234 } ;
198235
199- let slidingWindowLocation = 0 ;
200-
201236 return new SelectPrompt ( {
202237 options : opts . options ,
203238 initialValue : opts . initialValue ,
@@ -213,38 +248,12 @@ export const select = <Value>(opts: SelectOptions<Value>) => {
213248 'cancelled'
214249 ) } \n${ color . gray ( S_BAR ) } `;
215250 default : {
216- // We clamp to minimum 5 because anything less doesn't make sense UX wise
217- const maxItems = opts . maxItems === undefined ? Infinity : Math . max ( opts . maxItems , 5 ) ;
218- if ( this . cursor >= slidingWindowLocation + maxItems - 3 ) {
219- slidingWindowLocation = Math . max (
220- Math . min ( this . cursor - maxItems + 3 , this . options . length - maxItems ) ,
221- 0
222- ) ;
223- } else if ( this . cursor < slidingWindowLocation + 2 ) {
224- slidingWindowLocation = Math . max ( this . cursor - 2 , 0 ) ;
225- }
226-
227- const shouldRenderTopEllipsis =
228- maxItems < this . options . length && slidingWindowLocation > 0 ;
229- const shouldRenderBottomEllipsis =
230- maxItems < this . options . length &&
231- slidingWindowLocation + maxItems < this . options . length ;
232-
233- return `${ title } ${ color . cyan ( S_BAR ) } ${ this . options
234- . slice ( slidingWindowLocation , slidingWindowLocation + maxItems )
235- . map ( ( option , i , arr ) => {
236- if ( i === 0 && shouldRenderTopEllipsis ) {
237- return color . dim ( '...' ) ;
238- } else if ( i === arr . length - 1 && shouldRenderBottomEllipsis ) {
239- return color . dim ( '...' ) ;
240- } else {
241- return opt (
242- option ,
243- i + slidingWindowLocation === this . cursor ? 'active' : 'inactive'
244- ) ;
245- }
246- } )
247- . join ( `\n${ color . cyan ( S_BAR ) } ` ) } \n${ color . cyan ( S_BAR_END ) } \n`;
251+ return `${ title } ${ color . cyan ( S_BAR ) } ${ limitOptions ( {
252+ cursor : this . cursor ,
253+ options : this . options ,
254+ maxItems : opts . maxItems ,
255+ style : ( item , active ) => opt ( item , active ? 'active' : 'inactive' ) ,
256+ } ) . join ( `\n${ color . cyan ( S_BAR ) } ` ) } \n${ color . cyan ( S_BAR_END ) } \n`;
248257 }
249258 }
250259 } ,
@@ -301,6 +310,7 @@ export interface MultiSelectOptions<Value> {
301310 message : string ;
302311 options : Option < Value > [ ] ;
303312 initialValues ?: Value [ ] ;
313+ maxItems ?: number ;
304314 required ?: boolean ;
305315 cursorAt ?: Value ;
306316}
@@ -346,6 +356,17 @@ export const multiselect = <Value>(opts: MultiSelectOptions<Value>) => {
346356 render ( ) {
347357 let title = `${ color . gray ( S_BAR ) } \n${ symbol ( this . state ) } ${ opts . message } \n` ;
348358
359+ const styleOption = ( option : Option < Value > , active : boolean ) => {
360+ const selected = this . value . includes ( option . value ) ;
361+ if ( active && selected ) {
362+ return opt ( option , 'active-selected' ) ;
363+ }
364+ if ( selected ) {
365+ return opt ( option , 'selected' ) ;
366+ }
367+ return opt ( option , active ? 'active' : 'inactive' ) ;
368+ } ;
369+
349370 switch ( this . state ) {
350371 case 'submit' : {
351372 return `${ title } ${ color . gray ( S_BAR ) } ${
@@ -375,38 +396,24 @@ export const multiselect = <Value>(opts: MultiSelectOptions<Value>) => {
375396 title +
376397 color . yellow ( S_BAR ) +
377398 ' ' +
378- this . options
379- . map ( ( option , i ) => {
380- const selected = this . value . includes ( option . value ) ;
381- const active = i === this . cursor ;
382- if ( active && selected ) {
383- return opt ( option , 'active-selected' ) ;
384- }
385- if ( selected ) {
386- return opt ( option , 'selected' ) ;
387- }
388- return opt ( option , active ? 'active' : 'inactive' ) ;
389- } )
390- . join ( `\n${ color . yellow ( S_BAR ) } ` ) +
399+ limitOptions ( {
400+ options : this . options ,
401+ cursor : this . cursor ,
402+ maxItems : opts . maxItems ,
403+ style : styleOption ,
404+ } ) . join ( `\n${ color . yellow ( S_BAR ) } ` ) +
391405 '\n' +
392406 footer +
393407 '\n'
394408 ) ;
395409 }
396410 default : {
397- return `${ title } ${ color . cyan ( S_BAR ) } ${ this . options
398- . map ( ( option , i ) => {
399- const selected = this . value . includes ( option . value ) ;
400- const active = i === this . cursor ;
401- if ( active && selected ) {
402- return opt ( option , 'active-selected' ) ;
403- }
404- if ( selected ) {
405- return opt ( option , 'selected' ) ;
406- }
407- return opt ( option , active ? 'active' : 'inactive' ) ;
408- } )
409- . join ( `\n${ color . cyan ( S_BAR ) } ` ) } \n${ color . cyan ( S_BAR_END ) } \n`;
411+ return `${ title } ${ color . cyan ( S_BAR ) } ${ limitOptions ( {
412+ options : this . options ,
413+ cursor : this . cursor ,
414+ maxItems : opts . maxItems ,
415+ style : styleOption ,
416+ } ) . join ( `\n${ color . cyan ( S_BAR ) } ` ) } \n${ color . cyan ( S_BAR_END ) } \n`;
410417 }
411418 }
412419 } ,
0 commit comments