UNPKG

ascii-ui

Version:

Graphic terminal emulator for HTML canvas elements

289 lines 10.5 kB
import { isEmpty } from 'vanilla-type-check/isEmpty'; import { Widget } from '../Widget'; import { deepAssign } from '../util/deepAssign'; import { splitText } from '../util/tokenizer'; export const SELECT_INDEX_NONE = -1; export class Select extends Widget { constructor(terminal, options, parent) { super(terminal, deepAssign({}, Select.defaultOptions, options), parent); this.focusedIndex = SELECT_INDEX_NONE; this.firstLine = 0; this.render(); } render() { if (!this.selectOptions) { return; } const lastTerminalLine = this.options.line + this.options.height; let terminalLine = this.options.line; let optionIndex = 0; while (this.selectOptions[optionIndex].endLine <= this.firstLine) { optionIndex++; } let skipLines = this.firstLine - this.selectOptions[optionIndex].startLine; while (optionIndex < this.selectOptions.length && terminalLine < lastTerminalLine) { this.terminal.setTextStyle(this.getOptionStyle(optionIndex)); const text = this.getOptionText(optionIndex); for (const textLine of text) { if (terminalLine >= lastTerminalLine) { break; } if (skipLines > 0) { skipLines--; continue; } this.terminal.setText(textLine, this.options.col, terminalLine); terminalLine++; } optionIndex++; } this.terminal.clear(this.options.col, terminalLine, this.options.width, lastTerminalLine - terminalLine); } getOptionFromIndex(index) { const opt = this.selectOptions[index]; return opt ? opt.option : undefined; } getValueFromIndex(index) { const opt = this.selectOptions[index]; return opt ? opt.option.value : undefined; } getIndexFromOption(option) { for (let i = 0; i < this.options.options.length; i++) { if (this.options.options[i] === option) { return i; } } return SELECT_INDEX_NONE; } getIndexFromValue(value) { for (let i = 0; i < this.selectOptions.length; i++) { if (this.selectOptions[i].option.value === value) { return i; } } return SELECT_INDEX_NONE; } getSelectedIndexes() { const res = []; this.selectOptions.forEach((option, i) => { if (option.selected) { res.push(i); } }); return res; } getSelectedOptions() { return this.getSelectedIndexes().map((index) => this.selectOptions[index].option); } getSelectedValues() { return this.getSelectedIndexes().map((index) => this.selectOptions[index].option.value); } getFocusedIndex() { return this.focusedIndex; } getFocusedOption() { return this.getOptionFromIndex(this.focusedIndex); } getFocusedValue() { return this.getValueFromIndex(this.focusedIndex); } getIndexAt(column, line) { const maxHeight = this.options.line + this.options.height; if (column < this.options.col || column >= this.options.col + this.options.width || line < this.options.line || line >= maxHeight) { return SELECT_INDEX_NONE; } const realLine = this.firstLine + line - this.options.line; for (let i = 0; i < this.selectOptions.length; i++) { if (this.selectOptions[i].endLine > realLine) { return i; } } return SELECT_INDEX_NONE; } getOptionAt(column, line) { return this.getOptionFromIndex(this.getIndexAt(column, line)); } getValueAt(column, line) { return this.getValueFromIndex(this.getIndexAt(column, line)); } isIndexSelected(index) { const item = this.selectOptions[index]; return item ? item.selected : undefined; } isOptionSelected(option) { const index = this.getIndexFromOption(option); return index === SELECT_INDEX_NONE ? undefined : this.selectOptions[index].selected; } isValueSelected(value) { const index = this.getIndexFromValue(value); return index === SELECT_INDEX_NONE ? undefined : this.selectOptions[index].selected; } toggleIndex(index, selected) { const item = this.selectOptions[index]; const newState = selected === undefined ? !item.selected : selected; let change = item.selected !== newState; item.selected = newState; if (!this.options.multiple && item.selected) { for (let i = 0; i < this.selectOptions.length; i++) { const unselectedItem = this.selectOptions[i]; if (i !== index) { change = change || unselectedItem.selected; unselectedItem.selected = false; } } } if (change) { this.render(); return true; } return true; } selectOption(option) { return this.toggleIndex(this.getIndexFromOption(option)); } selectValue(value) { return this.toggleIndex(this.getIndexFromValue(value)); } focusIndex(index) { const oldIndex = this.focusedIndex; const selectedOption = this.selectOptions[index]; if (selectedOption) { if (!selectedOption.option.disabled && this.focusedIndex !== index) { this.focusedIndex = index; if (selectedOption.endLine > this.firstLine + this.options.height) { this.firstLine = selectedOption.endLine - selectedOption.processedText.length + 1; } if (selectedOption.startLine < this.firstLine) { this.firstLine = selectedOption.startLine; } } } else if (this.options.allowUnselect) { this.focusedIndex = SELECT_INDEX_NONE; } if (oldIndex !== this.focusedIndex) { this.render(); return true; } return false; } focusOption(option) { return this.focusIndex(this.getIndexFromOption(option)); } focusValue(value) { return this.focusIndex(this.getIndexFromValue(value)); } focusPrev() { return this.moveFocus(-1); } focusNext() { return this.moveFocus(+1); } updateOptions(changes) { if (isEmpty(changes)) { return; } let startLine = 0; this.selectOptions = this.options.options.map((option) => { const processedText = splitText(option.text, this.options.width, this.options.tokenizer); const opt = { startLine, processedText, endLine: startLine + processedText.length, option: Object.assign({}, option), selected: false, }; startLine = opt.endLine; return opt; }); } moveFocus(delta) { const selectOptions = this.selectOptions; let newIndex = this.focusedIndex; let tries = selectOptions.length; do { newIndex = newIndex + delta; if (newIndex < 0) { if (this.options.loop) { newIndex = selectOptions.length - 1; } else { newIndex = 0; break; } } else if (newIndex >= selectOptions.length) { if (this.options.loop) { newIndex = 0; } else { newIndex = selectOptions.length - 1; break; } } if (!selectOptions[newIndex].option.disabled) { break; } tries--; } while (newIndex !== this.focusedIndex && tries > 0); return tries > 0 ? this.focusIndex(newIndex) : false; } getOptionStyle(optionIndex) { const item = this.selectOptions[optionIndex]; const status = ((isSelected, isFocused, isDisabled) => { if (isSelected) { if (isDisabled) { return 'disabledSelectedStyle'; } if (isFocused) { return 'selectedFocusedStyle'; } return 'selectedStyle'; } if (isDisabled) { return 'disabledStyle'; } if (isFocused) { return 'baseFocusedStyle'; } return 'baseStyle'; })(item.selected, this.focusedIndex === optionIndex, item.option.disabled); return this.options[status]; } getOptionText(optionIndex) { const item = this.selectOptions[optionIndex]; const style = this.getOptionStyle(optionIndex); let width = this.options.width; if (style.prefix) { width -= style.prefix.length; } if (style.suffix) { width -= style.suffix.length; } const splittedText = splitText(item.option.text, width, this.options.tokenizer); if (!style.prefix && !style.suffix) { return splittedText; } const prefixIndentation = style.prefix ? ' '.repeat(style.prefix.length) : ''; const suffixIndentation = style.suffix ? ' '.repeat(style.suffix.length) : ''; return splittedText.map((line, i) => { const prefix = !style.prefix || i !== 0 ? prefixIndentation : style.prefix; const suffix = !style.suffix || i !== splittedText.length - 1 ? suffixIndentation : style.suffix; return prefix + line + suffix; }); } } Select.defaultOptions = { options: undefined, loop: true, multiple: false, allowUnselect: true, baseStyle: { fg: '#00ff00', bg: '#000000', prefix: ' ' }, baseFocusedStyle: { fg: '#ffffff', bg: '#000000', prefix: ' ' }, selectedStyle: { fg: '#00ff00', bg: '#000000', prefix: '*' }, selectedFocusedStyle: { fg: '#ffffff', bg: '#000000', prefix: '*' }, disabledStyle: { fg: '#009900', bg: '#000000', prefix: ' ' }, disabledSelectedStyle: { fg: '#009900', bg: '#000000', prefix: '*' }, }; //# sourceMappingURL=Select.js.map