neo-neo-bblessed
Version:
A fork of neo-blessed (which is a fork of blessed) with bug fixes and maintenance.
361 lines (313 loc) • 8.58 kB
text/typescript
/**
* form.js - form element for blessed
* Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
* https://github.com/chjj/blessed
*/
/**
* Modules
*/
import Node from './node.js';
import boxFactory from './box.js';
const Box = boxFactory.Box;
/**
* Type definitions
*/
interface FormOptions {
keys?: boolean;
autoNext?: boolean;
vi?: boolean;
ignoreKeys?: boolean;
[key: string]: any;
}
interface FormKey {
name: string;
shift?: boolean;
}
interface FormElement {
type: string;
name?: string;
value?: any;
keyable?: boolean;
visible: boolean;
children: FormElement[];
emit(event: string, ...args: any[]): boolean;
focus(): void;
select?: (index: number) => void;
clearInput?: () => void;
setProgress?: (progress: number) => void;
refresh?: (cwd: string) => void;
uncheck?: () => void;
write?: (data: string) => void;
options?: { cwd?: string };
}
interface FormScreen {
focused: FormElement | null;
_listenKeys(form: FormInterface): void;
}
interface FormSubmission {
[key: string]: any;
}
interface FormInterface extends Box {
type: string;
screen: FormScreen;
children: FormElement[];
_children?: FormElement[];
_selected?: FormElement | null;
submission?: FormSubmission;
on(event: string, listener: (...args: any[]) => void): void;
emit(event: string, ...args: any[]): boolean;
focus(): void;
_refresh(): void;
_visible(): boolean;
next(): FormElement | undefined;
previous(): FormElement | undefined;
focusNext(): void;
focusPrevious(): void;
resetSelected(): void;
focusFirst(): void;
focusLast(): void;
submit(): FormSubmission;
cancel(): void;
reset(): void;
}
/**
* Form - Modern ES6 Class
*/
class Form extends Box {
type = 'form';
_children?: FormElement[];
_selected?: FormElement | null = null;
submission?: FormSubmission;
constructor(options?: FormOptions) {
// Handle malformed options gracefully
if (!options || typeof options !== 'object' || Array.isArray(options)) {
options = {};
}
// Force ignoreKeys to true for forms
options.ignoreKeys = true;
// Disable ScrollableBox mixin to avoid circular dependency issues
options.scrollable = false;
super(options);
// Set up key handling if requested
if (options.keys && this.screen) {
this.screen._listenKeys(this);
this.on(
'element keypress',
(el: FormElement, ch: string, key: FormKey) => {
if (
(key.name === 'tab' && !key.shift) ||
(el.type === 'textbox' &&
options!.autoNext &&
key.name === 'enter') ||
key.name === 'down' ||
(options!.vi && key.name === 'j')
) {
if (el.type === 'textbox' || el.type === 'textarea') {
if (key.name === 'j') return;
if (key.name === 'tab') {
// Workaround, since we can't stop the tab from being added.
el.emit('keypress', null, { name: 'backspace' });
}
el.emit('keypress', '\x1b', { name: 'escape' });
}
this.focusNext();
return;
}
if (
(key.name === 'tab' && key.shift) ||
key.name === 'up' ||
(options!.vi && key.name === 'k')
) {
if (el.type === 'textbox' || el.type === 'textarea') {
if (key.name === 'k') return;
el.emit('keypress', '\x1b', { name: 'escape' });
}
this.focusPrevious();
return;
}
if (key.name === 'escape') {
this.focus();
return;
}
}
);
}
}
_refresh(): void {
// XXX Possibly remove this if statement and refresh on every focus.
// Also potentially only include *visible* focusable elements.
// This would remove the need to check for _selected.visible in previous()
// and next().
if (!this._children) {
const out: FormElement[] = [];
this.children.forEach(function fn(el: FormElement) {
if (el.keyable) out.push(el);
el.children.forEach(fn);
});
this._children = out;
}
}
_visible(): boolean {
return !!this._children!.filter(function (el: FormElement) {
return el.visible;
}).length;
}
next(): FormElement | undefined {
this._refresh();
if (!this._visible()) return;
if (!this._selected) {
this._selected = this._children![0];
if (!this._selected.visible) return this.next();
if (this.screen.focused !== this._selected) return this._selected;
}
const i = this._children!.indexOf(this._selected);
if (!~i || !this._children![i + 1]) {
this._selected = this._children![0];
if (!this._selected.visible) return this.next();
return this._selected;
}
this._selected = this._children![i + 1];
if (!this._selected.visible) return this.next();
return this._selected;
}
previous(): FormElement | undefined {
this._refresh();
if (!this._visible()) return;
if (!this._selected) {
this._selected = this._children![this._children!.length - 1];
if (!this._selected.visible) return this.previous();
if (this.screen.focused !== this._selected) return this._selected;
}
const i = this._children!.indexOf(this._selected);
if (!~i || !this._children![i - 1]) {
this._selected = this._children![this._children!.length - 1];
if (!this._selected.visible) return this.previous();
return this._selected;
}
this._selected = this._children![i - 1];
if (!this._selected.visible) return this.previous();
return this._selected;
}
focusNext(): void {
const next = this.next();
if (next) next.focus();
}
focusPrevious(): void {
const previous = this.previous();
if (previous) previous.focus();
}
resetSelected(): void {
this._selected = null;
}
focusFirst(): void {
this.resetSelected();
this.focusNext();
}
focusLast(): void {
this.resetSelected();
this.focusPrevious();
}
submit(): FormSubmission {
const out: FormSubmission = {};
this.children.forEach(function fn(el: FormElement) {
if (el.value != null) {
const name = el.name || el.type;
if (Array.isArray(out[name])) {
out[name].push(el.value);
} else if (out[name]) {
out[name] = [out[name], el.value];
} else {
out[name] = el.value;
}
}
el.children.forEach(fn);
});
this.emit('submit', out);
return (this.submission = out);
}
cancel(): void {
this.emit('cancel');
}
reset(): void {
this.children.forEach(function fn(el: FormElement) {
switch (el.type) {
case 'screen':
break;
case 'box':
break;
case 'text':
break;
case 'line':
break;
case 'scrollable-box':
break;
case 'list':
el.select!(0);
return;
case 'form':
break;
case 'input':
break;
case 'textbox':
el.clearInput!();
return;
case 'textarea':
el.clearInput!();
return;
case 'button':
delete el.value;
break;
case 'progress-bar':
el.setProgress!(0);
break;
case 'file-manager':
el.refresh!(el.options!.cwd!);
return;
case 'checkbox':
el.uncheck!();
return;
case 'radio-set':
break;
case 'radio-button':
el.uncheck!();
return;
case 'prompt':
break;
case 'question':
break;
case 'message':
break;
case 'info':
break;
case 'loading':
break;
case 'list-bar':
//el.select(0);
break;
case 'dir-manager':
el.refresh!(el.options!.cwd!);
return;
case 'terminal':
el.write!('');
return;
case 'image':
//el.clearImage();
return;
}
el.children.forEach(fn);
});
this.emit('reset');
}
}
/**
* Factory function for backward compatibility
*/
function form(options?: FormOptions): FormInterface {
return new Form(options) as FormInterface;
}
// Attach the class as a property for direct access
form.Form = Form;
/**
* Expose
*/
export default form;