| Index: sky/framework/sky-element/polymer-expressions.sky
|
| diff --git a/sky/framework/sky-element/polymer-expressions.sky b/sky/framework/sky-element/polymer-expressions.sky
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..eee7a3ec119197ba249e038fef06a55bde960d27
|
| --- /dev/null
|
| +++ b/sky/framework/sky-element/polymer-expressions.sky
|
| @@ -0,0 +1,622 @@
|
| +<!--
|
| +// Copyright 2014 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +-->
|
| +<link rel="import" href="third_party/esprima/esprima.sky" as="esprima" />
|
| +
|
| +<script>
|
| +function prepareBinding(expressionText, name, node, filterRegistry) {
|
| + var expression;
|
| + try {
|
| + expression = getExpression(expressionText);
|
| + if (expression.scopeIdent &&
|
| + (node.nodeType !== Node.ELEMENT_NODE ||
|
| + node.tagName !== 'TEMPLATE' ||
|
| + (name !== 'bind' && name !== 'repeat'))) {
|
| + throw Error('as and in can only be used within <template bind/repeat>');
|
| + }
|
| + } catch (ex) {
|
| + console.error('Invalid expression syntax: ' + expressionText, ex);
|
| + return;
|
| + }
|
| +
|
| + return function(model, node, oneTime) {
|
| + var binding = expression.getBinding(model, filterRegistry, oneTime);
|
| + if (expression.scopeIdent && binding) {
|
| + node.polymerExpressionScopeIdent_ = expression.scopeIdent;
|
| + if (expression.indexIdent)
|
| + node.polymerExpressionIndexIdent_ = expression.indexIdent;
|
| + }
|
| +
|
| + return binding;
|
| + }
|
| +}
|
| +
|
| +// TODO(rafaelw): Implement simple LRU.
|
| +var expressionParseCache = Object.create(null);
|
| +
|
| +function getExpression(expressionText) {
|
| + var expression = expressionParseCache[expressionText];
|
| + if (!expression) {
|
| + var delegate = new ASTDelegate();
|
| + esprima.parse(expressionText, delegate);
|
| + expression = new Expression(delegate);
|
| + expressionParseCache[expressionText] = expression;
|
| + }
|
| + return expression;
|
| +}
|
| +
|
| +function Literal(value) {
|
| + this.value = value;
|
| + this.valueFn_ = undefined;
|
| +}
|
| +
|
| +Literal.prototype = {
|
| + valueFn: function() {
|
| + if (!this.valueFn_) {
|
| + var value = this.value;
|
| + this.valueFn_ = function() {
|
| + return value;
|
| + }
|
| + }
|
| +
|
| + return this.valueFn_;
|
| + }
|
| +}
|
| +
|
| +function IdentPath(name) {
|
| + this.name = name;
|
| + this.path = Path.get(name);
|
| +}
|
| +
|
| +IdentPath.prototype = {
|
| + valueFn: function() {
|
| + if (!this.valueFn_) {
|
| + var name = this.name;
|
| + var path = this.path;
|
| + this.valueFn_ = function(model, observer) {
|
| + if (observer)
|
| + observer.addPath(model, path);
|
| +
|
| + return path.getValueFrom(model);
|
| + }
|
| + }
|
| +
|
| + return this.valueFn_;
|
| + },
|
| +
|
| + setValue: function(model, newValue) {
|
| + if (this.path.length == 1);
|
| + model = findScope(model, this.path[0]);
|
| +
|
| + return this.path.setValueFrom(model, newValue);
|
| + }
|
| +};
|
| +
|
| +function MemberExpression(object, property, accessor) {
|
| + this.computed = accessor == '[';
|
| +
|
| + this.dynamicDeps = typeof object == 'function' ||
|
| + object.dynamicDeps ||
|
| + (this.computed && !(property instanceof Literal));
|
| +
|
| + this.simplePath =
|
| + !this.dynamicDeps &&
|
| + (property instanceof IdentPath || property instanceof Literal) &&
|
| + (object instanceof MemberExpression || object instanceof IdentPath);
|
| +
|
| + this.object = this.simplePath ? object : getFn(object);
|
| + this.property = !this.computed || this.simplePath ?
|
| + property : getFn(property);
|
| +}
|
| +
|
| +MemberExpression.prototype = {
|
| + get fullPath() {
|
| + if (!this.fullPath_) {
|
| +
|
| + var parts = this.object instanceof MemberExpression ?
|
| + this.object.fullPath.slice() : [this.object.name];
|
| + parts.push(this.property instanceof IdentPath ?
|
| + this.property.name : this.property.value);
|
| + this.fullPath_ = Path.get(parts);
|
| + }
|
| +
|
| + return this.fullPath_;
|
| + },
|
| +
|
| + valueFn: function() {
|
| + if (!this.valueFn_) {
|
| + var object = this.object;
|
| +
|
| + if (this.simplePath) {
|
| + var path = this.fullPath;
|
| +
|
| + this.valueFn_ = function(model, observer) {
|
| + if (observer)
|
| + observer.addPath(model, path);
|
| +
|
| + return path.getValueFrom(model);
|
| + };
|
| + } else if (!this.computed) {
|
| + var path = Path.get(this.property.name);
|
| +
|
| + this.valueFn_ = function(model, observer, filterRegistry) {
|
| + var context = object(model, observer, filterRegistry);
|
| +
|
| + if (observer)
|
| + observer.addPath(context, path);
|
| +
|
| + return path.getValueFrom(context);
|
| + }
|
| + } else {
|
| + // Computed property.
|
| + var property = this.property;
|
| +
|
| + this.valueFn_ = function(model, observer, filterRegistry) {
|
| + var context = object(model, observer, filterRegistry);
|
| + var propName = property(model, observer, filterRegistry);
|
| + if (observer)
|
| + observer.addPath(context, [propName]);
|
| +
|
| + return context ? context[propName] : undefined;
|
| + };
|
| + }
|
| + }
|
| + return this.valueFn_;
|
| + },
|
| +
|
| + setValue: function(model, newValue) {
|
| + if (this.simplePath) {
|
| + this.fullPath.setValueFrom(model, newValue);
|
| + return newValue;
|
| + }
|
| +
|
| + var object = this.object(model);
|
| + var propName = this.property instanceof IdentPath ? this.property.name :
|
| + this.property(model);
|
| + return object[propName] = newValue;
|
| + }
|
| +};
|
| +
|
| +function Filter(name, args) {
|
| + this.name = name;
|
| + this.args = [];
|
| + for (var i = 0; i < args.length; i++) {
|
| + this.args[i] = getFn(args[i]);
|
| + }
|
| +}
|
| +
|
| +Filter.prototype = {
|
| + transform: function(model, observer, filterRegistry, toModelDirection,
|
| + initialArgs) {
|
| + var fn = filterRegistry[this.name];
|
| + var context = model;
|
| + if (fn) {
|
| + context = undefined;
|
| + } else {
|
| + fn = context[this.name];
|
| + if (!fn) {
|
| + console.error('Cannot find function or filter: ' + this.name);
|
| + return;
|
| + }
|
| + }
|
| +
|
| + // If toModelDirection is falsey, then the "normal" (dom-bound) direction
|
| + // is used. Otherwise, it looks for a 'toModel' property function on the
|
| + // object.
|
| + if (toModelDirection) {
|
| + fn = fn.toModel;
|
| + } else if (typeof fn.toDOM == 'function') {
|
| + fn = fn.toDOM;
|
| + }
|
| +
|
| + if (typeof fn != 'function') {
|
| + console.error('Cannot find function or filter: ' + this.name);
|
| + return;
|
| + }
|
| +
|
| + var args = initialArgs || [];
|
| + for (var i = 0; i < this.args.length; i++) {
|
| + args.push(getFn(this.args[i])(model, observer, filterRegistry));
|
| + }
|
| +
|
| + return fn.apply(context, args);
|
| + }
|
| +};
|
| +
|
| +function notImplemented() { throw Error('Not Implemented'); }
|
| +
|
| +var unaryOperators = {
|
| + '+': function(v) { return +v; },
|
| + '-': function(v) { return -v; },
|
| + '!': function(v) { return !v; }
|
| +};
|
| +
|
| +var binaryOperators = {
|
| + '+': function(l, r) { return l+r; },
|
| + '-': function(l, r) { return l-r; },
|
| + '*': function(l, r) { return l*r; },
|
| + '/': function(l, r) { return l/r; },
|
| + '%': function(l, r) { return l%r; },
|
| + '<': function(l, r) { return l<r; },
|
| + '>': function(l, r) { return l>r; },
|
| + '<=': function(l, r) { return l<=r; },
|
| + '>=': function(l, r) { return l>=r; },
|
| + '==': function(l, r) { return l==r; },
|
| + '!=': function(l, r) { return l!=r; },
|
| + '===': function(l, r) { return l===r; },
|
| + '!==': function(l, r) { return l!==r; },
|
| + '&&': function(l, r) { return l&&r; },
|
| + '||': function(l, r) { return l||r; },
|
| +};
|
| +
|
| +function getFn(arg) {
|
| + return typeof arg == 'function' ? arg : arg.valueFn();
|
| +}
|
| +
|
| +function ASTDelegate() {
|
| + this.expression = null;
|
| + this.filters = [];
|
| + this.deps = {};
|
| + this.currentPath = undefined;
|
| + this.scopeIdent = undefined;
|
| + this.indexIdent = undefined;
|
| + this.dynamicDeps = false;
|
| +}
|
| +
|
| +ASTDelegate.prototype = {
|
| + createUnaryExpression: function(op, argument) {
|
| + if (!unaryOperators[op])
|
| + throw Error('Disallowed operator: ' + op);
|
| +
|
| + argument = getFn(argument);
|
| +
|
| + return function(model, observer, filterRegistry) {
|
| + return unaryOperators[op](argument(model, observer, filterRegistry));
|
| + };
|
| + },
|
| +
|
| + createBinaryExpression: function(op, left, right) {
|
| + if (!binaryOperators[op])
|
| + throw Error('Disallowed operator: ' + op);
|
| +
|
| + left = getFn(left);
|
| + right = getFn(right);
|
| +
|
| + switch (op) {
|
| + case '||':
|
| + this.dynamicDeps = true;
|
| + return function(model, observer, filterRegistry) {
|
| + return left(model, observer, filterRegistry) ||
|
| + right(model, observer, filterRegistry);
|
| + };
|
| + case '&&':
|
| + this.dynamicDeps = true;
|
| + return function(model, observer, filterRegistry) {
|
| + return left(model, observer, filterRegistry) &&
|
| + right(model, observer, filterRegistry);
|
| + };
|
| + }
|
| +
|
| + return function(model, observer, filterRegistry) {
|
| + return binaryOperators[op](left(model, observer, filterRegistry),
|
| + right(model, observer, filterRegistry));
|
| + };
|
| + },
|
| +
|
| + createConditionalExpression: function(test, consequent, alternate) {
|
| + test = getFn(test);
|
| + consequent = getFn(consequent);
|
| + alternate = getFn(alternate);
|
| +
|
| + this.dynamicDeps = true;
|
| +
|
| + return function(model, observer, filterRegistry) {
|
| + return test(model, observer, filterRegistry) ?
|
| + consequent(model, observer, filterRegistry) :
|
| + alternate(model, observer, filterRegistry);
|
| + }
|
| + },
|
| +
|
| + createIdentifier: function(name) {
|
| + var ident = new IdentPath(name);
|
| + ident.type = 'Identifier';
|
| + return ident;
|
| + },
|
| +
|
| + createMemberExpression: function(accessor, object, property) {
|
| + var ex = new MemberExpression(object, property, accessor);
|
| + if (ex.dynamicDeps)
|
| + this.dynamicDeps = true;
|
| + return ex;
|
| + },
|
| +
|
| + createCallExpression: function(expression, args) {
|
| + if (!(expression instanceof IdentPath))
|
| + throw Error('Only identifier function invocations are allowed');
|
| +
|
| + var filter = new Filter(expression.name, args);
|
| +
|
| + return function(model, observer, filterRegistry) {
|
| + return filter.transform(model, observer, filterRegistry, false);
|
| + };
|
| + },
|
| +
|
| + createLiteral: function(token) {
|
| + return new Literal(token.value);
|
| + },
|
| +
|
| + createArrayExpression: function(elements) {
|
| + for (var i = 0; i < elements.length; i++)
|
| + elements[i] = getFn(elements[i]);
|
| +
|
| + return function(model, observer, filterRegistry) {
|
| + var arr = []
|
| + for (var i = 0; i < elements.length; i++)
|
| + arr.push(elements[i](model, observer, filterRegistry));
|
| + return arr;
|
| + }
|
| + },
|
| +
|
| + createProperty: function(kind, key, value) {
|
| + return {
|
| + key: key instanceof IdentPath ? key.name : key.value,
|
| + value: value
|
| + };
|
| + },
|
| +
|
| + createObjectExpression: function(properties) {
|
| + for (var i = 0; i < properties.length; i++)
|
| + properties[i].value = getFn(properties[i].value);
|
| +
|
| + return function(model, observer, filterRegistry) {
|
| + var obj = {};
|
| + for (var i = 0; i < properties.length; i++)
|
| + obj[properties[i].key] =
|
| + properties[i].value(model, observer, filterRegistry);
|
| + return obj;
|
| + }
|
| + },
|
| +
|
| + createFilter: function(name, args) {
|
| + this.filters.push(new Filter(name, args));
|
| + },
|
| +
|
| + createAsExpression: function(expression, scopeIdent) {
|
| + this.expression = expression;
|
| + this.scopeIdent = scopeIdent;
|
| + },
|
| +
|
| + createInExpression: function(scopeIdent, indexIdent, expression) {
|
| + this.expression = expression;
|
| + this.scopeIdent = scopeIdent;
|
| + this.indexIdent = indexIdent;
|
| + },
|
| +
|
| + createTopLevel: function(expression) {
|
| + this.expression = expression;
|
| + },
|
| +
|
| + createThisExpression: notImplemented
|
| +}
|
| +
|
| +function ConstantObservable(value) {
|
| + this.value_ = value;
|
| +}
|
| +
|
| +ConstantObservable.prototype = {
|
| + open: function() { return this.value_; },
|
| + discardChanges: function() { return this.value_; },
|
| + deliver: function() {},
|
| + close: function() {},
|
| +}
|
| +
|
| +function Expression(delegate) {
|
| + this.scopeIdent = delegate.scopeIdent;
|
| + this.indexIdent = delegate.indexIdent;
|
| +
|
| + if (!delegate.expression)
|
| + throw Error('No expression found.');
|
| +
|
| + this.expression = delegate.expression;
|
| + getFn(this.expression); // forces enumeration of path dependencies
|
| +
|
| + this.filters = delegate.filters;
|
| + this.dynamicDeps = delegate.dynamicDeps;
|
| +}
|
| +
|
| +Expression.prototype = {
|
| + getBinding: function(model, filterRegistry, oneTime) {
|
| + if (oneTime)
|
| + return this.getValue(model, undefined, filterRegistry);
|
| +
|
| + var observer = new CompoundObserver();
|
| + // captures deps.
|
| + var firstValue = this.getValue(model, observer, filterRegistry);
|
| + var firstTime = true;
|
| + var self = this;
|
| +
|
| + function valueFn() {
|
| + // deps cannot have changed on first value retrieval.
|
| + if (firstTime) {
|
| + firstTime = false;
|
| + return firstValue;
|
| + }
|
| +
|
| + if (self.dynamicDeps)
|
| + observer.startReset();
|
| +
|
| + var value = self.getValue(model,
|
| + self.dynamicDeps ? observer : undefined,
|
| + filterRegistry);
|
| + if (self.dynamicDeps)
|
| + observer.finishReset();
|
| +
|
| + return value;
|
| + }
|
| +
|
| + function setValueFn(newValue) {
|
| + self.setValue(model, newValue, filterRegistry);
|
| + return newValue;
|
| + }
|
| +
|
| + return new ObserverTransform(observer, valueFn, setValueFn, true);
|
| + },
|
| +
|
| + getValue: function(model, observer, filterRegistry) {
|
| + var value = getFn(this.expression)(model, observer, filterRegistry);
|
| + for (var i = 0; i < this.filters.length; i++) {
|
| + value = this.filters[i].transform(model, observer, filterRegistry,
|
| + false, [value]);
|
| + }
|
| +
|
| + return value;
|
| + },
|
| +
|
| + setValue: function(model, newValue, filterRegistry) {
|
| + var count = this.filters ? this.filters.length : 0;
|
| + while (count-- > 0) {
|
| + newValue = this.filters[count].transform(model, undefined,
|
| + filterRegistry, true, [newValue]);
|
| + }
|
| +
|
| + if (this.expression.setValue)
|
| + return this.expression.setValue(model, newValue);
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * Converts a style property name to a css property name. For example:
|
| + * "WebkitUserSelect" to "-webkit-user-select"
|
| + */
|
| +function convertStylePropertyName(name) {
|
| + return String(name).replace(/[A-Z]/g, function(c) {
|
| + return '-' + c.toLowerCase();
|
| + });
|
| +}
|
| +
|
| +var parentScopeName = '@' + Math.random().toString(36).slice(2);
|
| +
|
| +// Single ident paths must bind directly to the appropriate scope object.
|
| +// I.e. Pushed values in two-bindings need to be assigned to the actual model
|
| +// object.
|
| +function findScope(model, prop) {
|
| + while (model[parentScopeName] &&
|
| + !Object.prototype.hasOwnProperty.call(model, prop)) {
|
| + model = model[parentScopeName];
|
| + }
|
| +
|
| + return model;
|
| +}
|
| +
|
| +function isLiteralExpression(pathString) {
|
| + switch (pathString) {
|
| + case '':
|
| + return false;
|
| +
|
| + case 'false':
|
| + case 'null':
|
| + case 'true':
|
| + return true;
|
| + }
|
| +
|
| + if (!isNaN(Number(pathString)))
|
| + return true;
|
| +
|
| + return false;
|
| +};
|
| +
|
| +function PolymerExpressions() {}
|
| +
|
| +PolymerExpressions.prototype = {
|
| + // "built-in" filters
|
| + styleObject: function(value) {
|
| + var parts = [];
|
| + for (var key in value) {
|
| + parts.push(convertStylePropertyName(key) + ': ' + value[key]);
|
| + }
|
| + return parts.join('; ');
|
| + },
|
| +
|
| + tokenList: function(value) {
|
| + var tokens = [];
|
| + for (var key in value) {
|
| + if (value[key])
|
| + tokens.push(key);
|
| + }
|
| + return tokens.join(' ');
|
| + },
|
| +
|
| + // binding delegate API
|
| + prepareInstancePositionChanged: function(template) {
|
| + var indexIdent = template.polymerExpressionIndexIdent_;
|
| + if (!indexIdent)
|
| + return;
|
| +
|
| + return function(templateInstance, index) {
|
| + templateInstance.model[indexIdent] = index;
|
| + };
|
| + },
|
| +
|
| + prepareBinding: function(pathString, name, node) {
|
| + var path = Path.get(pathString);
|
| +
|
| + if (!isLiteralExpression(pathString) && path.valid) {
|
| + if (path.length == 1) {
|
| + return function(model, node, oneTime) {
|
| + if (oneTime)
|
| + return path.getValueFrom(model);
|
| +
|
| + var scope = findScope(model, path[0]);
|
| + return new PathObserver(scope, path);
|
| + };
|
| + }
|
| + return; // bail out early if pathString is simple path.
|
| + }
|
| +
|
| + return prepareBinding(pathString, name, node, this);
|
| + },
|
| +
|
| + prepareInstanceModel: function(template) {
|
| + var scopeName = template.polymerExpressionScopeIdent_;
|
| + if (!scopeName)
|
| + return;
|
| +
|
| + var parentScope = template.templateInstance ?
|
| + template.templateInstance.model :
|
| + template.model;
|
| +
|
| + var indexName = template.polymerExpressionIndexIdent_;
|
| +
|
| + return function(model) {
|
| + return createScopeObject(parentScope, model, scopeName, indexName);
|
| + };
|
| + }
|
| +};
|
| +
|
| +var createScopeObject = ('__proto__' in {}) ?
|
| + function(parentScope, model, scopeName, indexName) {
|
| + var scope = {};
|
| + scope[scopeName] = model;
|
| + scope[indexName] = undefined;
|
| + scope[parentScopeName] = parentScope;
|
| + scope.__proto__ = parentScope;
|
| + return scope;
|
| + } :
|
| + function(parentScope, model, scopeName, indexName) {
|
| + var scope = Object.create(parentScope);
|
| + Object.defineProperty(scope, scopeName,
|
| + { value: model, configurable: true, writable: true });
|
| + Object.defineProperty(scope, indexName,
|
| + { value: undefined, configurable: true, writable: true });
|
| + Object.defineProperty(scope, parentScopeName,
|
| + { value: parentScope, configurable: true, writable: true });
|
| + return scope;
|
| + };
|
| +
|
| +PolymerExpressions.getExpression = getExpression;
|
| +
|
| +module.exports = PolymerExpressions;
|
| +
|
| +</script>
|
|
|