UNPKG

@polymer/polymer

Version:

The Polymer library makes it easy to create your own web components. Give your element some markup and properties, and then use it on a site. Polymer provides features like dynamic templates and data binding to reduce the amount of boilerplate you need to

1,363 lines (1,295 loc) 109 kB
<!-- @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 * @enum {string} */ const TYPES = { COMPUTE: '__computeEffects', REFLECT: '__reflectEffects', NOTIFY: '__notifyEffects', PROPAGATE: '__propagateEffects', OBSERVE: '__observeEffects', READ_ONLY: '__readOnly' }; /** @const {RegExp} */ const capitalAttributeRegex = /[A-Z]/; /** * @typedef {{ * name: (string | undefined), * structured: (boolean | undefined), * wildcard: (boolean | undefined) * }} */ let DataTrigger; //eslint-disable-line no-unused-vars /** * @typedef {{ * info: ?, * trigger: (!DataTrigger | undefined), * fn: (!Function | undefined) * }} */ let DataEffect; //eslint-disable-line no-unused-vars let PropertyEffectsType; //eslint-disable-line no-unused-vars /** * 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 {!PropertyEffectsType} 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 {!PropertyEffectsType} 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 {DataTrigger} 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 {!PropertyEffectsType} 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 {?} info Effect metadata * @return {void} * @private */ function runObserverEffect(inst, property, props, oldProps, info) { let fn = typeof info.method === "string" ? inst[info.method] : info.method; let changedProp = info.property; if (fn) { fn.call(inst, inst.__data[changedProp], oldProps[changedProp]); } else if (!info.dynamicFn) { console.warn('observer method `' + info.method + '` 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 {!PropertyEffectsType} 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 * @return {void} * @private */ function runNotifyEffects(inst, notifyProps, props, oldProps, hasPaths) { // Notify let fxs = inst[TYPES.NOTIFY]; 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 {!PropertyEffectsType} 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; } return false; } /** * Dispatches {property}-changed events to indicate a property (or path) * changed. * * @param {!PropertyEffectsType} 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). * @return {void} * @private * @suppress {invalidCasts} */ function dispatchNotifyEvent(inst, eventName, value, path) { let detail = { value: value, queueProperty: true }; if (path) { detail.path = path; } /** @type {!HTMLElement} */(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 {!PropertyEffectsType} 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 {?} info Effect metadata * @param {boolean} hasPaths True with `props` contains one or more paths * @return {void} * @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 {CustomEvent} event Notification event (e.g. '<property>-changed') * @param {!PropertyEffectsType} 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 * @return {void} * @private */ function handleNotification(event, inst, fromProp, toPath, negate) { let value; let detail = /** @type {Object} */(event.detail); let fromPath = detail && detail.path; if (fromPath) { toPath = Polymer.Path.translate(fromProp, toPath, fromPath); value = detail && detail.value; } else { value = event.currentTarget[fromProp]; } value = negate ? !value : value; if (!inst[TYPES.READ_ONLY] || !inst[TYPES.READ_ONLY][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 {!PropertyEffectsType} 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 {?} info Effect metadata * @return {void} * @private */ function runReflectEffect(inst, property, props, oldProps, info) { let value = inst.__data[property]; if (Polymer.sanitizeDOMValue) { value = Polymer.sanitizeDOMValue(value, info.attrName, 'attribute', /** @type {Node} */(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 {!PropertyEffectsType} 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 * @return {void} * @private */ function runComputedEffects(inst, changedProps, oldProps, hasPaths) { let computeEffects = inst[TYPES.COMPUTE]; 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 {!PropertyEffectsType} 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 {?} info Effect metadata * @return {void} * @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 {!PropertyEffectsType} inst The instance whose props are changing * @param {string | !Array<(string|number)>} path Path that has changed * @param {*} value Value of changed path * @return {void} * @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 {TemplateInfo} templateInfo Template metadata for current template * @param {NodeInfo} 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<!BindingPart>} 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) * @return {void} * @private */ function addBinding(constructor, templateInfo, nodeInfo, kind, target, parts, literal) { // Create binding metadata and add to nodeInfo nodeInfo.bindings = nodeInfo.bindings || []; let /** Binding */ 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 {TemplateInfo} templateInfo Template metadata for current template * @param {!Binding} binding Binding metadata * @param {!BindingPart} part Binding part metadata * @param {number} index Index into `nodeInfoList` for this node * @return {void} */ 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 {!PropertyEffectsType} 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 {?} 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 * @return {void} * @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.__isPropertyEffectsClient && 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 {!PropertyEffectsType} inst The instance owning the binding effect * @param {Node} node Target node for binding * @param {!Binding} binding Binding metadata * @param {!BindingPart} part Binding part metadata * @param {*} value Value to set * @return {void} * @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(/** @type {Element} */(node), value, binding.target); } else { // Property binding let prop = binding.target; if (node.__isPropertyEffectsClient && node.__dataHasAccessor && node.__dataHasAccessor[prop]) { if (!node[TYPES.READ_ONLY] || !node[TYPES.READ_ONLY][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 {!Binding} binding Binding metadata * @param {!BindingPart} 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' || (binding.target === 'value' && (node.localName === 'input' || node.localName === 'textarea'))) { 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 {!Binding} binding Binding metadata * @return {boolean} True if 2-way listener should be added * @private */ function shouldAddListener(binding) { return Boolean(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 {!PropertyEffectsType} inst Instance that bas been previously bound * @param {TemplateInfo} templateInfo Template metadata * @return {void} * @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 {Binding} binding Binding metadata * @return {void} * @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 {!PropertyEffectsType} inst Host element instance to handle notification event * @param {Binding} binding Binding metadata * @return {void} * @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 {!MethodSignature} 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). * @return {void} * @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 {!PropertyEffectsType} 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 {?} 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 = inst._marshalArgs(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"); /** * Create a string from binding parts of all the literal parts * * @param {!Array<BindingPart>} parts All parts to stringify * @return {string} String made from the literal parts */ 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 {?MethodSignature} 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, args: emptyArray }; 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 { 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 {!MethodSignature} sig Method signature metadata object * @return {!MethodSignature} 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 {!MethodArg} 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, value: '', literal: false }; // 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; } // data api /** * Sends array splice notifications (`.splices` and `.length`) * * Note: this implementation only accepts normalized paths * * @param {!PropertyEffectsType} 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 * @return {void} * @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 {!PropertyEffectsType} 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 * @return {void} * @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. * * @mixinFunction * @polymer * @appliesMixin Polymer.TemplateStamp * @appliesMixin 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} * @unrestricted * @private */ const propertyEffectsBase = Polymer.TemplateStamp(Polymer.PropertyAccessors(superClass)); /** * @polymer * @mixinClass * @implements {Polymer_PropertyEffects} * @extends {propertyEffectsBase} * @unrestricted */ class PropertyEffects extends propertyEffectsBase { constructor() { super(); /** @type {boolean} */ // Used to identify users of this mixin, ala instanceof this.__isPropertyEffectsClient = true; /** @type {number} */ // NOTE: used to track re-entrant calls to `_flushProperties` // path changes dirty check against `__dataTemp` only during one "turn" // and are cleared when `__dataCounter` returns to 0. this.__dataCounter = 0; /** @type {boolean} */ this.__dataClientsReady; /** @type {Array} */ this.__dataPendingClients; /** @type {Object} */ this.__dataToNotify; /** @type {Object} */ this.__dataLinkedPaths; /** @type {boolean} */ this.__dataHasPaths; /** @type {Object} */ this.__dataCompoundStorage; /** @type {Polymer_PropertyEffects} */ this.__dataHost; /** @type {!Object} */ this.__dataTemp; /** @type {boolean} */ this.__dataClientsInitialized; /** @type {!Object} */ this.__data; /** @type {!Object} */ this.__dataPending; /** @type {!Object} */ this.__dataOld; /** @type {Object} */ this.__computeEffects; /** @type {Object} */ this.__reflectEffects; /** @type {Object} */ this.__notifyEffects; /** @type {Object} */ this.__propagateEffects; /** @type {Object} */ this.__observeEffects; /** @type {Object} */ this.__readOnly; /** @type {!TemplateInfo} */ this.__templateInfo; } get PROPERTY_EFFECT_TYPES() { return TYPES; } /** * @return {void} */ _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 = {}; this.__dataClientsInitialized = false; } /** * Overrides `Polymer.PropertyAccessors` implementation to provide a * more efficient implementation of initializing properties from * the prototype on the instance. * * @override * @param {Object} props Properties to initialize on the prototype * @return {void} */ _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 * @param {Object} props Properties to initialize on the instance * @return {void} */ _initializeInstanceProperties(props) { let readOnly = this[TYPES.READ_ONLY]; 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 * @return {void} * @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 * @return {void} */ _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(/**@type{string}*/(path), value, shouldNotify)) { computeLinkedPaths(this, path, value); return true; } } else { if (this.__dataHasAccessor && this.__dataHasAccessor[path]) { return this._setPendingProperty(/**@type{string}*/(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 propert