ember-material-icons
Version:
Google Material icons for your ember-cli app
495 lines (391 loc) • 14.3 kB
text/typescript
import { OpcodeJSON, UpdatingOpcode } from '../../opcodes';
import { VM, UpdatingVM } from '../../vm';
import * as Simple from '../../dom/interfaces';
import { FIX_REIFICATION } from '../../dom/interfaces';
import { Environment } from '../../environment';
import { FIXME, Option, Opaque, Dict, unwrap, expect } from '@glimmer/util';
import {
CachedReference,
Reference,
ReferenceCache,
RevisionTag,
Revision,
PathReference,
combineTagged,
isConst as isConstReference,
isModified
} from '@glimmer/reference';
import { ModifierManager } from '../../modifier/interfaces';
import { NULL_REFERENCE, PrimitiveReference } from '../../references';
import { CompiledArgs, EvaluatedArgs } from '../../compiled/expressions/args';
import { AttributeManager } from '../../dom/attribute-managers';
import { ElementOperations } from '../../builder';
import { Assert } from './vm';
import { APPEND_OPCODES, OpcodeName as Op } from '../../opcodes';
APPEND_OPCODES.add(Op.Text, (vm, { op1: text }) => {
vm.stack().appendText(vm.constants.getString(text));
});
APPEND_OPCODES.add(Op.Comment, (vm, { op1: text }) => {
vm.stack().appendComment(vm.constants.getString(text));
});
APPEND_OPCODES.add(Op.OpenElement, (vm, { op1: tag }) => {
vm.stack().openElement(vm.constants.getString(tag));
});
APPEND_OPCODES.add(Op.PushRemoteElement, vm => {
let reference = vm.frame.getOperand<Simple.Element>();
let cache = isConstReference(reference) ? undefined : new ReferenceCache(reference);
let element = cache ? cache.peek() : reference.value();
vm.stack().pushRemoteElement(element);
if (cache) {
vm.updateWith(new Assert(cache));
}
});
APPEND_OPCODES.add(Op.PopRemoteElement, vm => vm.stack().popRemoteElement());
APPEND_OPCODES.add(Op.OpenComponentElement, (vm, { op1: _tag }) => {
let tag = vm.constants.getString(_tag);
vm.stack().openElement(tag, new ComponentElementOperations(vm.env));
});
APPEND_OPCODES.add(Op.OpenDynamicElement, vm => {
let tagName = vm.frame.getOperand<string>().value();
vm.stack().openElement(tagName);
});
class ClassList {
private list: Option<Reference<string>[]> = null;
private isConst = true;
append(reference: Reference<string>) {
let { list, isConst } = this;
if (list === null) list = this.list = [];
list.push(reference);
this.isConst = isConst && isConstReference(reference);
}
toReference(): Reference<Option<string>> {
let { list, isConst } = this;
if (!list) return NULL_REFERENCE;
if (isConst) return PrimitiveReference.create(toClassName(list));
return new ClassListReference(list);
}
}
class ClassListReference extends CachedReference<Option<string>> {
public tag: RevisionTag;
private list: Reference<string>[] = [];
constructor(list: Reference<string>[]) {
super();
this.tag = combineTagged(list);
this.list = list;
}
protected compute(): Option<string> {
return toClassName(this.list);
}
}
function toClassName(list: Reference<string>[]): Option<string> {
let ret: Opaque[] = [];
for (let i = 0; i < list.length; i++) {
let value: FIXME<Opaque, 'use Opaque and normalize'> = list[i].value();
if (value !== false && value !== null && value !== undefined) ret.push(value);
}
return (ret.length === 0) ? null : ret.join(' ');
}
export class SimpleElementOperations implements ElementOperations {
private opcodes: Option<UpdatingOpcode[]> = null;
private classList: Option<ClassList> = null;
constructor(private env: Environment) {
}
addStaticAttribute(element: Simple.Element, name: string, value: string) {
if (name === 'class') {
this.addClass(PrimitiveReference.create(value));
} else {
this.env.getAppendOperations().setAttribute(element, name, value);
}
}
addStaticAttributeNS(element: Simple.Element, namespace: string, name: string, value: string) {
this.env.getAppendOperations().setAttribute(element, name, value, namespace);
}
addDynamicAttribute(element: Simple.Element, name: string, reference: PathReference<string>, isTrusting: boolean) {
if (name === 'class') {
this.addClass(reference);
} else {
let attributeManager = this.env.attributeFor(element, name, isTrusting);
let attribute = new DynamicAttribute(element, attributeManager, name, reference);
this.addAttribute(attribute);
}
}
addDynamicAttributeNS(element: Simple.Element, namespace: Simple.Namespace, name: string, reference: PathReference<string>, isTrusting: boolean) {
let attributeManager = this.env.attributeFor(element, name, isTrusting, namespace);
let nsAttribute = new DynamicAttribute(element, attributeManager, name, reference, namespace);
this.addAttribute(nsAttribute);
}
flush(element: Simple.Element, vm: VM) {
let { env } = vm;
let { opcodes, classList } = this;
for (let i = 0; opcodes && i < opcodes.length; i++) {
vm.updateWith(opcodes[i]);
}
if (classList) {
let attributeManager = env.attributeFor(element, 'class', false);
let attribute = new DynamicAttribute(element, attributeManager, 'class', classList.toReference());
let opcode = attribute.flush(env);
if (opcode) {
vm.updateWith(opcode);
}
}
this.opcodes = null;
this.classList = null;
}
private addClass(reference: PathReference<string>) {
let { classList } = this;
if (!classList) {
classList = this.classList = new ClassList();
}
classList.append(reference);
}
private addAttribute(attribute: Attribute) {
let opcode = attribute.flush(this.env);
if (opcode) {
let { opcodes } = this;
if (!opcodes) {
opcodes = this.opcodes = [];
}
opcodes.push(opcode);
}
}
}
export class ComponentElementOperations implements ElementOperations {
private attributeNames: Option<string[]> = null;
private attributes: Option<Attribute[]> = null;
private classList: Option<ClassList> = null;
constructor(private env: Environment) {
}
addStaticAttribute(element: Simple.Element, name: string, value: string) {
if (name === 'class') {
this.addClass(PrimitiveReference.create(value));
} else if (this.shouldAddAttribute(name)) {
this.addAttribute(name, new StaticAttribute(element, name, value));
}
}
addStaticAttributeNS(element: Simple.Element, namespace: string, name: string, value: string) {
if (this.shouldAddAttribute(name)) {
this.addAttribute(name, new StaticAttribute(element, name, value, namespace));
}
}
addDynamicAttribute(element: Simple.Element, name: string, reference: PathReference<string>, isTrusting: boolean) {
if (name === 'class') {
this.addClass(reference);
} else if (this.shouldAddAttribute(name)) {
let attributeManager = this.env.attributeFor(element, name, isTrusting);
let attribute = new DynamicAttribute(element, attributeManager, name, reference);
this.addAttribute(name, attribute);
}
}
addDynamicAttributeNS(element: Simple.Element, namespace: Simple.Namespace, name: string, reference: PathReference<string>, isTrusting: boolean) {
if (this.shouldAddAttribute(name)) {
let attributeManager = this.env.attributeFor(element, name, isTrusting, namespace);
let nsAttribute = new DynamicAttribute(element, attributeManager, name, reference, namespace);
this.addAttribute(name, nsAttribute);
}
}
flush(element: Simple.Element, vm: VM) {
let { env } = this;
let { attributes, classList } = this;
for (let i = 0; attributes && i < attributes.length; i++) {
let opcode = attributes[i].flush(env);
if (opcode) {
vm.updateWith(opcode);
}
}
if (classList) {
let attributeManager = env.attributeFor(element, 'class', false);
let attribute = new DynamicAttribute(element, attributeManager, 'class', classList.toReference());
let opcode = attribute.flush(env);
if (opcode) {
vm.updateWith(opcode);
}
}
}
private shouldAddAttribute(name: string): boolean {
return !this.attributeNames || this.attributeNames.indexOf(name) === -1;
}
private addClass(reference: PathReference<string>) {
let { classList } = this;
if (!classList) {
classList = this.classList = new ClassList();
}
classList.append(reference);
}
private addAttribute(name: string, attribute: Attribute) {
let { attributeNames, attributes } = this;
if (!attributeNames) {
attributeNames = this.attributeNames = [];
attributes = this.attributes = [];
}
attributeNames.push(name);
unwrap(attributes).push(attribute);
}
}
APPEND_OPCODES.add(Op.FlushElement, vm => {
let stack = vm.stack();
let action = 'FlushElementOpcode#evaluate';
stack.expectOperations(action).flush(stack.expectConstructing(action), vm);
stack.flushElement();
});
APPEND_OPCODES.add(Op.CloseElement, vm => vm.stack().closeElement());
APPEND_OPCODES.add(Op.PopElement, vm => vm.stack().popElement());
APPEND_OPCODES.add(Op.StaticAttr, (vm, { op1: _name, op2: _value, op3: _namespace }) => {
let name = vm.constants.getString(_name);
let value = vm.constants.getString(_value);
if (_namespace) {
let namespace = vm.constants.getString(_namespace);
vm.stack().setStaticAttributeNS(namespace, name, value);
} else {
vm.stack().setStaticAttribute(name, value);
}
});
APPEND_OPCODES.add(Op.Modifier, (vm, { op1: _name, op2: _manager, op3: _args }) => {
let manager = vm.constants.getOther<ModifierManager<Opaque>>(_manager);
let rawArgs = vm.constants.getExpression<CompiledArgs>(_args);
let stack = vm.stack();
let { constructing: element, updateOperations } = stack;
let args = rawArgs.evaluate(vm);
let dynamicScope = vm.dynamicScope();
let modifier = manager.create(element as FIX_REIFICATION<Element>, args, dynamicScope, updateOperations);
vm.env.scheduleInstallModifier(modifier, manager);
let destructor = manager.getDestructor(modifier);
if (destructor) {
vm.newDestroyable(destructor);
}
vm.updateWith(new UpdateModifierOpcode(
manager,
modifier,
args
));
});
export class UpdateModifierOpcode extends UpdatingOpcode {
public type = "update-modifier";
private lastUpdated: Revision;
constructor(
private manager: ModifierManager<Opaque>,
private modifier: Opaque,
private args: EvaluatedArgs
) {
super();
this.tag = args.tag;
this.lastUpdated = args.tag.value();
}
evaluate(vm: UpdatingVM) {
let { manager, modifier, tag, lastUpdated } = this;
if (!tag.validate(lastUpdated)) {
vm.env.scheduleUpdateModifier(modifier, manager);
this.lastUpdated = tag.value();
}
}
toJSON(): OpcodeJSON {
return {
guid: this._guid,
type: this.type,
args: [JSON.stringify(this.args)]
};
}
}
export interface Attribute {
name: string;
flush(env: Environment): Option<UpdatingOpcode>;
}
export class StaticAttribute implements Attribute {
constructor(
private element: Simple.Element,
public name: string,
private value: string,
private namespace?: string
) {}
flush(env: Environment): Option<UpdatingOpcode> {
env.getAppendOperations().setAttribute(this.element, this.name, this.value, this.namespace);
return null;
}
}
export class DynamicAttribute implements Attribute {
private cache: Option<ReferenceCache<Opaque>> = null;
public tag: RevisionTag;
constructor(
private element: Simple.Element,
private attributeManager: AttributeManager,
public name: string,
private reference: Reference<Opaque>,
private namespace?: Simple.Namespace
) {
this.tag = reference.tag;
}
patch(env: Environment) {
let { element, cache } = this;
let value = expect(cache, 'must patch after flush').revalidate();
if (isModified(value)) {
this.attributeManager.updateAttribute(env, element as FIXME<Element, 'needs to be reified properly'>, value, this.namespace);
}
}
flush(env: Environment): Option<UpdatingOpcode> {
let { reference, element } = this;
if (isConstReference(reference)) {
let value = reference.value();
this.attributeManager.setAttribute(env, element, value, this.namespace);
return null;
} else {
let cache = this.cache = new ReferenceCache(reference);
let value = cache.peek();
this.attributeManager.setAttribute(env, element, value, this.namespace);
return new PatchElementOpcode(this);
}
}
toJSON(): Dict<Option<string>> {
let { element, namespace, name, cache } = this;
let formattedElement = formatElement(element);
let lastValue = expect(cache, 'must serialize after flush').peek() as string;
if (namespace) {
return {
element: formattedElement,
type: 'attribute',
namespace,
name,
lastValue
};
}
return {
element: formattedElement,
type: 'attribute',
namespace: namespace === undefined ? null : namespace,
name,
lastValue
};
}
}
function formatElement(element: Simple.Element): string {
return JSON.stringify(`<${element.tagName.toLowerCase()} />`);
}
APPEND_OPCODES.add(Op.DynamicAttrNS, (vm, { op1: _name, op2: _namespace, op3: trusting }) => {
let name = vm.constants.getString(_name);
let namespace = vm.constants.getString(_namespace);
let reference = vm.frame.getOperand<string>();
vm.stack().setDynamicAttributeNS(namespace, name, reference, !!trusting);
});
APPEND_OPCODES.add(Op.DynamicAttr, (vm, { op1: _name, op2: trusting }) => {
let name = vm.constants.getString(_name);
let reference = vm.frame.getOperand<string>();
vm.stack().setDynamicAttribute(name, reference, !!trusting);
});
export class PatchElementOpcode extends UpdatingOpcode {
public type = "patch-element";
private operation: DynamicAttribute;
constructor(operation: DynamicAttribute) {
super();
this.tag = operation.tag;
this.operation = operation;
}
evaluate(vm: UpdatingVM) {
this.operation.patch(vm.env);
}
toJSON(): OpcodeJSON {
let { _guid, type, operation } = this;
return {
guid: _guid,
type,
details: operation.toJSON()
};
}
}