UNPKG

inquirer-select-pro

Version:

An inquirer select that supports multiple selections and filtering.

798 lines (779 loc) 23.9 kB
// 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