WatchForRendition.ts
204 lines
| 6.4 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"; | ||||
cin
|
r116 | import { isSubsribable, Subscribable } from "../observable"; | ||
import { isObservableResults, OrderedUpdate } from "../store"; | ||||
cin
|
r107 | |||
const trace = TraceSource.get(mid); | ||||
interface ItemRendition { | ||||
nodes: Node[]; | ||||
scope: IDestroyable; | ||||
destroy(): void; | ||||
} | ||||
cin
|
r116 | interface RenderTask<T> extends OrderedUpdate<T> { | ||
cin
|
r107 | animate: boolean; | ||
} | ||||
export interface AnimationAttrs { | ||||
animate?: boolean; | ||||
animateIn?: (nodes: Node[]) => Promise<void>; | ||||
animateOut?: (nodes: Node[]) => Promise<void>; | ||||
} | ||||
export interface WatchForRenditionAttrs<T> extends AnimationAttrs { | ||||
cin
|
r116 | subject: T[] | Subscribable<OrderedUpdate<T>>; | ||
cin
|
r107 | |||
component: (arg: T, index: number) => unknown; | ||||
} | ||||
cin
|
r109 | const noop = () => { }; | ||
cin
|
r107 | |||
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[] = []; | ||||
cin
|
r116 | private readonly _subject: T[] | Subscribable<OrderedUpdate<T>>; | ||
cin
|
r107 | |||
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) { | ||||
cin
|
r116 | if (isSubsribable<OrderedUpdate<T>>(result)) { | ||
cin
|
r107 | let animate = false; | ||
const subscription = result.subscribe({ | ||||
cin
|
r109 | next: ({ item, prevIndex, newIndex }) => this._onItemUpdated({ item, prevIndex, newIndex, animate }) | ||
cin
|
r107 | }); | ||
scope.own(subscription); | ||||
animate = this._animate; | ||||
} else { | ||||
cin
|
r110 | if (isObservableResults<T>(result)) | ||
cin
|
r107 | 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)); | ||||
} | ||||
cin
|
r109 | private readonly _onItemUpdated = (item: RenderTask<T>) => { | ||
cin
|
r107 | 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); | ||||
} | ||||
cin
|
r109 | }; | ||
cin
|
r107 | |||
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; | ||||
} | ||||
cin
|
r109 | private readonly _onRenderItem = ({ item, newIndex, prevIndex, animate: _animate }: RenderTask<T>) => { | ||
cin
|
r107 | 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)); | ||||
} | ||||
cin
|
r109 | }; | ||
cin
|
r107 | |||
protected _getDomNode() { | ||||
if (!this._node) | ||||
throw new Error("The instance of the widget isn't created"); | ||||
return this._node; | ||||
} | ||||
} | ||||