asciitorium
Version:
an ASCII ui framework for web + cli
156 lines (155 loc) • 5.41 kB
JavaScript
import { LayoutRegistry } from './layouts/LayoutStrategy';
import './layouts'; // Register layout strategies
export class Component {
constructor(props) {
this.showLabel = true;
this.fixed = false;
this.x = 0;
this.y = 0;
this.z = 0;
this.gap = 0;
this.focusable = false;
this.hasFocus = false;
this.transparentChar = '‽'; // ‽ = transparent character
this.unbindFns = [];
// Children support
this.children = [];
// Default dimensions if not provided
this.width = props.width ?? 1;
this.height = props.height ?? 1;
if (this.width < 1)
throw new Error('Component width must be > 0');
if (this.height < 1)
throw new Error('Component height must be > 0');
this.label = props.label;
this.comment = props.comment;
this.showLabel = props.showLabel ?? true;
this.border = props.border ?? false;
this.fill = props.fill ?? ' ';
this.align = props.align;
this.fixed = props.fixed ?? false;
this.x = props.x ?? 0;
this.y = props.y ?? 0;
this.z = props.z ?? 0;
this.gap = props.gap ?? 0;
this.buffer = [];
// Setup children and layout
this.layoutType = props.layout ?? 'horizontal'; // Default to horizontal layout
this.layoutOptions = props.layoutOptions;
// Store children for later addition (to avoid calling addChild during construction)
if (props.children) {
const childList = Array.isArray(props.children) ? props.children : [props.children];
for (const child of childList) {
child.setParent(this);
this.children.push(child);
}
this.recalculateLayout();
}
}
setParent(parent) {
this.parent = parent;
}
// Child management methods
addChild(child) {
child.setParent(this);
this.children.push(child);
this.recalculateLayout();
}
removeChild(child) {
const index = this.children.indexOf(child);
if (index !== -1) {
this.children.splice(index, 1);
this.recalculateLayout();
}
}
getChildren() {
return this.children;
}
getAllDescendants() {
const result = [];
for (const child of this.children) {
result.push(child);
const grandChildren = child.getAllDescendants();
result.push(...grandChildren);
}
return result;
}
invalidateLayout() {
this.layoutStrategy = undefined;
}
recalculateLayout() {
if (this.children.length === 0)
return;
if (!this.layoutStrategy) {
this.layoutStrategy = LayoutRegistry.create(this.layoutType, this.layoutOptions);
}
this.layoutStrategy.layout(this, this.children);
}
bind(state, apply) {
const listener = (val) => {
apply(val);
};
state.subscribe(listener);
this.unbindFns.push(() => state.unsubscribe(listener));
}
destroy() {
for (const unbind of this.unbindFns)
unbind();
this.unbindFns = [];
}
handleEvent(event) {
return false;
}
draw() {
// Recalculate layout for children
this.recalculateLayout();
// Create buffer and fill only if not transparent
this.buffer = Array.from({ length: this.height }, () => Array.from({ length: this.width }, () => this.fill === this.transparentChar ? '‽' : this.fill));
const drawChar = (x, y, char) => {
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
this.buffer[y][x] = char;
}
};
if (this.border) {
const w = this.width;
const h = this.height;
drawChar(0, 0, '╭');
drawChar(w - 1, 0, '╮');
drawChar(0, h - 1, '╰');
drawChar(w - 1, h - 1, '╯');
for (let x = 1; x < w - 1; x++) {
drawChar(x, 0, '─');
drawChar(x, h - 1, '─');
}
for (let y = 1; y < h - 1; y++) {
drawChar(0, y, '│');
drawChar(w - 1, y, '│');
}
}
if (this.label && this.showLabel) {
const label = ` ${this.label} `;
const start = 1;
for (let i = 0; i < label.length && i + start < this.width - 1; i++) {
drawChar(i + start, 0, label[i]);
}
}
// Render children sorted by z-index
const sorted = [...this.children].sort((a, b) => a.z - b.z);
for (const child of sorted) {
const buf = child.draw();
for (let j = 0; j < buf.length; j++) {
for (let i = 0; i < buf[j].length; i++) {
const px = child.x + i;
const py = child.y + j;
if (px >= 0 && px < this.width && py >= 0 && py < this.height) {
const char = buf[j][i];
if (char !== child.transparentChar) {
this.buffer[py][px] = char;
}
}
}
}
}
return this.buffer;
}
}