UNPKG

@teaui/preact

Version:

Preact renderer for TeaUI

370 lines 12.6 kB
import { render as preactRender, options } from 'preact'; import { Accordion as WrAccordion, Box as WrBox, Button as WrButton, Checkbox as WrCheckbox, Collapsible as WrCollapsible, CollapsibleText as WrCollapsibleText, ConsoleLog as WrConsoleLog, Container, Digits as WrDigits, H1, H2, H3, H4, H5, H6, Drawer as WrDrawer, Dropdown as WrDropdown, HotKey as WrHotKey, Keyboard as WrKeyboard, Mouse as WrMouse, Progress as WrProgress, Spinner as WrSpinner, ToggleGroup as WrToggleGroup, Tree as WrTree, Input as WrInput, Screen, Scrollable as WrScrollable, Separator as WrSeparator, Slider as WrSlider, Space as WrSpace, Stack as WrStack, Tabs as WrTabs, Window as WrWindow, } from '@teaui/core'; import { TextContainer, TextLiteral, TextProvider, TextStyle, } from './components/TextReact.js'; const customElements = new Map(); /** * Register a custom element type for the Preact reconciler. * External packages (e.g. @teaui/subprocess) can call this to add new JSX elements. * * @example * registerElement('tui-subprocess', (props) => new SubprocessView(props)) */ export function registerElement(type, factory) { customElements.set(type, factory); } function createView(type, props) { // Strip children/child from props before passing to constructors const { children, child, ...viewProps } = props; const factory = customElements.get(type); if (factory) { return factory(viewProps); } switch (type) { case 'text': return new TextLiteral(String(viewProps.text) ?? ''); case 'br': case 'tui-br': return new TextLiteral('\n'); case 'checkbox': case 'tui-checkbox': return new WrCheckbox(viewProps); case 'collapsible-text': case 'tui-collapsible-text': return new WrCollapsibleText(viewProps); case 'console': case 'tui-console': return new WrConsoleLog(viewProps); case 'digits': case 'tui-digits': return new WrDigits(viewProps); case 'dropdown': case 'tui-dropdown': return new WrDropdown(viewProps); case 'hotkey': case 'tui-hotkey': return new WrHotKey(viewProps); case 'keyboard': case 'tui-keyboard': return new WrKeyboard(viewProps); case 'mouse': case 'tui-mouse': return new WrMouse(viewProps); case 'h1': case 'tui-h1': return H1(viewProps.text ?? ''); case 'h2': case 'tui-h2': return H2(viewProps.text ?? ''); case 'h3': case 'tui-h3': return H3(viewProps.text ?? ''); case 'h4': case 'tui-h4': return H4(viewProps.text ?? ''); case 'h5': case 'tui-h5': return H5(viewProps.text ?? ''); case 'h6': case 'tui-h6': return H6(viewProps.text ?? ''); case 'progress': case 'tui-progress': return new WrProgress(viewProps); case 'spinner': case 'tui-spinner': return new WrSpinner(viewProps); case 'toggle-group': case 'tui-toggle-group': return new WrToggleGroup(viewProps); case 'input': case 'tui-input': return new WrInput(viewProps); case 'literal': case 'tui-literal': return new TextLiteral(viewProps.text ?? ''); case 'separator': case 'tui-separator': return new WrSeparator(viewProps); case 'slider': case 'tui-slider': return new WrSlider(viewProps); case 'space': case 'tui-space': return new WrSpace(viewProps); case 'spinner': case 'tui-spinner': return new WrSpinner(viewProps); case 'tree': case 'tui-tree': return new WrTree(viewProps); case 'box': case 'tui-box': return new WrBox(viewProps); case 'button': case 'tui-button': return new WrButton(viewProps); case 'collapsible': case 'tui-collapsible': return new WrCollapsible(viewProps); case 'scrollable': case 'tui-scrollable': return new WrScrollable(viewProps); case 'stack': case 'tui-stack': return new WrStack(viewProps); case 'style': case 'tui-style': return new TextStyle(viewProps); case 'tui-text': return new TextProvider(viewProps); case 'accordion': case 'tui-accordion': return new WrAccordion(viewProps); case 'accordion-section': case 'tui-accordion-section': return new WrAccordion.Section(viewProps); case 'drawer': case 'tui-drawer': return new WrDrawer(viewProps); case 'tabs': case 'tui-tabs': return new WrTabs(viewProps); case 'tabs-section': case 'tui-tabs-section': return new WrTabs.Section(viewProps); case 'tui-window': return new WrWindow(); default: throw new Error(`Unknown type: ${type}`); } } const defer = typeof Promise == 'function' ? (fn) => Promise.resolve().then(fn) : setTimeout; function removeFromTextContainer(container, child) { for (const viewChild of container.children) { if (viewChild instanceof TextContainer && child.parent === viewChild) { // TextContainer.add() puts TextLiterals/TextStyles into #nodes, // NOT into .children (which holds generated Text views). // Check child.parent instead of searching .children. viewChild.removeChild(child); if (viewChild.children.length === 0) { container.removeChild(viewChild); } return; } } } function removeChild(container, child) { if (child.parent === container) { container.removeChild(child); } else if (child instanceof TextLiteral || child instanceof TextStyle) { removeFromTextContainer(container, child); } } function appendChild(parentInstance, child, before) { if (parentInstance instanceof TextStyle && (child instanceof TextLiteral || child instanceof TextStyle)) { // do not do the TextContainer song and dance } else if (child instanceof TextLiteral || child instanceof TextStyle) { // find the last child (checking 'before') let lastChild = parentInstance.children.at(-1); if (before) { const index = parentInstance.children.indexOf(before); if (~index) { lastChild = parentInstance.children.at(index - 1); } } let textContainer; if (lastChild instanceof TextContainer) { textContainer = lastChild; } else { textContainer = new TextContainer(); parentInstance.add(textContainer); } textContainer.add(child); return; } let index = before ? parentInstance.children.indexOf(before) : -1; if (index === -1) { index = undefined; } parentInstance.add(child, index); } class RendererElement { renderer; localName; parentNode = null; nextSibling = null; previousSibling = null; firstChild = null; lastChild = null; props = {}; prevProps; node; nodeType; constructor(renderer, localName, nodeType = 1) { this.renderer = renderer; this.localName = localName; this.nodeType = nodeType; this._commit = this._commit.bind(this); } set data(text) { this.setAttribute('text', String(text)); } addEventListener(event, func) { this.setAttribute(`on${event}`, func); } setAttribute(name, value) { if (this.node && !this.prevProps) { this.prevProps = Object.assign({}, this.props); defer(this._commit); } this.props[name] = value; } removeAttribute(name) { if (this.node && !this.prevProps) { this.prevProps = Object.assign({}, this.props); defer(this._commit); } delete this.props[name]; } _attach() { return (this.node ||= this.renderer.create(this.localName, this.props)); } _commit() { const state = this.node; const prev = this.prevProps; if (!state || !prev) return; this.prevProps = undefined; this.renderer.update(state, this.props); } insertBefore(child, before) { if (child.parentNode === this) this.removeChild(child); if (before) { const prev = before.previousSibling; child.previousSibling = prev; before.previousSibling = child; if (prev) { prev.nextSibling = child; } if (before == this.firstChild) { this.firstChild = child; } } else { const last = this.lastChild; child.previousSibling = last; this.lastChild = child; if (last) last.nextSibling = child; if (!this.firstChild) this.firstChild = child; } child.parentNode = this; child.nextSibling = before ?? null; this.renderer.insert(this._attach(), child._attach(), before && before._attach()); } appendChild(child) { this.insertBefore(child); } removeChild(child) { if (this.firstChild === child) this.firstChild = child.nextSibling; if (this.lastChild === child) this.lastChild = child.previousSibling; child.parentNode = child.nextSibling = child.previousSibling = null; if (this.node && child.node) { this.renderer.remove(this.node, child.node); } } } function createRendererDom(renderer) { function createElement(type) { return new RendererElement(renderer, type, 1); } function createElementNS(_, type) { return new RendererElement(renderer, type, 1); } function createTextNode(text) { const node = new RendererElement(renderer, 'text', 3); node.props.text = String(text); return node; } function createRoot() { return createElement('tui-window'); } return { createElement, createElementNS, createTextNode, createRoot }; } let _rerender; // Hook into Preact's diffed option to trigger screen re-renders after commits const prevDiffed = options.diffed; options.diffed = vnode => { prevDiffed?.(vnode); _rerender?.(); }; const dom = createRendererDom({ create(type, props) { return createView(type, props); }, insert(parent, node, before) { if (!(parent instanceof Container)) { return; } appendChild(parent, node, before); }, remove(parent, node) { if (!(parent instanceof Container)) { return; } removeChild(parent, node); }, update(node, props) { const { children, child, ...updateProps } = props; if (node instanceof TextLiteral) { node.text = updateProps.text ?? ''; } else { node.update(updateProps); } }, }); // Preact accesses `document` directly (not ownerDocument), so we must // provide a global document shim with our custom DOM factory methods. const fakeDocument = Object.create(null); Object.assign(fakeDocument, dom); globalThis.document = fakeDocument; export function render(screen, window, component) { _rerender = () => { screen.render(); }; const root = dom.createRoot(); root.ownerDocument = fakeDocument; preactRender(component, root); // Reparent created views into the actual window if (root.node instanceof WrWindow) { for (const child of [...root.node.children]) { root.node.removeChild(child); window.add(child); } } return function unmount() { preactRender(null, root); _rerender = undefined; }; } export async function run(component, options) { // Start the screen first, then render (matching React package behavior) const window = new WrWindow(); const [screen, _] = await Screen.start(window, options); const unmount = render(screen, window, component); return [screen, window, component, unmount]; } //# sourceMappingURL=preact.js.map