##// END OF EJS Templates
Added tag v1.10.3 for changeset 078eca3dc271
Added tag v1.10.3 for changeset 078eca3dc271

File last commit:

r146:af4f8424e83d v1.9.0 default
r159:0b327f31e28f tip default
Show More
WatchForRendition.ts
207 lines | 6.6 KiB | video/mp2t | TypeScriptLexer
/ djx / src / main / ts / tsx / WatchForRendition.ts
import { id as mid } from "module";
import { TraceSource } from "@implab/core-amd/log/TraceSource";
import { argumentNotNull } from "@implab/core-amd/safe";
import { queueRenderTask, getPriority, 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 { isSubscribable, Subscribable } from "../observable";
import { isDjObservableResults, OrderedUpdate } from "../store";
const trace = TraceSource.get(mid);
interface ItemRendition {
nodes: Node[];
scope: IDestroyable;
destroy(): void;
}
interface RenderTask<T> extends OrderedUpdate<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<OrderedUpdate<T>> | undefined | null;
component: (arg: T, index: number) => unknown;
}
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<OrderedUpdate<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;
private _priority = 0;
constructor({ subject, component, animate, animateIn, animateOut }: WatchForRenditionAttrs<T>) {
super();
argumentNotNull(component, "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() {
this._priority = getPriority() + 1;
const scope = getScope();
scope.own(() => {
this._itemRenditions.forEach(safeDestroy);
safeDestroy(this._node);
});
const result = this._subject;
if (result) {
if (isSubscribable<OrderedUpdate<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 (isDjObservableResults<T>(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 readonly _onItemUpdated = (item: RenderTask<T>) => {
if (!this._renderTasks.length) {
// schedule a new job
this._renderTasks.push(item);
// fork
// use dummy scope, because every item will have it's own scope
queueRenderTask(this._render, Scope.dummy, this._priority);
} else {
// update existing job
this._renderTasks.push(item);
}
};
private readonly _render = () => {
// don't render destroyed rendition
if (this._ct.isRequested())
return;
this._renderTasks.forEach(this._onRenderItem);
this._renderTasks.length = 0;
};
private readonly _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 rendition isn't created");
return this._node;
}
}