Skip to content

Commit 9d2cce3

Browse files
author
chouchouji
committed
feat: support disabled for select and multiselect prompt
1 parent 5077381 commit 9d2cce3

File tree

5 files changed

+86
-16
lines changed

5 files changed

+86
-16
lines changed

packages/core/src/prompts/multi-select.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1+
import { findNextCursor, findPrevCursor } from '../utils/cursor.js';
12
import Prompt, { type PromptOptions } from './prompt.js';
23

3-
interface MultiSelectOptions<T extends { value: any }>
4+
interface MultiSelectOptions<T extends { value: any; disabled?: boolean }>
45
extends PromptOptions<T['value'][], MultiSelectPrompt<T>> {
56
options: T[];
67
initialValues?: T['value'][];
78
required?: boolean;
89
cursorAt?: T['value'];
910
}
10-
export default class MultiSelectPrompt<T extends { value: any }> extends Prompt<T['value'][]> {
11+
export default class MultiSelectPrompt<T extends { value: any; disabled?: boolean }> extends Prompt<
12+
T['value'][]
13+
> {
1114
options: T[];
1215
cursor = 0;
1316

@@ -17,15 +20,15 @@ export default class MultiSelectPrompt<T extends { value: any }> extends Prompt<
1720

1821
private toggleAll() {
1922
const allSelected = this.value !== undefined && this.value.length === this.options.length;
20-
this.value = allSelected ? [] : this.options.map((v) => v.value);
23+
this.value = allSelected ? [] : this.options.filter((v) => !v.disabled).map((v) => v.value);
2124
}
2225

2326
private toggleInvert() {
2427
const value = this.value;
2528
if (!value) {
2629
return;
2730
}
28-
const notSelected = this.options.filter((v) => !value.includes(v.value));
31+
const notSelected = this.options.filter((v) => !v.disabled && !value.includes(v.value));
2932
this.value = notSelected.map((v) => v.value);
3033
}
3134

@@ -43,11 +46,17 @@ export default class MultiSelectPrompt<T extends { value: any }> extends Prompt<
4346
super(opts, false);
4447

4548
this.options = opts.options;
49+
const disabledOptions = this.options.filter((option) => option.disabled);
50+
if (this.options.length === disabledOptions.length) return;
4651
this.value = [...(opts.initialValues ?? [])];
47-
this.cursor = Math.max(
52+
const cursor = Math.max(
4853
this.options.findIndex(({ value }) => value === opts.cursorAt),
4954
0
5055
);
56+
this.cursor = this.options[cursor].disabled ? findNextCursor<T>(
57+
cursor,
58+
this.options
59+
) : cursor;
5160
this.on('key', (char) => {
5261
if (char === 'a') {
5362
this.toggleAll();
@@ -61,11 +70,11 @@ export default class MultiSelectPrompt<T extends { value: any }> extends Prompt<
6170
switch (key) {
6271
case 'left':
6372
case 'up':
64-
this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
73+
this.cursor = findPrevCursor<T>(this.cursor, this.options);
6574
break;
6675
case 'down':
6776
case 'right':
68-
this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
77+
this.cursor = findNextCursor<T>(this.cursor, this.options);
6978
break;
7079
case 'space':
7180
this.toggleValue();

packages/core/src/prompts/select.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import { findNextCursor, findPrevCursor } from '../utils/cursor.js';
12
import Prompt, { type PromptOptions } from './prompt.js';
23

3-
interface SelectOptions<T extends { value: any }>
4+
interface SelectOptions<T extends { value: any; disabled?: boolean }>
45
extends PromptOptions<T['value'], SelectPrompt<T>> {
56
options: T[];
67
initialValue?: T['value'];
78
}
8-
export default class SelectPrompt<T extends { value: any }> extends Prompt<T['value']> {
9+
export default class SelectPrompt<T extends { value: any; disabled?: boolean }> extends Prompt<
10+
T['value']
11+
> {
912
options: T[];
1013
cursor = 0;
1114

@@ -21,19 +24,23 @@ export default class SelectPrompt<T extends { value: any }> extends Prompt<T['va
2124
super(opts, false);
2225

2326
this.options = opts.options;
24-
this.cursor = this.options.findIndex(({ value }) => value === opts.initialValue);
25-
if (this.cursor === -1) this.cursor = 0;
27+
const disabledOptions = this.options.filter((option) => option.disabled);
28+
if (this.options.length === disabledOptions.length) return;
29+
30+
const initialCursor = this.options.findIndex(({ value }) => value === opts.initialValue);
31+
const cursor = initialCursor === -1 ? 0 : initialCursor
32+
this.cursor = this.options[cursor].disabled ? findNextCursor<T>(cursor, this.options) : cursor;
2633
this.changeValue();
2734

2835
this.on('cursor', (key) => {
2936
switch (key) {
3037
case 'left':
3138
case 'up':
32-
this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
39+
this.cursor = findPrevCursor<T>(this.cursor, this.options);
3340
break;
3441
case 'down':
3542
case 'right':
36-
this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
43+
this.cursor = findNextCursor<T>(this.cursor, this.options);
3744
break;
3845
}
3946
this.changeValue();

packages/core/src/utils/cursor.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export function findPrevCursor<T extends { disabled?: boolean }>(cursor: number, options: T[]) {
2+
const prevCursor = cursor === 0 ? options.length - 1 : cursor - 1;
3+
const prevOption = options[prevCursor];
4+
if (prevOption.disabled) {
5+
return findPrevCursor(prevCursor, options);
6+
}
7+
return prevCursor;
8+
}
9+
10+
export function findNextCursor<T extends { disabled?: boolean }>(cursor: number, options: T[]) {
11+
const nextCursor = cursor === options.length - 1 ? 0 : cursor + 1;
12+
const nextOption = options[nextCursor];
13+
if (nextOption.disabled) {
14+
return findNextCursor(nextCursor, options);
15+
}
16+
return nextCursor;
17+
}

packages/prompts/src/multi-select.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,21 @@ export interface MultiSelectOptions<Value> extends CommonOptions {
2323
export const multiselect = <Value>(opts: MultiSelectOptions<Value>) => {
2424
const opt = (
2525
option: Option<Value>,
26-
state: 'inactive' | 'active' | 'selected' | 'active-selected' | 'submitted' | 'cancelled'
26+
state:
27+
| 'inactive'
28+
| 'active'
29+
| 'selected'
30+
| 'active-selected'
31+
| 'submitted'
32+
| 'cancelled'
33+
| 'disabled'
2734
) => {
2835
const label = option.label ?? String(option.value);
36+
if (state === 'disabled') {
37+
return `${color.black(S_CHECKBOX_SELECTED)} ${color.dim(label)}${
38+
option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''
39+
}`;
40+
}
2941
if (state === 'active') {
3042
return `${color.cyan(S_CHECKBOX_ACTIVE)} ${label}${
3143
option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''
@@ -74,6 +86,9 @@ export const multiselect = <Value>(opts: MultiSelectOptions<Value>) => {
7486
const value = this.value ?? [];
7587

7688
const styleOption = (option: Option<Value>, active: boolean) => {
89+
if (option.disabled) {
90+
return opt(option, 'disabled');
91+
}
7792
const selected = value.includes(option.value);
7893
if (active && selected) {
7994
return opt(option, 'active-selected');

packages/prompts/src/select.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ export type Option<Value> = Value extends Primitive
3131
* By default, no `hint` is displayed.
3232
*/
3333
hint?: string;
34+
/**
35+
* Whether this option is disabled.
36+
* Disabled options are visible but cannot be selected.
37+
*
38+
* By default, options are not disabled.
39+
*/
40+
disabled?: boolean;
3441
}
3542
: {
3643
/**
@@ -48,6 +55,13 @@ export type Option<Value> = Value extends Primitive
4855
* By default, no `hint` is displayed.
4956
*/
5057
hint?: string;
58+
/**
59+
* Whether this option is disabled.
60+
* Disabled options are visible but cannot be selected.
61+
*
62+
* By default, options are not disabled.
63+
*/
64+
disabled?: boolean;
5165
};
5266

5367
export interface SelectOptions<Value> extends CommonOptions {
@@ -58,9 +72,16 @@ export interface SelectOptions<Value> extends CommonOptions {
5872
}
5973

6074
export const select = <Value>(opts: SelectOptions<Value>) => {
61-
const opt = (option: Option<Value>, state: 'inactive' | 'active' | 'selected' | 'cancelled') => {
75+
const opt = (
76+
option: Option<Value>,
77+
state: 'inactive' | 'active' | 'selected' | 'cancelled' | 'disabled'
78+
) => {
6279
const label = option.label ?? String(option.value);
6380
switch (state) {
81+
case 'disabled':
82+
return `${color.black(S_RADIO_ACTIVE)} ${color.dim(label)}${
83+
option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''
84+
}`;
6485
case 'selected':
6586
return `${color.dim(label)}`;
6687
case 'active':
@@ -97,7 +118,8 @@ export const select = <Value>(opts: SelectOptions<Value>) => {
97118
cursor: this.cursor,
98119
options: this.options,
99120
maxItems: opts.maxItems,
100-
style: (item, active) => opt(item, active ? 'active' : 'inactive'),
121+
style: (item, active) =>
122+
opt(item, item.disabled ? 'disabled' : active ? 'active' : 'inactive'),
101123
}).join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`;
102124
}
103125
}

0 commit comments

Comments
 (0)