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); let _scope = Scope.dummy; 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); } } /** * Schedules rendering micro task * @returns Promise */ const beginRender = () => { renderCount++; return Promise.resolve(); } /** * Completes render operation */ const endRender = () => { if (!--renderCount) { hooks.forEach(guard); hooks.length = 0; } } export const renderHook = (hook: () => void) => { if (renderCount) hooks.push(hook); else guard(hook); } /** 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 */ export const render = async (rendition: unknown, refNode: Node, position: DojoNodePosition = "last", scope = Scope.dummy) => { await beginRender(); const prev = _scope; _scope = scope; try { const domNode = getItemDom(rendition); const startupPending = isDocumentFragmentNode(domNode) ? collectNodes(domNode.children) : [domNode]; placeAt(domNode, refNode, position); startupPending.forEach(autostartWidgets); return startupPending; } finally { _scope = prev; endRender(); } } /** Renders DOM element for different types of the argument. */ export const getItemDom = (v: unknown) => { 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 return v.getDomNode(); } 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(); v.map(item => getItemDom(item)) .forEach(node => fragment.appendChild(node)); return fragment; } else { // bug: explicit error otherwise throw new Error("Invalid parameter: " + v); } }