|
|
import { TraceSource } from "@implab/core-amd/log/TraceSource";
|
|
|
import { isPromise } from "@implab/core-amd/safe";
|
|
|
import { id as mid } from "module";
|
|
|
import { IScope, Scope } from "./Scope";
|
|
|
import { isNode, isRendition, isWidget } from "./traits";
|
|
|
|
|
|
const trace = TraceSource.get(mid);
|
|
|
|
|
|
interface Context {
|
|
|
readonly scope: IScope;
|
|
|
|
|
|
readonly hooks?: (() => void)[];
|
|
|
}
|
|
|
|
|
|
let _context: Context = {
|
|
|
scope: Scope.dummy
|
|
|
};
|
|
|
|
|
|
let _renderCount = 0;
|
|
|
let _renderId = 1;
|
|
|
let _renderedHooks: (() => 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);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
*
|
|
|
* @param scope
|
|
|
* @returns
|
|
|
*/
|
|
|
export const beginRender = (scope = getScope()) => {
|
|
|
const prev = _context;
|
|
|
_renderCount++;
|
|
|
const renderId = _renderId++;
|
|
|
trace.debug("beginRender [{0}], pending = {1}", renderId, _renderCount);
|
|
|
if (_renderCount === 1)
|
|
|
onRendering();
|
|
|
|
|
|
_context = {
|
|
|
scope,
|
|
|
hooks: []
|
|
|
};
|
|
|
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()) => {
|
|
|
_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);
|
|
|
const prev = _context;
|
|
|
|
|
|
_context = {
|
|
|
scope,
|
|
|
hooks: []
|
|
|
};
|
|
|
return endRender(prev, _context, renderId);
|
|
|
};
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Completes render operation
|
|
|
*/
|
|
|
const endRender = (prev: Context, current: Context, renderId: number) => () => {
|
|
|
if (_context !== current)
|
|
|
trace.error("endRender mismatched beginRender call");
|
|
|
|
|
|
const { hooks } = _context;
|
|
|
if (hooks)
|
|
|
hooks.forEach(guard);
|
|
|
|
|
|
_renderCount--;
|
|
|
_context = prev;
|
|
|
|
|
|
trace.debug("endRender [{0}], pending = {1}", renderId, _renderCount);
|
|
|
if (_renderCount === 0)
|
|
|
onRendered();
|
|
|
};
|
|
|
|
|
|
// 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();
|
|
|
});
|
|
|
|
|
|
export const renderHook = (hook: () => void) => {
|
|
|
const { hooks } = _context;
|
|
|
if (hooks)
|
|
|
hooks.push(hook);
|
|
|
else
|
|
|
guard(hook);
|
|
|
};
|
|
|
|
|
|
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));
|
|
|
};
|
|
|
|
|
|
/** Returns the current scope */
|
|
|
export const getScope = () => _context.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 = (rendition: unknown, scope = Scope.dummy) => {
|
|
|
const complete = beginRender(scope);
|
|
|
try {
|
|
|
return getItemDom(rendition);
|
|
|
} finally {
|
|
|
complete();
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const emptyFragment = document.createDocumentFragment();
|
|
|
|
|
|
/** 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
|
|
|
return emptyFragment;
|
|
|
} 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: ${String(v)}`);
|
|
|
}
|
|
|
};
|
|
|
|