##// END OF EJS Templates
linting
linting

File last commit:

r109:4a375b9c654a default
r109:4a375b9c654a default
Show More
WatchForRendition.ts
215 lines | 6.8 KiB | video/mp2t | TypeScriptLexer
/ djx / src / main / ts / tsx / WatchForRendition.ts
cin
Working on WatchForRendition
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;
}
cin
linting
r109 const isObservable = <T>(v: ArrayLike<T>): v is ArrayLike<T> & ObservableResults<T> =>
v && (typeof (v as { observe?: unknown; }).observe === "function");
cin
Working on WatchForRendition
r107
cin
linting
r109 const noop = () => { };
cin
Working on WatchForRendition
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[] = [];
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({
cin
linting
r109 next: ({ item, prevIndex, newIndex }) => this._onItemUpdated({ item, prevIndex, newIndex, animate })
cin
Working on WatchForRendition
r107 });
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));
}
cin
linting
r109 private readonly _onItemUpdated = (item: RenderTask<T>) => {
cin
Working on WatchForRendition
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
linting
r109 };
cin
Working on WatchForRendition
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
linting
r109 private readonly _onRenderItem = ({ item, newIndex, prevIndex, animate: _animate }: RenderTask<T>) => {
cin
Working on WatchForRendition
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
linting
r109 };
cin
Working on WatchForRendition
r107
protected _getDomNode() {
if (!this._node)
throw new Error("The instance of the widget isn't created");
return this._node;
}
}