import Graph from '../graph/Graph'; import { IObservable, ISubscriberFunction } from '../graph/IObservable'; import Observable from '../graph/Observable'; import Computed from '../graph/Computed'; import ObservableArray from '../graph/ObservableArray'; import ObservableHash from '../graph/ObservableHash'; import { IVirtualNode, IVirtualNodeProps } from '../dom/IVirtualNode'; import VirtualNode from '../dom/VirtualNode'; import Fragment from '../dom/Fragment'; import ComponentNode from '../dom/ComponentNode'; import { Component } from '../dom/Component'; import { CascadeError } from '../util/CascadeError'; export default class Cascade { /** * Dispose all Observables in a Graph * @param obj */ static disposeAll(obj: T) { var graph: Graph = obj['_graph']; for (var name in obj) { if (obj.hasOwnProperty(name)) { // Only dispose non-observable properties here. if (!graph || !graph.observables[name]) { Cascade.disposeAll(obj[name] as any); } } } if (graph) { for (var index in graph.observables) { if (graph.observables.hasOwnProperty(index)) { var value = graph.observables[index].value; Cascade.disposeAll(value); graph.observables[index].dispose(); } } } } /** * Attach a Graph to an object * @param obj the object on which to attach a Graph */ static attachGraph(obj: T) { if (!(obj as any)._graph) { Object.defineProperty(obj, '_graph', { configurable: true, writable: true, enumerable: false, value: new Graph(obj) }); } return (obj as any)._graph as Graph; } /** * * @param obj the object on which to define a property * @param property the name of the property * @param observable the IObservable to store the property value */ static createProperty(obj: T, property: U, observable: IObservable) { var graph = Cascade.attachGraph(obj); if (graph.observables[property as string]) { // TODO: move or delete subscriptions? observable.subscribers = graph.observables[property as string].subscribers; } graph.observables[property as string] = observable; } /** * * @param obj the object on which to define a property * @param property the name of the property * @param observable the IObservable to store the property value * @param readOnly the Boolean specifying if this property is read only */ static attachObservable(obj: T, property: U, observable: IObservable, readOnly: boolean = false) { Cascade.createProperty(obj, property, observable); Object.defineProperty(obj, property, { enumerable: true, configurable: true, get: function () { return observable.getValue(); }, set: readOnly ? undefined : function (value: T[U] | Array) { (observable as any).setValue(value); } }); } /** * * @param obj * @param property * @param value */ static createObservable(obj: T, property: U, value?: T[U]) { Cascade.attachObservable(obj, property, new Observable(value)); } /** * * @param obj * @param property * @param definition * @param defer * @param setter */ static createComputed(obj: T, property: U, definition: (n?: T[U]) => T[U], defer?: boolean, setter?: (n: T[U]) => any) { Cascade.attachObservable(obj, property, new Computed(definition, defer, undefined, setter), true); } /** * * @param obj * @param property * @param value */ static createObservableArray(obj: T, property: U, value?: T[U]) { Cascade.attachObservable(obj, property, new ObservableArray(value as any) as any); } /** * * @param obj * @param property * @param value */ static createObservableHash(obj: T, property: U, value?: T[U]) { Cascade.attachObservable(obj, property, new ObservableHash(value as any) as any); } /** * * @param obj * @param property * @param alwaysNotify */ static setAlwaysNotify(obj: T, property: U, alwaysNotify: boolean) { let graph = this.attachGraph(obj); graph.setAlwaysNotify(property, alwaysNotify); } /** * * @param obj * @param property * @param subscriberFunction */ static subscribe(obj: T, property: U, subscriberFunction: ISubscriberFunction, createDisposer: boolean = false) { let graph = this.attachGraph(obj); graph.subscribe(property, subscriberFunction); return createDisposer ? function () { graph.unsubscribe(property, subscriberFunction); } : undefined; } /** * * @param obj * @param property * @param subscriberFunction */ static subscribeOnly(obj: T, property: U, subscriberFunction: ISubscriberFunction, createDisposer: boolean = false) { let graph = this.attachGraph(obj); graph.subscribeOnly(property, subscriberFunction); return createDisposer ? function () { graph.unsubscribe(property, subscriberFunction); } : undefined; } static unsubscribe(obj: T, property: U, subscriberFunction: ISubscriberFunction) { var graph: Graph = obj['_graph']; if (graph) { graph.unsubscribe(property, subscriberFunction); } } static waitToEqual(obj: T, property: U, testValue: T[U], timeout?: number) { let graph = this.attachGraph(obj); return new Promise((resolve, reject) => { let resolved = false; let subscriberFunction = (value: T[U]) => { if (value === testValue) { if (timerId) { window.clearTimeout(timerId); } if (!resolved) { resolved = true; window.setTimeout(() => { graph.unsubscribe(property, subscriberFunction); }); resolve(value); } } }; if (timeout) { var timerId = window.setTimeout(() => { graph.unsubscribe(property, subscriberFunction); reject(new Error(CascadeError.TimeoutElapsed)); }, timeout); } graph.subscribeOnly(property, subscriberFunction); }); } /** * * @param obj * @param property */ static peek(obj: T, property: U) { return obj['_graph'] ? (obj['_graph'] as Graph).peek(property) : undefined; } /** * * @param obj * @param property */ static peekDirty(obj: T, property: U) { return obj['_graph'] ? (obj['_graph'] as Graph).peekDirty(property) : undefined; } /** * * @param obj * @param property */ static track(obj: T, property: U) { let graph = this.attachGraph(obj); let observable = graph.observables[property as string] as IObservable; if (observable) { return observable.track(); } else { throw new Error(CascadeError.NoObservable + property); } } /** * * @param obj */ static trackAll(obj: T) { let graph = this.attachGraph(obj); return graph.trackAll(); } /** * * @param obj * @param property */ static update(obj: T, property: U) { let graph = this.attachGraph(obj); let observable = graph.observables[property as string] as Computed; if (observable && observable.update) { return observable.update(); } else { throw new Error(CascadeError.NoObservable + property); } } static set(obj: T, property: U, value: T[U]) { let graph = this.attachGraph(obj); let observable = graph.observables[property as string] as IObservable; if (observable) { return observable.setValue(value); } else { throw new Error(CascadeError.NoObservable + property); } } /** * * @param obj * @param property */ static run(obj: T, property: U) { let graph = this.attachGraph(obj); let observable = graph.observables[property as string] as IObservable; if (observable) { if ((observable as Computed).runOnly) { return (observable as Computed).runOnly(); } else { return observable.peek(); } } else { throw new Error(CascadeError.NoObservable + property); } } /** * * @param obj * @param property */ static getObservable(obj: T, property: U) { let graph = this.attachGraph(obj); return graph.observables[property as string] as IObservable; } /** * * @param obj * @param property */ static getSubscribers(obj: T, property: U) { let graph = this.attachGraph(obj); return graph.getSubscribers(property); } /** * * @param obj * @param property */ static getReferences(obj: T, property: U) { let graph = this.attachGraph(obj); return graph.getReferences(property); } /** * * @param callback * @param thisArg */ static wrapContext(callback: () => any, thisArg?: any) { Observable.pushContext(); if (thisArg) { callback.call(thisArg); } else { callback(); } return Observable.popContext(); } static createElement(type: string | (new (props: T, children: Array) => Component), props: T, ...children: Array): IVirtualNode { children = VirtualNode.fixChildrenArrays(children); if (typeof type === 'string') { return new VirtualNode(type, props, children); } else { return new ComponentNode(type, props, children); } } static render(node: HTMLElement | string, virtualNode: IVirtualNode) { var fixedNode = typeof node === 'string' ? document.getElementById(node) : node; if (!fixedNode) { throw new Error(CascadeError.NoRootNode); } var renderedComponent = virtualNode.toNode(); while (fixedNode.firstChild) { fixedNode.removeChild(fixedNode.firstChild); } if (renderedComponent instanceof Node) { fixedNode.appendChild(renderedComponent); } else { throw new Error(CascadeError.InvalidRootRender); } return renderedComponent; } static Fragment = Fragment; static reflectAvailable: boolean = (typeof Reflect === 'object' && typeof Reflect.getMetadata === 'function'); // TODO: Remove once Safari fixes href static xlinkDeprecated: boolean = (function () { if (typeof SVGElement === 'undefined') { return true; } else { let use = document.createElementNS('http://www.w3.org/2000/svg', 'use'); use.setAttribute('href', 'abcd'); return use.href.baseVal === 'abcd'; } })(); }