UNPKG

@shutootaki/gwm

Version:
108 lines 7.63 kB
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; import { useEffect, useState } from 'react'; import { Text, Box, useInput } from 'ink'; import { useEditableText } from '../hooks/useEditableText.js'; export const MultiSelectList = ({ items, onConfirm, onCancel, placeholder = 'Type to search, Space to select...', initialQuery = '', maxDisplayItems = 15, title = 'Multi-select', showStats = true, }) => { const { value: query, cursorPosition } = useEditableText({ initialValue: initialQuery, skipChars: [' '], }); const [selectedIndex, setSelectedIndex] = useState(0); const [selectedItems, setSelectedItems] = useState(new Set()); const [scrollOffset, setScrollOffset] = useState(0); // フィルタリングされた項目 const filteredItems = items.filter((item) => item.label.toLowerCase().includes(query.toLowerCase())); // 選択インデックスを範囲内に調整 useEffect(() => { const maxIndex = Math.max(0, filteredItems.length - 1); if (selectedIndex > maxIndex) { setSelectedIndex(maxIndex); } }, [filteredItems.length, selectedIndex]); // スクロール位置を項目数の範囲内に収める useEffect(() => { const maxScroll = Math.max(0, filteredItems.length - maxDisplayItems); if (scrollOffset > maxScroll) { setScrollOffset(Math.max(0, Math.min(maxScroll, selectedIndex))); } }, [filteredItems.length, maxDisplayItems, scrollOffset, selectedIndex]); // ↓/↑ で選択を移動する共通ロジック const moveSelection = (delta) => { if (filteredItems.length === 0) return; let nextIndex = selectedIndex + delta; nextIndex = Math.max(0, Math.min(filteredItems.length - 1, nextIndex)); if (nextIndex < scrollOffset) { setScrollOffset(nextIndex); } else if (nextIndex >= scrollOffset + maxDisplayItems) { setScrollOffset(nextIndex - maxDisplayItems + 1); } setSelectedIndex(nextIndex); }; useInput((input, key) => { if (key.escape) { onCancel(); return; } if (key.return) { const selected = items.filter((item) => selectedItems.has(item.value)); onConfirm(selected); return; } if (key.ctrl && input === 'a') { if (filteredItems.length > 0) { const newSelected = new Set(selectedItems); const allVisibleSelected = filteredItems.every((item) => newSelected.has(item.value)); if (allVisibleSelected) { filteredItems.forEach((item) => newSelected.delete(item.value)); } else { filteredItems.forEach((item) => newSelected.add(item.value)); } setSelectedItems(newSelected); } return; } if (key.upArrow || (key.ctrl && input === 'p')) { moveSelection(-1); return; } if (key.downArrow || (key.ctrl && input === 'n')) { moveSelection(1); return; } // Space: 現在行の選択トグル if (input === ' ') { const currentItem = filteredItems[selectedIndex]; if (currentItem) { const newSelected = new Set(selectedItems); if (newSelected.has(currentItem.value)) { newSelected.delete(currentItem.value); } else { newSelected.add(currentItem.value); } setSelectedItems(newSelected); } return; } }); const hasItems = filteredItems.length > 0; // 可視アイテム計算 const visibleItems = filteredItems.slice(scrollOffset, scrollOffset + maxDisplayItems); const hiddenAbove = scrollOffset; const hiddenBelow = Math.max(0, filteredItems.length - (scrollOffset + visibleItems.length)); return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "cyan", bold: true, children: title }) }), showStats && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "gray", children: [filteredItems.length, " / ", items.length, " items", hasItems && _jsxs(_Fragment, { children: [" \u2022 cursor at ", selectedIndex + 1] }), " \u2022", ' ', _jsxs(Text, { color: "green", bold: true, children: [selectedItems.size, " selected"] })] }) })), _jsx(Box, { marginBottom: 1, children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", children: placeholder }), _jsxs(Box, { marginTop: 0, children: [_jsxs(Text, { color: "cyan", bold: true, children: ["\u276F", ' '] }), _jsx(Text, { children: query.slice(0, cursorPosition) }), _jsx(Text, { color: "cyan", children: "\u2588" }), _jsx(Text, { children: query.slice(cursorPosition) })] })] }) }), _jsx(Box, { marginBottom: 1, children: !hasItems ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: "red", children: "No matches found" }) })) : (_jsxs(Box, { flexDirection: "column", children: [hiddenAbove > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: ["\u2191 ", hiddenAbove, " more"] }) })), visibleItems.map((item, index) => { const globalIndex = scrollOffset + index; const isItemSelected = selectedItems.has(item.value); const isCurrent = globalIndex === selectedIndex; return (_jsx(Box, { children: _jsxs(Text, { color: isCurrent ? 'cyan' : 'white', children: [isCurrent ? '▶ ' : ' ', isItemSelected ? (_jsxs(Text, { color: "green", bold: true, children: ["[x]", ' '] })) : (_jsx(Text, { color: "gray", children: "[ ] " })), _jsx(Text, { color: isCurrent ? 'cyan' : 'white', bold: isCurrent, children: item.label })] }) }, item.value)); }), hiddenBelow > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: ["\u2193 ", hiddenBelow, " more"] }) }))] })) }), selectedItems.size > 0 && (_jsx(Box, { marginBottom: 1, children: _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "green", padding: 1, children: [_jsxs(Text, { color: "green", bold: true, children: ["Selected (", selectedItems.size, " items)"] }), _jsxs(Box, { flexDirection: "column", children: [Array.from(selectedItems) .slice(0, 5) .map((value) => { const item = items.find((i) => i.value === value); return item ? (_jsxs(Text, { color: "gray", children: ["\u2022 ", item.label] }, value)) : null; }), selectedItems.size > 5 && (_jsxs(Text, { color: "gray", children: ["... ", selectedItems.size - 5, " more"] }))] })] }) })), _jsx(Box, { children: _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "gray", children: [_jsx(Text, { color: "cyan", children: "\u2191/\u2193" }), " navigate \u2022", ' ', _jsx(Text, { color: "yellow", children: "Space" }), " toggle \u2022", ' ', _jsx(Text, { color: "green", children: "Enter" }), " confirm \u2022", ' ', _jsx(Text, { color: "red", children: "Esc" }), " cancel"] }), _jsxs(Text, { color: "gray", children: [_jsx(Text, { color: "cyan", children: "Ctrl+A" }), " select all / clear all"] })] }) })] })); }; //# sourceMappingURL=MultiSelectList.js.map