render.ts
104 lines
| 3.1 KiB
| video/mp2t
|
TypeScriptLexer
|
|
r101 | import { TraceSource } from "@implab/core-amd/log/TraceSource"; | ||
| import { isPromise } from "@implab/core-amd/safe"; | ||||
| import { id as mid } from "module"; | ||||
| import { Scope } from "./Scope"; | ||||
| import { autostartWidgets, collectNodes, DojoNodePosition, isDocumentFragmentNode, isNode, isRendition, isWidget, placeAt } from "./traits"; | ||||
| const trace = TraceSource.get(mid); | ||||
|
|
r98 | |||
| let _scope = Scope.dummy; | ||||
|
|
r101 | let renderCount = 0; | ||
| const hooks: (() => void)[] = []; | ||||
| const guard = (cb: () => unknown) => { | ||||
| try { | ||||
| const result = cb() | ||||
| if (isPromise(result)) { | ||||
| const warn = (ret: unknown) => trace.error("The callback {0} competed asynchronously. result = {1}", cb, ret); | ||||
| result.then(warn, warn); | ||||
| } | ||||
| } catch (e) { | ||||
| trace.error(e); | ||||
| } | ||||
|
|
r98 | } | ||
|
|
r101 | /** | ||
| * Schedules rendering micro task | ||||
| * @returns Promise | ||||
| */ | ||||
| const beginRender = () => { | ||||
| renderCount++; | ||||
| return Promise.resolve(); | ||||
| } | ||||
| /** | ||||
| * Completes render operation | ||||
| */ | ||||
|
|
r98 | const endRender = () => { | ||
|
|
r101 | if (!--renderCount) { | ||
| hooks.forEach(guard); | ||||
| hooks.length = 0; | ||||
| } | ||||
| } | ||||
| export const renderHook = (hook: () => void) => { | ||||
| if (renderCount) | ||||
| hooks.push(hook); | ||||
| else | ||||
| guard(hook); | ||||
|
|
r98 | } | ||
| /** Returns the current scope */ | ||||
| export const getScope = () => _scope; | ||||
| /** Schedules the rendition to be rendered to the DOM Node | ||||
| * @param rendition The rendition to be rendered | ||||
| * @param scope The scope | ||||
| */ | ||||
|
|
r101 | export const render = async (rendition: unknown, refNode: Node, position: DojoNodePosition = "last", scope = Scope.dummy) => { | ||
|
|
r98 | await beginRender(); | ||
| const prev = _scope; | ||||
| _scope = scope; | ||||
| try { | ||||
|
|
r101 | const domNode = getItemDom(rendition); | ||
| const startupPending = isDocumentFragmentNode(domNode) ? collectNodes(domNode.children) : [domNode]; | ||||
| placeAt(domNode, refNode, position); | ||||
| startupPending.forEach(autostartWidgets); | ||||
| return startupPending; | ||||
|
|
r98 | } finally { | ||
| _scope = prev; | ||||
| endRender(); | ||||
| } | ||||
| } | ||||
| /** Renders DOM element for different types of the argument. */ | ||||
|
|
r101 | export const getItemDom = (v: unknown) => { | ||
|
|
r98 | if (typeof v === "string" || typeof v === "number" || v instanceof RegExp || v instanceof Date) { | ||
| // primitive types converted to the text nodes | ||||
| return document.createTextNode(v.toString()); | ||||
| } else if (isNode(v)) { | ||||
| // nodes are kept as is | ||||
| return v; | ||||
| } else if (isRendition(v)) { | ||||
| // renditions are instantiated | ||||
|
|
r101 | return v.getDomNode(); | ||
|
|
r98 | } else if (isWidget(v)) { | ||
| // widgets are converted to it's markup | ||||
| return v.domNode; | ||||
| } else if (typeof v === "boolean" || v === null || v === undefined) { | ||||
| // null | undefined | boolean are removed, converted to comments | ||||
| return document.createComment(`[${typeof v} ${String(v)}]`); | ||||
| } else if (v instanceof Array) { | ||||
| // arrays will be translated to document fragments | ||||
| const fragment = document.createDocumentFragment(); | ||||
|
|
r101 | v.map(item => getItemDom(item)) | ||
|
|
r98 | .forEach(node => fragment.appendChild(node)); | ||
| return fragment; | ||||
| } else { | ||||
| // bug: explicit error otherwise | ||||
| throw new Error("Invalid parameter: " + v); | ||||
| } | ||||
| } | ||||
