{"version":3,"file":"grouped-select-list.d.ts","sourceRoot":"","sources":["../../src/components/grouped-select-list.ts"],"names":[],"mappings":"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","sourcesContent":["import { getKeybindings } from \"../keybindings.js\";\nimport { matchesKey } from \"../keys.js\";\nimport type { Component } from \"../tui.js\";\nimport { truncateToWidth } from \"../utils.js\";\n\nexport interface GroupedSelectGroup<T> {\n\tid: string;\n\theader: string;\n\titems: T[];\n\tinitiallyCollapsed?: boolean;\n\temptyHint?: string;\n\tdisabled?: boolean;\n}\n\nexport interface GroupedSelectListOptions<T> {\n\tmaxVisible: number;\n\trenderHeader: (group: GroupedSelectGroup<T>, isSelected: boolean, expanded: boolean) => string;\n\trenderItem: (item: T, group: GroupedSelectGroup<T>, isSelected: boolean) => string;\n\trenderEmpty?: (group: GroupedSelectGroup<T>) => string;\n\tnoMatch?: string;\n}\n\ninterface HeaderRow {\n\tkind: \"header\";\n\tgroupIndex: number;\n}\ninterface ItemRow<T> {\n\tkind: \"item\";\n\tgroupIndex: number;\n\titemIndex: number;\n\titem: T;\n}\ninterface EmptyRow {\n\tkind: \"empty\";\n\tgroupIndex: number;\n}\ntype Row<T> = HeaderRow | ItemRow<T> | EmptyRow;\n\nexport type GroupedSelection<T> =\n\t| { kind: \"header\"; group: GroupedSelectGroup<T> }\n\t| { kind: \"item\"; group: GroupedSelectGroup<T>; item: T };\n\nexport class GroupedSelectList<T> implements Component {\n\tprivate groups: GroupedSelectGroup<T>[] = [];\n\tprivate folded: Set<string> = new Set();\n\tprivate seen: Set<string> = new Set();\n\tprivate rows: Row<T>[] = [];\n\tprivate selectedIndex = 0;\n\tprivate scrollOffset = 0;\n\n\tpublic onSelect?: (selection: GroupedSelection<T>) => void;\n\tpublic onCancel?: () => void;\n\tpublic onSelectionChange?: (selection: GroupedSelection<T> | null) => void;\n\tpublic onToggleGroup?: (group: GroupedSelectGroup<T>, expanded: boolean) => void;\n\n\tconstructor(private options: GroupedSelectListOptions<T>) {}\n\n\tsetGroups(groups: GroupedSelectGroup<T>[], preserveSelection: boolean = false): void {\n\t\tconst previousSelection = preserveSelection ? this.currentSelection() : null;\n\t\tconst previouslyFolded = new Set(this.folded);\n\t\tthis.groups = groups;\n\t\t// Carry over fold state for already-known groups; only apply\n\t\t// `initiallyCollapsed` the first time a group id appears.\n\t\tthis.folded = new Set();\n\t\tfor (const g of groups) {\n\t\t\tif (this.seen.has(g.id)) {\n\t\t\t\tif (previouslyFolded.has(g.id)) this.folded.add(g.id);\n\t\t\t} else {\n\t\t\t\tthis.seen.add(g.id);\n\t\t\t\tif (g.initiallyCollapsed) this.folded.add(g.id);\n\t\t\t}\n\t\t}\n\t\tthis.flatten();\n\t\tif (previousSelection) {\n\t\t\tthis.restoreSelection(previousSelection);\n\t\t} else {\n\t\t\tthis.selectedIndex = this.firstSelectableIndex();\n\t\t}\n\t\tthis.notifyChange();\n\t}\n\n\tprivate flatten(): void {\n\t\tconst rows: Row<T>[] = [];\n\t\tfor (let g = 0; g < this.groups.length; g++) {\n\t\t\tconst group = this.groups[g]!;\n\t\t\trows.push({ kind: \"header\", groupIndex: g });\n\t\t\tif (this.folded.has(group.id) || group.disabled) continue;\n\t\t\tif (group.items.length === 0) {\n\t\t\t\tif (group.emptyHint) rows.push({ kind: \"empty\", groupIndex: g });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (let i = 0; i < group.items.length; i++) {\n\t\t\t\trows.push({ kind: \"item\", groupIndex: g, itemIndex: i, item: group.items[i]! });\n\t\t\t}\n\t\t}\n\t\tthis.rows = rows;\n\t\tif (this.selectedIndex >= rows.length) {\n\t\t\tthis.selectedIndex = Math.max(0, rows.length - 1);\n\t\t}\n\t}\n\n\tprivate firstSelectableIndex(): number {\n\t\tfor (let i = 0; i < this.rows.length; i++) {\n\t\t\tif (this.rows[i]!.kind !== \"empty\") return i;\n\t\t}\n\t\treturn 0;\n\t}\n\n\tcurrentSelection(): GroupedSelection<T> | null {\n\t\tconst row = this.rows[this.selectedIndex];\n\t\tif (!row) return null;\n\t\tconst group = this.groups[row.groupIndex];\n\t\tif (!group) return null;\n\t\tif (row.kind === \"header\") return { kind: \"header\", group };\n\t\tif (row.kind === \"item\") return { kind: \"item\", group, item: row.item };\n\t\treturn null;\n\t}\n\n\tprivate restoreSelection(target: GroupedSelection<T>): void {\n\t\t// Try exact match first, then fall back to header of the same group, then top.\n\t\tfor (let i = 0; i < this.rows.length; i++) {\n\t\t\tconst row = this.rows[i]!;\n\t\t\tconst group = this.groups[row.groupIndex]!;\n\t\t\tif (\n\t\t\t\ttarget.kind === \"item\" &&\n\t\t\t\trow.kind === \"item\" &&\n\t\t\t\tgroup.id === target.group.id &&\n\t\t\t\trow.item === target.item\n\t\t\t) {\n\t\t\t\tthis.selectedIndex = i;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (target.kind === \"header\" && row.kind === \"header\" && group.id === target.group.id) {\n\t\t\t\tthis.selectedIndex = i;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tfor (let i = 0; i < this.rows.length; i++) {\n\t\t\tconst row = this.rows[i]!;\n\t\t\tif (row.kind === \"header\" && this.groups[row.groupIndex]?.id === target.group.id) {\n\t\t\t\tthis.selectedIndex = i;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tthis.selectedIndex = this.firstSelectableIndex();\n\t}\n\n\tprivate notifyChange(): void {\n\t\tthis.onSelectionChange?.(this.currentSelection());\n\t}\n\n\tgetRows(): number {\n\t\treturn this.rows.length;\n\t}\n\n\tgetMaxVisible(): number {\n\t\treturn this.options.maxVisible;\n\t}\n\n\tisExpanded(groupId: string): boolean {\n\t\treturn !this.folded.has(groupId);\n\t}\n\n\ttoggleGroup(groupId: string): void {\n\t\tconst group = this.groups.find((g) => g.id === groupId);\n\t\tif (!group || group.disabled) return;\n\t\tif (this.folded.has(groupId)) this.folded.delete(groupId);\n\t\telse this.folded.add(groupId);\n\t\tconst previous = this.currentSelection();\n\t\tthis.flatten();\n\t\tif (previous) this.restoreSelection(previous);\n\t\tthis.onToggleGroup?.(group, !this.folded.has(groupId));\n\t\tthis.notifyChange();\n\t}\n\n\texpandGroup(groupId: string): void {\n\t\tif (!this.folded.has(groupId)) return;\n\t\tthis.toggleGroup(groupId);\n\t}\n\n\tcollapseGroup(groupId: string): void {\n\t\tif (this.folded.has(groupId)) return;\n\t\tthis.toggleGroup(groupId);\n\t}\n\n\texpandAll(): void {\n\t\tthis.folded.clear();\n\t\tconst previous = this.currentSelection();\n\t\tthis.flatten();\n\t\tif (previous) this.restoreSelection(previous);\n\t\tthis.notifyChange();\n\t}\n\n\tcollapseAll(): void {\n\t\tfor (const g of this.groups) this.folded.add(g.id);\n\t\tconst previous = this.currentSelection();\n\t\tthis.flatten();\n\t\tif (previous) this.restoreSelection(previous);\n\t\tthis.notifyChange();\n\t}\n\n\tmoveBy(delta: number): void {\n\t\tif (this.rows.length === 0) return;\n\t\tlet i = this.selectedIndex;\n\t\tconst step = delta > 0 ? 1 : -1;\n\t\tconst total = Math.abs(delta);\n\t\tfor (let k = 0; k < total; k++) {\n\t\t\tlet next = i + step;\n\t\t\t// Skip empty rows when navigating with arrow keys.\n\t\t\twhile (this.rows[next]?.kind === \"empty\") next += step;\n\t\t\tif (next < 0) next = this.rows.length - 1;\n\t\t\tif (next >= this.rows.length) next = 0;\n\t\t\twhile (this.rows[next]?.kind === \"empty\") next += step;\n\t\t\tif (next < 0) next = this.rows.length - 1;\n\t\t\tif (next >= this.rows.length) next = 0;\n\t\t\ti = next;\n\t\t}\n\t\tif (i !== this.selectedIndex) {\n\t\t\tthis.selectedIndex = i;\n\t\t\tthis.notifyChange();\n\t\t}\n\t}\n\n\tmoveToFirstOfGroup(groupIndex: number): void {\n\t\tfor (let i = 0; i < this.rows.length; i++) {\n\t\t\tif (this.rows[i]!.groupIndex === groupIndex && this.rows[i]!.kind === \"header\") {\n\t\t\t\tthis.selectedIndex = i;\n\t\t\t\tthis.notifyChange();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\tgetSelectedGroupIndex(): number | null {\n\t\tconst row = this.rows[this.selectedIndex];\n\t\treturn row ? row.groupIndex : null;\n\t}\n\n\tcurrentRowKind(): \"header\" | \"item\" | \"empty\" | null {\n\t\treturn this.rows[this.selectedIndex]?.kind ?? null;\n\t}\n\n\tinvalidate(): void {}\n\n\trender(width: number): string[] {\n\t\tif (this.rows.length === 0) {\n\t\t\treturn [this.options.noMatch ?? \"  No matches\"];\n\t\t}\n\n\t\tconst maxVisible = Math.max(1, this.options.maxVisible);\n\t\t// Keep the selected row centered when there are more rows than fit on screen.\n\t\tconst half = Math.floor(maxVisible / 2);\n\t\tconst desiredOffset = Math.max(0, Math.min(this.selectedIndex - half, this.rows.length - maxVisible));\n\t\tthis.scrollOffset = Math.max(0, desiredOffset);\n\t\tconst start = this.scrollOffset;\n\t\tconst end = Math.min(start + maxVisible, this.rows.length);\n\n\t\tconst out: string[] = [];\n\t\tfor (let i = start; i < end; i++) {\n\t\t\tconst row = this.rows[i]!;\n\t\t\tconst group = this.groups[row.groupIndex]!;\n\t\t\tconst selected = i === this.selectedIndex;\n\t\t\tlet line: string;\n\t\t\tif (row.kind === \"header\") {\n\t\t\t\tconst expanded = !this.folded.has(group.id) && !group.disabled;\n\t\t\t\tline = this.options.renderHeader(group, selected, expanded);\n\t\t\t} else if (row.kind === \"item\") {\n\t\t\t\tline = this.options.renderItem(row.item, group, selected);\n\t\t\t} else {\n\t\t\t\tline = this.options.renderEmpty?.(group) ?? \"\";\n\t\t\t}\n\t\t\tout.push(truncateToWidth(line, Math.max(1, width), \"\"));\n\t\t}\n\n\t\tif (start > 0 || end < this.rows.length) {\n\t\t\tout.push(truncateToWidth(`  (${this.selectedIndex + 1}/${this.rows.length})`, Math.max(1, width), \"\"));\n\t\t}\n\n\t\treturn out;\n\t}\n\n\thandleInput(keyData: string): boolean {\n\t\tconst kb = getKeybindings();\n\t\tif (kb.matches(keyData, \"tui.select.up\")) {\n\t\t\tthis.moveBy(-1);\n\t\t\treturn true;\n\t\t}\n\t\tif (kb.matches(keyData, \"tui.select.down\")) {\n\t\t\tthis.moveBy(1);\n\t\t\treturn true;\n\t\t}\n\t\tif (kb.matches(keyData, \"tui.select.pageUp\")) {\n\t\t\tthis.moveBy(-Math.max(1, Math.floor(this.options.maxVisible / 2)));\n\t\t\treturn true;\n\t\t}\n\t\tif (kb.matches(keyData, \"tui.select.pageDown\")) {\n\t\t\tthis.moveBy(Math.max(1, Math.floor(this.options.maxVisible / 2)));\n\t\t\treturn true;\n\t\t}\n\t\tif (matchesKey(keyData, \"left\")) {\n\t\t\tconst row = this.rows[this.selectedIndex];\n\t\t\tif (row) {\n\t\t\t\tconst group = this.groups[row.groupIndex];\n\t\t\t\tif (group) {\n\t\t\t\t\tif (this.folded.has(group.id) || row.kind !== \"header\") {\n\t\t\t\t\t\tthis.moveToFirstOfGroup(row.groupIndex);\n\t\t\t\t\t}\n\t\t\t\t\tthis.collapseGroup(group.id);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t\tif (matchesKey(keyData, \"right\")) {\n\t\t\tconst row = this.rows[this.selectedIndex];\n\t\t\tif (row) {\n\t\t\t\tconst group = this.groups[row.groupIndex];\n\t\t\t\tif (group) {\n\t\t\t\t\tif (this.folded.has(group.id)) {\n\t\t\t\t\t\tthis.expandGroup(group.id);\n\t\t\t\t\t\tthis.moveBy(1);\n\t\t\t\t\t} else if (row.kind === \"header\" && group.items.length > 0) {\n\t\t\t\t\t\tthis.moveBy(1);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t\tif (kb.matches(keyData, \"tui.select.confirm\")) {\n\t\t\tconst sel = this.currentSelection();\n\t\t\tif (!sel) return true;\n\t\t\tif (sel.kind === \"header\") {\n\t\t\t\tif (!sel.group.disabled) this.toggleGroup(sel.group.id);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tthis.onSelect?.(sel);\n\t\t\treturn true;\n\t\t}\n\t\tif (kb.matches(keyData, \"tui.select.cancel\")) {\n\t\t\tthis.onCancel?.();\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n}\n"]}