WatchForRendition.ts
215 lines
| 6.7 KiB
| video/mp2t
|
TypeScriptLexer
|
|
r107 | import { id as mid } from "module"; | ||
| import { TraceSource } from "@implab/core-amd/log/TraceSource"; | ||||
| import { argumentNotNull } from "@implab/core-amd/safe"; | ||||
| import { getScope, render } from "./render"; | ||||
| import { RenditionBase } from "./RenditionBase"; | ||||
| import { Scope } from "./Scope"; | ||||
| import { Cancellation } from "@implab/core-amd/Cancellation"; | ||||
| import { collectNodes, destroy as safeDestroy, isDocumentFragmentNode, isElementNode, isMounted, placeAt, startupWidgets } from "./traits"; | ||||
| import { IDestroyable } from "@implab/core-amd/interfaces"; | ||||
| import { play } from "../play"; | ||||
| import * as fx from "dojo/fx"; | ||||
| import { isSubsribable, Subscribable } from "../observable"; | ||||
| import { QueryResultUpdate } from "../tsx"; | ||||
| const trace = TraceSource.get(mid); | ||||
| interface ItemRendition { | ||||
| nodes: Node[]; | ||||
| scope: IDestroyable; | ||||
| destroy(): void; | ||||
| } | ||||
| interface ObservableResults<T> { | ||||
| /** | ||||
| * Allows observation of results | ||||
| */ | ||||
| observe(listener: (object: T, previousIndex: number, newIndex: number) => void, includeUpdates?: boolean): { | ||||
| remove(): void; | ||||
| }; | ||||
| } | ||||
| interface RenderTask<T> extends QueryResultUpdate<T> { | ||||
| animate: boolean; | ||||
| } | ||||
| export interface AnimationAttrs { | ||||
| animate?: boolean; | ||||
| animateIn?: (nodes: Node[]) => Promise<void>; | ||||
| animateOut?: (nodes: Node[]) => Promise<void>; | ||||
| } | ||||
| export interface WatchForRenditionAttrs<T> extends AnimationAttrs { | ||||
| subject: T[] | Subscribable<QueryResultUpdate<T>>; | ||||
| component: (arg: T, index: number) => unknown; | ||||
| } | ||||
| const isObservable = <T>(v: PromiseLike<ArrayLike<T>> | ArrayLike<T>): v is ArrayLike<T> & ObservableResults<T> => | ||||
| v && (typeof (v as any).observe === "function"); | ||||
| const noop = () => {}; | ||||
| const fadeIn = (nodes: Node[]) => Promise.all(nodes | ||||
| .filter(isElementNode) | ||||
| .map(el => play(fx.fadeIn({ node: el as HTMLElement }))) | ||||
| ).then(noop); | ||||
| const fadeOut = (nodes: Node[]) => Promise.all(nodes | ||||
| .filter(isElementNode) | ||||
| .map(el => play(fx.fadeOut({ node: el as HTMLElement }))) | ||||
| ).then(noop); | ||||
| export class WatchForRendition<T> extends RenditionBase<Node> { | ||||
| private readonly _component: (arg: T, index: number) => unknown; | ||||
| private readonly _node: Node; | ||||
| private readonly _itemRenditions: ItemRendition[] = []; | ||||
| private readonly _subject: T[] | Subscribable<QueryResultUpdate<T>>; | ||||
| private readonly _renderTasks: RenderTask<T>[] = []; | ||||
| private readonly _animate: boolean; | ||||
| private readonly _animateIn: (nodes: Node[]) => Promise<void>; | ||||
| private readonly _animateOut: (nodes: Node[]) => Promise<void>; | ||||
| private _ct = Cancellation.none; | ||||
| constructor({ subject, component, animate, animateIn, animateOut }: WatchForRenditionAttrs<T>) { | ||||
| super(); | ||||
| argumentNotNull(component, "component"); | ||||
| argumentNotNull(subject, "component"); | ||||
| this._component = component; | ||||
| this._subject = subject; | ||||
| this._node = document.createComment("[WatchFor]"); | ||||
| this._animate = !!animate; | ||||
| this._animateIn = animateIn ?? fadeIn; | ||||
| this._animateOut = animateOut ?? fadeOut; | ||||
| } | ||||
| protected _create() { | ||||
| const scope = getScope(); | ||||
| scope.own(() => { | ||||
| this._itemRenditions.forEach(safeDestroy); | ||||
| safeDestroy(this._node); | ||||
| }); | ||||
| const result = this._subject; | ||||
| if (result) { | ||||
| if (isSubsribable<QueryResultUpdate<T>>(result)) { | ||||
| let animate = false; | ||||
| const subscription = result.subscribe({ | ||||
| next: ({item, prevIndex, newIndex}) => this._onItemUpdated({ item, prevIndex, newIndex, animate }) | ||||
| }); | ||||
| scope.own(subscription); | ||||
| animate = this._animate; | ||||
| } else { | ||||
| if (isObservable(result)) | ||||
| scope.own(result.observe((item, prevIndex, newIndex) => this._onItemUpdated({ item, prevIndex, newIndex, animate: false }), true)); | ||||
| for (let i = 0, n = result.length; i < n; i++) | ||||
| this._onItemUpdated({ item: result[i], prevIndex: -1, newIndex: i, animate: this._animate }); | ||||
| } | ||||
| } | ||||
| this._ct = new Cancellation(cancel => scope.own(cancel)); | ||||
| } | ||||
| private _onItemUpdated = (item: RenderTask<T>) => { | ||||
| if (!this._renderTasks.length) { | ||||
| // schedule a new job | ||||
| this._renderTasks.push(item); | ||||
| this._render().catch(e => trace.error(e)); | ||||
| } else { | ||||
| // update existing job | ||||
| this._renderTasks.push(item); | ||||
| } | ||||
| } | ||||
| private async _render() { | ||||
| // fork | ||||
| await Promise.resolve(); | ||||
| // don't render destroyed rendition | ||||
| if (this._ct.isRequested()) | ||||
| return; | ||||
| this._renderTasks.forEach(this._onRenderItem); | ||||
| this._renderTasks.length = 0; | ||||
| } | ||||
| _onRenderItem = ({ item, newIndex, prevIndex, animate: _animate }: RenderTask<T>) => { | ||||
| const animate = _animate && prevIndex !== newIndex; | ||||
| if (prevIndex > -1) { | ||||
| // if we need to delete previous rendition | ||||
| const [{ nodes, destroy }] = this._itemRenditions.splice(prevIndex, 1); | ||||
| if (animate) { | ||||
| this._animateOut(nodes) | ||||
| .then(destroy) | ||||
| .catch(e => trace.error(e)); | ||||
| } else { | ||||
| destroy(); | ||||
| } | ||||
| } | ||||
| if (newIndex > -1) { | ||||
| // if we need to create the new rendition | ||||
| // 1. create a new scope for rendering a content | ||||
| const scope = new Scope(); | ||||
| // 2. render the content | ||||
| const itemNode = render(this._component(item, newIndex), scope); | ||||
| // 3. track nodes | ||||
| const nodes = isDocumentFragmentNode(itemNode) ? | ||||
| collectNodes(itemNode.childNodes) : | ||||
| [itemNode]; | ||||
| // 5. insert node at the correct position | ||||
| const { nodes: [beforeNode] } = this._itemRenditions[newIndex] ?? { nodes: [] }; | ||||
| if (beforeNode) | ||||
| placeAt(itemNode, beforeNode, "before"); | ||||
| else | ||||
| placeAt(itemNode, this._node, "before"); | ||||
| // 6. store information about rendition | ||||
| this._itemRenditions.splice(newIndex, 0, { | ||||
| scope, | ||||
| nodes, | ||||
| destroy: () => { | ||||
| scope.destroy(); | ||||
| nodes.forEach(safeDestroy); | ||||
| } | ||||
| }); | ||||
| // 7. startup widgets if needed | ||||
| if (isMounted(this._node)) | ||||
| nodes.forEach(n => startupWidgets(n)); | ||||
| // 8. optionally play the animation | ||||
| if (animate) | ||||
| this._animateIn(nodes).catch(e => trace.error(e)); | ||||
| } | ||||
| } | ||||
| protected _getDomNode() { | ||||
| if (!this._node) | ||||
| throw new Error("The instance of the widget isn't created"); | ||||
| return this._node; | ||||
| } | ||||
| } | ||||
