ascii-ui
Version:
Graphic terminal emulator for HTML canvas elements
289 lines • 10.5 kB
JavaScript
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