UNPKG

ember-material-icons

Version:

Google Material icons for your ember-cli app

435 lines (328 loc) 11.3 kB
import Bounds, { Cursor, DestroyableBounds, clear } from './bounds'; import { DOMChanges, DOMTreeConstruction } from './dom/helper'; import { Option, Destroyable, Stack, LinkedList, LinkedListNode, assert, expect } from '@glimmer/util'; import { Environment } from './environment'; import { VM } from './vm'; import { PathReference } from '@glimmer/reference'; import { SimpleElementOperations } from './compiled/opcodes/dom'; import * as Simple from './dom/interfaces'; export interface FirstNode { firstNode(): Option<Simple.Node>; } export interface LastNode { lastNode(): Option<Simple.Node>; } class First { constructor(private node: Node) { } firstNode(): Node { return this.node; } } class Last { constructor(private node: Node) { } lastNode(): Node { return this.node; } } export interface ElementOperations { addStaticAttribute(element: Simple.Element, name: string, value: string): void; addStaticAttributeNS(element: Simple.Element, namespace: string, name: string, value: string): void; addDynamicAttribute(element: Simple.Element, name: string, value: PathReference<string>, isTrusting: boolean): void; addDynamicAttributeNS(element: Simple.Element, namespace: string, name: string, value: PathReference<string>, isTrusting: boolean): void; flush(element: Simple.Element, vm: VM): void; } export class Fragment implements Bounds { private bounds: Bounds; constructor(bounds: Bounds) { this.bounds = bounds; } parentElement(): Simple.Element { return this.bounds.parentElement(); } firstNode(): Option<Simple.Node> { return this.bounds.firstNode(); } lastNode(): Option<Simple.Node> { return this.bounds.lastNode(); } update(bounds: Bounds) { this.bounds = bounds; } } export class ElementStack implements Cursor { public nextSibling: Option<Simple.Node>; public dom: DOMTreeConstruction; public updateOperations: DOMChanges; public constructing: Option<Simple.Element> = null; public operations: Option<ElementOperations> = null; public element: Simple.Element; public env: Environment; private elementStack = new Stack<Simple.Element>(); private nextSiblingStack = new Stack<Option<Simple.Node>>(); private blockStack = new Stack<Tracker>(); private defaultOperations: ElementOperations; static forInitialRender(env: Environment, parentNode: Simple.Element, nextSibling: Option<Simple.Node>) { return new ElementStack(env, parentNode, nextSibling); } static resume(env: Environment, tracker: Tracker, nextSibling: Option<Simple.Node>) { let parentNode = tracker.parentElement(); let stack = new ElementStack(env, parentNode, nextSibling); stack.pushBlockTracker(tracker); return stack; } constructor(env: Environment, parentNode: Simple.Element, nextSibling: Option<Simple.Node>) { this.env = env; this.dom = env.getAppendOperations(); this.updateOperations = env.getDOM(); this.element = parentNode; this.nextSibling = nextSibling; this.defaultOperations = new SimpleElementOperations(env); this.elementStack.push(this.element); this.nextSiblingStack.push(this.nextSibling); } expectConstructing(method: string): Simple.Element { return expect(this.constructing, `${method} should only be called while constructing an element`); } expectOperations(method: string): ElementOperations { return expect(this.operations, `${method} should only be called while constructing an element`); } block(): Tracker { return expect(this.blockStack.current, "Expected a current block tracker"); } popElement() { let { elementStack, nextSiblingStack } = this; let topElement = elementStack.pop(); nextSiblingStack.pop(); // LOGGER.debug(`-> element stack ${this.elementStack.toArray().map(e => e.tagName).join(', ')}`); this.element = expect(elementStack.current, "can't pop past the last element"); this.nextSibling = nextSiblingStack.current; return topElement; } pushSimpleBlock(): Tracker { let tracker = new SimpleBlockTracker(this.element); this.pushBlockTracker(tracker); return tracker; } pushUpdatableBlock(): UpdatableTracker { let tracker = new UpdatableBlockTracker(this.element); this.pushBlockTracker(tracker); return tracker; } private pushBlockTracker(tracker: Tracker, isRemote = false) { let current = this.blockStack.current; if (current !== null) { current.newDestroyable(tracker); if (!isRemote) { current.newBounds(tracker); } } this.blockStack.push(tracker); return tracker; } pushBlockList(list: LinkedList<LinkedListNode & Bounds & Destroyable>): Tracker { let tracker = new BlockListTracker(this.element, list); let current = this.blockStack.current; if (current !== null) { current.newDestroyable(tracker); current.newBounds(tracker); } this.blockStack.push(tracker); return tracker; } popBlock(): Tracker { this.block().finalize(this); return expect(this.blockStack.pop(), "Expected popBlock to return a block"); } openElement(tag: string, operations = this.defaultOperations): Simple.Element { let element = this.dom.createElement(tag, this.element); this.constructing = element; this.operations = operations; return element; } flushElement() { let parent = this.element; let element = expect(this.constructing, `flushElement should only be called when constructing an element`); this.dom.insertBefore(parent, element, this.nextSibling); this.constructing = null; this.operations = null; this.pushElement(element); this.block().openElement(element); } pushRemoteElement(element: Simple.Element) { this.pushElement(element); let tracker = new RemoteBlockTracker(element); this.pushBlockTracker(tracker, true); } popRemoteElement() { this.popBlock(); this.popElement(); } private pushElement(element: Simple.Element) { this.element = element; this.elementStack.push(element); // LOGGER.debug(`-> element stack ${this.elementStack.toArray().map(e => e.tagName).join(', ')}`); this.nextSibling = null; this.nextSiblingStack.push(null); } newDestroyable(d: Destroyable) { this.block().newDestroyable(d); } newBounds(bounds: Bounds) { this.block().newBounds(bounds); } appendText(string: string): Simple.Text { let { dom } = this; let text = dom.createTextNode(string); dom.insertBefore(this.element, text, this.nextSibling); this.block().newNode(text); return text; } appendComment(string: string): Simple.Comment { let { dom } = this; let comment = dom.createComment(string); dom.insertBefore(this.element, comment, this.nextSibling); this.block().newNode(comment); return comment; } setStaticAttribute(name: string, value: string) { this.expectOperations('setStaticAttribute').addStaticAttribute(this.expectConstructing('setStaticAttribute'), name, value); } setStaticAttributeNS(namespace: string, name: string, value: string) { this.expectOperations('setStaticAttributeNS').addStaticAttributeNS(this.expectConstructing('setStaticAttributeNS'), namespace, name, value); } setDynamicAttribute(name: string, reference: PathReference<string>, isTrusting: boolean) { this.expectOperations('setDynamicAttribute').addDynamicAttribute(this.expectConstructing('setDynamicAttribute'), name, reference, isTrusting); } setDynamicAttributeNS(namespace: string, name: string, reference: PathReference<string>, isTrusting: boolean) { this.expectOperations('setDynamicAttributeNS').addDynamicAttributeNS(this.expectConstructing('setDynamicAttributeNS'), namespace, name, reference, isTrusting); } closeElement() { this.block().closeElement(); this.popElement(); } } export interface Tracker extends DestroyableBounds { openElement(element: Simple.Element): void; closeElement(): void; newNode(node: Simple.Node): void; newBounds(bounds: Bounds): void; newDestroyable(d: Destroyable): void; finalize(stack: ElementStack): void; } export class SimpleBlockTracker implements Tracker { protected first: Option<FirstNode> = null; protected last: Option<LastNode> = null; protected destroyables: Option<Destroyable[]> = null; protected nesting = 0; constructor(private parent: Simple.Element){} destroy() { let { destroyables } = this; if (destroyables && destroyables.length) { for (let i=0; i<destroyables.length; i++) { destroyables[i].destroy(); } } } parentElement() { return this.parent; } firstNode(): Option<Simple.Node> { return this.first && this.first.firstNode(); } lastNode(): Option<Simple.Node> { return this.last && this.last.lastNode(); } openElement(element: Element) { this.newNode(element); this.nesting++; } closeElement() { this.nesting--; } newNode(node: Node) { if (this.nesting !== 0) return; if (!this.first) { this.first = new First(node); } this.last = new Last(node); } newBounds(bounds: Bounds) { if (this.nesting !== 0) return; if (!this.first) { this.first = bounds; } this.last = bounds; } newDestroyable(d: Destroyable) { this.destroyables = this.destroyables || []; this.destroyables.push(d); } finalize(stack: ElementStack) { if (!this.first) { stack.appendComment(''); } } } class RemoteBlockTracker extends SimpleBlockTracker { destroy() { super.destroy(); clear(this); } } export interface UpdatableTracker extends Tracker { reset(env: Environment): Option<Simple.Node>; } export class UpdatableBlockTracker extends SimpleBlockTracker implements UpdatableTracker { reset(env: Environment): Option<Simple.Node> { let { destroyables } = this; if (destroyables && destroyables.length) { for (let i=0; i<destroyables.length; i++) { env.didDestroy(destroyables[i]); } } let nextSibling = clear(this); this.destroyables = null; this.first = null; this.last = null; return nextSibling; } } class BlockListTracker implements Tracker { constructor(private parent: Simple.Element, private boundList: LinkedList<LinkedListNode & Bounds & Destroyable>) { this.parent = parent; this.boundList = boundList; } destroy() { this.boundList.forEachNode(node => node.destroy()); } parentElement() { return this.parent; } firstNode(): Option<Simple.Node> { let head = this.boundList.head(); return head && head.firstNode(); } lastNode(): Option<Simple.Node> { let tail = this.boundList.tail(); return tail && tail.lastNode(); } openElement(_element: Element) { assert(false, 'Cannot openElement directly inside a block list'); } closeElement() { assert(false, 'Cannot closeElement directly inside a block list'); } newNode(_node: Node) { assert(false, 'Cannot create a new node directly inside a block list'); } newBounds(_bounds: Bounds) { } newDestroyable(_d: Destroyable) { } finalize(_stack: ElementStack) { } }