neo-neo-bblessed
Version:
A fork of neo-blessed (which is a fork of blessed) with bug fixes and maintenance.
553 lines (468 loc) • 13.1 kB
text/typescript
/**
* listbar.js - listbar element for blessed
* Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
* https://github.com/chjj/blessed
*/
/**
* Modules
*/
import * as helpers from '../helpers.js';
import Node from './node.js';
import boxFactory from './box.js';
const Box = boxFactory.Box;
/**
* Type definitions
*/
interface ListbarOptions {
mouse?: boolean;
commands?: ListbarCommand[] | { [key: string]: ListbarCommand | Function };
items?: ListbarCommand[] | { [key: string]: ListbarCommand | Function };
keys?: boolean;
vi?: boolean;
autoCommandKeys?: boolean;
[key: string]: any;
}
interface ListbarCommand {
text?: string;
prefix?: string;
callback?: Function;
keys?: string[];
}
interface ListbarKey {
name: string;
shift?: boolean;
}
interface ListbarStyle {
selected?: { [key: string]: any };
item?: { [key: string]: any };
prefix?: { [key: string]: any };
[key: string]: any;
}
interface ListbarCoords {
xi: number;
xl: number;
yi: number;
yl: number;
}
interface ListbarElement {
aleft?: number;
width: number;
hide(): void;
show(): void;
rleft?: number;
_?: { cmd: ListbarCommand };
_getCoords(): ListbarCoords | undefined;
detach(): void;
on(event: string, listener: Function): void;
}
interface ListbarScreen {
autoPadding?: boolean;
render(): void;
key(keys: string[], callback: Function): void;
}
interface ListbarInterface extends Box {
type: string;
items: ListbarElement[];
ritems: string[];
commands: ListbarCommand[];
leftBase: number;
leftOffset: number;
mouse: boolean;
style: ListbarStyle;
screen: ListbarScreen;
parent?: any;
ileft: number;
itop: number;
iwidth: number;
_: { [key: string]: any };
selected: number;
on(event: string, listener: Function): void;
emit(event: string, ...args: any[]): boolean;
onScreenEvent(event: string, listener: Function): void;
append(element: any): void;
remove(element: any): void;
_getCoords(): ListbarCoords | undefined;
_render(): any;
setItems(
commands: ListbarCommand[] | { [key: string]: ListbarCommand | Function }
): void;
add(item: string | Function | ListbarCommand, callback?: Function): void;
addItem(item: string | Function | ListbarCommand, callback?: Function): void;
appendItem(
item: string | Function | ListbarCommand,
callback?: Function
): void;
render(): any;
select(offset: number | ListbarElement): void;
removeItem(child: number | ListbarElement): void;
move(offset: number): void;
moveLeft(offset?: number): void;
moveRight(offset?: number): void;
selectTab(index: number): void;
}
/**
* Listbar / HorizontalList - Modern ES6 Class
*/
class Listbar extends Box {
type = 'listbar';
items: ListbarElement[];
ritems: string[];
commands: ListbarCommand[];
leftBase: number;
leftOffset: number;
mouse: boolean;
constructor(options?: ListbarOptions) {
// Handle malformed options gracefully
if (!options || typeof options !== 'object' || Array.isArray(options)) {
options = {};
}
super(options);
this.items = [];
this.ritems = [];
this.commands = [];
this.leftBase = 0;
this.leftOffset = 0;
this.mouse = options.mouse || false;
if (!this.style.selected) {
this.style.selected = {};
}
if (!this.style.item) {
this.style.item = {};
}
if (options.commands || options.items) {
this.setItems(options.commands || options.items);
}
if (options.keys) {
this.on('keypress', (ch: string, key: ListbarKey) => {
if (
key.name === 'left' ||
(options!.vi && key.name === 'h') ||
(key.shift && key.name === 'tab')
) {
this.moveLeft();
this.screen.render();
// Stop propagation if we're in a form.
if (key.name === 'tab') return false;
return;
}
if (
key.name === 'right' ||
(options!.vi && key.name === 'l') ||
key.name === 'tab'
) {
this.moveRight();
this.screen.render();
// Stop propagation if we're in a form.
if (key.name === 'tab') return false;
return;
}
if (
key.name === 'enter' ||
(options!.vi && key.name === 'k' && !key.shift)
) {
this.emit('action', this.items[this.selected], this.selected);
this.emit('select', this.items[this.selected], this.selected);
const item = this.items[this.selected];
if (item._!.cmd.callback) {
item._!.cmd.callback();
}
this.screen.render();
return;
}
if (key.name === 'escape' || (options!.vi && key.name === 'q')) {
this.emit('action');
this.emit('cancel');
return;
}
});
}
if (options.autoCommandKeys) {
this.onScreenEvent('keypress', (ch: string) => {
if (/^[0-9]$/.test(ch)) {
let i = +ch - 1;
if (!~i) i = 9;
return this.selectTab(i);
}
});
}
this.on('focus', () => {
this.select(this.selected);
});
}
get selected(): number {
return this.leftBase + this.leftOffset;
}
setItems(
commands: ListbarCommand[] | { [key: string]: ListbarCommand | Function }
): void {
let commandsArray: ListbarCommand[];
if (!Array.isArray(commands)) {
commandsArray = Object.keys(commands).reduce(
(obj: ListbarCommand[], key: string, i: number): ListbarCommand[] => {
let cmd = commands[key];
let cb: Function;
if (typeof cmd === 'function') {
cb = cmd;
cmd = { callback: cb };
}
if ((cmd as ListbarCommand).text == null)
(cmd as ListbarCommand).text = key;
if ((cmd as ListbarCommand).prefix == null)
(cmd as ListbarCommand).prefix = ++i + '';
if (
(cmd as ListbarCommand).text == null &&
(cmd as ListbarCommand).callback
) {
(cmd as ListbarCommand).text = (
cmd as ListbarCommand
).callback!.name;
}
obj.push(cmd as ListbarCommand);
return obj;
},
[]
);
} else {
commandsArray = commands;
}
this.items.forEach((el: ListbarElement) => {
el.detach();
});
this.items = [];
this.ritems = [];
this.commands = [];
commandsArray.forEach((cmd: ListbarCommand) => {
this.add(cmd);
});
this.emit('set items');
}
add(item: string | Function | ListbarCommand, callback?: Function): void {
this.addItem(item, callback);
}
addItem(item: string | Function | ListbarCommand, callback?: Function): void {
this.appendItem(item, callback);
}
appendItem(
item: string | Function | ListbarCommand,
callback?: Function
): void {
const prev = this.items[this.items.length - 1];
let drawn: number;
let cmd: ListbarCommand;
let title: string;
let len: number;
if (!this.parent) {
drawn = 0;
} else {
drawn = prev ? prev.aleft + prev.width : 0;
if (!this.screen.autoPadding) {
drawn += this.ileft;
}
}
if (typeof item === 'object') {
cmd = item;
if (cmd.prefix == null) cmd.prefix = this.items.length + 1 + '';
}
if (typeof item === 'string') {
cmd = {
prefix: this.items.length + 1 + '',
text: item,
callback: callback,
};
}
if (typeof item === 'function') {
cmd = {
prefix: this.items.length + 1 + '',
text: item.name,
callback: item,
};
}
if (cmd.keys && cmd.keys[0]) {
cmd.prefix = cmd.keys[0];
}
const t = helpers.generateTags(this.style.prefix || { fg: 'lightblack' });
title =
(cmd.prefix != null ? t.open + cmd.prefix + t.close + ':' : '') +
cmd.text;
len = ((cmd.prefix != null ? cmd.prefix + ':' : '') + cmd.text).length;
const options = {
screen: this.screen,
top: 0,
left: drawn + 1,
height: 1,
content: title,
width: len + 2,
align: 'center',
autoFocus: false,
tags: true,
mouse: true,
style: helpers.merge({}, this.style.item),
noOverflow: true,
};
if (!this.screen.autoPadding) {
options.top += this.itop;
options.left += this.ileft;
}
['bg', 'fg', 'bold', 'underline', 'blink', 'inverse', 'invisible'].forEach(
(name: string) => {
options.style[name] = () => {
const attr =
this.items[this.selected] === el
? this.style.selected[name]
: this.style.item[name];
if (typeof attr === 'function') return attr(el);
return attr;
};
}
);
const el = new Box(options);
this._[cmd.text] = el;
cmd.element = el;
el._.cmd = cmd;
this.ritems.push(cmd.text);
this.items.push(el);
this.commands.push(cmd);
this.append(el);
if (cmd.callback) {
if (cmd.keys) {
this.screen.key(cmd.keys, () => {
this.emit('action', el, this.selected);
this.emit('select', el, this.selected);
if (el._.cmd.callback) {
el._.cmd.callback();
}
this.select(el);
this.screen.render();
});
}
}
if (this.items.length === 1) {
this.select(0);
}
// XXX May be affected by new element.options.mouse option.
if (this.mouse) {
el.on('click', () => {
this.emit('action', el, this.selected);
this.emit('select', el, this.selected);
if (el._.cmd.callback) {
el._.cmd.callback();
}
this.select(el);
this.screen.render();
});
}
this.emit('add item');
}
render(): any {
let drawn = 0;
if (!this.screen.autoPadding) {
drawn += this.ileft;
}
this.items.forEach((el: ListbarElement, i: number) => {
if (i < this.leftBase) {
el.hide();
} else {
el.rleft = drawn + 1;
drawn += el.width + 2;
el.show();
}
});
return this._render();
}
select(offset: number | ListbarElement): void {
let offsetNum: number;
if (typeof offset !== 'number') {
offsetNum = this.items.indexOf(offset);
} else {
offsetNum = offset;
}
if (offsetNum < 0) {
offsetNum = 0;
} else if (offsetNum >= this.items.length) {
offsetNum = this.items.length - 1;
}
if (!this.parent) {
this.emit('select item', this.items[offsetNum], offsetNum);
return;
}
const lpos = this._getCoords();
if (!lpos) return;
const width = lpos.xl - lpos.xi - this.iwidth;
let drawn = 0;
let visible = 0;
const el = this.items[offsetNum];
if (!el) return;
this.items.forEach((el: ListbarElement, i: number) => {
if (i < this.leftBase) return;
const lpos = el._getCoords();
if (!lpos) return;
if (lpos.xl - lpos.xi <= 0) return;
drawn += lpos.xl - lpos.xi + 2;
if (drawn <= width) visible++;
});
let diff = offsetNum - (this.leftBase + this.leftOffset);
if (offsetNum > this.leftBase + this.leftOffset) {
if (offsetNum > this.leftBase + visible - 1) {
this.leftOffset = 0;
this.leftBase = offsetNum;
} else {
this.leftOffset += diff;
}
} else if (offsetNum < this.leftBase + this.leftOffset) {
diff = -diff;
if (offsetNum < this.leftBase) {
this.leftOffset = 0;
this.leftBase = offsetNum;
} else {
this.leftOffset -= diff;
}
}
// XXX Move `action` and `select` events here.
this.emit('select item', el, offsetNum);
}
removeItem(child: number | ListbarElement): void {
const i = typeof child !== 'number' ? this.items.indexOf(child) : child;
if (~i && this.items[i]) {
child = this.items.splice(i, 1)[0];
this.ritems.splice(i, 1);
this.commands.splice(i, 1);
this.remove(child);
if (i === this.selected) {
this.select(i - 1);
}
}
this.emit('remove item');
}
move(offset: number): void {
this.select(this.selected + offset);
}
moveLeft(offset?: number): void {
this.move(-(offset || 1));
}
moveRight(offset?: number): void {
this.move(offset || 1);
}
selectTab(index: number): void {
const item = this.items[index];
if (item) {
if (item._!.cmd.callback) {
item._!.cmd.callback();
}
this.select(index);
this.screen.render();
}
this.emit('select tab', item, index);
}
}
/**
* Factory function for backward compatibility
*/
function listbar(options?: ListbarOptions): ListbarInterface {
return new Listbar(options) as ListbarInterface;
}
// Attach the class as a property for direct access
listbar.Listbar = Listbar;
/**
* Expose
*/
export default listbar;