Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(3866)

Unified Diff: appengine/config_service/ui/bower_components/polymer/lib/mixins/property-effects.html

Issue 2923973003: Added base template for config ui. (Closed)
Patch Set: Created 3 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: appengine/config_service/ui/bower_components/polymer/lib/mixins/property-effects.html
diff --git a/appengine/config_service/ui/bower_components/polymer/lib/mixins/property-effects.html b/appengine/config_service/ui/bower_components/polymer/lib/mixins/property-effects.html
new file mode 100644
index 0000000000000000000000000000000000000000..9dc5d3273efa8b8270122b712f986c9042ba6db5
--- /dev/null
+++ b/appengine/config_service/ui/bower_components/polymer/lib/mixins/property-effects.html
@@ -0,0 +1,2589 @@
+<!--
+@license
+Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
+This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
+The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
+The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
+Code distributed by Google as part of the polymer project is also
+subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
+-->
+
+
+<link rel="import" href="../utils/boot.html">
+<link rel="import" href="../utils/mixin.html">
+<link rel="import" href="../utils/path.html">
+<!-- for notify, reflect -->
+<link rel="import" href="../utils/case-map.html">
+<link rel="import" href="property-accessors.html">
+<!-- for annotated effects -->
+<link rel="import" href="template-stamp.html">
+
+
+<script>
+(function() {
+
+ 'use strict';
+
+ /** @const {Object} */
+ const CaseMap = Polymer.CaseMap;
+
+ // Monotonically increasing unique ID used for de-duping effects triggered
+ // from multiple properties in the same turn
+ let dedupeId = 0;
+
+ // Property effect types; effects are stored on the prototype using these keys
+ const TYPES = {
+ COMPUTE: '__computeEffects',
+ REFLECT: '__reflectEffects',
+ NOTIFY: '__notifyEffects',
+ PROPAGATE: '__propagateEffects',
+ OBSERVE: '__observeEffects',
+ READ_ONLY: '__readOnly'
+ }
+
+ /**
+ * Ensures that the model has an own-property map of effects for the given type.
+ * The model may be a prototype or an instance.
+ *
+ * Property effects are stored as arrays of effects by property in a map,
+ * by named type on the model. e.g.
+ *
+ * __computeEffects: {
+ * foo: [ ... ],
+ * bar: [ ... ]
+ * }
+ *
+ * If the model does not yet have an effect map for the type, one is created
+ * and returned. If it does, but it is not an own property (i.e. the
+ * prototype had effects), the the map is deeply cloned and the copy is
+ * set on the model and returned, ready for new effects to be added.
+ *
+ * @param {Object} model Prototype or instance
+ * @param {string} type Property effect type
+ * @return {Object} The own-property map of effects for the given type
+ * @private
+ */
+ function ensureOwnEffectMap(model, type) {
+ let effects = model[type];
+ if (!effects) {
+ effects = model[type] = {};
+ } else if (!model.hasOwnProperty(type)) {
+ effects = model[type] = Object.create(model[type]);
+ for (let p in effects) {
+ let protoFx = effects[p];
+ let instFx = effects[p] = Array(protoFx.length);
+ for (let i=0; i<protoFx.length; i++) {
+ instFx[i] = protoFx[i];
+ }
+ }
+ }
+ return effects;
+ }
+
+ // -- effects ----------------------------------------------
+
+ /**
+ * Runs all effects of a given type for the given set of property changes
+ * on an instance.
+ *
+ * @param {Object} inst The instance with effects to run
+ * @param {Object} effects Object map of property-to-Array of effects
+ * @param {Object} props Bag of current property changes
+ * @param {Object=} oldProps Bag of previous values for changed properties
+ * @param {boolean=} hasPaths True with `props` contains one or more paths
+ * @param {*=} extraArgs Additional metadata to pass to effect function
+ * @return {boolean} True if an effect ran for this property
+ * @private
+ */
+ function runEffects(inst, effects, props, oldProps, hasPaths, extraArgs) {
+ if (effects) {
+ let ran = false;
+ let id = dedupeId++;
+ for (let prop in props) {
+ if (runEffectsForProperty(inst, effects, id, prop, props, oldProps, hasPaths, extraArgs)) {
+ ran = true;
+ }
+ }
+ return ran;
+ }
+ return false;
+ }
+
+ /**
+ * Runs a list of effects for a given property.
+ *
+ * @param {Object} inst The instance with effects to run
+ * @param {Object} effects Object map of property-to-Array of effects
+ * @param {number} dedupeId Counter used for de-duping effects
+ * @param {string} prop Name of changed property
+ * @param {*} props Changed properties
+ * @param {*} oldProps Old properties
+ * @param {boolean=} hasPaths True with `props` contains one or more paths
+ * @param {*=} extraArgs Additional metadata to pass to effect function
+ * @return {boolean} True if an effect ran for this property
+ * @private
+ */
+ function runEffectsForProperty(inst, effects, dedupeId, prop, props, oldProps, hasPaths, extraArgs) {
+ let ran = false;
+ let rootProperty = hasPaths ? Polymer.Path.root(prop) : prop;
+ let fxs = effects[rootProperty];
+ if (fxs) {
+ for (let i=0, l=fxs.length, fx; (i<l) && (fx=fxs[i]); i++) {
+ if ((!fx.info || fx.info.lastRun !== dedupeId) &&
+ (!hasPaths || pathMatchesTrigger(prop, fx.trigger))) {
+ if (fx.info) {
+ fx.info.lastRun = dedupeId;
+ }
+ fx.fn(inst, prop, props, oldProps, fx.info, hasPaths, extraArgs);
+ ran = true;
+ }
+ }
+ }
+ return ran;
+ }
+
+ /**
+ * Determines whether a property/path that has changed matches the trigger
+ * criteria for an effect. A trigger is a descriptor with the following
+ * structure, which matches the descriptors returned from `parseArg`.
+ * e.g. for `foo.bar.*`:
+ * ```
+ * trigger: {
+ * name: 'a.b',
+ * structured: true,
+ * wildcard: true
+ * }
+ * ```
+ * If no trigger is given, the path is deemed to match.
+ *
+ * @param {string} path Path or property that changed
+ * @param {Object} trigger Descriptor
+ * @return {boolean} Whether the path matched the trigger
+ */
+ function pathMatchesTrigger(path, trigger) {
+ if (trigger) {
+ let triggerPath = trigger.name;
+ return (triggerPath == path) ||
+ (trigger.structured && Polymer.Path.isAncestor(triggerPath, path)) ||
+ (trigger.wildcard && Polymer.Path.isDescendant(triggerPath, path));
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Implements the "observer" effect.
+ *
+ * Calls the method with `info.methodName` on the instance, passing the
+ * new and old values.
+ *
+ * @param {Object} inst The instance the effect will be run on
+ * @param {string} property Name of property
+ * @param {Object} props Bag of current property changes
+ * @param {Object} oldProps Bag of previous values for changed properties
+ * @param {Object} info Effect metadata
+ * @private
+ */
+ function runObserverEffect(inst, property, props, oldProps, info) {
+ let fn = inst[info.methodName];
+ let changedProp = info.property;
+ if (fn) {
+ fn.call(inst, inst.__data[changedProp], oldProps[changedProp]);
+ } else if (!info.dynamicFn) {
+ console.warn('observer method `' + info.methodName + '` not defined');
+ }
+ }
+
+ /**
+ * Runs "notify" effects for a set of changed properties.
+ *
+ * This method differs from the generic `runEffects` method in that it
+ * will dispatch path notification events in the case that the property
+ * changed was a path and the root property for that path didn't have a
+ * "notify" effect. This is to maintain 1.0 behavior that did not require
+ * `notify: true` to ensure object sub-property notifications were
+ * sent.
+ *
+ * @param {Element} inst The instance with effects to run
+ * @param {Object} notifyProps Bag of properties to notify
+ * @param {Object} props Bag of current property changes
+ * @param {Object} oldProps Bag of previous values for changed properties
+ * @param {boolean} hasPaths True with `props` contains one or more paths
+ * @private
+ */
+ function runNotifyEffects(inst, notifyProps, props, oldProps, hasPaths) {
+ // Notify
+ let fxs = inst.__notifyEffects;
+ let notified;
+ let id = dedupeId++;
+ // Try normal notify effects; if none, fall back to try path notification
+ for (let prop in notifyProps) {
+ if (notifyProps[prop]) {
+ if (fxs && runEffectsForProperty(inst, fxs, id, prop, props, oldProps, hasPaths)) {
+ notified = true;
+ } else if (hasPaths && notifyPath(inst, prop, props)) {
+ notified = true;
+ }
+ }
+ }
+ // Flush host if we actually notified and host was batching
+ // And the host has already initialized clients; this prevents
+ // an issue with a host observing data changes before clients are ready.
+ let host;
+ if (notified && (host = inst.__dataHost) && host._invalidateProperties) {
+ host._invalidateProperties();
+ }
+ }
+
+ /**
+ * Dispatches {property}-changed events with path information in the detail
+ * object to indicate a sub-path of the property was changed.
+ *
+ * @param {Element} inst The element from which to fire the event
+ * @param {string} path The path that was changed
+ * @param {Object} props Bag of current property changes
+ * @return {boolean} Returns true if the path was notified
+ * @private
+ */
+ function notifyPath(inst, path, props) {
+ let rootProperty = Polymer.Path.root(path);
+ if (rootProperty !== path) {
+ let eventName = Polymer.CaseMap.camelToDashCase(rootProperty) + '-changed';
+ dispatchNotifyEvent(inst, eventName, props[path], path);
+ return true;
+ }
+ }
+
+ /**
+ * Dispatches {property}-changed events to indicate a property (or path)
+ * changed.
+ *
+ * @param {Element} inst The element from which to fire the event
+ * @param {string} eventName The name of the event to send ('{property}-changed')
+ * @param {*} value The value of the changed property
+ * @param {string | null | undefined} path If a sub-path of this property changed, the path
+ * that changed (optional).
+ * @private
+ */
+ function dispatchNotifyEvent(inst, eventName, value, path) {
+ let detail = {
+ value: value,
+ queueProperty: true
+ };
+ if (path) {
+ detail.path = path;
+ }
+ inst.dispatchEvent(new CustomEvent(eventName, { detail }));
+ }
+
+ /**
+ * Implements the "notify" effect.
+ *
+ * Dispatches a non-bubbling event named `info.eventName` on the instance
+ * with a detail object containing the new `value`.
+ *
+ * @param {Element} inst The instance the effect will be run on
+ * @param {string} property Name of property
+ * @param {Object} props Bag of current property changes
+ * @param {Object} oldProps Bag of previous values for changed properties
+ * @param {Object} info Effect metadata
+ * @param {boolean} hasPaths True with `props` contains one or more paths
+ * @private
+ */
+ function runNotifyEffect(inst, property, props, oldProps, info, hasPaths) {
+ let rootProperty = hasPaths ? Polymer.Path.root(property) : property;
+ let path = rootProperty != property ? property : null;
+ let value = path ? Polymer.Path.get(inst, path) : inst.__data[property];
+ if (path && value === undefined) {
+ value = props[property]; // specifically for .splices
+ }
+ dispatchNotifyEvent(inst, info.eventName, value, path);
+ }
+
+ /**
+ * Handler function for 2-way notification events. Receives context
+ * information captured in the `addNotifyListener` closure from the
+ * `__notifyListeners` metadata.
+ *
+ * Sets the value of the notified property to the host property or path. If
+ * the event contained path information, translate that path to the host
+ * scope's name for that path first.
+ *
+ * @param {Event} event Notification event (e.g. '<property>-changed')
+ * @param {Object} inst Host element instance handling the notification event
+ * @param {string} fromProp Child element property that was bound
+ * @param {string} toPath Host property/path that was bound
+ * @param {boolean} negate Whether the binding was negated
+ * @private
+ */
+ function handleNotification(event, inst, fromProp, toPath, negate) {
+ let value;
+ let detail = event.detail;
+ let fromPath = detail && detail.path;
+ if (fromPath) {
+ toPath = Polymer.Path.translate(fromProp, toPath, fromPath);
+ value = detail && detail.value;
+ } else {
+ value = event.target[fromProp];
+ }
+ value = negate ? !value : value;
+ if (!inst.__readOnly || !inst.__readOnly[toPath]) {
+ if (inst._setPendingPropertyOrPath(toPath, value, true, Boolean(fromPath))
+ && (!detail || !detail.queueProperty)) {
+ inst._invalidateProperties();
+ }
+ }
+ }
+
+ /**
+ * Implements the "reflect" effect.
+ *
+ * Sets the attribute named `info.attrName` to the given property value.
+ *
+ * @param {Object} inst The instance the effect will be run on
+ * @param {string} property Name of property
+ * @param {Object} props Bag of current property changes
+ * @param {Object} oldProps Bag of previous values for changed properties
+ * @param {Object} info Effect metadata
+ * @private
+ */
+ function runReflectEffect(inst, property, props, oldProps, info) {
+ let value = inst.__data[property];
+ if (Polymer.sanitizeDOMValue) {
+ value = Polymer.sanitizeDOMValue(value, info.attrName, 'attribute', inst);
+ }
+ inst._propertyToAttribute(property, info.attrName, value);
+ }
+
+ /**
+ * Runs "computed" effects for a set of changed properties.
+ *
+ * This method differs from the generic `runEffects` method in that it
+ * continues to run computed effects based on the output of each pass until
+ * there are no more newly computed properties. This ensures that all
+ * properties that will be computed by the initial set of changes are
+ * computed before other effects (binding propagation, observers, and notify)
+ * run.
+ *
+ * @param {Element} inst The instance the effect will be run on
+ * @param {Object} changedProps Bag of changed properties
+ * @param {Object} oldProps Bag of previous values for changed properties
+ * @param {boolean} hasPaths True with `props` contains one or more paths
+ * @private
+ */
+ function runComputedEffects(inst, changedProps, oldProps, hasPaths) {
+ let computeEffects = inst.__computeEffects;
+ if (computeEffects) {
+ let inputProps = changedProps;
+ while (runEffects(inst, computeEffects, inputProps, oldProps, hasPaths)) {
+ Object.assign(oldProps, inst.__dataOld);
+ Object.assign(changedProps, inst.__dataPending);
+ inputProps = inst.__dataPending;
+ inst.__dataPending = null;
+ }
+ }
+ }
+
+ /**
+ * Implements the "computed property" effect by running the method with the
+ * values of the arguments specified in the `info` object and setting the
+ * return value to the computed property specified.
+ *
+ * @param {Object} inst The instance the effect will be run on
+ * @param {string} property Name of property
+ * @param {Object} props Bag of current property changes
+ * @param {Object} oldProps Bag of previous values for changed properties
+ * @param {Object} info Effect metadata
+ * @private
+ */
+ function runComputedEffect(inst, property, props, oldProps, info) {
+ let result = runMethodEffect(inst, property, props, oldProps, info);
+ let computedProp = info.methodInfo;
+ if (inst.__dataHasAccessor && inst.__dataHasAccessor[computedProp]) {
+ inst._setPendingProperty(computedProp, result, true);
+ } else {
+ inst[computedProp] = result;
+ }
+ }
+
+ /**
+ * Computes path changes based on path links set up using the `linkPaths`
+ * API.
+ *
+ * @param {Element} inst The instance whose props are changing
+ * @param {string} path Path that has changed
+ * @param {*} value Value of changed path
+ * @private
+ */
+ function computeLinkedPaths(inst, path, value) {
+ let links = inst.__dataLinkedPaths;
+ if (links) {
+ let link;
+ for (let a in links) {
+ let b = links[a];
+ if (Polymer.Path.isDescendant(a, path)) {
+ link = Polymer.Path.translate(a, b, path);
+ inst._setPendingPropertyOrPath(link, value, true, true);
+ } else if (Polymer.Path.isDescendant(b, path)) {
+ link = Polymer.Path.translate(b, a, path);
+ inst._setPendingPropertyOrPath(link, value, true, true);
+ }
+ }
+ }
+ }
+
+ // -- bindings ----------------------------------------------
+
+ /**
+ * Adds binding metadata to the current `nodeInfo`, and binding effects
+ * for all part dependencies to `templateInfo`.
+ *
+ * @param {Function} constructor Class that `_parseTemplate` is currently
+ * running on
+ * @param {Object} templateInfo Template metadata for current template
+ * @param {Object} nodeInfo Node metadata for current template node
+ * @param {string} kind Binding kind, either 'property', 'attribute', or 'text'
+ * @param {string} target Target property name
+ * @param {Array<Object>} parts Array of binding part metadata
+ * @param {string} literal Literal text surrounding binding parts (specified
+ * only for 'property' bindings, since these must be initialized as part
+ * of boot-up)
+ * @private
+ */
+ function addBinding(constructor, templateInfo, nodeInfo, kind, target, parts, literal) {
+ // Create binding metadata and add to nodeInfo
+ nodeInfo.bindings = nodeInfo.bindings || [];
+ let binding = { kind, target, parts, literal, isCompound: (parts.length !== 1) };
+ nodeInfo.bindings.push(binding);
+ // Add listener info to binding metadata
+ if (shouldAddListener(binding)) {
+ let {event, negate} = binding.parts[0];
+ binding.listenerEvent = event || (CaseMap.camelToDashCase(target) + '-changed');
+ binding.listenerNegate = negate;
+ }
+ // Add "propagate" property effects to templateInfo
+ let index = templateInfo.nodeInfoList.length;
+ for (let i=0; i<binding.parts.length; i++) {
+ let part = binding.parts[i];
+ part.compoundIndex = i;
+ addEffectForBindingPart(constructor, templateInfo, binding, part, index);
+ }
+ }
+
+ /**
+ * Adds property effects to the given `templateInfo` for the given binding
+ * part.
+ *
+ * @param {Function} constructor Class that `_parseTemplate` is currently
+ * running on
+ * @param {Object} templateInfo Template metadata for current template
+ * @param {Object} binding Binding metadata
+ * @param {Object} part Binding part metadata
+ * @param {number} index Index into `nodeInfoList` for this node
+ */
+ function addEffectForBindingPart(constructor, templateInfo, binding, part, index) {
+ if (!part.literal) {
+ if (binding.kind === 'attribute' && binding.target[0] === '-') {
+ console.warn('Cannot set attribute ' + binding.target +
+ ' because "-" is not a valid attribute starting character');
+ } else {
+ let dependencies = part.dependencies;
+ let info = { index, binding, part, evaluator: constructor };
+ for (let j=0; j<dependencies.length; j++) {
+ let trigger = dependencies[j];
+ if (typeof trigger == 'string') {
+ trigger = parseArg(trigger);
+ trigger.wildcard = true;
+ }
+ constructor._addTemplatePropertyEffect(templateInfo, trigger.rootProperty, {
+ fn: runBindingEffect,
+ info, trigger
+ });
+ }
+ }
+ }
+ }
+
+ /**
+ * Implements the "binding" (property/path binding) effect.
+ *
+ * Note that binding syntax is overridable via `_parseBindings` and
+ * `_evaluateBinding`. This method will call `_evaluateBinding` for any
+ * non-literal parts returned from `_parseBindings`. However,
+ * there is no support for _path_ bindings via custom binding parts,
+ * as this is specific to Polymer's path binding syntax.
+ *
+ * @param {Element} inst The instance the effect will be run on
+ * @param {string} path Name of property
+ * @param {Object} props Bag of current property changes
+ * @param {Object} oldProps Bag of previous values for changed properties
+ * @param {Object} info Effect metadata
+ * @param {boolean} hasPaths True with `props` contains one or more paths
+ * @param {Array} nodeList List of nodes associated with `nodeInfoList` template
+ * metadata
+ * @private
+ */
+ function runBindingEffect(inst, path, props, oldProps, info, hasPaths, nodeList) {
+ let node = nodeList[info.index];
+ let binding = info.binding;
+ let part = info.part;
+ // Subpath notification: transform path and set to client
+ // e.g.: foo="{{obj.sub}}", path: 'obj.sub.prop', set 'foo.prop'=obj.sub.prop
+ if (hasPaths && part.source && (path.length > part.source.length) &&
+ (binding.kind == 'property') && !binding.isCompound &&
+ node.__dataHasAccessor && node.__dataHasAccessor[binding.target]) {
+ let value = props[path];
+ path = Polymer.Path.translate(part.source, binding.target, path);
+ if (node._setPendingPropertyOrPath(path, value, false, true)) {
+ inst._enqueueClient(node);
+ }
+ } else {
+ let value = info.evaluator._evaluateBinding(inst, part, path, props, oldProps, hasPaths);
+ // Propagate value to child
+ applyBindingValue(inst, node, binding, part, value);
+ }
+ }
+
+ /**
+ * Sets the value for an "binding" (binding) effect to a node,
+ * either as a property or attribute.
+ *
+ * @param {Object} inst The instance owning the binding effect
+ * @param {Node} node Target node for binding
+ * @param {Object} binding Binding metadata
+ * @param {Object} part Binding part metadata
+ * @param {*} value Value to set
+ * @private
+ */
+ function applyBindingValue(inst, node, binding, part, value) {
+ value = computeBindingValue(node, value, binding, part);
+ if (Polymer.sanitizeDOMValue) {
+ value = Polymer.sanitizeDOMValue(value, binding.target, binding.kind, node);
+ }
+ if (binding.kind == 'attribute') {
+ // Attribute binding
+ inst._valueToNodeAttribute(node, value, binding.target);
+ } else {
+ // Property binding
+ let prop = binding.target;
+ if (node.__dataHasAccessor && node.__dataHasAccessor[prop]) {
+ if (!node.__readOnly || !node.__readOnly[prop]) {
+ if (node._setPendingProperty(prop, value)) {
+ inst._enqueueClient(node);
+ }
+ }
+ } else {
+ inst._setUnmanagedPropertyToNode(node, prop, value);
+ }
+ }
+ }
+
+ /**
+ * Transforms an "binding" effect value based on compound & negation
+ * effect metadata, as well as handling for special-case properties
+ *
+ * @param {Node} node Node the value will be set to
+ * @param {*} value Value to set
+ * @param {Object} binding Binding metadata
+ * @param {Object} part Binding part metadata
+ * @return {*} Transformed value to set
+ * @private
+ */
+ function computeBindingValue(node, value, binding, part) {
+ if (binding.isCompound) {
+ let storage = node.__dataCompoundStorage[binding.target];
+ storage[part.compoundIndex] = value;
+ value = storage.join('');
+ }
+ if (binding.kind !== 'attribute') {
+ // Some browsers serialize `undefined` to `"undefined"`
+ if (binding.target === 'textContent' ||
+ (node.localName == 'input' && binding.target == 'value')) {
+ value = value == undefined ? '' : value;
+ }
+ }
+ return value;
+ }
+
+ /**
+ * Returns true if a binding's metadata meets all the requirements to allow
+ * 2-way binding, and therefore a `<property>-changed` event listener should be
+ * added:
+ * - used curly braces
+ * - is a property (not attribute) binding
+ * - is not a textContent binding
+ * - is not compound
+ *
+ * @param {Object} binding Binding metadata
+ * @return {boolean} True if 2-way listener should be added
+ * @private
+ */
+ function shouldAddListener(binding) {
+ return binding.target &&
+ binding.kind != 'attribute' &&
+ binding.kind != 'text' &&
+ !binding.isCompound &&
+ binding.parts[0].mode === '{';
+ }
+
+ /**
+ * Setup compound binding storage structures, notify listeners, and dataHost
+ * references onto the bound nodeList.
+ *
+ * @param {Object} inst Instance that bas been previously bound
+ * @param {Object} templateInfo Template metadata
+ * @private
+ */
+ function setupBindings(inst, templateInfo) {
+ // Setup compound storage, dataHost, and notify listeners
+ let {nodeList, nodeInfoList} = templateInfo;
+ if (nodeInfoList.length) {
+ for (let i=0; i < nodeInfoList.length; i++) {
+ let info = nodeInfoList[i];
+ let node = nodeList[i];
+ let bindings = info.bindings;
+ if (bindings) {
+ for (let i=0; i<bindings.length; i++) {
+ let binding = bindings[i];
+ setupCompoundStorage(node, binding);
+ addNotifyListener(node, inst, binding);
+ }
+ }
+ node.__dataHost = inst;
+ }
+ }
+ }
+
+ /**
+ * Initializes `__dataCompoundStorage` local storage on a bound node with
+ * initial literal data for compound bindings, and sets the joined
+ * literal parts to the bound property.
+ *
+ * When changes to compound parts occur, they are first set into the compound
+ * storage array for that property, and then the array is joined to result in
+ * the final value set to the property/attribute.
+ *
+ * @param {Node} node Bound node to initialize
+ * @param {Object} binding Binding metadata
+ * @private
+ */
+ function setupCompoundStorage(node, binding) {
+ if (binding.isCompound) {
+ // Create compound storage map
+ let storage = node.__dataCompoundStorage ||
+ (node.__dataCompoundStorage = {});
+ let parts = binding.parts;
+ // Copy literals from parts into storage for this binding
+ let literals = new Array(parts.length);
+ for (let j=0; j<parts.length; j++) {
+ literals[j] = parts[j].literal;
+ }
+ let target = binding.target;
+ storage[target] = literals;
+ // Configure properties with their literal parts
+ if (binding.literal && binding.kind == 'property') {
+ node[target] = binding.literal;
+ }
+ }
+ }
+
+ /**
+ * Adds a 2-way binding notification event listener to the node specified
+ *
+ * @param {Object} node Child element to add listener to
+ * @param {Object} inst Host element instance to handle notification event
+ * @param {Object} binding Binding metadata
+ * @private
+ */
+ function addNotifyListener(node, inst, binding) {
+ if (binding.listenerEvent) {
+ let part = binding.parts[0];
+ node.addEventListener(binding.listenerEvent, function(e) {
+ handleNotification(e, inst, binding.target, part.source, part.negate);
+ });
+ }
+ }
+
+ // -- for method-based effects (complexObserver & computed) --------------
+
+ /**
+ * Adds property effects for each argument in the method signature (and
+ * optionally, for the method name if `dynamic` is true) that calls the
+ * provided effect function.
+ *
+ * @param {Element | Object} model Prototype or instance
+ * @param {Object} sig Method signature metadata
+ * @param {string} type Type of property effect to add
+ * @param {Function} effectFn Function to run when arguments change
+ * @param {*=} methodInfo Effect-specific information to be included in
+ * method effect metadata
+ * @param {boolean|Object=} dynamicFn Boolean or object map indicating whether
+ * method names should be included as a dependency to the effect. Note,
+ * defaults to true if the signature is static (sig.static is true).
+ * @private
+ */
+ function createMethodEffect(model, sig, type, effectFn, methodInfo, dynamicFn) {
+ dynamicFn = sig.static || (dynamicFn &&
+ (typeof dynamicFn !== 'object' || dynamicFn[sig.methodName]));
+ let info = {
+ methodName: sig.methodName,
+ args: sig.args,
+ methodInfo,
+ dynamicFn
+ };
+ for (let i=0, arg; (i<sig.args.length) && (arg=sig.args[i]); i++) {
+ if (!arg.literal) {
+ model._addPropertyEffect(arg.rootProperty, type, {
+ fn: effectFn, info: info, trigger: arg
+ });
+ }
+ }
+ if (dynamicFn) {
+ model._addPropertyEffect(sig.methodName, type, {
+ fn: effectFn, info: info
+ });
+ }
+ }
+
+ /**
+ * Calls a method with arguments marshaled from properties on the instance
+ * based on the method signature contained in the effect metadata.
+ *
+ * Multi-property observers, computed properties, and inline computing
+ * functions call this function to invoke the method, then use the return
+ * value accordingly.
+ *
+ * @param {Object} inst The instance the effect will be run on
+ * @param {string} property Name of property
+ * @param {Object} props Bag of current property changes
+ * @param {Object} oldProps Bag of previous values for changed properties
+ * @param {Object} info Effect metadata
+ * @return {*} Returns the return value from the method invocation
+ * @private
+ */
+ function runMethodEffect(inst, property, props, oldProps, info) {
+ // Instances can optionally have a _methodHost which allows redirecting where
+ // to find methods. Currently used by `templatize`.
+ let context = inst._methodHost || inst;
+ let fn = context[info.methodName];
+ if (fn) {
+ let args = marshalArgs(inst.__data, info.args, property, props);
+ return fn.apply(context, args);
+ } else if (!info.dynamicFn) {
+ console.warn('method `' + info.methodName + '` not defined');
+ }
+ }
+
+ const emptyArray = [];
+
+ // Regular expressions used for binding
+ const IDENT = '(?:' + '[a-zA-Z_$][\\w.:$\\-*]*' + ')';
+ const NUMBER = '(?:' + '[-+]?[0-9]*\\.?[0-9]+(?:[eE][-+]?[0-9]+)?' + ')';
+ const SQUOTE_STRING = '(?:' + '\'(?:[^\'\\\\]|\\\\.)*\'' + ')';
+ const DQUOTE_STRING = '(?:' + '"(?:[^"\\\\]|\\\\.)*"' + ')';
+ const STRING = '(?:' + SQUOTE_STRING + '|' + DQUOTE_STRING + ')';
+ const ARGUMENT = '(?:' + IDENT + '|' + NUMBER + '|' + STRING + '\\s*' + ')';
+ const ARGUMENTS = '(?:' + ARGUMENT + '(?:,\\s*' + ARGUMENT + ')*' + ')';
+ const ARGUMENT_LIST = '(?:' + '\\(\\s*' +
+ '(?:' + ARGUMENTS + '?' + ')' +
+ '\\)\\s*' + ')';
+ const BINDING = '(' + IDENT + '\\s*' + ARGUMENT_LIST + '?' + ')'; // Group 3
+ const OPEN_BRACKET = '(\\[\\[|{{)' + '\\s*';
+ const CLOSE_BRACKET = '(?:]]|}})';
+ const NEGATE = '(?:(!)\\s*)?'; // Group 2
+ const EXPRESSION = OPEN_BRACKET + NEGATE + BINDING + CLOSE_BRACKET;
+ const bindingRegex = new RegExp(EXPRESSION, "g");
+
+ function literalFromParts(parts) {
+ let s = '';
+ for (let i=0; i<parts.length; i++) {
+ let literal = parts[i].literal;
+ s += literal || '';
+ }
+ return s;
+ }
+
+ /**
+ * Parses an expression string for a method signature, and returns a metadata
+ * describing the method in terms of `methodName`, `static` (whether all the
+ * arguments are literals), and an array of `args`
+ *
+ * @param {string} expression The expression to parse
+ * @return {?Object} The method metadata object if a method expression was
+ * found, otherwise `undefined`
+ * @private
+ */
+ function parseMethod(expression) {
+ // tries to match valid javascript property names
+ let m = expression.match(/([^\s]+?)\(([\s\S]*)\)/);
+ if (m) {
+ let methodName = m[1];
+ let sig = { methodName, static: true };
+ if (m[2].trim()) {
+ // replace escaped commas with comma entity, split on un-escaped commas
+ let args = m[2].replace(/\\,/g, '&comma;').split(',');
+ return parseArgs(args, sig);
+ } else {
+ sig.args = emptyArray;
+ return sig;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Parses an array of arguments and sets the `args` property of the supplied
+ * signature metadata object. Sets the `static` property to false if any
+ * argument is a non-literal.
+ *
+ * @param {Array<string>} argList Array of argument names
+ * @param {Object} sig Method signature metadata object
+ * @return {Object} The updated signature metadata object
+ * @private
+ */
+ function parseArgs(argList, sig) {
+ sig.args = argList.map(function(rawArg) {
+ let arg = parseArg(rawArg);
+ if (!arg.literal) {
+ sig.static = false;
+ }
+ return arg;
+ }, this);
+ return sig;
+ }
+
+ /**
+ * Parses an individual argument, and returns an argument metadata object
+ * with the following fields:
+ *
+ * {
+ * value: 'prop', // property/path or literal value
+ * literal: false, // whether argument is a literal
+ * structured: false, // whether the property is a path
+ * rootProperty: 'prop', // the root property of the path
+ * wildcard: false // whether the argument was a wildcard '.*' path
+ * }
+ *
+ * @param {string} rawArg The string value of the argument
+ * @return {Object} Argument metadata object
+ * @private
+ */
+ function parseArg(rawArg) {
+ // clean up whitespace
+ let arg = rawArg.trim()
+ // replace comma entity with comma
+ .replace(/&comma;/g, ',')
+ // repair extra escape sequences; note only commas strictly need
+ // escaping, but we allow any other char to be escaped since its
+ // likely users will do this
+ .replace(/\\(.)/g, '\$1')
+ ;
+ // basic argument descriptor
+ let a = {
+ name: arg
+ };
+ // detect literal value (must be String or Number)
+ let fc = arg[0];
+ if (fc === '-') {
+ fc = arg[1];
+ }
+ if (fc >= '0' && fc <= '9') {
+ fc = '#';
+ }
+ switch(fc) {
+ case "'":
+ case '"':
+ a.value = arg.slice(1, -1);
+ a.literal = true;
+ break;
+ case '#':
+ a.value = Number(arg);
+ a.literal = true;
+ break;
+ }
+ // if not literal, look for structured path
+ if (!a.literal) {
+ a.rootProperty = Polymer.Path.root(arg);
+ // detect structured path (has dots)
+ a.structured = Polymer.Path.isPath(arg);
+ if (a.structured) {
+ a.wildcard = (arg.slice(-2) == '.*');
+ if (a.wildcard) {
+ a.name = arg.slice(0, -2);
+ }
+ }
+ }
+ return a;
+ }
+
+ /**
+ * Gather the argument values for a method specified in the provided array
+ * of argument metadata.
+ *
+ * The `path` and `value` arguments are used to fill in wildcard descriptor
+ * when the method is being called as a result of a path notification.
+ *
+ * @param {Object} data Instance data storage object to read properties from
+ * @param {Array<Object>} args Array of argument metadata
+ * @param {string} path Property/path name that triggered the method effect
+ * @param {Object} props Bag of current property changes
+ * @return {Array<*>} Array of argument values
+ * @private
+ */
+ function marshalArgs(data, args, path, props) {
+ let values = [];
+ for (let i=0, l=args.length; i<l; i++) {
+ let arg = args[i];
+ let name = arg.name;
+ let v;
+ if (arg.literal) {
+ v = arg.value;
+ } else {
+ if (arg.structured) {
+ v = Polymer.Path.get(data, name);
+ // when data is not stored e.g. `splices`
+ if (v === undefined) {
+ v = props[name];
+ }
+ } else {
+ v = data[name];
+ }
+ }
+ if (arg.wildcard) {
+ // Only send the actual path changed info if the change that
+ // caused the observer to run matched the wildcard
+ let baseChanged = (name.indexOf(path + '.') === 0);
+ let matches = (path.indexOf(name) === 0 && !baseChanged);
+ values[i] = {
+ path: matches ? path : name,
+ value: matches ? props[path] : v,
+ base: v
+ };
+ } else {
+ values[i] = v;
+ }
+ }
+ return values;
+ }
+
+ // data api
+
+ /**
+ * Sends array splice notifications (`.splices` and `.length`)
+ *
+ * Note: this implementation only accepts normalized paths
+ *
+ * @param {Element} inst Instance to send notifications to
+ * @param {Array} array The array the mutations occurred on
+ * @param {string} path The path to the array that was mutated
+ * @param {Array} splices Array of splice records
+ * @private
+ */
+ function notifySplices(inst, array, path, splices) {
+ let splicesPath = path + '.splices';
+ inst.notifyPath(splicesPath, { indexSplices: splices });
+ inst.notifyPath(path + '.length', array.length);
+ // Null here to allow potentially large splice records to be GC'ed.
+ inst.__data[splicesPath] = {indexSplices: null};
+ }
+
+ /**
+ * Creates a splice record and sends an array splice notification for
+ * the described mutation
+ *
+ * Note: this implementation only accepts normalized paths
+ *
+ * @param {Element} inst Instance to send notifications to
+ * @param {Array} array The array the mutations occurred on
+ * @param {string} path The path to the array that was mutated
+ * @param {number} index Index at which the array mutation occurred
+ * @param {number} addedCount Number of added items
+ * @param {Array} removed Array of removed items
+ * @private
+ */
+ function notifySplice(inst, array, path, index, addedCount, removed) {
+ notifySplices(inst, array, path, [{
+ index: index,
+ addedCount: addedCount,
+ removed: removed,
+ object: array,
+ type: 'splice'
+ }]);
+ }
+
+ /**
+ * Returns an upper-cased version of the string.
+ *
+ * @param {string} name String to uppercase
+ * @return {string} Uppercased string
+ * @private
+ */
+ function upper(name) {
+ return name[0].toUpperCase() + name.substring(1);
+ }
+
+ /**
+ * Element class mixin that provides meta-programming for Polymer's template
+ * binding and data observation (collectively, "property effects") system.
+ *
+ * This mixin uses provides the following key static methods for adding
+ * property effects to an element class:
+ * - `addPropertyEffect`
+ * - `createPropertyObserver`
+ * - `createMethodObserver`
+ * - `createNotifyingProperty`
+ * - `createReadOnlyProperty`
+ * - `createReflectedProperty`
+ * - `createComputedProperty`
+ * - `bindTemplate`
+ *
+ * Each method creates one or more property accessors, along with metadata
+ * used by this mixin's implementation of `_propertiesChanged` to perform
+ * the property effects.
+ *
+ * Underscored versions of the above methods also exist on the element
+ * prototype for adding property effects on instances at runtime.
+ *
+ * Note that this mixin overrides several `PropertyAccessors` methods, in
+ * many cases to maintain guarantees provided by the Polymer 1.x features;
+ * notably it changes property accessors to be synchronous by default
+ * whereas the default when using `PropertyAccessors` standalone is to be
+ * async by default.
+ *
+ * @polymerMixin
+ * @mixes Polymer.TemplateStamp
+ * @mixes Polymer.PropertyAccessors
+ * @memberof Polymer
+ * @summary Element class mixin that provides meta-programming for Polymer's
+ * template binding and data observation system.
+ */
+ Polymer.PropertyEffects = Polymer.dedupingMixin(superClass => {
+
+ /**
+ * @constructor
+ * @extends {superClass}
+ * @implements {Polymer_PropertyAccessors}
+ * @implements {Polymer_TemplateStamp}
+ */
+ const propertyEffectsBase = Polymer.TemplateStamp(Polymer.PropertyAccessors(superClass));
+
+ /**
+ * @polymerMixinClass
+ * @unrestricted
+ * @implements {Polymer_PropertyEffects}
+ */
+ class PropertyEffects extends propertyEffectsBase {
+
+ get PROPERTY_EFFECT_TYPES() {
+ return TYPES;
+ }
+
+ /**
+ * Overrides `Polymer.PropertyAccessors` implementation to initialize
+ * additional property-effect related properties.
+ *
+ * @override
+ */
+ _initializeProperties() {
+ super._initializeProperties();
+ hostStack.registerHost(this);
+ this.__dataClientsReady = false;
+ this.__dataPendingClients = null;
+ this.__dataToNotify = null;
+ this.__dataLinkedPaths = null;
+ this.__dataHasPaths = false;
+ // May be set on instance prior to upgrade
+ this.__dataCompoundStorage = this.__dataCompoundStorage || null;
+ this.__dataHost = this.__dataHost || null;
+ this.__dataTemp = {};
+ }
+
+ /**
+ * Overrides `Polymer.PropertyAccessors` implementation to provide a
+ * more efficient implementation of initializing properties from
+ * the prototype on the instance.
+ *
+ * @override
+ */
+ _initializeProtoProperties(props) {
+ this.__data = Object.create(props);
+ this.__dataPending = Object.create(props);
+ this.__dataOld = {};
+ }
+
+ /**
+ * Overrides `Polymer.PropertyAccessors` implementation to avoid setting
+ * `_setProperty`'s `shouldNotify: true`.
+ *
+ * @override
+ */
+ _initializeInstanceProperties(props) {
+ let readOnly = this.__readOnly;
+ for (let prop in props) {
+ if (!readOnly || !readOnly[prop]) {
+ this.__dataPending = this.__dataPending || {};
+ this.__dataOld = this.__dataOld || {};
+ this.__data[prop] = this.__dataPending[prop] = props[prop];
+ }
+ }
+ }
+
+ // Prototype setup ----------------------------------------
+
+ /**
+ * Equivalent to static `addPropertyEffect` API but can be called on
+ * an instance to add effects at runtime. See that method for
+ * full API docs.
+ *
+ * @param {string} property Property that should trigger the effect
+ * @param {string} type Effect type, from this.PROPERTY_EFFECT_TYPES
+ * @param {Object=} effect Effect metadata object
+ * @protected
+ */
+ _addPropertyEffect(property, type, effect) {
+ this._createPropertyAccessor(property, type == TYPES.READ_ONLY);
+ // effects are accumulated into arrays per property based on type
+ let effects = ensureOwnEffectMap(this, type)[property];
+ if (!effects) {
+ effects = this[type][property] = [];
+ }
+ effects.push(effect);
+ }
+
+ /**
+ * Removes the given property effect.
+ *
+ * @param {string} property Property the effect was associated with
+ * @param {string} type Effect type, from this.PROPERTY_EFFECT_TYPES
+ * @param {Object=} effect Effect metadata object to remove
+ */
+ _removePropertyEffect(property, type, effect) {
+ let effects = ensureOwnEffectMap(this, type)[property];
+ let idx = effects.indexOf(effect);
+ if (idx >= 0) {
+ effects.splice(idx, 1);
+ }
+ }
+
+ /**
+ * Returns whether the current prototype/instance has a property effect
+ * of a certain type.
+ *
+ * @param {string} property Property name
+ * @param {string=} type Effect type, from this.PROPERTY_EFFECT_TYPES
+ * @return {boolean} True if the prototype/instance has an effect of this type
+ * @protected
+ */
+ _hasPropertyEffect(property, type) {
+ let effects = this[type];
+ return Boolean(effects && effects[property]);
+ }
+
+ /**
+ * Returns whether the current prototype/instance has a "read only"
+ * accessor for the given property.
+ *
+ * @param {string} property Property name
+ * @return {boolean} True if the prototype/instance has an effect of this type
+ * @protected
+ */
+ _hasReadOnlyEffect(property) {
+ return this._hasPropertyEffect(property, TYPES.READ_ONLY);
+ }
+
+ /**
+ * Returns whether the current prototype/instance has a "notify"
+ * property effect for the given property.
+ *
+ * @param {string} property Property name
+ * @return {boolean} True if the prototype/instance has an effect of this type
+ * @protected
+ */
+ _hasNotifyEffect(property) {
+ return this._hasPropertyEffect(property, TYPES.NOTIFY);
+ }
+
+ /**
+ * Returns whether the current prototype/instance has a "reflect to attribute"
+ * property effect for the given property.
+ *
+ * @param {string} property Property name
+ * @return {boolean} True if the prototype/instance has an effect of this type
+ * @protected
+ */
+ _hasReflectEffect(property) {
+ return this._hasPropertyEffect(property, TYPES.REFLECT);
+ }
+
+ /**
+ * Returns whether the current prototype/instance has a "computed"
+ * property effect for the given property.
+ *
+ * @param {string} property Property name
+ * @return {boolean} True if the prototype/instance has an effect of this type
+ * @protected
+ */
+ _hasComputedEffect(property) {
+ return this._hasPropertyEffect(property, TYPES.COMPUTE);
+ }
+
+ // Runtime ----------------------------------------
+
+ /**
+ * Sets a pending property or path. If the root property of the path in
+ * question had no accessor, the path is set, otherwise it is enqueued
+ * via `_setPendingProperty`.
+ *
+ * This function isolates relatively expensive functionality necessary
+ * for the public API (`set`, `setProperties`, `notifyPath`, and property
+ * change listeners via {{...}} bindings), such that it is only done
+ * when paths enter the system, and not at every propagation step. It
+ * also sets a `__dataHasPaths` flag on the instance which is used to
+ * fast-path slower path-matching code in the property effects host paths.
+ *
+ * `path` can be a path string or array of path parts as accepted by the
+ * public API.
+ *
+ * @param {string | !Array<number|string>} path Path to set
+ * @param {*} value Value to set
+ * @param {boolean=} shouldNotify Set to true if this change should
+ * cause a property notification event dispatch
+ * @param {boolean=} isPathNotification If the path being set is a path
+ * notification of an already changed value, as opposed to a request
+ * to set and notify the change. In the latter `false` case, a dirty
+ * check is performed and then the value is set to the path before
+ * enqueuing the pending property change.
+ * @return {boolean} Returns true if the property/path was enqueued in
+ * the pending changes bag.
+ * @protected
+ */
+ _setPendingPropertyOrPath(path, value, shouldNotify, isPathNotification) {
+ if (isPathNotification ||
+ Polymer.Path.root(Array.isArray(path) ? path[0] : path) !== path) {
+ // Dirty check changes being set to a path against the actual object,
+ // since this is the entry point for paths into the system; from here
+ // the only dirty checks are against the `__dataTemp` cache to prevent
+ // duplicate work in the same turn only. Note, if this was a notification
+ // of a change already set to a path (isPathNotification: true),
+ // we always let the change through and skip the `set` since it was
+ // already dirty checked at the point of entry and the underlying
+ // object has already been updated
+ if (!isPathNotification) {
+ let old = Polymer.Path.get(this, path);
+ path = /** @type {string} */ (Polymer.Path.set(this, path, value));
+ // Use property-accessor's simpler dirty check
+ if (!path || !super._shouldPropertyChange(path, value, old)) {
+ return false;
+ }
+ }
+ this.__dataHasPaths = true;
+ if (this._setPendingProperty(path, value, shouldNotify)) {
+ computeLinkedPaths(this, path, value);
+ return true;
+ }
+ } else {
+ if (this.__dataHasAccessor && this.__dataHasAccessor[path]) {
+ return this._setPendingProperty(path, value, shouldNotify);
+ } else {
+ this[path] = value;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Applies a value to a non-Polymer element/node's property.
+ *
+ * The implementation makes a best-effort at binding interop:
+ * Some native element properties have side-effects when
+ * re-setting the same value (e.g. setting `<input>.value` resets the
+ * cursor position), so we do a dirty-check before setting the value.
+ * However, for better interop with non-Polymer custom elements that
+ * accept objects, we explicitly re-set object changes coming from the
+ * Polymer world (which may include deep object changes without the
+ * top reference changing), erring on the side of providing more
+ * information.
+ *
+ * Users may override this method to provide alternate approaches.
+ *
+ * @param {Node} node The node to set a property on
+ * @param {string} prop The property to set
+ * @param {*} value The value to set
+ * @protected
+ */
+ _setUnmanagedPropertyToNode(node, prop, value) {
+ // It is a judgment call that resetting primitives is
+ // "bad" and resettings objects is also "good"; alternatively we could
+ // implement a whitelist of tag & property values that should never
+ // be reset (e.g. <input>.value && <select>.value)
+ if (value !== node[prop] || typeof value == 'object') {
+ node[prop] = value;
+ }
+ }
+
+ /**
+ * Overrides the `PropertyAccessors` implementation to introduce special
+ * dirty check logic depending on the property & value being set:
+ *
+ * 1. Any value set to a path (e.g. 'obj.prop': 42 or 'obj.prop': {...})
+ * Stored in `__dataTemp`, dirty checked against `__dataTemp`
+ * 2. Object set to simple property (e.g. 'prop': {...})
+ * Stored in `__dataTemp` and `__data`, dirty checked against
+ * `__dataTemp` by default implementation of `_shouldPropertyChange`
+ * 3. Primitive value set to simple property (e.g. 'prop': 42)
+ * Stored in `__data`, dirty checked against `__data`
+ *
+ * The dirty-check is important to prevent cycles due to two-way
+ * notification, but paths and objects are only dirty checked against any
+ * previous value set during this turn via a "temporary cache" that is
+ * cleared when the last `_propertiesChaged` exits. This is so:
+ * a. any cached array paths (e.g. 'array.3.prop') may be invalidated
+ * due to array mutations like shift/unshift/splice; this is fine
+ * since path changes are dirty-checked at user entry points like `set`
+ * b. dirty-checking for objects only lasts one turn to allow the user
+ * to mutate the object in-place and re-set it with the same identity
+ * and have all sub-properties re-propagated in a subsequent turn.
+ *
+ * The temp cache is not necessarily sufficient to prevent invalid array
+ * paths, since a splice can happen during the same turn (with pathological
+ * user code); we could introduce a "fixup" for temporarily cached array
+ * paths if needed: https://github.com/Polymer/polymer/issues/4227
+ *
+ * @override
+ */
+ _setPendingProperty(property, value, shouldNotify) {
+ let isPath = this.__dataHasPaths && Polymer.Path.isPath(property);
+ let prevProps = isPath ? this.__dataTemp : this.__data;
+ if (this._shouldPropertyChange(property, value, prevProps[property])) {
+ if (!this.__dataPending) {
+ this.__dataPending = {};
+ this.__dataOld = {};
+ }
+ // Ensure old is captured from the last turn
+ if (!(property in this.__dataOld)) {
+ this.__dataOld[property] = this.__data[property];
+ }
+ // Paths are stored in temporary cache (cleared at end of turn),
+ // which is used for dirty-checking, all others stored in __data
+ if (isPath) {
+ this.__dataTemp[property] = value;
+ } else {
+ this.__data[property] = value;
+ }
+ // All changes go into pending property bag, passed to _propertiesChanged
+ this.__dataPending[property] = value;
+ // Track properties that should notify separately
+ if (isPath || (this.__notifyEffects && this.__notifyEffects[property])) {
+ this.__dataToNotify = this.__dataToNotify || {};
+ this.__dataToNotify[property] = shouldNotify;
+ }
+ return true;
+ }
+ }
+
+ /**
+ * Overrides base implementation to ensure all accessors set `shouldNotify`
+ * to true, for per-property notification tracking.
+ *
+ * @override
+ */
+ _setProperty(property, value) {
+ if (this._setPendingProperty(property, value, true)) {
+ this._invalidateProperties();
+ }
+ }
+
+ /**
+ * Overrides `PropertyAccessor`'s default async queuing of
+ * `_propertiesChanged`: if `__dataReady` is false (has not yet been
+ * manually flushed), the function no-ops; otherwise flushes
+ * `_propertiesChanged` synchronously.
+ *
+ * @override
+ */
+ _invalidateProperties() {
+ if (this.__dataReady) {
+ this._flushProperties();
+ }
+ }
+
+ /**
+ * Enqueues the given client on a list of pending clients, whose
+ * pending property changes can later be flushed via a call to
+ * `_flushClients`.
+ *
+ * @param {Object} client PropertyEffects client to enqueue
+ * @protected
+ */
+ _enqueueClient(client) {
+ this.__dataPendingClients = this.__dataPendingClients || [];
+ if (client !== this) {
+ this.__dataPendingClients.push(client);
+ }
+ }
+
+ /**
+ * Flushes any clients previously enqueued via `_enqueueClient`, causing
+ * their `_flushProperties` method to run.
+ *
+ * @protected
+ */
+ _flushClients() {
+ if (!this.__dataClientsReady) {
+ this.__dataClientsReady = true;
+ this._readyClients();
+ // Override point where accessors are turned on; importantly,
+ // this is after clients have fully readied, providing a guarantee
+ // that any property effects occur only after all clients are ready.
+ this.__dataReady = true;
+ } else {
+ this.__enableOrFlushClients();
+ }
+ }
+
+ // NOTE: We ensure clients either enable or flush as appropriate. This
+ // handles two corner cases:
+ // (1) clients flush properly when connected/enabled before the host
+ // enables; e.g.
+ // (a) Templatize stamps with no properties and does not flush and
+ // (b) the instance is inserted into dom and
+ // (c) then the instance flushes.
+ // (2) clients enable properly when not connected/enabled when the host
+ // flushes; e.g.
+ // (a) a template is runtime stamped and not yet connected/enabled
+ // (b) a host sets a property, causing stamped dom to flush
+ // (c) the stamped dom enables.
+ __enableOrFlushClients() {
+ let clients = this.__dataPendingClients;
+ if (clients) {
+ this.__dataPendingClients = null;
+ for (let i=0; i < clients.length; i++) {
+ let client = clients[i];
+ if (!client.__dataEnabled) {
+ client._enableProperties();
+ } else if (client.__dataPending) {
+ client._flushProperties();
+ }
+ }
+ }
+ }
+
+ /**
+ * Perform any initial setup on client dom. Called before the first
+ * `_flushProperties` call on client dom and before any element
+ * observers are called.
+ *
+ * @protected
+ */
+ _readyClients() {
+ this.__enableOrFlushClients();
+ }
+
+ /**
+ * Sets a bag of property changes to this instance, and
+ * synchronously processes all effects of the properties as a batch.
+ *
+ * Property names must be simple properties, not paths. Batched
+ * path propagation is not supported.
+ *
+ * @param {Object} props Bag of one or more key-value pairs whose key is
+ * a property and value is the new value to set for that property.
+ * @param {boolean=} setReadOnly When true, any private values set in
+ * `props` will be set. By default, `setProperties` will not set
+ * `readOnly: true` root properties.
+ * @public
+ */
+ setProperties(props, setReadOnly) {
+ for (let path in props) {
+ if (setReadOnly || !this.__readOnly || !this.__readOnly[path]) {
+ //TODO(kschaaf): explicitly disallow paths in setProperty?
+ // wildcard observers currently only pass the first changed path
+ // in the `info` object, and you could do some odd things batching
+ // paths, e.g. {'foo.bar': {...}, 'foo': null}
+ this._setPendingPropertyOrPath(path, props[path], true);
+ }
+ }
+ this._invalidateProperties();
+ }
+
+ /**
+ * Overrides `PropertyAccessors` so that property accessor
+ * side effects are not enabled until after client dom is fully ready.
+ * Also calls `_flushClients` callback to ensure client dom is enabled
+ * that was not enabled as a result of flushing properties.
+ *
+ * @override
+ */
+ ready() {
+ // It is important that `super.ready()` is not called here as it
+ // immediately turns on accessors. Instead, we wait until `readyClients`
+ // to enable accessors to provide a guarantee that clients are ready
+ // before processing any accessors side effects.
+ this._flushProperties();
+ // If no data was pending, `_flushProperties` will not `flushClients`
+ // so ensure this is done.
+ if (!this.__dataClientsReady) {
+ this._flushClients();
+ }
+ // Before ready, client notifications do not trigger _flushProperties.
+ // Therefore a flush is necessary here if data has been set.
+ if (this.__dataPending) {
+ this._flushProperties();
+ }
+ }
+
+ /**
+ * Implements `PropertyAccessors`'s properties changed callback.
+ *
+ * Runs each class of effects for the batch of changed properties in
+ * a specific order (compute, propagate, reflect, observe, notify).
+ *
+ * @override
+ */
+ _propertiesChanged(currentProps, changedProps, oldProps) {
+ // ----------------------------
+ // let c = Object.getOwnPropertyNames(changedProps || {});
+ // window.debug && console.group(this.localName + '#' + this.id + ': ' + c);
+ // if (window.debug) { debugger; }
+ // ----------------------------
+ let hasPaths = this.__dataHasPaths;
+ this.__dataHasPaths = false;
+ // Compute properties
+ runComputedEffects(this, changedProps, oldProps, hasPaths);
+ // Clear notify properties prior to possible reentry (propagate, observe),
+ // but after computing effects have a chance to add to them
+ let notifyProps = this.__dataToNotify;
+ this.__dataToNotify = null;
+ // Propagate properties to clients
+ this._propagatePropertyChanges(changedProps, oldProps, hasPaths);
+ // Flush clients
+ this._flushClients();
+ // Reflect properties
+ runEffects(this, this.__reflectEffects, changedProps, oldProps, hasPaths);
+ // Observe properties
+ runEffects(this, this.__observeEffects, changedProps, oldProps, hasPaths);
+ // Notify properties to host
+ if (notifyProps) {
+ runNotifyEffects(this, notifyProps, changedProps, oldProps, hasPaths);
+ }
+ // Clear temporary cache at end of turn
+ if (this.__dataCounter == 1) {
+ this.__dataTemp = {};
+ }
+ // ----------------------------
+ // window.debug && console.groupEnd(this.localName + '#' + this.id + ': ' + c);
+ // ----------------------------
+ }
+
+ /**
+ * Called to propagate any property changes to stamped template nodes
+ * managed by this element.
+ *
+ * @param {Object} changedProps Bag of changed properties
+ * @param {Object} oldProps Bag of previous values for changed properties
+ * @param {boolean} hasPaths True with `props` contains one or more paths
+ * @protected
+ */
+ _propagatePropertyChanges(changedProps, oldProps, hasPaths) {
+ if (this.__propagateEffects) {
+ runEffects(this, this.__propagateEffects, changedProps, oldProps, hasPaths);
+ }
+ let templateInfo = this.__templateInfo;
+ while (templateInfo) {
+ runEffects(this, templateInfo.propertyEffects, changedProps, oldProps,
+ hasPaths, templateInfo.nodeList);
+ templateInfo = templateInfo.nextTemplateInfo;
+ }
+ }
+
+ /**
+ * Aliases one data path as another, such that path notifications from one
+ * are routed to the other.
+ *
+ * @param {string | !Array<string|number>} to Target path to link.
+ * @param {string | !Array<string|number>} from Source path to link.
+ * @public
+ */
+ linkPaths(to, from) {
+ to = Polymer.Path.normalize(to);
+ from = Polymer.Path.normalize(from);
+ this.__dataLinkedPaths = this.__dataLinkedPaths || {};
+ this.__dataLinkedPaths[to] = from;
+ }
+
+ /**
+ * Removes a data path alias previously established with `_linkPaths`.
+ *
+ * Note, the path to unlink should be the target (`to`) used when
+ * linking the paths.
+ *
+ * @param {string | !Array<string|number>} path Target path to unlink.
+ * @public
+ */
+ unlinkPaths(path) {
+ path = Polymer.Path.normalize(path);
+ if (this.__dataLinkedPaths) {
+ delete this.__dataLinkedPaths[path];
+ }
+ }
+
+ /**
+ * Notify that an array has changed.
+ *
+ * Example:
+ *
+ * this.items = [ {name: 'Jim'}, {name: 'Todd'}, {name: 'Bill'} ];
+ * ...
+ * this.items.splice(1, 1, {name: 'Sam'});
+ * this.items.push({name: 'Bob'});
+ * this.notifySplices('items', [
+ * { index: 1, removed: [{name: 'Todd'}], addedCount: 1, obect: this.items, type: 'splice' },
+ * { index: 3, removed: [], addedCount: 1, object: this.items, type: 'splice'}
+ * ]);
+ *
+ * @param {string} path Path that should be notified.
+ * @param {Array} splices Array of splice records indicating ordered
+ * changes that occurred to the array. Each record should have the
+ * following fields:
+ * * index: index at which the change occurred
+ * * removed: array of items that were removed from this index
+ * * addedCount: number of new items added at this index
+ * * object: a reference to the array in question
+ * * type: the string literal 'splice'
+ *
+ * Note that splice records _must_ be normalized such that they are
+ * reported in index order (raw results from `Object.observe` are not
+ * ordered and must be normalized/merged before notifying).
+ * @public
+ */
+ notifySplices(path, splices) {
+ let info = {};
+ let array = /** @type {Array} */(Polymer.Path.get(this, path, info));
+ notifySplices(this, array, info.path, splices);
+ }
+
+ /**
+ * Convenience method for reading a value from a path.
+ *
+ * Note, if any part in the path is undefined, this method returns
+ * `undefined` (this method does not throw when dereferencing undefined
+ * paths).
+ *
+ * @param {(string|!Array<(string|number)>)} path Path to the value
+ * to read. The path may be specified as a string (e.g. `foo.bar.baz`)
+ * or an array of path parts (e.g. `['foo.bar', 'baz']`). Note that
+ * bracketed expressions are not supported; string-based path parts
+ * *must* be separated by dots. Note that when dereferencing array
+ * indices, the index may be used as a dotted part directly
+ * (e.g. `users.12.name` or `['users', 12, 'name']`).
+ * @param {Object=} root Root object from which the path is evaluated.
+ * @return {*} Value at the path, or `undefined` if any part of the path
+ * is undefined.
+ * @public
+ */
+ get(path, root) {
+ return Polymer.Path.get(root || this, path);
+ }
+
+ /**
+ * Convenience method for setting a value to a path and notifying any
+ * elements bound to the same path.
+ *
+ * Note, if any part in the path except for the last is undefined,
+ * this method does nothing (this method does not throw when
+ * dereferencing undefined paths).
+ *
+ * @param {(string|!Array<(string|number)>)} path Path to the value
+ * to write. The path may be specified as a string (e.g. `'foo.bar.baz'`)
+ * or an array of path parts (e.g. `['foo.bar', 'baz']`). Note that
+ * bracketed expressions are not supported; string-based path parts
+ * *must* be separated by dots. Note that when dereferencing array
+ * indices, the index may be used as a dotted part directly
+ * (e.g. `'users.12.name'` or `['users', 12, 'name']`).
+ * @param {*} value Value to set at the specified path.
+ * @param {Object=} root Root object from which the path is evaluated.
+ * When specified, no notification will occur.
+ * @public
+ */
+ set(path, value, root) {
+ if (root) {
+ Polymer.Path.set(root, path, value);
+ } else {
+ if (!this.__readOnly || !this.__readOnly[/** @type {string} */(path)]) {
+ if (this._setPendingPropertyOrPath(path, value, true)) {
+ this._invalidateProperties();
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds items onto the end of the array at the path specified.
+ *
+ * The arguments after `path` and return value match that of
+ * `Array.prototype.push`.
+ *
+ * This method notifies other paths to the same array that a
+ * splice occurred to the array.
+ *
+ * @param {string} path Path to array.
+ * @param {...*} items Items to push onto array
+ * @return {number} New length of the array.
+ * @public
+ */
+ push(path, ...items) {
+ let info = {};
+ let array = /** @type {Array}*/(Polymer.Path.get(this, path, info));
+ let len = array.length;
+ let ret = array.push(...items);
+ if (items.length) {
+ notifySplice(this, array, info.path, len, items.length, []);
+ }
+ return ret;
+ }
+
+ /**
+ * Removes an item from the end of array at the path specified.
+ *
+ * The arguments after `path` and return value match that of
+ * `Array.prototype.pop`.
+ *
+ * This method notifies other paths to the same array that a
+ * splice occurred to the array.
+ *
+ * @param {string} path Path to array.
+ * @return {*} Item that was removed.
+ * @public
+ */
+ pop(path) {
+ let info = {};
+ let array = /** @type {Array} */(Polymer.Path.get(this, path, info));
+ let hadLength = Boolean(array.length);
+ let ret = array.pop();
+ if (hadLength) {
+ notifySplice(this, array, info.path, array.length, 0, [ret]);
+ }
+ return ret;
+ }
+
+ /**
+ * Starting from the start index specified, removes 0 or more items
+ * from the array and inserts 0 or more new items in their place.
+ *
+ * The arguments after `path` and return value match that of
+ * `Array.prototype.splice`.
+ *
+ * This method notifies other paths to the same array that a
+ * splice occurred to the array.
+ *
+ * @param {string} path Path to array.
+ * @param {number} start Index from which to start removing/inserting.
+ * @param {number} deleteCount Number of items to remove.
+ * @param {...*} items Items to insert into array.
+ * @return {Array} Array of removed items.
+ * @public
+ */
+ splice(path, start, deleteCount, ...items) {
+ let info = {};
+ let array = /** @type {Array} */(Polymer.Path.get(this, path, info));
+ // Normalize fancy native splice handling of crazy start values
+ if (start < 0) {
+ start = array.length - Math.floor(-start);
+ } else {
+ start = Math.floor(start);
+ }
+ if (!start) {
+ start = 0;
+ }
+ let ret = array.splice(start, deleteCount, ...items);
+ if (items.length || ret.length) {
+ notifySplice(this, array, info.path, start, items.length, ret);
+ }
+ return ret;
+ }
+
+ /**
+ * Removes an item from the beginning of array at the path specified.
+ *
+ * The arguments after `path` and return value match that of
+ * `Array.prototype.pop`.
+ *
+ * This method notifies other paths to the same array that a
+ * splice occurred to the array.
+ *
+ * @param {string} path Path to array.
+ * @return {*} Item that was removed.
+ * @public
+ */
+ shift(path) {
+ let info = {};
+ let array = /** @type {Array} */(Polymer.Path.get(this, path, info));
+ let hadLength = Boolean(array.length);
+ let ret = array.shift();
+ if (hadLength) {
+ notifySplice(this, array, info.path, 0, 0, [ret]);
+ }
+ return ret;
+ }
+
+ /**
+ * Adds items onto the beginning of the array at the path specified.
+ *
+ * The arguments after `path` and return value match that of
+ * `Array.prototype.push`.
+ *
+ * This method notifies other paths to the same array that a
+ * splice occurred to the array.
+ *
+ * @param {string} path Path to array.
+ * @param {...*} items Items to insert info array
+ * @return {number} New length of the array.
+ * @public
+ */
+ unshift(path, ...items) {
+ let info = {};
+ let array = /** @type {Array} */(Polymer.Path.get(this, path, info));
+ let ret = array.unshift(...items);
+ if (items.length) {
+ notifySplice(this, array, info.path, 0, items.length, []);
+ }
+ return ret;
+ }
+
+ /**
+ * Notify that a path has changed.
+ *
+ * Example:
+ *
+ * this.item.user.name = 'Bob';
+ * this.notifyPath('item.user.name');
+ *
+ * @param {string} path Path that should be notified.
+ * @param {*=} value Value at the path (optional).
+ * @public
+ */
+ notifyPath(path, value) {
+ /** @type {string} */
+ let propPath;
+ if (arguments.length == 1) {
+ // Get value if not supplied
+ let info = {};
+ value = Polymer.Path.get(this, path, info);
+ propPath = info.path;
+ } else if (Array.isArray(path)) {
+ // Normalize path if needed
+ propPath = Polymer.Path.normalize(path);
+ } else {
+ propPath = /** @type{string} */(path);
+ }
+ if (this._setPendingPropertyOrPath(propPath, value, true, true)) {
+ this._invalidateProperties();
+ }
+ }
+
+ /**
+ * Equivalent to static `createReadOnlyProperty` API but can be called on
+ * an instance to add effects at runtime. See that method for
+ * full API docs.
+ *
+ * @param {string} property Property name
+ * @param {boolean=} protectedSetter Creates a custom protected setter
+ * when `true`.
+ * @protected
+ */
+ _createReadOnlyProperty(property, protectedSetter) {
+ this._addPropertyEffect(property, TYPES.READ_ONLY);
+ if (protectedSetter) {
+ this['_set' + upper(property)] = function(value) {
+ this._setProperty(property, value);
+ }
+ }
+ }
+
+ /**
+ * Equivalent to static `createPropertyObserver` API but can be called on
+ * an instance to add effects at runtime. See that method for
+ * full API docs.
+ *
+ * @param {string} property Property name
+ * @param {string} methodName Name of observer method to call
+ * @param {boolean=} dynamicFn Whether the method name should be included as
+ * a dependency to the effect.
+ * @protected
+ */
+ _createPropertyObserver(property, methodName, dynamicFn) {
+ let info = { property, methodName, dynamicFn };
+ this._addPropertyEffect(property, TYPES.OBSERVE, {
+ fn: runObserverEffect, info, trigger: {name: property}
+ });
+ if (dynamicFn) {
+ this._addPropertyEffect(methodName, TYPES.OBSERVE, {
+ fn: runObserverEffect, info, trigger: {name: methodName}
+ });
+ }
+ }
+
+ /**
+ * Equivalent to static `createMethodObserver` API but can be called on
+ * an instance to add effects at runtime. See that method for
+ * full API docs.
+ *
+ * @param {string} expression Method expression
+ * @param {boolean|Object=} dynamicFn Boolean or object map indicating
+ * whether method names should be included as a dependency to the effect.
+ * @protected
+ */
+ _createMethodObserver(expression, dynamicFn) {
+ let sig = parseMethod(expression);
+ if (!sig) {
+ throw new Error("Malformed observer expression '" + expression + "'");
+ }
+ createMethodEffect(this, sig, TYPES.OBSERVE, runMethodEffect, null, dynamicFn);
+ }
+
+ /**
+ * Equivalent to static `createNotifyingProperty` API but can be called on
+ * an instance to add effects at runtime. See that method for
+ * full API docs.
+ *
+ * @param {string} property Property name
+ * @protected
+ */
+ _createNotifyingProperty(property) {
+ this._addPropertyEffect(property, TYPES.NOTIFY, {
+ fn: runNotifyEffect,
+ info: {
+ eventName: CaseMap.camelToDashCase(property) + '-changed',
+ property: property
+ }
+ });
+ }
+
+ /**
+ * Equivalent to static `createReflectedProperty` API but can be called on
+ * an instance to add effects at runtime. See that method for
+ * full API docs.
+ *
+ * @param {string} property Property name
+ * @protected
+ */
+ _createReflectedProperty(property) {
+ let attr = CaseMap.camelToDashCase(property);
+ if (attr[0] === '-') {
+ console.warn('Property ' + property + ' cannot be reflected to attribute ' +
+ attr + ' because "-" is not a valid starting attribute name. Use a lowercase first letter for the property thisead.');
+ } else {
+ this._addPropertyEffect(property, TYPES.REFLECT, {
+ fn: runReflectEffect,
+ info: {
+ attrName: attr
+ }
+ });
+ }
+ }
+
+ /**
+ * Equivalent to static `createComputedProperty` API but can be called on
+ * an instance to add effects at runtime. See that method for
+ * full API docs.
+ *
+ * @param {string} property Name of computed property to set
+ * @param {string} expression Method expression
+ * @param {boolean|Object=} dynamicFn Boolean or object map indicating
+ * whether method names should be included as a dependency to the effect.
+ * @protected
+ */
+ _createComputedProperty(property, expression, dynamicFn) {
+ let sig = parseMethod(expression);
+ if (!sig) {
+ throw new Error("Malformed computed expression '" + expression + "'");
+ }
+ createMethodEffect(this, sig, TYPES.COMPUTE, runComputedEffect, property, dynamicFn);
+ }
+
+ // -- static class methods ------------
+
+ /**
+ * Ensures an accessor exists for the specified property, and adds
+ * to a list of "property effects" that will run when the accessor for
+ * the specified property is set. Effects are grouped by "type", which
+ * roughly corresponds to a phase in effect processing. The effect
+ * metadata should be in the following form:
+ *
+ * {
+ * fn: effectFunction, // Reference to function to call to perform effect
+ * info: { ... } // Effect metadata passed to function
+ * trigger: { // Optional triggering metadata; if not provided
+ * name: string // the property is treated as a wildcard
+ * structured: boolean
+ * wildcard: boolean
+ * }
+ * }
+ *
+ * Effects are called from `_propertiesChanged` in the following order by
+ * type:
+ *
+ * 1. COMPUTE
+ * 2. PROPAGATE
+ * 3. REFLECT
+ * 4. OBSERVE
+ * 5. NOTIFY
+ *
+ * Effect functions are called with the following signature:
+ *
+ * effectFunction(inst, path, props, oldProps, info, hasPaths)
+ *
+ * @param {string} property Property that should trigger the effect
+ * @param {string} type Effect type, from this.PROPERTY_EFFECT_TYPES
+ * @param {Object=} effect Effect metadata object
+ * @protected
+ */
+ static addPropertyEffect(property, type, effect) {
+ this.prototype._addPropertyEffect(property, type, effect);
+ }
+
+ /**
+ * Creates a single-property observer for the given property.
+ *
+ * @param {string} property Property name
+ * @param {string} methodName Name of observer method to call
+ * @param {boolean=} dynamicFn Whether the method name should be included as
+ * a dependency to the effect.
+ * @protected
+ */
+ static createPropertyObserver(property, methodName, dynamicFn) {
+ this.prototype._createPropertyObserver(property, methodName, dynamicFn);
+ }
+
+ /**
+ * Creates a multi-property "method observer" based on the provided
+ * expression, which should be a string in the form of a normal Javascript
+ * function signature: `'methodName(arg1, [..., argn])'`. Each argument
+ * should correspond to a property or path in the context of this
+ * prototype (or instance), or may be a literal string or number.
+ *
+ * @param {string} expression Method expression
+ * @param {boolean|Object=} dynamicFn Boolean or object map indicating
+ * whether method names should be included as a dependency to the effect.
+ * @protected
+ */
+ static createMethodObserver(expression, dynamicFn) {
+ this.prototype._createMethodObserver(expression, dynamicFn);
+ }
+
+ /**
+ * Causes the setter for the given property to dispatch `<property>-changed`
+ * events to notify of changes to the property.
+ *
+ * @param {string} property Property name
+ * @protected
+ */
+ static createNotifyingProperty(property) {
+ this.prototype._createNotifyingProperty(property);
+ }
+
+ /**
+ * Creates a read-only accessor for the given property.
+ *
+ * To set the property, use the protected `_setProperty` API.
+ * To create a custom protected setter (e.g. `_setMyProp()` for
+ * property `myProp`), pass `true` for `protectedSetter`.
+ *
+ * Note, if the property will have other property effects, this method
+ * should be called first, before adding other effects.
+ *
+ * @param {string} property Property name
+ * @param {boolean=} protectedSetter Creates a custom protected setter
+ * when `true`.
+ * @protected
+ */
+ static createReadOnlyProperty(property, protectedSetter) {
+ this.prototype._createReadOnlyProperty(property, protectedSetter);
+ }
+
+ /**
+ * Causes the setter for the given property to reflect the property value
+ * to a (dash-cased) attribute of the same name.
+ *
+ * @param {string} property Property name
+ * @protected
+ */
+ static createReflectedProperty(property) {
+ this.prototype._createReflectedProperty(property);
+ }
+
+ /**
+ * Creates a computed property whose value is set to the result of the
+ * method described by the given `expression` each time one or more
+ * arguments to the method changes. The expression should be a string
+ * in the form of a normal Javascript function signature:
+ * `'methodName(arg1, [..., argn])'`
+ *
+ * @param {string} property Name of computed property to set
+ * @param {string} expression Method expression
+ * @param {boolean|Object=} dynamicFn Boolean or object map indicating whether
+ * method names should be included as a dependency to the effect.
+ * @protected
+ */
+ static createComputedProperty(property, expression, dynamicFn) {
+ this.prototype._createComputedProperty(property, expression, dynamicFn);
+ }
+
+ /**
+ * Parses the provided template to ensure binding effects are created
+ * for them, and then ensures property accessors are created for any
+ * dependent properties in the template. Binding effects for bound
+ * templates are stored in a linked list on the instance so that
+ * templates can be efficiently stamped and unstamped.
+ *
+ * @param {HTMLTemplateElement} template Template containing binding
+ * bindings
+ * @return {Object} Template metadata object
+ * @protected
+ */
+ static bindTemplate(template) {
+ return this.prototype._bindTemplate(template);
+ }
+
+ // -- binding ----------------------------------------------
+
+ /**
+ * Equivalent to static `bindTemplate` API but can be called on
+ * an instance to add effects at runtime. See that method for
+ * full API docs.
+ *
+ * This method may be called on the prototype (for prototypical template
+ * binding, to avoid creating accessors every instance) once per prototype,
+ * and will be called with `runtimeBinding: true` by `_stampTemplate` to
+ * create and link an instance of the template metadata associated with a
+ * particular stamping.
+ *
+ * @param {HTMLTemplateElement} template Template containing binding
+ * bindings
+ * @param {boolean=} instanceBinding When false (default), performs
+ * "prototypical" binding of the template and overwrites any previously
+ * bound template for the class. When true (as passed from
+ * `_stampTemplate`), the template info is instanced and linked into
+ * the list of bound templates.
+ * @return {Object} Template metadata object; for `runtimeBinding`,
+ * this is an instance of the prototypical template info
+ * @protected
+ */
+ _bindTemplate(template, instanceBinding) {
+ let templateInfo = this.constructor._parseTemplate(template);
+ let wasPreBound = this.__templateInfo == templateInfo;
+ // Optimization: since this is called twice for proto-bound templates,
+ // don't attempt to recreate accessors if this template was pre-bound
+ if (!wasPreBound) {
+ for (let prop in templateInfo.propertyEffects) {
+ this._createPropertyAccessor(prop);
+ }
+ }
+ if (instanceBinding) {
+ // For instance-time binding, create instance of template metadata
+ // and link into list of templates if necessary
+ templateInfo = Object.create(templateInfo);
+ templateInfo.wasPreBound = wasPreBound;
+ if (!wasPreBound && this.__templateInfo) {
+ let last = this.__templateInfoLast || this.__templateInfo;
+ this.__templateInfoLast = last.nextTemplateInfo = templateInfo;
+ templateInfo.previousTemplateInfo = last;
+ return templateInfo;
+ }
+ }
+ return this.__templateInfo = templateInfo;
+ }
+
+ /**
+ * Adds a property effect to the given template metadata, which is run
+ * at the "propagate" stage of `_propertiesChanged` when the template
+ * has been bound to the element via `_bindTemplate`.
+ *
+ * The `effect` object should match the format in `_addPropertyEffect`.
+ *
+ * @param {Object} templateInfo Template metadata to add effect to
+ * @param {string} prop Property that should trigger the effect
+ * @param {Object=} effect Effect metadata object
+ * @protected
+ */
+ static _addTemplatePropertyEffect(templateInfo, prop, effect) {
+ let hostProps = templateInfo.hostProps = templateInfo.hostProps || {};
+ hostProps[prop] = true;
+ let effects = templateInfo.propertyEffects = templateInfo.propertyEffects || {};
+ let propEffects = effects[prop] = effects[prop] || [];
+ propEffects.push(effect);
+ }
+
+ /**
+ * Stamps the provided template and performs instance-time setup for
+ * Polymer template features, including data bindings, declarative event
+ * listeners, and the `this.$` map of `id`'s to nodes. A document fragment
+ * is returned containing the stamped DOM, ready for insertion into the
+ * DOM.
+ *
+ * This method may be called more than once; however note that due to
+ * `shadycss` polyfill limitations, only styles from templates prepared
+ * using `ShadyCSS.prepareTemplate` will be correctly polyfilled (scoped
+ * to the shadow root and support CSS custom properties), and note that
+ * `ShadyCSS.prepareTemplate` may only be called once per element. As such,
+ * any styles required by in runtime-stamped templates must be included
+ * in the main element template.
+ *
+ * @param {HTMLTemplateElement} template Template to stamp
+ * @return {DocumentFragment} Cloned template content
+ * @protected
+ */
+ _stampTemplate(template) {
+ // Ensures that created dom is `_enqueueClient`'d to this element so
+ // that it can be flushed on next call to `_flushProperties`
+ hostStack.beginHosting(this);
+ let dom = super._stampTemplate(template);
+ hostStack.endHosting(this);
+ let templateInfo = this._bindTemplate(template, true);
+ // Add template-instance-specific data to instanced templateInfo
+ templateInfo.nodeList = dom.nodeList;
+ // Capture child nodes to allow unstamping of non-prototypical templates
+ if (!templateInfo.wasPreBound) {
+ let nodes = templateInfo.childNodes = [];
+ for (let n=dom.firstChild; n; n=n.nextSibling) {
+ nodes.push(n);
+ }
+ }
+ dom.templateInfo = templateInfo;
+ // Setup compound storage, 2-way listeners, and dataHost for bindings
+ setupBindings(this, templateInfo);
+ // Flush properties into template nodes if already booted
+ if (this.__dataReady) {
+ runEffects(this, templateInfo.propertyEffects, this.__data, null,
+ false, templateInfo.nodeList);
+ }
+ return dom;
+ }
+
+ /**
+ * Removes and unbinds the nodes previously contained in the provided
+ * DocumentFragment returned from `_stampTemplate`.
+ *
+ * @param {DocumentFragment} dom DocumentFragment previously returned
+ * from `_stampTemplate` associated with the nodes to be removed
+ * @protected
+ */
+ _removeBoundDom(dom) {
+ // Unlink template info
+ let templateInfo = dom.templateInfo;
+ if (templateInfo.previousTemplateInfo) {
+ templateInfo.previousTemplateInfo.nextTemplateInfo =
+ templateInfo.nextTemplateInfo;
+ }
+ if (templateInfo.nextTemplateInfo) {
+ templateInfo.nextTemplateInfo.previousTemplateInfo =
+ templateInfo.previousTemplateInfo;
+ }
+ if (this.__templateInfoLast == templateInfo) {
+ this.__templateInfoLast = templateInfo.previousTemplateInfo;
+ }
+ templateInfo.previousTemplateInfo = templateInfo.nextTemplateInfo = null;
+ // Remove stamped nodes
+ let nodes = templateInfo.childNodes;
+ for (let i=0; i<nodes.length; i++) {
+ let node = nodes[i];
+ node.parentNode.removeChild(node);
+ }
+ }
+
+ /**
+ * Overrides default `TemplateStamp` implementation to add support for
+ * parsing bindings from `TextNode`'s' `textContent`. A `bindings`
+ * array is added to `nodeInfo` and populated with binding metadata
+ * with information capturing the binding target, and a `parts` array
+ * with one or more metadata objects capturing the source(s) of the
+ * binding.
+ *
+ * @override
+ * @param {Node} node Node to parse
+ * @param {Object} templateInfo Template metadata for current template
+ * @param {Object} nodeInfo Node metadata for current template node
+ * @return {boolean} `true` if the visited node added node-specific
+ * metadata to `nodeInfo`
+ * @protected
+ */
+ static _parseTemplateNode(node, templateInfo, nodeInfo) {
+ let noted = super._parseTemplateNode(node, templateInfo, nodeInfo);
+ if (node.nodeType === Node.TEXT_NODE) {
+ let parts = this._parseBindings(node.textContent, templateInfo);
+ if (parts) {
+ // Initialize the textContent with any literal parts
+ // NOTE: default to a space here so the textNode remains; some browsers
+ // (IE) evacipate an empty textNode following cloneNode/importNode.
+ node.textContent = literalFromParts(parts) || ' ';
+ addBinding(this, templateInfo, nodeInfo, 'text', 'textContent', parts);
+ noted = true;
+ }
+ }
+ return noted;
+ }
+
+ /**
+ * Overrides default `TemplateStamp` implementation to add support for
+ * parsing bindings from attributes. A `bindings`
+ * array is added to `nodeInfo` and populated with binding metadata
+ * with information capturing the binding target, and a `parts` array
+ * with one or more metadata objects capturing the source(s) of the
+ * binding.
+ *
+ * @override
+ * @param {Node} node Node to parse
+ * @param {Object} templateInfo Template metadata for current template
+ * @param {Object} nodeInfo Node metadata for current template node
+ * @return {boolean} `true` if the visited node added node-specific
+ * metadata to `nodeInfo`
+ * @protected
+ */
+ static _parseTemplateNodeAttribute(node, templateInfo, nodeInfo, name, value) {
+ let parts = this._parseBindings(value, templateInfo);
+ if (parts) {
+ // Attribute or property
+ let origName = name;
+ let kind = 'property';
+ if (name[name.length-1] == '$') {
+ name = name.slice(0, -1);
+ kind = 'attribute';
+ }
+ // Initialize attribute bindings with any literal parts
+ let literal = literalFromParts(parts);
+ if (literal && kind == 'attribute') {
+ node.setAttribute(name, literal);
+ }
+ // Clear attribute before removing, since IE won't allow removing
+ // `value` attribute if it previously had a value (can't
+ // unconditionally set '' before removing since attributes with `$`
+ // can't be set using setAttribute)
+ if (node.localName === 'input' && origName === 'value') {
+ node.setAttribute(origName, '');
+ }
+ // Remove annotation
+ node.removeAttribute(origName);
+ // Case hackery: attributes are lower-case, but bind targets
+ // (properties) are case sensitive. Gambit is to map dash-case to
+ // camel-case: `foo-bar` becomes `fooBar`.
+ // Attribute bindings are excepted.
+ if (kind === 'property') {
+ name = Polymer.CaseMap.dashToCamelCase(name);
+ }
+ addBinding(this, templateInfo, nodeInfo, kind, name, parts, literal);
+ return true;
+ } else {
+ return super._parseTemplateNodeAttribute(node, templateInfo, nodeInfo, name, value);
+ }
+ }
+
+ /**
+ * Overrides default `TemplateStamp` implementation to add support for
+ * binding the properties that a nested template depends on to the template
+ * as `_host_<property>`.
+ *
+ * @override
+ * @param {Node} node Node to parse
+ * @param {Object} templateInfo Template metadata for current template
+ * @param {Object} nodeInfo Node metadata for current template node
+ * @return {boolean} `true` if the visited node added node-specific
+ * metadata to `nodeInfo`
+ * @protected
+ */
+ static _parseTemplateNestedTemplate(node, templateInfo, nodeInfo) {
+ let noted = super._parseTemplateNestedTemplate(node, templateInfo, nodeInfo);
+ // Merge host props into outer template and add bindings
+ let hostProps = nodeInfo.templateInfo.hostProps;
+ let mode = '{';
+ for (let source in hostProps) {
+ let parts = [{ mode, source, dependencies: [source] }];
+ addBinding(this, templateInfo, nodeInfo, 'property', '_host_' + source, parts);
+ }
+ return noted;
+ }
+
+ /**
+ * Called to parse text in a template (either attribute values or
+ * textContent) into binding metadata.
+ *
+ * Any overrides of this method should return an array of binding part
+ * metadata representing one or more bindings found in the provided text
+ * and any "literal" text in between. Any non-literal parts will be passed
+ * to `_evaluateBinding` when any dependencies change. The only required
+ * fields of each "part" in the returned array are as follows:
+ *
+ * - `dependencies` - Array containing trigger metadata for each property
+ * that should trigger the binding to update
+ * - `literal` - String containing text if the part represents a literal;
+ * in this case no `dependencies` are needed
+ *
+ * Additional metadata for use by `_evaluateBinding` may be provided in
+ * each part object as needed.
+ *
+ * The default implementation handles the following types of bindings
+ * (one or more may be intermixed with literal strings):
+ * - Property binding: `[[prop]]`
+ * - Path binding: `[[object.prop]]`
+ * - Negated property or path bindings: `[[!prop]]` or `[[!object.prop]]`
+ * - Two-way property or path bindings (supports negation):
+ * `{{prop}}`, `{{object.prop}}`, `{{!prop}}` or `{{!object.prop}}`
+ * - Inline computed method (supports negation):
+ * `[[compute(a, 'literal', b)]]`, `[[!compute(a, 'literal', b)]]`
+ *
+ * @param {string} text Text to parse from attribute or textContent
+ * @param {Object} templateInfo Current template metadata
+ * @return {Array<Object>} Array of binding part metadata
+ * @protected
+ */
+ static _parseBindings(text, templateInfo) {
+ let parts = [];
+ let lastIndex = 0;
+ let m;
+ // Example: "literal1{{prop}}literal2[[!compute(foo,bar)]]final"
+ // Regex matches:
+ // Iteration 1: Iteration 2:
+ // m[1]: '{{' '[['
+ // m[2]: '' '!'
+ // m[3]: 'prop' 'compute(foo,bar)'
+ while ((m = bindingRegex.exec(text)) !== null) {
+ // Add literal part
+ if (m.index > lastIndex) {
+ parts.push({literal: text.slice(lastIndex, m.index)});
+ }
+ // Add binding part
+ let mode = m[1][0];
+ let negate = Boolean(m[2]);
+ let source = m[3].trim();
+ let customEvent, notifyEvent, colon;
+ if (mode == '{' && (colon = source.indexOf('::')) > 0) {
+ notifyEvent = source.substring(colon + 2);
+ source = source.substring(0, colon);
+ customEvent = true;
+ }
+ let signature = parseMethod(source);
+ let dependencies = [];
+ if (signature) {
+ // Inline computed function
+ let {args, methodName} = signature;
+ for (let i=0; i<args.length; i++) {
+ let arg = args[i];
+ if (!arg.literal) {
+ dependencies.push(arg);
+ }
+ }
+ let dynamicFns = templateInfo.dynamicFns;
+ if (dynamicFns && dynamicFns[methodName] || signature.static) {
+ dependencies.push(methodName);
+ signature.dynamicFn = true;
+ }
+ } else {
+ // Property or path
+ dependencies.push(source);
+ }
+ parts.push({
+ source, mode, negate, customEvent, signature, dependencies,
+ event: notifyEvent
+ });
+ lastIndex = bindingRegex.lastIndex;
+ }
+ // Add a final literal part
+ if (lastIndex && lastIndex < text.length) {
+ let literal = text.substring(lastIndex);
+ if (literal) {
+ parts.push({
+ literal: literal
+ });
+ }
+ }
+ if (parts.length) {
+ return parts;
+ }
+ }
+
+ /**
+ * Called to evaluate a previously parsed binding part based on a set of
+ * one or more changed dependencies.
+ *
+ * @param {HTMLElement} inst Element that should be used as scope for
+ * binding dependencies
+ * @param {Object} part Binding part metadata
+ * @param {string} path Property/path that triggered this effect
+ * @param {Object} props Bag of current property changes
+ * @param {Object} oldProps Bag of previous values for changed properties
+ * @param {boolean} hasPaths True with `props` contains one or more paths
+ * @return {*} Value the binding part evaluated to
+ * @protected
+ */
+ static _evaluateBinding(inst, part, path, props, oldProps, hasPaths) {
+ let value;
+ if (part.signature) {
+ value = runMethodEffect(inst, path, props, oldProps, part.signature);
+ } else if (path != part.source) {
+ value = Polymer.Path.get(inst, part.source);
+ } else {
+ if (hasPaths && Polymer.Path.isPath(path)) {
+ value = Polymer.Path.get(inst, path);
+ } else {
+ value = inst.__data[path];
+ }
+ }
+ if (part.negate) {
+ value = !value;
+ }
+ return value;
+ }
+
+ }
+
+ return PropertyEffects;
+ });
+
+ /**
+ * Helper api for enqueing client dom created by a host element.
+ *
+ * By default elements are flushed via `_flushProperties` when
+ * `connectedCallback` is called. Elements attach their client dom to
+ * themselves at `ready` time which results from this first flush.
+ * This provides an ordering guarantee that the client dom an element
+ * creates is flushed before the element itself (i.e. client `ready`
+ * fires before host `ready`).
+ *
+ * However, if `_flushProperties` is called *before* an element is connected,
+ * as for example `Templatize` does, this ordering guarantee cannot be
+ * satisfied because no elements are connected. (Note: Bound elements that
+ * receive data do become enqueued clients and are properly ordered but
+ * unbound elements are not.)
+ *
+ * To maintain the desired "client before host" ordering guarantee for this
+ * case we rely on the "host stack. Client nodes registers themselves with
+ * the creating host element when created. This ensures that all client dom
+ * is readied in the proper order, maintaining the desired guarantee.
+ *
+ * @private
+ */
+ let hostStack = {
+
+ stack: [],
+
+ registerHost(inst) {
+ if (this.stack.length) {
+ let host = this.stack[this.stack.length-1];
+ host._enqueueClient(inst);
+ }
+ },
+
+ beginHosting(inst) {
+ this.stack.push(inst);
+ },
+
+ endHosting(inst) {
+ let stackLen = this.stack.length;
+ if (stackLen && this.stack[stackLen-1] == inst) {
+ this.stack.pop();
+ }
+ }
+
+ }
+
+})();
+</script>

Powered by Google App Engine
This is Rietveld 408576698