inquirer-select-pro
Version:
An inquirer select that supports multiple selections and filtering.
798 lines (779 loc) • 23.9 kB
JavaScript
// src/index.ts
import {
Separator as Separator2,
createPrompt,
makeTheme,
useMemo as useMemo3,
usePagination,
usePrefix
} from "@inquirer/core";
import chalk from "chalk";
import figures from "@inquirer/figures";
import ansiEscapes from "ansi-escapes";
// src/useSelect.ts
import {
ValidationError,
isBackspaceKey,
isEnterKey,
useEffect,
useKeypress,
useMemo as useMemo2,
useRef as useRef2,
useState
} from "@inquirer/core";
// node_modules/.pnpm/lodash-es@4.17.21/node_modules/lodash-es/isObject.js
function isObject(value) {
var type = typeof value;
return value != null && (type == "object" || type == "function");
}
var isObject_default = isObject;
// node_modules/.pnpm/lodash-es@4.17.21/node_modules/lodash-es/_freeGlobal.js
var freeGlobal = typeof global == "object" && global && global.Object === Object && global;
var freeGlobal_default = freeGlobal;
// node_modules/.pnpm/lodash-es@4.17.21/node_modules/lodash-es/_root.js
var freeSelf = typeof self == "object" && self && self.Object === Object && self;
var root = freeGlobal_default || freeSelf || Function("return this")();
var root_default = root;
// node_modules/.pnpm/lodash-es@4.17.21/node_modules/lodash-es/now.js
var now = function() {
return root_default.Date.now();
};
var now_default = now;
// node_modules/.pnpm/lodash-es@4.17.21/node_modules/lodash-es/_trimmedEndIndex.js
var reWhitespace = /\s/;
function trimmedEndIndex(string) {
var index = string.length;
while (index-- && reWhitespace.test(string.charAt(index))) {
}
return index;
}
var trimmedEndIndex_default = trimmedEndIndex;
// node_modules/.pnpm/lodash-es@4.17.21/node_modules/lodash-es/_baseTrim.js
var reTrimStart = /^\s+/;
function baseTrim(string) {
return string ? string.slice(0, trimmedEndIndex_default(string) + 1).replace(reTrimStart, "") : string;
}
var baseTrim_default = baseTrim;
// node_modules/.pnpm/lodash-es@4.17.21/node_modules/lodash-es/_Symbol.js
var Symbol = root_default.Symbol;
var Symbol_default = Symbol;
// node_modules/.pnpm/lodash-es@4.17.21/node_modules/lodash-es/_getRawTag.js
var objectProto = Object.prototype;
var hasOwnProperty = objectProto.hasOwnProperty;
var nativeObjectToString = objectProto.toString;
var symToStringTag = Symbol_default ? Symbol_default.toStringTag : void 0;
function getRawTag(value) {
var isOwn = hasOwnProperty.call(value, symToStringTag), tag = value[symToStringTag];
try {
value[symToStringTag] = void 0;
var unmasked = true;
} catch (e) {
}
var result = nativeObjectToString.call(value);
if (unmasked) {
if (isOwn) {
value[symToStringTag] = tag;
} else {
delete value[symToStringTag];
}
}
return result;
}
var getRawTag_default = getRawTag;
// node_modules/.pnpm/lodash-es@4.17.21/node_modules/lodash-es/_objectToString.js
var objectProto2 = Object.prototype;
var nativeObjectToString2 = objectProto2.toString;
function objectToString(value) {
return nativeObjectToString2.call(value);
}
var objectToString_default = objectToString;
// node_modules/.pnpm/lodash-es@4.17.21/node_modules/lodash-es/_baseGetTag.js
var nullTag = "[object Null]";
var undefinedTag = "[object Undefined]";
var symToStringTag2 = Symbol_default ? Symbol_default.toStringTag : void 0;
function baseGetTag(value) {
if (value == null) {
return value === void 0 ? undefinedTag : nullTag;
}
return symToStringTag2 && symToStringTag2 in Object(value) ? getRawTag_default(value) : objectToString_default(value);
}
var baseGetTag_default = baseGetTag;
// node_modules/.pnpm/lodash-es@4.17.21/node_modules/lodash-es/isObjectLike.js
function isObjectLike(value) {
return value != null && typeof value == "object";
}
var isObjectLike_default = isObjectLike;
// node_modules/.pnpm/lodash-es@4.17.21/node_modules/lodash-es/isSymbol.js
var symbolTag = "[object Symbol]";
function isSymbol(value) {
return typeof value == "symbol" || isObjectLike_default(value) && baseGetTag_default(value) == symbolTag;
}
var isSymbol_default = isSymbol;
// node_modules/.pnpm/lodash-es@4.17.21/node_modules/lodash-es/toNumber.js
var NAN = 0 / 0;
var reIsBadHex = /^[-+]0x[0-9a-f]+$/i;
var reIsBinary = /^0b[01]+$/i;
var reIsOctal = /^0o[0-7]+$/i;
var freeParseInt = parseInt;
function toNumber(value) {
if (typeof value == "number") {
return value;
}
if (isSymbol_default(value)) {
return NAN;
}
if (isObject_default(value)) {
var other = typeof value.valueOf == "function" ? value.valueOf() : value;
value = isObject_default(other) ? other + "" : other;
}
if (typeof value != "string") {
return value === 0 ? value : +value;
}
value = baseTrim_default(value);
var isBinary = reIsBinary.test(value);
return isBinary || reIsOctal.test(value) ? freeParseInt(value.slice(2), isBinary ? 2 : 8) : reIsBadHex.test(value) ? NAN : +value;
}
var toNumber_default = toNumber;
// node_modules/.pnpm/lodash-es@4.17.21/node_modules/lodash-es/debounce.js
var FUNC_ERROR_TEXT = "Expected a function";
var nativeMax = Math.max;
var nativeMin = Math.min;
function debounce(func, wait, options) {
var lastArgs, lastThis, maxWait, result, timerId, lastCallTime, lastInvokeTime = 0, leading = false, maxing = false, trailing = true;
if (typeof func != "function") {
throw new TypeError(FUNC_ERROR_TEXT);
}
wait = toNumber_default(wait) || 0;
if (isObject_default(options)) {
leading = !!options.leading;
maxing = "maxWait" in options;
maxWait = maxing ? nativeMax(toNumber_default(options.maxWait) || 0, wait) : maxWait;
trailing = "trailing" in options ? !!options.trailing : trailing;
}
function invokeFunc(time) {
var args = lastArgs, thisArg = lastThis;
lastArgs = lastThis = void 0;
lastInvokeTime = time;
result = func.apply(thisArg, args);
return result;
}
function leadingEdge(time) {
lastInvokeTime = time;
timerId = setTimeout(timerExpired, wait);
return leading ? invokeFunc(time) : result;
}
function remainingWait(time) {
var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime, timeWaiting = wait - timeSinceLastCall;
return maxing ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting;
}
function shouldInvoke(time) {
var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime;
return lastCallTime === void 0 || timeSinceLastCall >= wait || timeSinceLastCall < 0 || maxing && timeSinceLastInvoke >= maxWait;
}
function timerExpired() {
var time = now_default();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
timerId = setTimeout(timerExpired, remainingWait(time));
}
function trailingEdge(time) {
timerId = void 0;
if (trailing && lastArgs) {
return invokeFunc(time);
}
lastArgs = lastThis = void 0;
return result;
}
function cancel() {
if (timerId !== void 0) {
clearTimeout(timerId);
}
lastInvokeTime = 0;
lastArgs = lastCallTime = lastThis = timerId = void 0;
}
function flush() {
return timerId === void 0 ? result : trailingEdge(now_default());
}
function debounced() {
var time = now_default(), isInvoking = shouldInvoke(time);
lastArgs = arguments;
lastThis = this;
lastCallTime = time;
if (isInvoking) {
if (timerId === void 0) {
return leadingEdge(lastCallTime);
}
if (maxing) {
clearTimeout(timerId);
timerId = setTimeout(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
if (timerId === void 0) {
timerId = setTimeout(timerExpired, wait);
}
return result;
}
debounced.cancel = cancel;
debounced.flush = flush;
return debounced;
}
var debounce_default = debounce;
// src/utils.ts
import { Separator, useMemo, useRef } from "@inquirer/core";
function isUpKey(key) {
return key.name === "up";
}
function isDownKey(key) {
return key.name === "down";
}
function isDirectionKey(key) {
return key.name === "up" || key.name === "down" || key.name === "left" || key.name === "right";
}
function isEscKey(key) {
return key.name === "escape";
}
function isTabKey(key) {
return key.name === "tab";
}
function isSelectAllKey(key) {
return key.name === "a" && key.ctrl;
}
function isSelectable(item) {
return !Separator.isSeparator(item) && !item.disabled;
}
function toggle(item) {
return isSelectable(item) ? { ...item, checked: !item.checked } : item;
}
function check(item, checked = true) {
return isSelectable(item) && item.checked !== checked ? { ...item, checked } : item;
}
function useDebounce(func, wait) {
const ref = useRef();
ref.current = func;
return useMemo(
() => debounce_default(() => {
ref.current?.();
}, wait),
[wait]
);
}
// src/types.ts
var SelectStatus = /* @__PURE__ */ ((SelectStatus2) => {
SelectStatus2["UNLOADED"] = "unloaded";
SelectStatus2["FILTERING"] = "filtering";
SelectStatus2["LOADED"] = "loaded";
SelectStatus2["SUBMITTED"] = "submitted";
return SelectStatus2;
})(SelectStatus || {});
// src/useSelect.ts
function value2Name(value) {
return typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean" ? value.toString() : "";
}
function transformDefaultValue(values, multiple) {
if (!values)
return [];
if (multiple) {
if (!Array.isArray(values))
throw new ValidationError(
"In `multiple` mode, please pass the array as the default value."
);
return values.map(
(value) => ({
name: value2Name(value),
value,
focused: false
})
);
}
return [
{
name: value2Name(values),
value: values
}
];
}
function transformSelections(selections, multiple) {
if (multiple) {
return selections.map((s) => s.value);
}
return selections.length > 0 ? selections[0].value : null;
}
function useSelect(props) {
const {
options,
loop = false,
multiple = true,
selectFocusedOnSubmit = false,
filter = true,
required = false,
defaultValue,
clearInputWhenSelected = false,
canToggleAll = false,
confirmDelete = false,
inputDelay = 200,
validate = () => true,
equals = (a, b) => a === b,
onSubmitted
} = props;
const enableFilter = Array.isArray(options) ? false : filter;
const [displayItems, setDisplayItems] = useState([]);
const bounds = useMemo2(() => {
const first = displayItems.findIndex(isSelectable);
const last = displayItems.findLastIndex(isSelectable);
return { first, last };
}, [displayItems]);
const [cursor, setCursor] = useState(-1);
const [status, setStatus] = useState("unloaded" /* UNLOADED */);
const [error, setError] = useState("");
const loader = useRef2();
const [filterInput, setFilterInput] = useState("");
const [focused, setFocused] = useState(-1);
const selections = useRef2(
transformDefaultValue(defaultValue, multiple)
);
const [behaviors, setBehaviors] = useState({
submit: false,
select: false,
deselect: false,
filter: false,
setCursor: false,
deleteOption: false,
blur: false
});
function setBehavior(key, value) {
if (behaviors[key] === value)
return;
setBehaviors({
...behaviors,
[key]: value
});
}
function clearFilterInput(rl) {
setFilterInput("");
rl.clearLine(0);
}
function keepFilterInput(rl) {
rl.clearLine(0);
rl.write(filterInput);
}
function handleSelect(rl, clearInput = clearInputWhenSelected) {
if (cursor < 0 || displayItems.length <= 0) {
if (enableFilter) {
keepFilterInput(rl);
}
return;
}
const targetOption = displayItems[cursor];
if (isSelectable(targetOption)) {
if (multiple) {
if (targetOption.checked) {
const currentSelection = selections.current.filter(
(op) => !equals(targetOption.value, op.value)
);
setBehavior("deselect", true);
selections.current = currentSelection;
} else {
setBehavior("select", true);
selections.current = [...selections.current, { ...targetOption }];
}
} else {
setBehavior("select", true);
selections.current = [{ ...targetOption }];
}
if (enableFilter && !targetOption.checked && clearInput) {
clearFilterInput(rl);
} else {
keepFilterInput(rl);
}
}
setDisplayItems(
displayItems.map((item, i) => {
return i === cursor ? toggle(item) : item;
})
);
}
function toggleAll(rl) {
if (cursor < 0 || displayItems.length <= 0) {
if (enableFilter) {
keepFilterInput(rl);
}
return;
}
const hasSelectAll = !displayItems.find(
(item) => isSelectable(item) && !item.checked
);
if (hasSelectAll) {
selections.current = [];
setBehavior("deselect", true);
setDisplayItems(displayItems.map((item) => check(item, false)));
} else {
selections.current = displayItems.reduce((ss, item) => {
if (isSelectable(item)) {
ss.push({ ...item });
}
return ss;
}, []);
setBehavior("select", true);
setDisplayItems(displayItems.map((item) => check(item, true)));
}
}
function removeLastSection() {
if (selections.current.length <= 0)
return;
const lastIndex = selections.current.length - 1;
const lastSection = selections.current[lastIndex];
if (confirmDelete && focused < 0) {
lastSection.focused = true;
setFocused(lastIndex);
return;
}
const ss = selections.current.slice(0, lastIndex);
setBehavior("deleteOption", true);
selections.current = ss;
setDisplayItems(
displayItems.map(
(item) => isSelectable(item) && equals(item.value, lastSection.value) ? toggle(item) : item
)
);
}
async function submit() {
setBehavior("submit", true);
const isValid = await validate([...selections.current]);
if (required && selections.current.length <= 0) {
setError("At least one option must be selected");
} else if (isValid === true) {
setStatus("submitted" /* SUBMITTED */);
if (onSubmitted) {
const finalValue = transformSelections(
selections.current,
multiple
);
onSubmitted(finalValue);
}
} else {
setError(isValid || "You must select a valid value");
}
}
useKeypress(async (key, rl) => {
if (focused >= 0) {
if (isBackspaceKey(key)) {
removeLastSection();
setFocused(-1);
} else if (isDirectionKey(key) || isEscKey(key)) {
const focusedSelection = selections.current[focused];
focusedSelection.focused = false;
setFocused(-1);
setBehavior("blur", true);
}
clearFilterInput(rl);
return;
}
if (isEnterKey(key)) {
if (status !== "loaded" /* LOADED */) {
return;
}
if (!multiple || selectFocusedOnSubmit && selections.current.length === 0) {
handleSelect(rl);
}
await submit();
} else if (isBackspaceKey(key) && !filterInput) {
setFilterInput("");
removeLastSection();
} else if (isUpKey(key) || isDownKey(key)) {
if (bounds.first < 0 || status !== "loaded" /* LOADED */)
return;
if (loop || isUpKey(key) && cursor !== bounds.first || isDownKey(key) && cursor !== bounds.last) {
const offset = isUpKey(key) ? -1 : 1;
let next = cursor;
do {
next = (next + offset + displayItems.length) % displayItems.length;
} while (!isSelectable(displayItems[next]));
setBehavior("setCursor", true);
setCursor(next);
}
} else if (canToggleAll && multiple && isSelectAllKey(key)) {
toggleAll(rl);
} else if (multiple && isTabKey(key)) {
handleSelect(rl);
} else {
if (!enableFilter || status === "unloaded" /* UNLOADED */)
return;
setError("");
setBehavior("filter", true);
setFilterInput(rl.line);
}
});
function handleItems(items) {
if (items.length <= 0) {
setDisplayItems([]);
setCursor(-1);
return;
}
const ss = [...selections.current];
let firstChecked = -1;
let firstSelectable = -1;
const finalItems = items.map((item, index) => {
const finalItem = { ...item };
if (isSelectable(finalItem)) {
if (firstSelectable < 0) {
firstSelectable = index;
}
ss.forEach((op) => {
if (equals(op.value, finalItem.value)) {
finalItem.checked = true;
op.name = finalItem.name;
if (firstChecked < 0) {
firstChecked = index;
}
}
});
}
return finalItem;
});
setDisplayItems(finalItems);
selections.current = ss;
if (multiple) {
setCursor(firstSelectable);
} else {
setCursor(firstChecked < 0 ? firstSelectable : firstChecked);
}
}
async function loadData() {
if (status !== "unloaded" /* UNLOADED */) {
setStatus("filtering" /* FILTERING */);
}
setError("");
if (loader.current) {
await loader.current;
}
const result = options instanceof Function ? enableFilter ? options(filterInput) : options() : options;
loader.current = (result instanceof Promise ? result : Promise.resolve(result)).then(handleItems).catch((err) => setError(err)).finally(() => setStatus("loaded" /* LOADED */));
}
const debouncedLoadData = useDebounce(loadData, inputDelay);
if (enableFilter) {
useEffect(() => {
debouncedLoadData();
}, [filterInput]);
} else {
useEffect(() => {
loadData();
}, []);
}
return {
selections: selections.current,
focusedSelection: focused,
confirmDelete,
filterInput,
displayItems,
cursor,
status,
error,
loop,
multiple,
enableFilter,
canToggleAll,
required,
behaviors
};
}
// src/index.ts
import { Separator as Separator3 } from "@inquirer/core";
var defaultSelectTheme = (multiple) => ({
icon: {
checked: multiple ? `[${chalk.green(figures.tick)}]` : "",
unchecked: multiple ? "[ ]" : "",
cursor: ">",
inputCursor: chalk.cyan(">>")
},
style: {
disabledOption: (text) => chalk.dim(`-[x] ${text}`),
renderSelectedOptions: (selectedOptions) => selectedOptions.map(
(option) => option.focused ? (
/* v8 ignore next 3 */
chalk.inverse(option.name || option.value)
) : option.name || option.value
).join(", "),
emptyText: (text) => `${chalk.blue(figures.info)} ${chalk.bold(text)}`,
placeholder: (text) => chalk.dim(text)
},
helpMode: "auto"
});
function renderPage({
theme,
cursor,
displayItems,
pageSize,
loop,
status,
emptyText
}) {
if (displayItems.length <= 0) {
if (status === "unloaded" /* UNLOADED */) {
return "";
}
return theme.style.emptyText(emptyText);
}
return usePagination({
items: displayItems,
active: cursor < 0 || cursor >= displayItems.length ? 0 : cursor,
renderItem(renderOpts) {
const { item, isActive } = renderOpts;
if (Separator2.isSeparator(item)) {
return ` ${item.separator}`;
}
const line = item.name || item.value;
if (item.disabled) {
const disabledLabel = typeof item.disabled === "string" ? item.disabled : "(disabled)";
return theme.style.disabledOption(`${line} ${disabledLabel}`);
}
const checkbox = item.checked ? theme.icon.checked : theme.icon.unchecked;
const color = isActive ? theme.style.highlight : (x) => x;
const cursor2 = isActive ? theme.icon.cursor : " ";
return color(`${cursor2}${checkbox} ${line}`);
},
pageSize,
loop
});
}
function renderHelpTip(context) {
const {
theme,
instructions,
displayItems,
pageSize,
behaviors,
multiple,
canToggleAll,
focusedSelection
} = context;
let helpTipTop = "";
let helpTipBottom = "";
if (theme.helpMode === "always" || theme.helpMode === "auto" && (instructions === void 0 || instructions) && (!behaviors.select || !behaviors.deselect || !behaviors.deleteOption || !behaviors.setCursor)) {
if (instructions instanceof Function) {
helpTipTop = instructions(context);
} else {
const keys = [];
if (!behaviors.select && !behaviors.deselect) {
if (multiple) {
keys.push(`${theme.style.key("tab")} to select/deselect`);
}
if (canToggleAll) {
keys.push(
`${theme.style.key("ctrl")} + ${theme.style.key("a")} to toggle all`
);
}
keys.push(`${theme.style.key("enter")} to proceed`);
}
if (behaviors.select && !behaviors.deleteOption) {
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(", ")})`;
}
}
if (displayItems.length > pageSize && (theme.helpMode === "always" || theme.helpMode === "auto" && !behaviors.setCursor)) {
helpTipBottom = `
${theme.style.help("(Use arrow keys to reveal more options)")}`;
}
}
return {
top: helpTipTop,
bottom: helpTipBottom
};
}
function renderFilterInput({
theme,
filterInput,
status,
placeholder,
focusedSelection,
confirmDelete
}, answer) {
if (status === "unloaded" /* UNLOADED */)
return "";
let input = `
${theme.icon.inputCursor} `;
if (!answer && !filterInput) {
input += theme.style.placeholder(placeholder);
} else {
input += `${answer ? `${answer} ` : ""}${filterInput}`;
}
if (confirmDelete) {
input += focusedSelection >= 0 ? ansiEscapes.cursorHide : ansiEscapes.cursorShow;
}
return input;
}
var select = createPrompt(
(props, done) => {
const {
instructions,
pageSize = 10,
emptyText = "No results.",
placeholder = "Type to search"
} = props;
const selectData = useSelect({
...props,
onSubmitted: done
});
const {
status,
selections,
displayItems,
error: errorMsg,
multiple,
enableFilter
} = selectData;
const defaultTheme = useMemo3(
() => defaultSelectTheme(multiple),
[multiple]
);
const theme = makeTheme(defaultTheme, props.theme, {
icon: Object.assign(defaultTheme.icon, props.theme?.icon)
});
const isLoading = status === "unloaded" /* UNLOADED */ || status === "filtering" /* FILTERING */;
const prefix = usePrefix({ theme, isLoading });
const message = theme.style.message(props.message);
const answer = theme.style.answer(
theme.style.renderSelectedOptions(selections, displayItems)
);
if (status === "submitted" /* SUBMITTED */) {
return `${prefix} ${message} ${answer}`;
}
const context = {
...selectData,
theme,
pageSize,
instructions,
emptyText,
placeholder
};
const page = renderPage(context);
const help = renderHelpTip(context);
let error = "";
if (errorMsg) {
error = `
${theme.style.error(errorMsg)}`;
}
const input = enableFilter ? renderFilterInput(context, answer) : ` ${answer}${ansiEscapes.cursorHide}`;
return [
`${prefix} ${message}${help.top}${input}`,
`${page}${help.bottom}${error}`
];
}
);
export {
SelectStatus,
Separator3 as Separator,
select,
useSelect
};
//# sourceMappingURL=index.js.map