Skip to content

Commit

Permalink
feat: add confirmDelete mode
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffwcx committed Oct 29, 2024
1 parent b8ed1ea commit 264271b
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 9 deletions.
31 changes: 28 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,29 @@ const answer = await select({
});
```

### clearInputWhenSelected

Clear the filter input when the option is selected (also causes the option list to change).

### confirmDelete(multiple only)

The first time you try the delete key, it will focus on the option to be deleted, and the second time it will remove the focused option.

```ts
import { select } from 'inquirer-select-pro';
const answer = await select({
message: 'select',
confirmDelete: true,
options: async (input) => {
const res = await fetch('<url>', {
body: new URLSearchParams({ keyword: input }),
});
if (!res.ok) throw new Error('fail to get list!');
return await res.json();
},
});
```

## API

### select()
Expand All @@ -64,7 +87,7 @@ An inquirer select that supports multiple selections and filtering

#### Parameters

- `config` [**_SelectProps_**](./src/types.ts#L166) <!-- -->**_\<Value, Multiple>_**
- `config` [**_SelectProps_**](./src/types.ts#L191) <!-- -->**_\<Value, Multiple>_**

#### Returns

Expand Down Expand Up @@ -101,11 +124,11 @@ declare function useSelect<Value, Multiple extends boolean>(

#### Parameters

- `props` [**_UseSelectOptions_**](./src/types.ts#L58)<!-- -->**_\<Value, Multiple>_**
- `props` [**_UseSelectOptions_**](./src/types.ts#L63)<!-- -->**_\<Value, Multiple>_**

#### Returns

[**_UseSelectReturnValue_**](./src/types.ts#L149)<!-- -->**_\<Value>_**
[**_UseSelectReturnValue_**](./src/types.ts#L163)<!-- -->**_\<Value>_**

### Theming

Expand Down Expand Up @@ -216,6 +239,8 @@ Parameters can also be fixed. The following parameters can be fixed:
- required
- loop
- multiple
- canToggleAll
- confirmDelete

```bash
pnpm dev filter-demo --multiple=false
Expand Down
1 change: 1 addition & 0 deletions examples/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const availableOptions = [
'loop',
'multiple',
'canToggleAll',
'confirmDelete',
];

