UNPKG

neo-neo-bblessed

Version:

A fork of neo-blessed (which is a fork of blessed) with bug fixes and maintenance.

661 lines (571 loc) 17.3 kB
/** * list.js - list 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; import scrollableBoxFactory from './scrollablebox.js'; const ScrollableBox = scrollableBoxFactory.ScrollableBox; /** * List - Modern ES6 Class */ class List extends ScrollableBox { type = 'list'; value: string; items: any[]; ritems: string[]; selected: number; _isList: boolean; interactive: boolean; mouse: boolean; _listInitialized?: boolean; constructor(options?: any) { // Handle malformed options gracefully if (!options || typeof options !== 'object' || Array.isArray(options)) { options = {}; } // Force list-specific options options.ignoreKeys = true; super(options); this.value = ''; this.items = []; this.ritems = []; this.selected = 0; this._isList = true; // Set up selected styles if (!this.style.selected) { this.style.selected = {}; this.style.selected.bg = options.selectedBg; this.style.selected.fg = options.selectedFg; this.style.selected.bold = options.selectedBold; this.style.selected.underline = options.selectedUnderline; this.style.selected.blink = options.selectedBlink; this.style.selected.inverse = options.selectedInverse; this.style.selected.invisible = options.selectedInvisible; } // Set up item styles if (!this.style.item) { this.style.item = {}; this.style.item.bg = options.itemBg; this.style.item.fg = options.itemFg; this.style.item.bold = options.itemBold; this.style.item.underline = options.itemUnderline; this.style.item.blink = options.itemBlink; this.style.item.inverse = options.itemInverse; this.style.item.invisible = options.itemInvisible; } // Legacy: for apps written before the addition of item attributes. ['bg', 'fg', 'bold', 'underline', 'blink', 'inverse', 'invisible'].forEach( (name: string) => { if (this.style[name] != null && this.style.item![name] == null) { this.style.item![name] = this.style[name]; } } ); // Set up hover and focus effects if (this.options.itemHoverBg && !this.options.itemHoverEffects) { this.options.itemHoverEffects = { bg: this.options.itemHoverBg }; } if (this.options.itemHoverEffects) { this.style.item!.hover = this.options.itemHoverEffects; } if (this.options.itemFocusEffects) { this.style.item!.focus = this.options.itemFocusEffects; } if (this.options.itemHeight) { this.style.item!.height = this.options.itemHeight; } this.interactive = options.interactive !== false; this.mouse = options.mouse || false; // Set up mouse interaction if (options.mouse) { this.screen._listenMouse(this); this.on('element wheeldown', () => { this.select(this.selected + 2); this.screen.render(); }); this.on('element wheelup', () => { this.select(this.selected - 2); this.screen.render(); }); } // Set up keyboard interaction if (options.keys) { this.on('keypress', (ch: string, key: any) => { if (key.name === 'up' || (options!.vi && key.name === 'k')) { this.up(); this.screen.render(); return; } if (key.name === 'down' || (options!.vi && key.name === 'j')) { this.down(); this.screen.render(); return; } if ( key.name === 'enter' || (options!.vi && key.name === 'l' && !key.shift) ) { this.enterSelected(); return; } if (key.name === 'escape' || (options!.vi && key.name === 'q')) { this.cancelSelected(); return; } if (options!.vi && key.name === 'u' && key.ctrl) { this.move(-((this.height - this.iheight) / 2) | 0); this.screen.render(); return; } if (options!.vi && key.name === 'd' && key.ctrl) { this.move(((this.height - this.iheight) / 2) | 0); this.screen.render(); return; } if (options!.vi && key.name === 'b' && key.ctrl) { this.move(-(this.height - this.iheight)); this.screen.render(); return; } if (options!.vi && key.name === 'f' && key.ctrl) { this.move(this.height - this.iheight); this.screen.render(); return; } if (options!.vi && key.name === 'h' && key.shift) { this.move(this.childBase - this.selected); this.screen.render(); return; } if (options!.vi && key.name === 'm' && key.shift) { const visible = (Math.min(this.height - this.iheight, this.items.length) / 2) | 0; this.move(this.childBase + visible - this.selected); this.screen.render(); return; } if (options!.vi && key.name === 'l' && key.shift) { this.down( this.childBase + Math.min(this.height - this.iheight, this.items.length) - this.selected ); this.screen.render(); return; } if (options!.vi && key.name === 'g' && !key.shift) { this.select(0); this.screen.render(); return; } if (options!.vi && key.name === 'g' && key.shift) { this.select(this.items.length - 1); this.screen.render(); return; } if (options!.vi && (key.ch === '/' || key.ch === '?')) { if (typeof this.options.search !== 'function') { return; } return this.options.search((err: any, value?: string) => { if ( typeof err === 'string' || typeof err === 'function' || typeof err === 'number' || (err && err.test) ) { value = err; err = null; } if (err || !value) return this.screen.render(); this.select(this.fuzzyFind(value, key.ch === '?')); this.screen.render(); }); } }); } // Set up event handlers this.on('resize', () => { const visible = this.height - this.iheight; if (visible >= this.selected + 1) { this.childBase = 0; this.childOffset = this.selected; } else { this.childBase = this.selected - visible + 1; this.childOffset = visible - 1; } }); this.on('adopt', (el: any) => { if (!~this.items.indexOf(el)) { el.fixed = true; } }); this.on('remove', (el: any) => { this.removeItem(el); }); // Initialize with items if provided if (options.items) { options.items.forEach((item: any) => this.add(item)); } this.select(0); } createItem = (content: string): any => { const options: any = { screen: this.screen, content: content, align: this.align || 'left', top: 0, left: 0, right: this.scrollbar ? 1 : 0, tags: this.parseTags, height: this.style.item.height || 1, hoverEffects: this.mouse ? this.style.item.hover : null, focusEffects: this.mouse ? this.style.item.focus : null, autoFocus: false, }; if (!this.screen.autoPadding) { options.top = 1; options.left = this.ileft; options.right = this.iright + (this.scrollbar ? 1 : 0); } if (this.shrink && this.options.normalShrink) { delete options.right; options.width = 'shrink'; } let item: any; ['bg', 'fg', 'bold', 'underline', 'blink', 'inverse', 'invisible'].forEach( (name: string) => { options[name] = () => { const attr = this.items[this.selected] === item && this.interactive ? this.style.selected[name] : this.style.item[name]; if (typeof attr === 'function') return attr(item); return attr; }; } ); item = new Box(options); if (this.style.transparent) { options.transparent = true; } if (this.mouse) { item.on('click', () => { this.focus(); if (this.items[this.selected] === item) { this.emit('action', item, this.selected); this.emit('select', item, this.selected); return; } this.select(item); this.screen.render(); }); } this.emit('create item'); return item; }; add = (content: any): any => { content = typeof content === 'string' ? content : content.getContent(); const item = this.createItem(content); item.position.top = this.items .map(i => i.height) .reduce((a, b) => a + b, 0); if (!this.screen.autoPadding) { item.position.top = this.itop + this.items.length; } this.ritems.push(content); this.items.push(item); this.append(item); if (this.items.length === 1) { this.select(0); } this.emit('add item'); return item; }; addItem(content: any): any { return this.add(content); } appendItem(content: any): any { return this.add(content); } removeItem(child: any): any { const i = this.getItemIndex(child); if (~i && this.items[i]) { child = this.items.splice(i, 1)[0]; this.ritems.splice(i, 1); this.remove(child); for (let j = i; j < this.items.length; j++) { this.items[j].position.top--; } if (i === this.selected) { this.select(i - 1); } } this.emit('remove item'); return child; } insertItem(child: any, content: any): void { content = typeof content === 'string' ? content : content.getContent(); const i = this.getItemIndex(child); if (!~i) return; if (i >= this.items.length) return this.appendItem(content); const item = this.createItem(content); for (let j = i; j < this.items.length; j++) { this.items[j].position.top++; } item.position.top = i + (!this.screen.autoPadding ? 1 : 0); this.ritems.splice(i, 0, content); this.items.splice(i, 0, item); this.append(item); if (i === this.selected) { this.select(i + 1); } this.emit('insert item'); } getItem(child: any): any { return this.items[this.getItemIndex(child)]; } setItem(child: any, content: any): void { content = typeof content === 'string' ? content : content.getContent(); const i = this.getItemIndex(child); if (!~i) return; this.items[i].setContent(content); this.ritems[i] = content; } clearItems(): void { return this.setItems([]); } setItems(items: string[]): void { const original = this.items.slice(); const selected = this.selected; const sel = this.ritems[this.selected]; let i = 0; items = items.slice(); this.select(0); for (; i < items.length; i++) { if (this.items[i]) { this.items[i].setContent(items[i]); } else { this.add(items[i]); } } for (; i < original.length; i++) { this.remove(original[i]); } this.ritems = items; // Try to find our old item if it still exists. const newSel = items.indexOf(sel); if (~newSel) { this.select(newSel); } else if (items.length === original.length) { this.select(selected); } else { this.select(Math.min(selected, items.length - 1)); } this.emit('set items'); } pushItem(content: any): number { this.appendItem(content); return this.items.length; } popItem(): any { return this.removeItem(this.items.length - 1); } unshiftItem(content: any): number { this.insertItem(0, content); return this.items.length; } shiftItem(): any { return this.removeItem(0); } spliceItem(child: any, n: number, ...items: any[]): any[] { let i = this.getItemIndex(child); if (!~i) return []; const removed: any[] = []; while (n--) { removed.push(this.removeItem(i)); } items.forEach(item => { this.insertItem(i++, item); }); return removed; } find(search: string | RegExp | Function, back?: boolean): number { return this.fuzzyFind(search, back); } fuzzyFind(search: any, back?: boolean): number { const start = this.selected + (back ? -1 : 1); let i: number; if (typeof search === 'number') search += ''; if ( search && typeof search === 'string' && search[0] === '/' && search[search.length - 1] === '/' ) { try { search = new RegExp(search.slice(1, -1)); } catch (e) {} } const test = typeof search === 'string' ? (item: string) => { return !!~item.indexOf(search as string); } : (search as any).test ? (search as any).test.bind(search) : search; if (typeof test !== 'function') { if (this.screen.options.debug) { throw new Error('fuzzyFind(): `test` is not a function.'); } return this.selected; } if (!back) { for (i = start; i < this.ritems.length; i++) { if (test(helpers.cleanTags(this.ritems[i]))) return i; } for (i = 0; i < start; i++) { if (test(helpers.cleanTags(this.ritems[i]))) return i; } } else { for (i = start; i >= 0; i--) { if (test(helpers.cleanTags(this.ritems[i]))) return i; } for (i = this.ritems.length - 1; i > start; i--) { if (test(helpers.cleanTags(this.ritems[i]))) return i; } } return this.selected; } getItemIndex(child: any): number { if (typeof child === 'number') { return child; } else if (typeof child === 'string') { let i = this.ritems.indexOf(child); if (~i) return i; for (i = 0; i < this.ritems.length; i++) { if (helpers.cleanTags(this.ritems[i]) === child) { return i; } } return -1; } else { return this.items.indexOf(child); } } // Use parent ScrollableBox scrollTo implementation scrollTo(offset: number, always?: boolean): any { return super.scrollTo(offset, always); } select = (index: any): void => { if (!this.interactive) { return; } if (!this.items.length) { this.selected = 0; this.value = ''; if (this.scrollTo) this.scrollTo(0); return; } if (typeof index === 'object') { index = this.items.indexOf(index); } if (index < 0) { index = 0; } else if (index >= this.items.length) { index = this.items.length - 1; } if (this.selected === index && this._listInitialized) return; this._listInitialized = true; this.selected = index; this.value = helpers.cleanTags(this.ritems[this.selected]); if (!this.parent) return; if (this.scrollTo && typeof this.scrollTo === 'function') { this.scrollTo(this.selected); } this.items.forEach((item: any) => { if (item.clearPos) { item.clearPos(); } }); this.emit('select item', this.items[this.selected], this.selected); }; move = (offset: number): void => { this.select(this.selected + offset); }; up = (offset?: number): void => { this.move(-(offset || 1)); }; down = (offset?: number): void => { this.move(offset || 1); }; pick(label: any, callback?: Function): void { if (!callback) { callback = label; label = null; } if (!this.interactive) { return callback!(); } const focused = this.screen.focused; if (focused && (focused as any)._done) (focused as any)._done('stop'); this.screen.saveFocus(); this.focus(); this.show(); this.select(0); if (label) this.setLabel(label); this.screen.render(); this.once('action', (el: any, selected: number) => { if (label) this.removeLabel(); this.screen.restoreFocus(); this.hide(); this.screen.render(); if (!el) return callback!(); return callback!(null, helpers.cleanTags(this.ritems[selected])); }); } enterSelected(i?: number): void { if (i != null) this.select(i); this.emit('action', this.items[this.selected], this.selected); this.emit('select', this.items[this.selected], this.selected); } cancelSelected(i?: number): void { if (i != null) this.select(i); this.emit('action'); this.emit('cancel'); } } /** * Factory function for backward compatibility */ function list(options?: any): List { return new List(options); } // Add legacy method aliases to List prototype for backward compatibility List.prototype.remove = function (index: number) { if (typeof index === 'number') { const item = this.items[index]; if (item) { this.removeItem(item); } } return this; }; List.prototype.clear = function () { this.clearItems(); return this; }; // Attach the class as a property for direct access list.List = List; /** * Expose */ export default list;