render.ts
197 lines
| 5.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"; | ||||
|
|
r102 | import { IScope, Scope } from "./Scope"; | ||
| import { isNode, isRendition, isWidget } from "./traits"; | ||||
|
|
r101 | |||
| const trace = TraceSource.get(mid); | ||||
|
|
r98 | |||
|
|
r102 | interface Context { | ||
|
|
r118 | readonly scope: IScope; | ||
|
|
r98 | |||
|
|
r118 | readonly hooks?: (() => void)[]; | ||
|
|
r102 | } | ||
|
|
r101 | |||
|
|
r102 | let _context: Context = { | ||
| scope: Scope.dummy | ||||
|
|
r109 | }; | ||
|
|
r101 | |||
|
|
r118 | let _renderCount = 0; | ||
| let _renderId = 1; | ||||
| let _renderedHooks: (() => void)[] = []; | ||||
|
|
r101 | const guard = (cb: () => unknown) => { | ||
| try { | ||||
|
|
r109 | const result = cb(); | ||
|
|
r101 | 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); | ||||
| } | ||||
|
|
r109 | }; | ||
|
|
r98 | |||
|
|
r118 | /** | ||
| * | ||||
| * @param scope | ||||
| * @returns | ||||
| */ | ||||
| export const beginRender = (scope = getScope()) => { | ||||
|
|
r102 | const prev = _context; | ||
|
|
r118 | _renderCount++; | ||
| const renderId = _renderId++; | ||||
| trace.debug("beginRender [{0}], pending = {1}", renderId, _renderCount); | ||||
| if (_renderCount === 1) | ||||
| onRendering(); | ||||
|
|
r102 | _context = { | ||
| scope, | ||||
| hooks: [] | ||||
| }; | ||||
|
|
r118 | return endRender(prev, _context, renderId); | ||
| }; | ||||
| /** | ||||
| * Method for a deferred rendering. Returns a promise with `beginRender()` function. | ||||
| * Call to `scheduleRender` will save the current context, and will increment pending | ||||
| * operations counter. | ||||
| * | ||||
| * @example | ||||
| * | ||||
| * const begin = await scheduleRender(); | ||||
| * const end = begin(); | ||||
| * try { | ||||
| * // do some DOM manipulations | ||||
| * } finally { | ||||
| * end(); | ||||
| * } | ||||
| * | ||||
| * @param scope | ||||
| * @returns | ||||
| */ | ||||
| export const scheduleRender = async (scope = getScope()) => { | ||||
| const prev = _context; | ||||
| _renderCount++; | ||||
| const renderId = _renderId ++; | ||||
| trace.debug("scheduleRender [{0}], pending = {1}", renderId, _renderCount); | ||||
| if (_renderCount === 1) | ||||
| onRendering(); | ||||
| await Promise.resolve(); | ||||
| return () => { | ||||
| trace.debug("beginRender [{0}], pending = {1}", renderId, _renderCount); | ||||
| _context = { | ||||
| scope, | ||||
| hooks: [] | ||||
| }; | ||||
| return endRender(prev, _context, renderId); | ||||
| }; | ||||
|
|
r109 | }; | ||
|
|
r101 | |||
| /** | ||||
| * Completes render operation | ||||
| */ | ||||
|
|
r118 | const endRender = (prev: Context, current: Context, renderId: number) => () => { | ||
| if (_context !== current) | ||||
| trace.error("endRender mismatched beginRender call"); | ||||
|
|
r102 | const { hooks } = _context; | ||
| if (hooks) | ||||
|
|
r101 | hooks.forEach(guard); | ||
|
|
r102 | |||
|
|
r118 | _renderCount--; | ||
|
|
r102 | _context = prev; | ||
|
|
r118 | |||
| trace.debug("endRender [{0}], pending = {1}", renderId, _renderCount); | ||||
| if (_renderCount === 0) | ||||
| onRendered(); | ||||
|
|
r109 | }; | ||
|
|
r101 | |||
|
|
r118 | // called when the first beginRender is called for this iteration | ||
| const onRendering = () => { | ||||
| setTimeout(() => { | ||||
| if (_renderCount !== 0) | ||||
| trace.error("Rendering tasks aren't finished, currently running = {0}", _renderCount); | ||||
| }); | ||||
| }; | ||||
| // called when all render operations are complete | ||||
| const onRendered = () => { | ||||
| _renderedHooks.forEach(guard); | ||||
| _renderedHooks = []; | ||||
| }; | ||||
| export const whenRendered = () => new Promise<void>((resolve) => { | ||||
| if (_renderCount) | ||||
| _renderedHooks.push(resolve); | ||||
| else | ||||
| resolve(); | ||||
| }); | ||||
|
|
r101 | export const renderHook = (hook: () => void) => { | ||
|
|
r102 | const { hooks } = _context; | ||
| if (hooks) | ||||
|
|
r101 | hooks.push(hook); | ||
| else | ||||
| guard(hook); | ||||
|
|
r109 | }; | ||
|
|
r98 | |||
|
|
r102 | export const refHook = <T>(value: T, ref: JSX.Ref<T>) => { | ||
| const { hooks, scope } = _context; | ||||
| if (hooks) | ||||
| hooks.push(() => ref(value)); | ||||
| else | ||||
| guard(() => ref(value)); | ||||
| scope.own(() => ref(undefined)); | ||||
|
|
r109 | }; | ||
|
|
r102 | |||
|
|
r98 | /** Returns the current scope */ | ||
|
|
r102 | export const getScope = () => _context.scope; | ||
|
|
r98 | |||
| /** Schedules the rendition to be rendered to the DOM Node | ||||
| * @param rendition The rendition to be rendered | ||||
| * @param scope The scope | ||||
| */ | ||||
|
|
r102 | export const render = (rendition: unknown, scope = Scope.dummy) => { | ||
| const complete = beginRender(scope); | ||||
|
|
r98 | try { | ||
|
|
r102 | return getItemDom(rendition); | ||
|
|
r98 | } finally { | ||
|
|
r102 | complete(); | ||
|
|
r98 | } | ||
|
|
r109 | }; | ||
|
|
r98 | |||
|
|
r120 | const emptyFragment = document.createDocumentFragment(); | ||
|
|
r98 | /** 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) { | ||||
|
|
r102 | // null | undefined | boolean are removed | ||
|
|
r120 | return emptyFragment; | ||
|
|
r98 | } 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 | ||||
|
|
r109 | throw new Error(`Invalid parameter: ${String(v)}`); | ||
|
|
r98 | } | ||
|
|
r109 | }; | ||