let whichDemo: Demos | null;
Expand Down
34 changes: 31 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@ const defaultSelectTheme: (multiple: boolean) => SelectTheme = (multiple) => ({
style: {
disabledOption: (text: string) => chalk.dim(`-[x] ${text}`),
renderSelectedOptions: (selectedOptions) =>
selectedOptions.map((option) => option.name || option.value).join(', '),
selectedOptions
.map((option) =>
option.focused
? chalk.inverse(option.name || option.value)
: option.name || option.value,
)
.join(', '),
emptyText: (text) => `${chalk.blue(figures.info)} ${chalk.bold(text)}`,
placeholder: (text: string) => chalk.dim(text),
},
Expand Down Expand Up @@ -89,6 +95,7 @@ function renderHelpTip<Value>(context: SelectContext<Value>) {
behaviors,
multiple,
canToggleAll,
focusedSelection,
} = context;
let helpTipTop = '';
let helpTipBottom = '';
Expand Down Expand Up @@ -118,7 +125,17 @@ function renderHelpTip<Value>(context: SelectContext<Value>) {
}

if (behaviors.select && !behaviors.deleteOption) {
keys.push(`${theme.style.key('backspace')} to remove option`);
keys.push(
`${
theme.style.key('backspace') +
(focusedSelection >= 0 ? ` ${theme.style.highlight('again')}` : '')
} to remove option`,
);
}
if (!behaviors.blur && focusedSelection >= 0) {
keys.push(
`${theme.style.key('up/down')} or ${theme.style.key('esc')} to exit`,
);
}
if (keys.length > 0) {
helpTipTop = ` (Press ${keys.join(', ')})`;
Expand All @@ -140,7 +157,14 @@ function renderHelpTip<Value>(context: SelectContext<Value>) {
}

function renderFilterInput<Value>(
{ theme, filterInput, status, placeholder }: SelectContext<Value>,
{
theme,
filterInput,
status,
placeholder,
focusedSelection,
confirmDelete,
}: SelectContext<Value>,
answer: string,
) {
if (status === SelectStatus.UNLOADED) return '';
Expand All @@ -150,6 +174,10 @@ function renderFilterInput<Value>(
} else {
input += `${answer ? `${answer} ` : ''}${filterInput}`;
}
if (confirmDelete) {
input +=
focusedSelection >= 0 ? ansiEscapes.cursorHide : ansiEscapes.cursorShow;
}
return input;
}

Expand Down
18 changes: 17 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ export type SelectOption<Value> = {
disabled?: boolean | string;
};

/**
* @internal
*/
export type SelectedOption<Value> = SelectOption<Value> & { focused?: boolean };

export enum SelectStatus {
UNLOADED = 'unloaded',
FILTERING = 'filtering',
Expand All @@ -33,7 +38,7 @@ export type SelectTheme = {
style: {
disabledOption: (text: string) => string;
renderSelectedOptions: <T>(
selectedOptions: ReadonlyArray<SelectOption<T>>,
selectedOptions: ReadonlyArray<SelectedOption<T>>,
allOptions: ReadonlyArray<SelectItem<T>>,
) => string;
emptyText: (text: string) => string;
Expand Down Expand Up @@ -74,6 +79,14 @@ export interface UseSelectOptions<Value, Multiple extends boolean = true> {
* `false`
*/
clearInputWhenSelected?: boolean;

/**
* Only valid when multiple is true, confirmation is required when deleting selectable options
*
* @defaultValue
* `false`
*/
confirmDelete?: boolean;
/**
* Enable toggle all options
*
Expand Down Expand Up @@ -144,10 +157,13 @@ export interface SelectBehaviors {
setCursor: boolean;
filter: boolean;
deleteOption: boolean;
blur: boolean;
}

export interface UseSelectReturnValue<Value> {
selections: SelectOption<Value>[];
focusedSelection: number;
confirmDelete: boolean;
filterInput: string;
displayItems: ReadonlyArray<InternalSelectItem<Value>>;
cursor: number;
Expand Down
33 changes: 31 additions & 2 deletions src/useSelect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import {
} from '@inquirer/core';
import {
check,
isDirectionKey,
isDownKey,
isEscKey,
isSelectAllKey,
isSelectable,
isTabKey,
Expand All @@ -25,6 +27,7 @@ import {
type SelectOption,
SelectStatus,
type SelectValue,
type SelectedOption,
type UseSelectOptions,
type UseSelectReturnValue,
} from './types';
Expand Down Expand Up @@ -53,7 +56,8 @@ function transformDefaultValue<Value, Multiple>(
({
name: value2Name(value),
value,
}) satisfies SelectOption<Value>,
focused: false,
}) satisfies SelectedOption<Value>,
);
}
return [
Expand Down Expand Up @@ -91,6 +95,7 @@ export function useSelect<Value, Multiple extends boolean>(
defaultValue,
clearInputWhenSelected = false,
canToggleAll = false,
confirmDelete = false,
inputDelay = 200,
validate = () => true,
equals = (a, b) => a === b,
Expand All @@ -116,7 +121,8 @@ export function useSelect<Value, Multiple extends boolean>(
const loader = useRef<Promise<any>>();
const [filterInput, setFilterInput] = useState<string>('');

const selections = useRef<SelectOption<Value>[]>(
const [focused, setFocused] = useState(-1);
const selections = useRef<SelectedOption<Value>[]>(
transformDefaultValue(defaultValue, multiple),
);

Expand All @@ -127,6 +133,7 @@ export function useSelect<Value, Multiple extends boolean>(
filter: false,
setCursor: false,
deleteOption: false,
blur: false,
});

function setBehavior(key: keyof SelectBehaviors, value: boolean) {
Expand Down Expand Up @@ -221,6 +228,12 @@ export function useSelect<Value, Multiple extends boolean>(
if (selections.current.length <= 0) return;
const lastIndex = selections.current.length - 1;
const lastSection = selections.current[lastIndex];
// enter focus mode
if (confirmDelete && focused < 0) {
lastSection.focused = true;
setFocused(lastIndex);
return;
}
const ss = selections.current.slice(0, lastIndex);
setBehavior('deleteOption', true);
selections.current = ss;
Expand Down Expand Up @@ -254,6 +267,20 @@ export function useSelect<Value, Multiple extends boolean>(
}

useKeypress(async (key, rl) => {
if (focused >= 0) {
if (isBackspaceKey(key)) {
removeLastSection();
setFocused(-1);
} else if (isDirectionKey(key) || isEscKey(key)) {
// quit focus mode
const focusedSelection = selections.current[focused];
focusedSelection.focused = false;
setFocused(-1);
setBehavior('blur', true);
}
clearFilterInput(rl);
return;
}
if (isEnterKey(key)) {
if (status !== SelectStatus.LOADED) {
return;
Expand Down Expand Up @@ -367,6 +394,8 @@ export function useSelect<Value, Multiple extends boolean>(

return {
selections: selections.current,
focusedSelection: focused,
confirmDelete,
filterInput,
displayItems,
cursor,
Expand Down
13 changes: 13 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ export function isDownKey(key: KeypressEvent) {
return key.name === 'down';
}

export function isDirectionKey(key: KeypressEvent) {
return (
key.name === 'up' ||
key.name === 'down' ||
key.name === 'left' ||
key.name === 'right'
);
}

export function isEscKey(key: KeypressEvent) {
return key.name === 'escape';
}

export function isTabKey(key: KeypressEvent) {
return key.name === 'tab';
}
Expand Down
46 changes: 46 additions & 0 deletions tests/__snapshots__/index.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,52 @@ exports[`inquirer-select-pro > interactions > should toggle all options when pre
(Use arrow keys to reveal more options)"
`;
exports[`inquirer-select-pro > interactions > should work as expected when confirmDelete=true 1`] = `
"? Choose movie: (Press <backspace> to remove option)
>> The Shawshank Redemption (1994)
[✔] The Shawshank Redemption (1994)
>[ ] The Godfather (1972)
[ ] The Godfather: Part II (1974)
[ ] The Dark Knight (2008)"
`;
exports[`inquirer-select-pro > interactions > should work as expected when confirmDelete=true 2`] = `
"? Choose movie: (Press <backspace> again to remove option, <up/down> or <esc> to
exit)
>> The Shawshank Redemption (1994)
[✔] The Shawshank Redemption (1994)
>[ ] The Godfather (1972)
[ ] The Godfather: Part II (1974)
[ ] The Dark Knight (2008)"
`;
exports[`inquirer-select-pro > interactions > should work as expected when confirmDelete=true 3`] = `
"? Choose movie: (Press <backspace> to remove option)
>> The Shawshank Redemption (1994)
[✔] The Shawshank Redemption (1994)
>[ ] The Godfather (1972)
[ ] The Godfather: Part II (1974)
[ ] The Dark Knight (2008)"
`;
exports[`inquirer-select-pro > interactions > should work as expected when confirmDelete=true 4`] = `
"? Choose movie: (Press <backspace> again to remove option)
>> The Shawshank Redemption (1994)
[✔] The Shawshank Redemption (1994)
>[ ] The Godfather (1972)
[ ] The Godfather: Part II (1974)
[ ] The Dark Knight (2008)"
`;
exports[`inquirer-select-pro > interactions > should work as expected when confirmDelete=true 5`] = `
"? Choose movie:
>> Type to search
[ ] The Shawshank Redemption (1994)
>[ ] The Godfather (1972)
[ ] The Godfather: Part II (1974)
[ ] The Dark Knight (2008)"
`;
exports[`inquirer-select-pro > interactions > should work when options is a function and filter disabled 1`] = `
"? Choose movie: (Press <tab> to select/deselect, <enter> to proceed)
>[ ] The Shawshank Redemption (1994)
Expand Down
21 changes: 21 additions & 0 deletions tests/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,27 @@ describe('inquirer-select-pro', () => {
events.keypress('enter');
await expect(answer).resolves.toHaveLength(4);
});

it('should work as expected when confirmDelete=true', async () => {
await renderPrompt({
message,
options: quickRemoteData,
pageSize: 4,
confirmDelete: true,
});
await waitForInteraction();
events.keypress('tab');
events.keypress('down');
expect(getScreen()).toMatchSnapshot();
events.keypress('backspace');
expect(getScreen()).toMatchSnapshot();
events.keypress('escape');
expect(getScreen()).toMatchSnapshot();
events.keypress('backspace');
expect(getScreen()).toMatchSnapshot();
events.keypress('backspace');
expect(getScreen()).toMatchSnapshot();
});
});

describe('appearance', () => {
Expand Down

0 comments on commit 264271b

Please sign in to comment.