UNPKG

@frontmeans/drek

Version:
877 lines (854 loc) 28.7 kB
import { newNamespaceAliaser, css__naming, NamespaceAliaser, NamespaceDef } from '@frontmeans/namespace-aliaser'; import { Supply } from '@proc7ts/supply'; import { isDocumentNode, nodeHost, isElementNode, nodeDocument, removeNodeContent } from '@frontmeans/dom-primitives'; import { onceOn, onEventBy, AfterEvent__symbol, afterThe, trackValue, EventEmitter, translateAfter_ } from '@proc7ts/fun-events'; import { valueProvider } from '@proc7ts/primitives'; import { newRenderSchedule, RenderScheduler, queuedRenderScheduler } from '@frontmeans/render-scheduler'; import { cxDefaultScoped, CxGlobals, cxSingle } from '@proc7ts/context-values'; let DrekContext$registrar = DrekContext$autoRegister; /** * @internal */ function DrekContext$register(context) { DrekContext$registrar(context); } /** * @internal */ function DrekContext$setRegistrar(registrar) { const priorRegistrar = DrekContext$registrar; DrekContext$registrar = registrar; return priorRegistrar === DrekContext$autoRegister ? () => { DrekContext$registrar = priorRegistrar; return DrekContext$dontRegister; } : () => (DrekContext$registrar = priorRegistrar); } function DrekContext$dontRegister(_context) { // Do not auto-register the context already failed to lift. } let DrekContext$autoRegistrar = DrekContext$autoRegisterFirst; function DrekContext$autoRegister(context) { DrekContext$autoRegistrar(context); } function DrekContext$autoRegisterFirst(context) { const registered = [context]; DrekContext$autoRegistrar = DrekContext$createAutoRegistrar(registered); Promise.resolve() .then(() => { DrekContext$autoRegistrar = DrekContext$autoRegisterFirst; for (const context of registered) { context.lift(); } }) .catch(console.error); } function DrekContext$createAutoRegistrar(registered) { return context => registered.push(context); } /** * Executes a DOM builder function and then {@link DrekContext.lift lifts} all unrooted rendering contexts created by * it. * * This helps to track a {@link DrekContext.whenConnected document connection} or {@link DrekContext.whenSettled * settlement} of any unrooted rendering contexts that created before its node added to document or * {@link DrekFragment rendered fragment}. This may happen e.g. when the rendering context {@link drekContextOf * accessed} from inside a custom element constructor when calling `document.createElement('custom-element')`. * * @typeParam TResult - DOM builder result type. * @param builder - A DOM builder function to call. * * @returns The value returned from DOM `builder` function. */ function drekBuild(builder) { const registered = []; const resetRegistrar = DrekContext$setRegistrar(context => registered.push(context)); try { return builder(); } finally { const registrar = resetRegistrar(); for (const context of registered) { const lifted = context.lift(); if (lifted === context) { // Not lifted. // Try next time. registrar(context); } } } } /** * @internal */ const DrekPlacement$Status__symbol = /*#__PURE__*/ Symbol('DrekPlacement.status'); /** * @internal */ class DrekPlacement$Status { constructor(placement) { this.placement = placement; } onceConnected() { return (this.onceConnected = valueProvider(this.placement.readStatus.do(DrekPlacement$once(({ connected }) => connected))))(); } whenConnected() { return (this.whenConnected = valueProvider(this.onceConnected().do(onceOn)))(); } } function DrekPlacement$once(test) { return input => onEventBy(receiver => { let value = false; input({ supply: receiver.supply, receive(eventCtx, ...status) { const newValue = test(...status); if (newValue || value !== newValue) { value = newValue; receiver.receive(eventCtx, ...status); } }, }); }); } /** * A rendered content placement. * * @typeParam TStatus - A type of the tuple containing a rendered content status as its first element. */ class DrekPlacement { constructor() { this[DrekPlacement$Status__symbol] = new DrekPlacement$Status(this); } /** * An alias of {@link readStatus}. * * @returns An `AfterEvent` keeper of content placement status. */ [AfterEvent__symbol]() { return this.readStatus; } /** * An `OnEvent` sender of placed content connection event. * * The registered receiver is called when placed content is {@link DrekContentStatus.connected connected}. * If connected already the receiver is called immediately. */ get onceConnected() { return this[DrekPlacement$Status__symbol].onceConnected(); } /** * An `OnEvent` sender of single placed content connection event. * * The registered receiver is called when placed content is {@link DrekContentStatus.connected connected}. * If connected already the receiver is called immediately. * * In contrast to {@link onceConnected}, cuts off the event supply after sending the first event. */ get whenConnected() { return this[DrekPlacement$Status__symbol].whenConnected(); } } /** * Document rendering context. * * Can be obtained by {@link drekContextOf} function, or {@link DrekFragment#innerContext provided} by rendered * fragment. * * There are three kinds of rendering contexts: * * 1. Document rendering context. * * Such context is always available in document and returned by {@link drekContextOf} function for any DOM node * connected to the document. * * 2. Fragment content rendering context. * * It is created for each rendered fragment and is available via {@link DrekFragment#innerContext} property. * The {@link drekContextOf} function returns this context for fragment's {@link DrekFragment#content content}, * as well as for each DOM node added to it. * * 3. Unrooted rendering context. * * When a DOM node is neither connected to a document, nor part of a rendered fragment's * {@link DrekFragment#content content}, the {@link drekContextOf} function creates an unrooted context for the * [root node] of that node. * * Unrooted context tracks a {@link DrekPlacement#whenConnected document connection} and * {@link DrekContext#whenSettled settlement} semi-automatically. A {@link DrekContext#lift} method can be used * to forcibly update them. * * Semi-automatic tracking means that each time an unrooted context {@link drekContextOf created}, it is registered * for automatic lifting. The lifting happens either asynchronously, or synchronously right before the * {@link drekBuild} function exit. * * Alternatively, a {@link drekLift} function can be used to lift a context of the [root node] after adding it to * another one. * * [root node]: https://developer.mozilla.org/en-US/docs/Web/API/Node/getRootNode * * @typeParam TStatus - A type of the tuple containing a context content status as its first element. */ class DrekContext extends DrekPlacement { /** * An `OnEvent` sender of a settlement event. * * Such event can be sent by {@link DrekFragment.settle rendered fragment}. * * The same as {@link whenConnected} by default. * * Cuts off the event supply after sending the first event. */ get whenSettled() { return this.whenConnected; } } /** * @internal */ const DrekContext__symbol = /*#__PURE__*/ Symbol('DrekContext'); /** * @internal */ class DrekContext$State { constructor({ nsAlias, scheduler }) { this._nsAlias = nsAlias; this.nsAlias = ns => this._nsAlias(ns); this._scheduler = scheduler; this.scheduler = options => { let scheduler = this._scheduler; let schedule = scheduler(options); return shot => { if (scheduler !== this._scheduler) { scheduler = this._scheduler; schedule = scheduler(options); } return schedule(shot); }; }; } set({ nsAlias, scheduler }) { this._nsAlias = nsAlias; this._scheduler = scheduler; } } /** * @internal */ function DrekContext$ofDocument(document) { const existing = document[DrekContext__symbol]; if (existing) { return existing; } const state = new DrekContext$State({ nsAlias: newNamespaceAliaser(), scheduler: newRenderSchedule, }); const view = document.defaultView || window; const scheduler = (options) => state.scheduler({ window: view, ...options, }); const readStatus = afterThe({ connected: true }); class DrekContext$OfDocument extends DrekContext { get fragment() { return; } get window() { return view; } get document() { return document; } get nsAlias() { return state.nsAlias; } get scheduler() { return scheduler; } get readStatus() { return readStatus; } lift() { return this; } update({ nsAlias = state._nsAlias, scheduler = state._scheduler }) { state.set({ nsAlias, scheduler }); return this; } } return (document[DrekContext__symbol] = new DrekContext$OfDocument()); } /** * @internal */ function DrekContext$ofRootNode(root) { return isDocumentNode(root) ? DrekContext$ofDocument(root) : DrekContext$unrooted(root); } function DrekContext$unrooted(root) { const existing = root[DrekContext__symbol]; if (existing) { return existing.lift(); } const status = trackValue({ connected: false }); const settled = new EventEmitter(); let derivedCtx = DrekContext$ofDocument(root.ownerDocument /* Not a document, so `ownerDocument` is set */); const scheduler = new DrekContext$State(derivedCtx); let getFragment = () => derivedCtx.fragment; let lift = (ctx) => { const newRoot = root.getRootNode({ composed: true }); if (newRoot === root) { return ctx; } const lifted = DrekContext$ofRootNode(newRoot); root[DrekContext__symbol] = undefined; getFragment = () => lifted.fragment; scheduler.set(lifted); lifted.whenSettled(status => settled.send(status)).cuts(settled); status.by(lifted); lift = _ctx => lifted; derivedCtx = lifted; return lifted; }; class DrekContext$Unrooted extends DrekContext { get fragment() { return getFragment(); } get window() { return derivedCtx.window; } get document() { return derivedCtx.document; } get nsAlias() { return derivedCtx.nsAlias; } get scheduler() { return scheduler.scheduler; } get readStatus() { return status.read; } get whenSettled() { return settled.on; } lift() { return lift(this); } } const context = (root[DrekContext__symbol] = new DrekContext$Unrooted()); DrekContext$register(context); return context; } function drekContextOf(node) { for (;;) { const root = node.getRootNode({ composed: true }); if (root === node) { return DrekContext$ofRootNode(node); } node = root; } } const DrekCssClasses__symbol = /*#__PURE__*/ Symbol('DrekCssClasses'); function drekCssClassesOf(element) { return (element[DrekCssClasses__symbol] || (element[DrekCssClasses__symbol] = new DrekCssClasses$(element))); } class DrekCssClasses$ { constructor(_element) { this._element = _element; this._uses = new Map(); this._context = drekContextOf(_element); } add(className, user) { return this._add(this._context, className, user); } _add({ nsAlias, scheduler }, className, user) { const supply = user ? user.supply : new Supply(); if (supply.isOff) { return supply; } const name = css__naming.name(className, nsAlias); const schedule = scheduler({ node: this._element }); const use = this._use(name); const render = () => { if (use.n) { if (!use.s) { this._element.classList.add(name); use.s = 1; } } else { if (use.s && !use.i) { // Do not remove the class if it present initially. this._element.classList.remove(name); use.s = 0; } this._uses.delete(name); } }; if (use.n === 1) { schedule(render); } return supply.whenOff(() => { if (!--use.n) { schedule(render); } }); } _use(name) { let use = this._uses.get(name); if (use) { ++use.n; } else { if (this._element.classList.contains(name)) { use = { i: 1, n: 1, s: 1, }; } else { use = { i: 0, n: 1, s: 0, }; } this._uses.set(name, use); } return use; } has(className) { return this._has(this._context, className); } _has({ nsAlias }, className) { const name = css__naming.name(className, nsAlias); const use = this._uses.get(name); return use ? !!use.n || !!use.i : this._element.classList.contains(name); } renderIn(context) { return context !== this._context ? { add: className => this._add(context, className), has: className => this._has(context, className), renderIn: newContext => this.renderIn(newContext), } : this; } } /** * Creates a rendering context based on another one. * * @typeParam TStatus - A type of the tuple containing a context content status as its first element. * @param base - Base rendering context. * @param update - Context update. * * @returns Updated rendering context, or the `base` one if nothing to update. */ function deriveDrekContext(base, update = {}) { const { nsAlias: initialNsAlias = base.nsAlias, scheduler: initialScheduler = base.scheduler } = update; if (initialNsAlias === base.nsAlias && initialScheduler === base.scheduler) { return base; } const state = new DrekContext$State({ nsAlias: initialNsAlias, scheduler: initialScheduler, }); let lift = (derived) => { const lifted = base.lift(); if (lifted === base) { return derived; } state.set(lifted); lift = _derived => lifted; return lifted; }; class DrekContext$Derived extends DrekContext { get fragment() { return base.fragment; } get window() { return base.window; } get document() { return base.document; } get nsAlias() { return state.nsAlias; } get scheduler() { return state.scheduler; } get readStatus() { return base.readStatus; } lift() { return lift(this); } } return new DrekContext$Derived(); } /** * Finds a host element of the given DOM node with respect to rendering targets. * * Crosses shadow DOM and {@link DrekFragment rendered fragment} bounds. In the latter case returns a * {@link DrekTarget.host rendering target host} instead of the document fragment. * * @param node - Target DOM element. * * @returns Either parent element of the given node, or `undefined` when not found. */ function drekHost(node) { var _a, _b; const host = nodeHost(node); if (host) { return host; } const parent = node.parentNode || node; const renderHost = (_b = (_a = parent[DrekContext__symbol]) === null || _a === void 0 ? void 0 : _a.fragment) === null || _b === void 0 ? void 0 : _b.target.host; return !renderHost || isElementNode(renderHost) ? renderHost : drekHost(renderHost); } function drekLift(node) { var _a; (_a = node[DrekContext__symbol]) === null || _a === void 0 ? void 0 : _a.lift(); return node; } /** * Context entry containing {@link DocumentRenderKit} instance. * * Initiated lazily. So the replacement should be provided before the kit used for the first time. * * Constructs global render kit instance by default. */ const DocumentRenderKit = { perContext: /*#__PURE__*/ cxDefaultScoped(CxGlobals, /*#__PURE__*/ cxSingle({ byDefault: DocumentRenderKit$byDefault, })), toString: () => '[DocumentRenderKit]', }; function DocumentRenderKit$byDefault(target) { const docs = new WeakMap(); const initDoc = (doc) => { if (!docs.get(doc)) { docs.set(doc, 1); drekContextOf(doc).update({ nsAlias: target.get(NamespaceAliaser), scheduler: target.get(RenderScheduler), }); } }; return { contextOf(node) { initDoc(nodeDocument(node)); return drekContextOf(node); }, }; } /** * Default Drek namespace definition. */ const Drek__NS = /*#__PURE__*/ new NamespaceDef('https://frontmeans.github.io/ns/drek', 'drek'); /** * @internal */ const DrekFragment$Context__symbol = /*#__PURE__*/ Symbol('DrekFragment.context'); /** * @internal */ class DrekFragment$Context extends DrekContext { static attach(fragment, target, { nsAlias = target.context.nsAlias, scheduler = queuedRenderScheduler, content, }) { if (!content) { content = target.context.document.createDocumentFragment(); } else if (content.getRootNode({ composed: true }) !== content) { throw new TypeError('Not a standalone DocumentFragment'); } else if (content[DrekContext__symbol]) { throw new TypeError('Can not render content of another fragment'); } return (content[DrekContext__symbol] = new DrekFragment$Context(fragment, target, content, nsAlias, scheduler)); } constructor(_fragment, _target, _content, nsAlias, scheduler) { super(); this._fragment = _fragment; this._target = _target; this._content = _content; this._status = trackValue([ { connected: false, withinFragment: 'added' }, ]); this._settled = new EventEmitter(); this._rendered = new EventEmitter(); this._getFragment = () => _fragment; this._lift = this; this.readStatus = this._status.read.do(translateAfter_((send, status) => send(...status))); this._state = new DrekContext$State({ nsAlias, scheduler }); this.scheduler = this._createSchedule.bind(this); this.whenConnected((...status) => { // `whenSettled` is the same as `whenConnected` now. this._whenSettled = this.whenConnected; // Send a settlement event one last time. this._settled.send(...status); }); } get fragment() { return this._getFragment(); } get window() { return this._target.context.window; } get document() { return this._target.context.document; } get nsAlias() { return this._state.nsAlias; } get whenSettled() { return this._whenSettled || (this._whenSettled = this._settled.on.do(onceOn)); } lift() { return this._lift; } _settle() { this.scheduler()(_ => { this._settled.send(...this._status.it); }); } _render() { // Make the `.lift()` method return the target context. this._lift = this._target.context; // Signal the rendering started. this._status.it = [{ connected: false, withinFragment: 'rendered' }]; const schedule = this._state._scheduler(); this._state.set(this._target.context); schedule(({ postpone }) => { // Await for all scheduled shots to render. postpone(() => { this._target.context.scheduler()(() => { // Place the rendered content within target's scheduler. const placement = this._target.placeContent(this._content); // Update target fragment. this._getFragment = () => placement.fragment; // Reset the inner context. this._content[DrekContext__symbol] = this._fragment[DrekFragment$Context__symbol] = new DrekFragment$Context(this._fragment, this._target, this._content, this.nsAlias, this.scheduler); // Derive the status from the target context. this._status.by(placement, (...status) => afterThe(status)); // Send `whenRendered` event. this._rendered.send(placement); }); }); }); return this; } _whenRendered() { return (this._whenRendered = valueProvider(this._rendered.on.do(onceOn)))(); } _createSchedule(options = {}) { const schedule = this._state.scheduler({ ...options, window: this.window, }); return shot => schedule(execution => shot(this._createExecution(execution))); } _createExecution(execution) { const fragmentExecution = { ...execution, fragment: this._fragment, content: this._content, postpone(postponed) { execution.postpone(_execution => postponed(fragmentExecution)); }, }; return fragmentExecution; } } /** * A fragment of DOM tree, which content is to be {@link DrekTarget#placeContent placed} to the document once rendered. * * Provides separate {@link DrekContext rendering context} for its nodes. * * @typeParam TStatus - A type of the tuple containing a rendered content status as its first element. */ class DrekFragment { /** * Rendering target. * * When the fragment is {@link render rendered}, the rendered content is placed to this target. */ get target() { return this[DrekFragment$Context__symbol]._target; } /** * Inner rendering context of the fragment. * * This context as available to the {@link content} nodes. * * This context updated each time the fragment is {@link render rendered}. */ get innerContext() { return this[DrekFragment$Context__symbol]; } /** * The content of the fragment. */ get content() { return this[DrekFragment$Context__symbol]._content; } /** * An `OnEvent` sender of fragment rendering event. * * Sends a fragment content {@link DrekTarget#placeContent placement} to {@link target} when the fragment is actually * {@link render rendered}. * * Cuts off the event supply after sending the first event. */ get whenRendered() { return this[DrekFragment$Context__symbol]._whenRendered(); } /** * Construct rendered fragment. * * @param target - Rendering target to place the * @param options - Fragment rendering options. */ constructor(target, options = {}) { this[DrekFragment$Context__symbol] = DrekFragment$Context.attach(this, target, options); } /** * Settles previously rendered content. * * A {@link DrekContext#whenSettled} event sender notifies its receivers once settled. * * @returns `this` instance. */ settle() { this[DrekFragment$Context__symbol]._settle(); return this; } /** * Renders this fragment by {@link DrekTarget#placeContent placing} its {@link DrekFragmentRenderExecution#content * content} to {@link target rendering target}. * * Once rendered the fragment {@link content} becomes empty and can be reused. Its rendering context is updated. * * @returns Content {@link DrekTarget#placeContent placement} to {@link target}. */ render() { return this[DrekFragment$Context__symbol]._render(); } } /** * Creates a rendering target that appends content to parent node. * * @param host - A node to append content to. * @param context - Custom rendering context. Defaults to `host` node context. * * @returns Rendering target. */ function drekAppender(host, context = drekContextOf(host)) { return { context, host, placeContent(content) { host.appendChild(content); return context; }, }; } /** * Creates a rendering target that charges rendered content prior to placing it to another target. * * @typeParam TStatus - A tuple type reflecting a content {@link DrekContentStatus placement status}. * @param target - Rendering target of charged content. * @param spec - Content charging options. * * @returns Rendering target. */ function drekCharger(target, spec) { const charger = DrekCharger$custom(target, spec); return { context: target.context, host: target.host, placeContent(content) { return charger.charge(content, target); }, }; } function DrekCharger$custom(target, spec) { if (typeof spec === 'function') { return DrekCharger$custom(target, spec(target)); } if (typeof spec === 'string') { return DrekCharger$commentWrapper(target, spec); } if (spec) { return spec; } return DrekCharger$commentWrapper(target, Math.random().toString(32).substr(2)); } function DrekCharger$commentWrapper({ context: { document } }, rem) { let wrapContent = (content, target) => { const start = document.createComment(` [[ ${rem} [[ `); const end = document.createComment(` ]] ${rem} ]] `); let placement; wrapContent = (content, _target) => { const range = document.createRange(); range.setStartAfter(start); range.setEndBefore(end); range.deleteContents(); range.insertNode(content); return placement; }; const fragment = document.createDocumentFragment(); fragment.append(start, content, end); return (placement = target.placeContent(fragment)); }; return { charge: (content, target) => wrapContent(content, target), }; } /** * Creates a rendering target that inserts content to parent node at particular position. * * @param host - A node to insert content to. * @param before - A child node of `host` one to insert the content before, or `null` to append it as the last child * of `host` node. * @param context - Custom rendering context. Defaults to `host` node context. * * @returns Rendering target. */ function drekInserter(host, before, context = drekContextOf(host)) { return { context, host, placeContent(content) { host.insertBefore(content, before); return context; }, }; } /** * Creates a rendering target that replaces content of the `host` node. * * @param host - A node to replace the content of. * @param context - Custom rendering context. Defaults to `host` node context. * * @returns Rendering target. */ function drekReplacer(host, context = drekContextOf(host)) { return { context, host, placeContent(content) { removeNodeContent(host); host.appendChild(content); return context; }, }; } export { DocumentRenderKit, DrekContext, DrekFragment, DrekPlacement, Drek__NS, deriveDrekContext, drekAppender, drekBuild, drekCharger, drekContextOf, drekCssClassesOf, drekHost, drekInserter, drekLift, drekReplacer }; //# sourceMappingURL=drek.js.map