WatchForRendition.ts
215 lines
| 6.7 KiB
| video/mp2t
|
TypeScriptLexer
cin
|
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; | ||||
} | ||||
} | ||||