/** * @license * Copyright (c) 2014 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 */ // @version 0.5.5 window.PolymerGestures = {}; (function(scope) { var hasFullPath = false; // test for full event path support var pathTest = document.createElement('meta'); if (pathTest.createShadowRoot) { var sr = pathTest.createShadowRoot(); var s = document.createElement('span'); sr.appendChild(s); pathTest.addEventListener('testpath', function(ev) { if (ev.path) { // if the span is in the event path, then path[0] is the real source for all events hasFullPath = ev.path[0] === s; } ev.stopPropagation(); }); var ev = new CustomEvent('testpath', {bubbles: true}); // must add node to DOM to trigger event listener document.head.appendChild(pathTest); s.dispatchEvent(ev); pathTest.parentNode.removeChild(pathTest); sr = s = null; } pathTest = null; var target = { shadow: function(inEl) { if (inEl) { return inEl.shadowRoot || inEl.webkitShadowRoot; } }, canTarget: function(shadow) { return shadow && Boolean(shadow.elementFromPoint); }, targetingShadow: function(inEl) { var s = this.shadow(inEl); if (this.canTarget(s)) { return s; } }, olderShadow: function(shadow) { var os = shadow.olderShadowRoot; if (!os) { var se = shadow.querySelector('shadow'); if (se) { os = se.olderShadowRoot; } } return os; }, allShadows: function(element) { var shadows = [], s = this.shadow(element); while(s) { shadows.push(s); s = this.olderShadow(s); } return shadows; }, searchRoot: function(inRoot, x, y) { var t, st, sr, os; if (inRoot) { t = inRoot.elementFromPoint(x, y); if (t) { // found element, check if it has a ShadowRoot sr = this.targetingShadow(t); } else if (inRoot !== document) { // check for sibling roots sr = this.olderShadow(inRoot); } // search other roots, fall back to light dom element return this.searchRoot(sr, x, y) || t; } }, owner: function(element) { if (!element) { return document; } var s = element; // walk up until you hit the shadow root or document while (s.parentNode) { s = s.parentNode; } // the owner element is expected to be a Document or ShadowRoot if (s.nodeType != Node.DOCUMENT_NODE && s.nodeType != Node.DOCUMENT_FRAGMENT_NODE) { s = document; } return s; }, findTarget: function(inEvent) { if (hasFullPath && inEvent.path && inEvent.path.length) { return inEvent.path[0]; } var x = inEvent.clientX, y = inEvent.clientY; // if the listener is in the shadow root, it is much faster to start there var s = this.owner(inEvent.target); // if x, y is not in this root, fall back to document search if (!s.elementFromPoint(x, y)) { s = document; } return this.searchRoot(s, x, y); }, findTouchAction: function(inEvent) { var n; if (hasFullPath && inEvent.path && inEvent.path.length) { var path = inEvent.path; for (var i = 0; i < path.length; i++) { n = path[i]; if (n.nodeType === Node.ELEMENT_NODE && n.hasAttribute('touch-action')) { return n.getAttribute('touch-action'); } } } else { n = inEvent.target; while(n) { if (n.nodeType === Node.ELEMENT_NODE && n.hasAttribute('touch-action')) { return n.getAttribute('touch-action'); } n = n.parentNode || n.host; } } // auto is default return "auto"; }, LCA: function(a, b) { if (a === b) { return a; } if (a && !b) { return a; } if (b && !a) { return b; } if (!b && !a) { return document; } // fast case, a is a direct descendant of b or vice versa if (a.contains && a.contains(b)) { return a; } if (b.contains && b.contains(a)) { return b; } var adepth = this.depth(a); var bdepth = this.depth(b); var d = adepth - bdepth; if (d >= 0) { a = this.walk(a, d); } else { b = this.walk(b, -d); } while (a && b && a !== b) { a = a.parentNode || a.host; b = b.parentNode || b.host; } return a; }, walk: function(n, u) { for (var i = 0; n && (i < u); i++) { n = n.parentNode || n.host; } return n; }, depth: function(n) { var d = 0; while(n) { d++; n = n.parentNode || n.host; } return d; }, deepContains: function(a, b) { var common = this.LCA(a, b); // if a is the common ancestor, it must "deeply" contain b return common === a; }, insideNode: function(node, x, y) { var rect = node.getBoundingClientRect(); return (rect.left <= x) && (x <= rect.right) && (rect.top <= y) && (y <= rect.bottom); }, path: function(event) { var p; if (hasFullPath && event.path && event.path.length) { p = event.path; } else { p = []; var n = this.findTarget(event); while (n) { p.push(n); n = n.parentNode || n.host; } } return p; } }; scope.targetFinding = target; /** * Given an event, finds the "deepest" node that could have been the original target before ShadowDOM retargetting * * @param {Event} Event An event object with clientX and clientY properties * @return {Element} The probable event origninator */ scope.findTarget = target.findTarget.bind(target); /** * Determines if the "container" node deeply contains the "containee" node, including situations where the "containee" is contained by one or more ShadowDOM * roots. * * @param {Node} container * @param {Node} containee * @return {Boolean} */ scope.deepContains = target.deepContains.bind(target); /** * Determines if the x/y position is inside the given node. * * Example: * * function upHandler(event) { * var innode = PolymerGestures.insideNode(event.target, event.clientX, event.clientY); * if (innode) { * // wait for tap? * } else { * // tap will never happen * } * } * * @param {Node} node * @param {Number} x Screen X position * @param {Number} y screen Y position * @return {Boolean} */ scope.insideNode = target.insideNode; })(window.PolymerGestures); (function() { function shadowSelector(v) { return 'html /deep/ ' + selector(v); } function selector(v) { return '[touch-action="' + v + '"]'; } function rule(v) { return '{ -ms-touch-action: ' + v + '; touch-action: ' + v + ';}'; } var attrib2css = [ 'none', 'auto', 'pan-x', 'pan-y', { rule: 'pan-x pan-y', selectors: [ 'pan-x pan-y', 'pan-y pan-x' ] }, 'manipulation' ]; var styles = ''; // only install stylesheet if the browser has touch action support var hasTouchAction = typeof document.head.style.touchAction === 'string'; // only add shadow selectors if shadowdom is supported var hasShadowRoot = !window.ShadowDOMPolyfill && document.head.createShadowRoot; if (hasTouchAction) { attrib2css.forEach(function(r) { if (String(r) === r) { styles += selector(r) + rule(r) + '\n'; if (hasShadowRoot) { styles += shadowSelector(r) + rule(r) + '\n'; } } else { styles += r.selectors.map(selector) + rule(r.rule) + '\n'; if (hasShadowRoot) { styles += r.selectors.map(shadowSelector) + rule(r.rule) + '\n'; } } }); var el = document.createElement('style'); el.textContent = styles; document.head.appendChild(el); } })(); /** * This is the constructor for new PointerEvents. * * New Pointer Events must be given a type, and an optional dictionary of * initialization properties. * * Due to certain platform requirements, events returned from the constructor * identify as MouseEvents. * * @constructor * @param {String} inType The type of the event to create. * @param {Object} [inDict] An optional dictionary of initial event properties. * @return {Event} A new PointerEvent of type `inType` and initialized with properties from `inDict`. */ (function(scope) { var MOUSE_PROPS = [ 'bubbles', 'cancelable', 'view', 'detail', 'screenX', 'screenY', 'clientX', 'clientY', 'ctrlKey', 'altKey', 'shiftKey', 'metaKey', 'button', 'relatedTarget', 'pageX', 'pageY' ]; var MOUSE_DEFAULTS = [ false, false, null, null, 0, 0, 0, 0, false, false, false, false, 0, null, 0, 0 ]; var NOP_FACTORY = function(){ return function(){}; }; var eventFactory = { // TODO(dfreedm): this is overridden by tap recognizer, needs review preventTap: NOP_FACTORY, makeBaseEvent: function(inType, inDict) { var e = document.createEvent('Event'); e.initEvent(inType, inDict.bubbles || false, inDict.cancelable || false); e.preventTap = eventFactory.preventTap(e); return e; }, makeGestureEvent: function(inType, inDict) { inDict = inDict || Object.create(null); var e = this.makeBaseEvent(inType, inDict); for (var i = 0, keys = Object.keys(inDict), k; i < keys.length; i++) { k = keys[i]; if( k !== 'bubbles' && k !== 'cancelable' ) { e[k] = inDict[k]; } } return e; }, makePointerEvent: function(inType, inDict) { inDict = inDict || Object.create(null); var e = this.makeBaseEvent(inType, inDict); // define inherited MouseEvent properties for(var i = 2, p; i < MOUSE_PROPS.length; i++) { p = MOUSE_PROPS[i]; e[p] = inDict[p] || MOUSE_DEFAULTS[i]; } e.buttons = inDict.buttons || 0; // Spec requires that pointers without pressure specified use 0.5 for down // state and 0 for up state. var pressure = 0; if (inDict.pressure) { pressure = inDict.pressure; } else { pressure = e.buttons ? 0.5 : 0; } // add x/y properties aliased to clientX/Y e.x = e.clientX; e.y = e.clientY; // define the properties of the PointerEvent interface e.pointerId = inDict.pointerId || 0; e.width = inDict.width || 0; e.height = inDict.height || 0; e.pressure = pressure; e.tiltX = inDict.tiltX || 0; e.tiltY = inDict.tiltY || 0; e.pointerType = inDict.pointerType || ''; e.hwTimestamp = inDict.hwTimestamp || 0; e.isPrimary = inDict.isPrimary || false; e._source = inDict._source || ''; return e; } }; scope.eventFactory = eventFactory; })(window.PolymerGestures); /** * This module implements an map of pointer states */ (function(scope) { var USE_MAP = window.Map && window.Map.prototype.forEach; var POINTERS_FN = function(){ return this.size; }; function PointerMap() { if (USE_MAP) { var m = new Map(); m.pointers = POINTERS_FN; return m; } else { this.keys = []; this.values = []; } } PointerMap.prototype = { set: function(inId, inEvent) { var i = this.keys.indexOf(inId); if (i > -1) { this.values[i] = inEvent; } else { this.keys.push(inId); this.values.push(inEvent); } }, has: function(inId) { return this.keys.indexOf(inId) > -1; }, 'delete': function(inId) { var i = this.keys.indexOf(inId); if (i > -1) { this.keys.splice(i, 1); this.values.splice(i, 1); } }, get: function(inId) { var i = this.keys.indexOf(inId); return this.values[i]; }, clear: function() { this.keys.length = 0; this.values.length = 0; }, // return value, key, map forEach: function(callback, thisArg) { this.values.forEach(function(v, i) { callback.call(thisArg, v, this.keys[i], this); }, this); }, pointers: function() { return this.keys.length; } }; scope.PointerMap = PointerMap; })(window.PolymerGestures); (function(scope) { var CLONE_PROPS = [ // MouseEvent 'bubbles', 'cancelable', 'view', 'detail', 'screenX', 'screenY', 'clientX', 'clientY', 'ctrlKey', 'altKey', 'shiftKey', 'metaKey', 'button', 'relatedTarget', // DOM Level 3 'buttons', // PointerEvent 'pointerId', 'width', 'height', 'pressure', 'tiltX', 'tiltY', 'pointerType', 'hwTimestamp', 'isPrimary', // event instance 'type', 'target', 'currentTarget', 'which', 'pageX', 'pageY', 'timeStamp', // gesture addons 'preventTap', 'tapPrevented', '_source' ]; var CLONE_DEFAULTS = [ // MouseEvent false, false, null, null, 0, 0, 0, 0, false, false, false, false, 0, null, // DOM Level 3 0, // PointerEvent 0, 0, 0, 0, 0, 0, '', 0, false, // event instance '', null, null, 0, 0, 0, 0, function(){}, false ]; var HAS_SVG_INSTANCE = (typeof SVGElementInstance !== 'undefined'); var eventFactory = scope.eventFactory; // set of recognizers to run for the currently handled event var currentGestures; /** * This module is for normalizing events. Mouse and Touch events will be * collected here, and fire PointerEvents that have the same semantics, no * matter the source. * Events fired: * - pointerdown: a pointing is added * - pointerup: a pointer is removed * - pointermove: a pointer is moved * - pointerover: a pointer crosses into an element * - pointerout: a pointer leaves an element * - pointercancel: a pointer will no longer generate events */ var dispatcher = { IS_IOS: false, pointermap: new scope.PointerMap(), requiredGestures: new scope.PointerMap(), eventMap: Object.create(null), // Scope objects for native events. // This exists for ease of testing. eventSources: Object.create(null), eventSourceList: [], gestures: [], // map gesture event -> {listeners: int, index: gestures[int]} dependencyMap: { // make sure down and up are in the map to trigger "register" down: {listeners: 0, index: -1}, up: {listeners: 0, index: -1} }, gestureQueue: [], /** * Add a new event source that will generate pointer events. * * `inSource` must contain an array of event names named `events`, and * functions with the names specified in the `events` array. * @param {string} name A name for the event source * @param {Object} source A new source of platform events. */ registerSource: function(name, source) { var s = source; var newEvents = s.events; if (newEvents) { newEvents.forEach(function(e) { if (s[e]) { this.eventMap[e] = s[e].bind(s); } }, this); this.eventSources[name] = s; this.eventSourceList.push(s); } }, registerGesture: function(name, source) { var obj = Object.create(null); obj.listeners = 0; obj.index = this.gestures.length; for (var i = 0, g; i < source.exposes.length; i++) { g = source.exposes[i].toLowerCase(); this.dependencyMap[g] = obj; } this.gestures.push(source); }, register: function(element, initial) { var l = this.eventSourceList.length; for (var i = 0, es; (i < l) && (es = this.eventSourceList[i]); i++) { // call eventsource register es.register.call(es, element, initial); } }, unregister: function(element) { var l = this.eventSourceList.length; for (var i = 0, es; (i < l) && (es = this.eventSourceList[i]); i++) { // call eventsource register es.unregister.call(es, element); } }, // EVENTS down: function(inEvent) { this.requiredGestures.set(inEvent.pointerId, currentGestures); this.fireEvent('down', inEvent); }, move: function(inEvent) { // pipe move events into gesture queue directly inEvent.type = 'move'; this.fillGestureQueue(inEvent); }, up: function(inEvent) { this.fireEvent('up', inEvent); this.requiredGestures.delete(inEvent.pointerId); }, cancel: function(inEvent) { inEvent.tapPrevented = true; this.fireEvent('up', inEvent); this.requiredGestures.delete(inEvent.pointerId); }, addGestureDependency: function(node, currentGestures) { var gesturesWanted = node._pgEvents; if (gesturesWanted && currentGestures) { var gk = Object.keys(gesturesWanted); for (var i = 0, r, ri, g; i < gk.length; i++) { // gesture g = gk[i]; if (gesturesWanted[g] > 0) { // lookup gesture recognizer r = this.dependencyMap[g]; // recognizer index ri = r ? r.index : -1; currentGestures[ri] = true; } } } }, // LISTENER LOGIC eventHandler: function(inEvent) { // This is used to prevent multiple dispatch of events from // platform events. This can happen when two elements in different scopes // are set up to create pointer events, which is relevant to Shadow DOM. var type = inEvent.type; // only generate the list of desired events on "down" if (type === 'touchstart' || type === 'mousedown' || type === 'pointerdown' || type === 'MSPointerDown') { if (!inEvent._handledByPG) { currentGestures = {}; } // in IOS mode, there is only a listener on the document, so this is not re-entrant if (this.IS_IOS) { var ev = inEvent; if (type === 'touchstart') { var ct = inEvent.changedTouches[0]; // set up a fake event to give to the path builder ev = {target: inEvent.target, clientX: ct.clientX, clientY: ct.clientY, path: inEvent.path}; } // use event path if available, otherwise build a path from target finding var nodes = inEvent.path || scope.targetFinding.path(ev); for (var i = 0, n; i < nodes.length; i++) { n = nodes[i]; this.addGestureDependency(n, currentGestures); } } else { this.addGestureDependency(inEvent.currentTarget, currentGestures); } } if (inEvent._handledByPG) { return; } var fn = this.eventMap && this.eventMap[type]; if (fn) { fn(inEvent); } inEvent._handledByPG = true; }, // set up event listeners listen: function(target, events) { for (var i = 0, l = events.length, e; (i < l) && (e = events[i]); i++) { this.addEvent(target, e); } }, // remove event listeners unlisten: function(target, events) { for (var i = 0, l = events.length, e; (i < l) && (e = events[i]); i++) { this.removeEvent(target, e); } }, addEvent: function(target, eventName) { target.addEventListener(eventName, this.boundHandler); }, removeEvent: function(target, eventName) { target.removeEventListener(eventName, this.boundHandler); }, // EVENT CREATION AND TRACKING /** * Creates a new Event of type `inType`, based on the information in * `inEvent`. * * @param {string} inType A string representing the type of event to create * @param {Event} inEvent A platform event with a target * @return {Event} A PointerEvent of type `inType` */ makeEvent: function(inType, inEvent) { var e = eventFactory.makePointerEvent(inType, inEvent); e.preventDefault = inEvent.preventDefault; e.tapPrevented = inEvent.tapPrevented; e._target = e._target || inEvent.target; return e; }, // make and dispatch an event in one call fireEvent: function(inType, inEvent) { var e = this.makeEvent(inType, inEvent); return this.dispatchEvent(e); }, /** * Returns a snapshot of inEvent, with writable properties. * * @param {Event} inEvent An event that contains properties to copy. * @return {Object} An object containing shallow copies of `inEvent`'s * properties. */ cloneEvent: function(inEvent) { var eventCopy = Object.create(null), p; for (var i = 0; i < CLONE_PROPS.length; i++) { p = CLONE_PROPS[i]; eventCopy[p] = inEvent[p] || CLONE_DEFAULTS[i]; // Work around SVGInstanceElement shadow tree // Return the <use> element that is represented by the instance for Safari, Chrome, IE. // This is the behavior implemented by Firefox. if (p === 'target' || p === 'relatedTarget') { if (HAS_SVG_INSTANCE && eventCopy[p] instanceof SVGElementInstance) { eventCopy[p] = eventCopy[p].correspondingUseElement; } } } // keep the semantics of preventDefault eventCopy.preventDefault = function() { inEvent.preventDefault(); }; return eventCopy; }, /** * Dispatches the event to its target. * * @param {Event} inEvent The event to be dispatched. * @return {Boolean} True if an event handler returns true, false otherwise. */ dispatchEvent: function(inEvent) { var t = inEvent._target; if (t) { t.dispatchEvent(inEvent); // clone the event for the gesture system to process // clone after dispatch to pick up gesture prevention code var clone = this.cloneEvent(inEvent); clone.target = t; this.fillGestureQueue(clone); } }, gestureTrigger: function() { // process the gesture queue for (var i = 0, e, rg; i < this.gestureQueue.length; i++) { e = this.gestureQueue[i]; rg = e._requiredGestures; if (rg) { for (var j = 0, g, fn; j < this.gestures.length; j++) { // only run recognizer if an element in the source event's path is listening for those gestures if (rg[j]) { g = this.gestures[j]; fn = g[e.type]; if (fn) { fn.call(g, e); } } } } } this.gestureQueue.length = 0; }, fillGestureQueue: function(ev) { // only trigger the gesture queue once if (!this.gestureQueue.length) { requestAnimationFrame(this.boundGestureTrigger); } ev._requiredGestures = this.requiredGestures.get(ev.pointerId); this.gestureQueue.push(ev); } }; dispatcher.boundHandler = dispatcher.eventHandler.bind(dispatcher); dispatcher.boundGestureTrigger = dispatcher.gestureTrigger.bind(dispatcher); scope.dispatcher = dispatcher; /** * Listen for `gesture` on `node` with the `handler` function * * If `handler` is the first listener for `gesture`, the underlying gesture recognizer is then enabled. * * @param {Element} node * @param {string} gesture * @return Boolean `gesture` is a valid gesture */ scope.activateGesture = function(node, gesture) { var g = gesture.toLowerCase(); var dep = dispatcher.dependencyMap[g]; if (dep) { var recognizer = dispatcher.gestures[dep.index]; if (!node._pgListeners) { dispatcher.register(node); node._pgListeners = 0; } // TODO(dfreedm): re-evaluate bookkeeping to avoid using attributes if (recognizer) { var touchAction = recognizer.defaultActions && recognizer.defaultActions[g]; var actionNode; switch(node.nodeType) { case Node.ELEMENT_NODE: actionNode = node; break; case Node.DOCUMENT_FRAGMENT_NODE: actionNode = node.host; break; default: actionNode = null; break; } if (touchAction && actionNode && !actionNode.hasAttribute('touch-action')) { actionNode.setAttribute('touch-action', touchAction); } } if (!node._pgEvents) { node._pgEvents = {}; } node._pgEvents[g] = (node._pgEvents[g] || 0) + 1; node._pgListeners++; } return Boolean(dep); }; /** * * Listen for `gesture` from `node` with `handler` function. * * @param {Element} node * @param {string} gesture * @param {Function} handler * @param {Boolean} capture */ scope.addEventListener = function(node, gesture, handler, capture) { if (handler) { scope.activateGesture(node, gesture); node.addEventListener(gesture, handler, capture); } }; /** * Tears down the gesture configuration for `node` * * If `handler` is the last listener for `gesture`, the underlying gesture recognizer is disabled. * * @param {Element} node * @param {string} gesture * @return Boolean `gesture` is a valid gesture */ scope.deactivateGesture = function(node, gesture) { var g = gesture.toLowerCase(); var dep = dispatcher.dependencyMap[g]; if (dep) { if (node._pgListeners > 0) { node._pgListeners--; } if (node._pgListeners === 0) { dispatcher.unregister(node); } if (node._pgEvents) { if (node._pgEvents[g] > 0) { node._pgEvents[g]--; } else { node._pgEvents[g] = 0; } } } return Boolean(dep); }; /** * Stop listening for `gesture` from `node` with `handler` function. * * @param {Element} node * @param {string} gesture * @param {Function} handler * @param {Boolean} capture */ scope.removeEventListener = function(node, gesture, handler, capture) { if (handler) { scope.deactivateGesture(node, gesture); node.removeEventListener(gesture, handler, capture); } }; })(window.PolymerGestures); (function(scope) { var dispatcher = scope.dispatcher; var pointermap = dispatcher.pointermap; // radius around touchend that swallows mouse events var DEDUP_DIST = 25; var WHICH_TO_BUTTONS = [0, 1, 4, 2]; var currentButtons = 0; var FIREFOX_LINUX = /Linux.*Firefox\//i; var HAS_BUTTONS = (function() { // firefox on linux returns spec-incorrect values for mouseup.buttons // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent.buttons#See_also // https://codereview.chromium.org/727593003/#msg16 if (FIREFOX_LINUX.test(navigator.userAgent)) { return false; } try { return new MouseEvent('test', {buttons: 1}).buttons === 1; } catch (e) { return false; } })(); // handler block for native mouse events var mouseEvents = { POINTER_ID: 1, POINTER_TYPE: 'mouse', events: [ 'mousedown', 'mousemove', 'mouseup' ], exposes: [ 'down', 'up', 'move' ], register: function(target) { dispatcher.listen(target, this.events); }, unregister: function(target) { if (target.nodeType === Node.DOCUMENT_NODE) { return; } dispatcher.unlisten(target, this.events); }, lastTouches: [], // collide with the global mouse listener isEventSimulatedFromTouch: function(inEvent) { var lts = this.lastTouches; var x = inEvent.clientX, y = inEvent.clientY; for (var i = 0, l = lts.length, t; i < l && (t = lts[i]); i++) { // simulated mouse events will be swallowed near a primary touchend var dx = Math.abs(x - t.x), dy = Math.abs(y - t.y); if (dx <= DEDUP_DIST && dy <= DEDUP_DIST) { return true; } } }, prepareEvent: function(inEvent) { var e = dispatcher.cloneEvent(inEvent); e.pointerId = this.POINTER_ID; e.isPrimary = true; e.pointerType = this.POINTER_TYPE; e._source = 'mouse'; if (!HAS_BUTTONS) { var type = inEvent.type; var bit = WHICH_TO_BUTTONS[inEvent.which] || 0; if (type === 'mousedown') { currentButtons |= bit; } else if (type === 'mouseup') { currentButtons &= ~bit; } e.buttons = currentButtons; } return e; }, mousedown: function(inEvent) { if (!this.isEventSimulatedFromTouch(inEvent)) { var p = pointermap.has(this.POINTER_ID); var e = this.prepareEvent(inEvent); e.target = scope.findTarget(inEvent); pointermap.set(this.POINTER_ID, e.target); dispatcher.down(e); } }, mousemove: function(inEvent) { if (!this.isEventSimulatedFromTouch(inEvent)) { var target = pointermap.get(this.POINTER_ID); if (target) { var e = this.prepareEvent(inEvent); e.target = target; // handle case where we missed a mouseup if ((HAS_BUTTONS ? e.buttons : e.which) === 0) { if (!HAS_BUTTONS) { currentButtons = e.buttons = 0; } dispatcher.cancel(e); this.cleanupMouse(e.buttons); } else { dispatcher.move(e); } } } }, mouseup: function(inEvent) { if (!this.isEventSimulatedFromTouch(inEvent)) { var e = this.prepareEvent(inEvent); e.relatedTarget = scope.findTarget(inEvent); e.target = pointermap.get(this.POINTER_ID); dispatcher.up(e); this.cleanupMouse(e.buttons); } }, cleanupMouse: function(buttons) { if (buttons === 0) { pointermap.delete(this.POINTER_ID); } } }; scope.mouseEvents = mouseEvents; })(window.PolymerGestures); (function(scope) { var dispatcher = scope.dispatcher; var allShadows = scope.targetFinding.allShadows.bind(scope.targetFinding); var pointermap = dispatcher.pointermap; var touchMap = Array.prototype.map.call.bind(Array.prototype.map); // This should be long enough to ignore compat mouse events made by touch var DEDUP_TIMEOUT = 2500; var DEDUP_DIST = 25; var CLICK_COUNT_TIMEOUT = 200; var HYSTERESIS = 20; var ATTRIB = 'touch-action'; // TODO(dfreedm): disable until http://crbug.com/399765 is resolved // var HAS_TOUCH_ACTION = ATTRIB in document.head.style; var HAS_TOUCH_ACTION = false; // handler block for native touch events var touchEvents = { IS_IOS: false, events: [ 'touchstart', 'touchmove', 'touchend', 'touchcancel' ], exposes: [ 'down', 'up', 'move' ], register: function(target, initial) { if (this.IS_IOS ? initial : !initial) { dispatcher.listen(target, this.events); } }, unregister: function(target) { if (!this.IS_IOS) { dispatcher.unlisten(target, this.events); } }, scrollTypes: { EMITTER: 'none', XSCROLLER: 'pan-x', YSCROLLER: 'pan-y', }, touchActionToScrollType: function(touchAction) { var t = touchAction; var st = this.scrollTypes; if (t === st.EMITTER) { return 'none'; } else if (t === st.XSCROLLER) { return 'X'; } else if (t === st.YSCROLLER) { return 'Y'; } else { return 'XY'; } }, POINTER_TYPE: 'touch', firstTouch: null, isPrimaryTouch: function(inTouch) { return this.firstTouch === inTouch.identifier; }, setPrimaryTouch: function(inTouch) { // set primary touch if there no pointers, or the only pointer is the mouse if (pointermap.pointers() === 0 || (pointermap.pointers() === 1 && pointermap.has(1))) { this.firstTouch = inTouch.identifier; this.firstXY = {X: inTouch.clientX, Y: inTouch.clientY}; this.firstTarget = inTouch.target; this.scrolling = null; this.cancelResetClickCount(); } }, removePrimaryPointer: function(inPointer) { if (inPointer.isPrimary) { this.firstTouch = null; this.firstXY = null; this.resetClickCount(); } }, clickCount: 0, resetId: null, resetClickCount: function() { var fn = function() { this.clickCount = 0; this.resetId = null; }.bind(this); this.resetId = setTimeout(fn, CLICK_COUNT_TIMEOUT); }, cancelResetClickCount: function() { if (this.resetId) { clearTimeout(this.resetId); } }, typeToButtons: function(type) { var ret = 0; if (type === 'touchstart' || type === 'touchmove') { ret = 1; } return ret; }, findTarget: function(touch, id) { if (this.currentTouchEvent.type === 'touchstart') { if (this.isPrimaryTouch(touch)) { var fastPath = { clientX: touch.clientX, clientY: touch.clientY, path: this.currentTouchEvent.path, target: this.currentTouchEvent.target }; return scope.findTarget(fastPath); } else { return scope.findTarget(touch); } } // reuse target we found in touchstart return pointermap.get(id); }, touchToPointer: function(inTouch) { var cte = this.currentTouchEvent; var e = dispatcher.cloneEvent(inTouch); // Spec specifies that pointerId 1 is reserved for Mouse. // Touch identifiers can start at 0. // Add 2 to the touch identifier for compatibility. var id = e.pointerId = inTouch.identifier + 2; e.target = this.findTarget(inTouch, id); e.bubbles = true; e.cancelable = true; e.detail = this.clickCount; e.buttons = this.typeToButtons(cte.type); e.width = inTouch.webkitRadiusX || inTouch.radiusX || 0; e.height = inTouch.webkitRadiusY || inTouch.radiusY || 0; e.pressure = inTouch.webkitForce || inTouch.force || 0.5; e.isPrimary = this.isPrimaryTouch(inTouch); e.pointerType = this.POINTER_TYPE; e._source = 'touch'; // forward touch preventDefaults var self = this; e.preventDefault = function() { self.scrolling = false; self.firstXY = null; cte.preventDefault(); }; return e; }, processTouches: function(inEvent, inFunction) { var tl = inEvent.changedTouches; this.currentTouchEvent = inEvent; for (var i = 0, t, p; i < tl.length; i++) { t = tl[i]; p = this.touchToPointer(t); if (inEvent.type === 'touchstart') { pointermap.set(p.pointerId, p.target); } if (pointermap.has(p.pointerId)) { inFunction.call(this, p); } if (inEvent.type === 'touchend' || inEvent._cancel) { this.cleanUpPointer(p); } } }, // For single axis scrollers, determines whether the element should emit // pointer events or behave as a scroller shouldScroll: function(inEvent) { if (this.firstXY) { var ret; var touchAction = scope.targetFinding.findTouchAction(inEvent); var scrollAxis = this.touchActionToScrollType(touchAction); if (scrollAxis === 'none') { // this element is a touch-action: none, should never scroll ret = false; } else if (scrollAxis === 'XY') { // this element should always scroll ret = true; } else { var t = inEvent.changedTouches[0]; // check the intended scroll axis, and other axis var a = scrollAxis; var oa = scrollAxis === 'Y' ? 'X' : 'Y'; var da = Math.abs(t['client' + a] - this.firstXY[a]); var doa = Math.abs(t['client' + oa] - this.firstXY[oa]); // if delta in the scroll axis > delta other axis, scroll instead of // making events ret = da >= doa; } return ret; } }, findTouch: function(inTL, inId) { for (var i = 0, l = inTL.length, t; i < l && (t = inTL[i]); i++) { if (t.identifier === inId) { return true; } } }, // In some instances, a touchstart can happen without a touchend. This // leaves the pointermap in a broken state. // Therefore, on every touchstart, we remove the touches that did not fire a // touchend event. // To keep state globally consistent, we fire a // pointercancel for this "abandoned" touch vacuumTouches: function(inEvent) { var tl = inEvent.touches; // pointermap.pointers() should be < tl.length here, as the touchstart has not // been processed yet. if (pointermap.pointers() >= tl.length) { var d = []; pointermap.forEach(function(value, key) { // Never remove pointerId == 1, which is mouse. // Touch identifiers are 2 smaller than their pointerId, which is the // index in pointermap. if (key !== 1 && !this.findTouch(tl, key - 2)) { var p = value; d.push(p); } }, this); d.forEach(function(p) { this.cancel(p); pointermap.delete(p.pointerId); }, this); } }, touchstart: function(inEvent) { this.vacuumTouches(inEvent); this.setPrimaryTouch(inEvent.changedTouches[0]); this.dedupSynthMouse(inEvent); if (!this.scrolling) { this.clickCount++; this.processTouches(inEvent, this.down); } }, down: function(inPointer) { dispatcher.down(inPointer); }, touchmove: function(inEvent) { if (HAS_TOUCH_ACTION) { // touchevent.cancelable == false is sent when the page is scrolling under native Touch Action in Chrome 36 // https://groups.google.com/a/chromium.org/d/msg/input-dev/wHnyukcYBcA/b9kmtwM1jJQJ if (inEvent.cancelable) { this.processTouches(inEvent, this.move); } } else { if (!this.scrolling) { if (this.scrolling === null && this.shouldScroll(inEvent)) { this.scrolling = true; } else { this.scrolling = false; inEvent.preventDefault(); this.processTouches(inEvent, this.move); } } else if (this.firstXY) { var t = inEvent.changedTouches[0]; var dx = t.clientX - this.firstXY.X; var dy = t.clientY - this.firstXY.Y; var dd = Math.sqrt(dx * dx + dy * dy); if (dd >= HYSTERESIS) { this.touchcancel(inEvent); this.scrolling = true; this.firstXY = null; } } } }, move: function(inPointer) { dispatcher.move(inPointer); }, touchend: function(inEvent) { this.dedupSynthMouse(inEvent); this.processTouches(inEvent, this.up); }, up: function(inPointer) { inPointer.relatedTarget = scope.findTarget(inPointer); dispatcher.up(inPointer); }, cancel: function(inPointer) { dispatcher.cancel(inPointer); }, touchcancel: function(inEvent) { inEvent._cancel = true; this.processTouches(inEvent, this.cancel); }, cleanUpPointer: function(inPointer) { pointermap['delete'](inPointer.pointerId); this.removePrimaryPointer(inPointer); }, // prevent synth mouse events from creating pointer events dedupSynthMouse: function(inEvent) { var lts = scope.mouseEvents.lastTouches; var t = inEvent.changedTouches[0]; // only the primary finger will synth mouse events if (this.isPrimaryTouch(t)) { // remember x/y of last touch var lt = {x: t.clientX, y: t.clientY}; lts.push(lt); var fn = (function(lts, lt){ var i = lts.indexOf(lt); if (i > -1) { lts.splice(i, 1); } }).bind(null, lts, lt); setTimeout(fn, DEDUP_TIMEOUT); } } }; // prevent "ghost clicks" that come from elements that were removed in a touch handler var STOP_PROP_FN = Event.prototype.stopImmediatePropagation || Event.prototype.stopPropagation; document.addEventListener('click', function(ev) { var x = ev.clientX, y = ev.clientY; // check if a click is within DEDUP_DIST px radius of the touchstart var closeTo = function(touch) { var dx = Math.abs(x - touch.x), dy = Math.abs(y - touch.y); return (dx <= DEDUP_DIST && dy <= DEDUP_DIST); }; // if click coordinates are close to touch coordinates, assume the click came from a touch var wasTouched = scope.mouseEvents.lastTouches.some(closeTo); // if the click came from touch, and the touchstart target is not in the path of the click event, // then the touchstart target was probably removed, and the click should be "busted" var path = scope.targetFinding.path(ev); if (wasTouched) { for (var i = 0; i < path.length; i++) { if (path[i] === touchEvents.firstTarget) { return; } } ev.preventDefault(); STOP_PROP_FN.call(ev); } }, true); scope.touchEvents = touchEvents; })(window.PolymerGestures); (function(scope) { var dispatcher = scope.dispatcher; var pointermap = dispatcher.pointermap; var HAS_BITMAP_TYPE = window.MSPointerEvent && typeof window.MSPointerEvent.MSPOINTER_TYPE_MOUSE === 'number'; var msEvents = { events: [ 'MSPointerDown', 'MSPointerMove', 'MSPointerUp', 'MSPointerCancel', ], register: function(target) { dispatcher.listen(target, this.events); }, unregister: function(target) { if (target.nodeType === Node.DOCUMENT_NODE) { return; } dispatcher.unlisten(target, this.events); }, POINTER_TYPES: [ '', 'unavailable', 'touch', 'pen', 'mouse' ], prepareEvent: function(inEvent) { var e = inEvent; e = dispatcher.cloneEvent(inEvent); if (HAS_BITMAP_TYPE) { e.pointerType = this.POINTER_TYPES[inEvent.pointerType]; } e._source = 'ms'; return e; }, cleanup: function(id) { pointermap['delete'](id); }, MSPointerDown: function(inEvent) { var e = this.prepareEvent(inEvent); e.target = scope.findTarget(inEvent); pointermap.set(inEvent.pointerId, e.target); dispatcher.down(e); }, MSPointerMove: function(inEvent) { var target = pointermap.get(inEvent.pointerId); if (target) { var e = this.prepareEvent(inEvent); e.target = target; dispatcher.move(e); } }, MSPointerUp: function(inEvent) { var e = this.prepareEvent(inEvent); e.relatedTarget = scope.findTarget(inEvent); e.target = pointermap.get(e.pointerId); dispatcher.up(e); this.cleanup(inEvent.pointerId); }, MSPointerCancel: function(inEvent) { var e = this.prepareEvent(inEvent); e.relatedTarget = scope.findTarget(inEvent); e.target = pointermap.get(e.pointerId); dispatcher.cancel(e); this.cleanup(inEvent.pointerId); } }; scope.msEvents = msEvents; })(window.PolymerGestures); (function(scope) { var dispatcher = scope.dispatcher; var pointermap = dispatcher.pointermap; var pointerEvents = { events: [ 'pointerdown', 'pointermove', 'pointerup', 'pointercancel' ], prepareEvent: function(inEvent) { var e = dispatcher.cloneEvent(inEvent); e._source = 'pointer'; return e; }, register: function(target) { dispatcher.listen(target, this.events); }, unregister: function(target) { if (target.nodeType === Node.DOCUMENT_NODE) { return; } dispatcher.unlisten(target, this.events); }, cleanup: function(id) { pointermap['delete'](id); }, pointerdown: function(inEvent) { var e = this.prepareEvent(inEvent); e.target = scope.findTarget(inEvent); pointermap.set(e.pointerId, e.target); dispatcher.down(e); }, pointermove: function(inEvent) { var target = pointermap.get(inEvent.pointerId); if (target) { var e = this.prepareEvent(inEvent); e.target = target; dispatcher.move(e); } }, pointerup: function(inEvent) { var e = this.prepareEvent(inEvent); e.relatedTarget = scope.findTarget(inEvent); e.target = pointermap.get(e.pointerId); dispatcher.up(e); this.cleanup(inEvent.pointerId); }, pointercancel: function(inEvent) { var e = this.prepareEvent(inEvent); e.relatedTarget = scope.findTarget(inEvent); e.target = pointermap.get(e.pointerId); dispatcher.cancel(e); this.cleanup(inEvent.pointerId); } }; scope.pointerEvents = pointerEvents; })(window.PolymerGestures); /** * This module contains the handlers for native platform events. * From here, the dispatcher is called to create unified pointer events. * Included are touch events (v1), mouse events, and MSPointerEvents. */ (function(scope) { var dispatcher = scope.dispatcher; var nav = window.navigator; if (window.PointerEvent) { dispatcher.registerSource('pointer', scope.pointerEvents); } else if (nav.msPointerEnabled) { dispatcher.registerSource('ms', scope.msEvents); } else { dispatcher.registerSource('mouse', scope.mouseEvents); if (window.ontouchstart !== undefined) { dispatcher.registerSource('touch', scope.touchEvents); } } // Work around iOS bugs https://bugs.webkit.org/show_bug.cgi?id=135628 and https://bugs.webkit.org/show_bug.cgi?id=136506 var ua = navigator.userAgent; var IS_IOS = ua.match(/iPad|iPhone|iPod/) && 'ontouchstart' in window; dispatcher.IS_IOS = IS_IOS; scope.touchEvents.IS_IOS = IS_IOS; dispatcher.register(document, true); })(window.PolymerGestures); /** * This event denotes the beginning of a series of tracking events. * * @module PointerGestures * @submodule Events * @class trackstart */ /** * Pixels moved in the x direction since trackstart. * @type Number * @property dx */ /** * Pixes moved in the y direction since trackstart. * @type Number * @property dy */ /** * Pixels moved in the x direction since the last track. * @type Number * @property ddx */ /** * Pixles moved in the y direction since the last track. * @type Number * @property ddy */ /** * The clientX position of the track gesture. * @type Number * @property clientX */ /** * The clientY position of the track gesture. * @type Number * @property clientY */ /** * The pageX position of the track gesture. * @type Number * @property pageX */ /** * The pageY position of the track gesture. * @type Number * @property pageY */ /** * The screenX position of the track gesture. * @type Number * @property screenX */ /** * The screenY position of the track gesture. * @type Number * @property screenY */ /** * The last x axis direction of the pointer. * @type Number * @property xDirection */ /** * The last y axis direction of the pointer. * @type Number * @property yDirection */ /** * A shared object between all tracking events. * @type Object * @property trackInfo */ /** * The element currently under the pointer. * @type Element * @property relatedTarget */ /** * The type of pointer that make the track gesture. * @type String * @property pointerType */ /** * * This event fires for all pointer movement being tracked. * * @class track * @extends trackstart */ /** * This event fires when the pointer is no longer being tracked. * * @class trackend * @extends trackstart */ (function(scope) { var dispatcher = scope.dispatcher; var eventFactory = scope.eventFactory; var pointermap = new scope.PointerMap(); var track = { events: [ 'down', 'move', 'up', ], exposes: [ 'trackstart', 'track', 'trackx', 'tracky', 'trackend' ], defaultActions: { 'track': 'none', 'trackx': 'pan-y', 'tracky': 'pan-x' }, WIGGLE_THRESHOLD: 4, clampDir: function(inDelta) { return inDelta > 0 ? 1 : -1; }, calcPositionDelta: function(inA, inB) { var x = 0, y = 0; if (inA && inB) { x = inB.pageX - inA.pageX; y = inB.pageY - inA.pageY; } return {x: x, y: y}; }, fireTrack: function(inType, inEvent, inTrackingData) { var t = inTrackingData; var d = this.calcPositionDelta(t.downEvent, inEvent); var dd = this.calcPositionDelta(t.lastMoveEvent, inEvent); if (dd.x) { t.xDirection = this.clampDir(dd.x); } else if (inType === 'trackx') { return; } if (dd.y) { t.yDirection = this.clampDir(dd.y); } else if (inType === 'tracky') { return; } var gestureProto = { bubbles: true, cancelable: true, trackInfo: t.trackInfo, relatedTarget: inEvent.relatedTarget, pointerType: inEvent.pointerType, pointerId: inEvent.pointerId, _source: 'track' }; if (inType !== 'tracky') { gestureProto.x = inEvent.x; gestureProto.dx = d.x; gestureProto.ddx = dd.x; gestureProto.clientX = inEvent.clientX; gestureProto.pageX = inEvent.pageX; gestureProto.screenX = inEvent.screenX; gestureProto.xDirection = t.xDirection; } if (inType !== 'trackx') { gestureProto.dy = d.y; gestureProto.ddy = dd.y; gestureProto.y = inEvent.y; gestureProto.clientY = inEvent.clientY; gestureProto.pageY = inEvent.pageY; gestureProto.screenY = inEvent.screenY; gestureProto.yDirection = t.yDirection; } var e = eventFactory.makeGestureEvent(inType, gestureProto); t.downTarget.dispatchEvent(e); }, down: function(inEvent) { if (inEvent.isPrimary && (inEvent.pointerType === 'mouse' ? inEvent.buttons === 1 : true)) { var p = { downEvent: inEvent, downTarget: inEvent.target, trackInfo: {}, lastMoveEvent: null, xDirection: 0, yDirection: 0, tracking: false }; pointermap.set(inEvent.pointerId, p); } }, move: function(inEvent) { var p = pointermap.get(inEvent.pointerId); if (p) { if (!p.tracking) { var d = this.calcPositionDelta(p.downEvent, inEvent); var move = d.x * d.x + d.y * d.y; // start tracking only if finger moves more than WIGGLE_THRESHOLD if (move > this.WIGGLE_THRESHOLD) { p.tracking = true; p.lastMoveEvent = p.downEvent; this.fireTrack('trackstart', inEvent, p); } } if (p.tracking) { this.fireTrack('track', inEvent, p); this.fireTrack('trackx', inEvent, p); this.fireTrack('tracky', inEvent, p); } p.lastMoveEvent = inEvent; } }, up: function(inEvent) { var p = pointermap.get(inEvent.pointerId); if (p) { if (p.tracking) { this.fireTrack('trackend', inEvent, p); } pointermap.delete(inEvent.pointerId); } } }; dispatcher.registerGesture('track', track); })(window.PolymerGestures); /** * This event is fired when a pointer is held down for 200ms. * * @module PointerGestures * @submodule Events * @class hold */ /** * Type of pointer that made the holding event. * @type String * @property pointerType */ /** * Screen X axis position of the held pointer * @type Number * @property clientX */ /** * Screen Y axis position of the held pointer * @type Number * @property clientY */ /** * Type of pointer that made the holding event. * @type String * @property pointerType */ /** * This event is fired every 200ms while a pointer is held down. * * @class holdpulse * @extends hold */ /** * Milliseconds pointer has been held down. * @type Number * @property holdTime */ /** * This event is fired when a held pointer is released or moved. * * @class release */ (function(scope) { var dispatcher = scope.dispatcher; var eventFactory = scope.eventFactory; var hold = { // wait at least HOLD_DELAY ms between hold and pulse events HOLD_DELAY: 200, // pointer can move WIGGLE_THRESHOLD pixels before not counting as a hold WIGGLE_THRESHOLD: 16, events: [ 'down', 'move', 'up', ], exposes: [ 'hold', 'holdpulse', 'release' ], heldPointer: null, holdJob: null, pulse: function() { var hold = Date.now() - this.heldPointer.timeStamp; var type = this.held ? 'holdpulse' : 'hold'; this.fireHold(type, hold); this.held = true; }, cancel: function() { clearInterval(this.holdJob); if (this.held) { this.fireHold('release'); } this.held = false; this.heldPointer = null; this.target = null; this.holdJob = null; }, down: function(inEvent) { if (inEvent.isPrimary && !this.heldPointer) { this.heldPointer = inEvent; this.target = inEvent.target; this.holdJob = setInterval(this.pulse.bind(this), this.HOLD_DELAY); } }, up: function(inEvent) { if (this.heldPointer && this.heldPointer.pointerId === inEvent.pointerId) { this.cancel(); } }, move: function(inEvent) { if (this.heldPointer && this.heldPointer.pointerId === inEvent.pointerId) { var x = inEvent.clientX - this.heldPointer.clientX; var y = inEvent.clientY - this.heldPointer.clientY; if ((x * x + y * y) > this.WIGGLE_THRESHOLD) { this.cancel(); } } }, fireHold: function(inType, inHoldTime) { var p = { bubbles: true, cancelable: true, pointerType: this.heldPointer.pointerType, pointerId: this.heldPointer.pointerId, x: this.heldPointer.clientX, y: this.heldPointer.clientY, _source: 'hold' }; if (inHoldTime) { p.holdTime = inHoldTime; } var e = eventFactory.makeGestureEvent(inType, p); this.target.dispatchEvent(e); } }; dispatcher.registerGesture('hold', hold); })(window.PolymerGestures); /** * This event is fired when a pointer quickly goes down and up, and is used to * denote activation. * * Any gesture event can prevent the tap event from being created by calling * `event.preventTap`. * * Any pointer event can prevent the tap by setting the `tapPrevented` property * on itself. * * @module PointerGestures * @submodule Events * @class tap */ /** * X axis position of the tap. * @property x * @type Number */ /** * Y axis position of the tap. * @property y * @type Number */ /** * Type of the pointer that made the tap. * @property pointerType * @type String */ (function(scope) { var dispatcher = scope.dispatcher; var eventFactory = scope.eventFactory; var pointermap = new scope.PointerMap(); var tap = { events: [ 'down', 'up' ], exposes: [ 'tap' ], down: function(inEvent) { if (inEvent.isPrimary && !inEvent.tapPrevented) { pointermap.set(inEvent.pointerId, { target: inEvent.target, buttons: inEvent.buttons, x: inEvent.clientX, y: inEvent.clientY }); } }, shouldTap: function(e, downState) { var tap = true; if (e.pointerType === 'mouse') { // only allow left click to tap for mouse tap = (e.buttons ^ 1) && (downState.buttons & 1); } return tap && !e.tapPrevented; }, up: function(inEvent) { var start = pointermap.get(inEvent.pointerId); if (start && this.shouldTap(inEvent, start)) { // up.relatedTarget is target currently under finger var t = scope.targetFinding.LCA(start.target, inEvent.relatedTarget); if (t) { var e = eventFactory.makeGestureEvent('tap', { bubbles: true, cancelable: true, x: inEvent.clientX, y: inEvent.clientY, detail: inEvent.detail, pointerType: inEvent.pointerType, pointerId: inEvent.pointerId, altKey: inEvent.altKey, ctrlKey: inEvent.ctrlKey, metaKey: inEvent.metaKey, shiftKey: inEvent.shiftKey, _source: 'tap' }); t.dispatchEvent(e); } } pointermap.delete(inEvent.pointerId); } }; // patch eventFactory to remove id from tap's pointermap for preventTap calls eventFactory.preventTap = function(e) { return function() { e.tapPrevented = true; pointermap.delete(e.pointerId); }; }; dispatcher.registerGesture('tap', tap); })(window.PolymerGestures); /* * Basic strategy: find the farthest apart points, use as diameter of circle * react to size change and rotation of the chord */ /** * @module pointer-gestures * @submodule Events * @class pinch */ /** * Scale of the pinch zoom gesture * @property scale * @type Number */ /** * Center X position of pointers causing pinch * @property centerX * @type Number */ /** * Center Y position of pointers causing pinch * @property centerY * @type Number */ /** * @module pointer-gestures * @submodule Events * @class rotate */ /** * Angle (in degrees) of rotation. Measured from starting positions of pointers. * @property angle * @type Number */ /** * Center X position of pointers causing rotation * @property centerX * @type Number */ /** * Center Y position of pointers causing rotation * @property centerY * @type Number */ (function(scope) { var dispatcher = scope.dispatcher; var eventFactory = scope.eventFactory; var pointermap = new scope.PointerMap(); var RAD_TO_DEG = 180 / Math.PI; var pinch = { events: [ 'down', 'up', 'move', 'cancel' ], exposes: [ 'pinchstart', 'pinch', 'pinchend', 'rotate' ], defaultActions: { 'pinch': 'none', 'rotate': 'none' }, reference: {}, down: function(inEvent) { pointermap.set(inEvent.pointerId, inEvent); if (pointermap.pointers() == 2) { var points = this.calcChord(); var angle = this.calcAngle(points); this.reference = { angle: angle, diameter: points.diameter, target: scope.targetFinding.LCA(points.a.target, points.b.target) }; this.firePinch('pinchstart', points.diameter, points); } }, up: function(inEvent) { var p = pointermap.get(inEvent.pointerId); var num = pointermap.pointers(); if (p) { if (num === 2) { // fire 'pinchend' before deleting pointer var points = this.calcChord(); this.firePinch('pinchend', points.diameter, points); } pointermap.delete(inEvent.pointerId); } }, move: function(inEvent) { if (pointermap.has(inEvent.pointerId)) { pointermap.set(inEvent.pointerId, inEvent); if (pointermap.pointers() > 1) { this.calcPinchRotate(); } } }, cancel: function(inEvent) { this.up(inEvent); }, firePinch: function(type, diameter, points) { var zoom = diameter / this.reference.diameter; var e = eventFactory.makeGestureEvent(type, { bubbles: true, cancelable: true, scale: zoom, centerX: points.center.x, centerY: points.center.y, _source: 'pinch' }); this.reference.target.dispatchEvent(e); }, fireRotate: function(angle, points) { var diff = Math.round((angle - this.reference.angle) % 360); var e = eventFactory.makeGestureEvent('rotate', { bubbles: true, cancelable: true, angle: diff, centerX: points.center.x, centerY: points.center.y, _source: 'pinch' }); this.reference.target.dispatchEvent(e); }, calcPinchRotate: function() { var points = this.calcChord(); var diameter = points.diameter; var angle = this.calcAngle(points); if (diameter != this.reference.diameter) { this.firePinch('pinch', diameter, points); } if (angle != this.reference.angle) { this.fireRotate(angle, points); } }, calcChord: function() { var pointers = []; pointermap.forEach(function(p) { pointers.push(p); }); var dist = 0; // start with at least two pointers var points = {a: pointers[0], b: pointers[1]}; var x, y, d; for (var i = 0; i < pointers.length; i++) { var a = pointers[i]; for (var j = i + 1; j < pointers.length; j++) { var b = pointers[j]; x = Math.abs(a.clientX - b.clientX); y = Math.abs(a.clientY - b.clientY); d = x + y; if (d > dist) { dist = d; points = {a: a, b: b}; } } } x = Math.abs(points.a.clientX + points.b.clientX) / 2; y = Math.abs(points.a.clientY + points.b.clientY) / 2; points.center = { x: x, y: y }; points.diameter = dist; return points; }, calcAngle: function(points) { var x = points.a.clientX - points.b.clientX; var y = points.a.clientY - points.b.clientY; return (360 + Math.atan2(y, x) * RAD_TO_DEG) % 360; } }; dispatcher.registerGesture('pinch', pinch); })(window.PolymerGestures); (function (global) { 'use strict'; var Token, TokenName, Syntax, Messages, source, index, length, delegate, lookahead, state; Token = { BooleanLiteral: 1, EOF: 2, Identifier: 3, Keyword: 4, NullLiteral: 5, NumericLiteral: 6, Punctuator: 7, StringLiteral: 8 }; TokenName = {}; TokenName[Token.BooleanLiteral] = 'Boolean'; TokenName[Token.EOF] = '<end>'; TokenName[Token.Identifier] = 'Identifier'; TokenName[Token.Keyword] = 'Keyword'; TokenName[Token.NullLiteral] = 'Null'; TokenName[Token.NumericLiteral] = 'Numeric'; TokenName[Token.Punctuator] = 'Punctuator'; TokenName[Token.StringLiteral] = 'String'; Syntax = { ArrayExpression: 'ArrayExpression', BinaryExpression: 'BinaryExpression', CallExpression: 'CallExpression', ConditionalExpression: 'ConditionalExpression', EmptyStatement: 'EmptyStatement', ExpressionStatement: 'ExpressionStatement', Identifier: 'Identifier', Literal: 'Literal', LabeledStatement: 'LabeledStatement', LogicalExpression: 'LogicalExpression', MemberExpression: 'MemberExpression', ObjectExpression: 'ObjectExpression', Program: 'Program', Property: 'Property', ThisExpression: 'ThisExpression', UnaryExpression: 'UnaryExpression' }; // Error messages should be identical to V8. Messages = { UnexpectedToken: 'Unexpected token %0', UnknownLabel: 'Undefined label \'%0\'', Redeclaration: '%0 \'%1\' has already been declared' }; // Ensure the condition is true, otherwise throw an error. // This is only to have a better contract semantic, i.e. another safety net // to catch a logic error. The condition shall be fulfilled in normal case. // Do NOT use this to enforce a certain condition on any user input. function assert(condition, message) { if (!condition) { throw new Error('ASSERT: ' + message); } } function isDecimalDigit(ch) { return (ch >= 48 && ch <= 57); // 0..9 } // 7.2 White Space function isWhiteSpace(ch) { return (ch === 32) || // space (ch === 9) || // tab (ch === 0xB) || (ch === 0xC) || (ch === 0xA0) || (ch >= 0x1680 && '\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\uFEFF'.indexOf(String.fromCharCode(ch)) > 0); } // 7.3 Line Terminators function isLineTerminator(ch) { return (ch === 10) || (ch === 13) || (ch === 0x2028) || (ch === 0x2029); } // 7.6 Identifier Names and Identifiers function isIdentifierStart(ch) { return (ch === 36) || (ch === 95) || // $ (dollar) and _ (underscore) (ch >= 65 && ch <= 90) || // A..Z (ch >= 97 && ch <= 122); // a..z } function isIdentifierPart(ch) { return (ch === 36) || (ch === 95) || // $ (dollar) and _ (underscore) (ch >= 65 && ch <= 90) || // A..Z (ch >= 97 && ch <= 122) || // a..z (ch >= 48 && ch <= 57); // 0..9 } // 7.6.1.1 Keywords function isKeyword(id) { return (id === 'this') } // 7.4 Comments function skipWhitespace() { while (index < length && isWhiteSpace(source.charCodeAt(index))) { ++index; } } function getIdentifier() { var start, ch; start = index++; while (index < length) { ch = source.charCodeAt(index); if (isIdentifierPart(ch)) { ++index; } else { break; } } return source.slice(start, index); } function scanIdentifier() { var start, id, type; start = index; id = getIdentifier(); // There is no keyword or literal with only one character. // Thus, it must be an identifier. if (id.length === 1) { type = Token.Identifier; } else if (isKeyword(id)) { type = Token.Keyword; } else if (id === 'null') { type = Token.NullLiteral; } else if (id === 'true' || id === 'false') { type = Token.BooleanLiteral; } else { type = Token.Identifier; } return { type: type, value: id, range: [start, index] }; } // 7.7 Punctuators function scanPunctuator() { var start = index, code = source.charCodeAt(index), code2, ch1 = source[index], ch2; switch (code) { // Check for most common single-character punctuators. case 46: // . dot case 40: // ( open bracket case 41: // ) close bracket case 59: // ; semicolon case 44: // , comma case 123: // { open curly brace case 125: // } close curly brace case 91: // [ case 93: // ] case 58: // : case 63: // ? ++index; return { type: Token.Punctuator, value: String.fromCharCode(code), range: [start, index] }; default: code2 = source.charCodeAt(index + 1); // '=' (char #61) marks an assignment or comparison operator. if (code2 === 61) { switch (code) { case 37: // % case 38: // & case 42: // *: case 43: // + case 45: // - case 47: // / case 60: // < case 62: // > case 124: // | index += 2; return { type: Token.Punctuator, value: String.fromCharCode(code) + String.fromCharCode(code2), range: [start, index] }; case 33: // ! case 61: // = index += 2; // !== and === if (source.charCodeAt(index) === 61) { ++index; } return { type: Token.Punctuator, value: source.slice(start, index), range: [start, index] }; default: break; } } break; } // Peek more characters. ch2 = source[index + 1]; // Other 2-character punctuators: && || if (ch1 === ch2 && ('&|'.indexOf(ch1) >= 0)) { index += 2; return { type: Token.Punctuator, value: ch1 + ch2, range: [start, index] }; } if ('<>=!+-*%&|^/'.indexOf(ch1) >= 0) { ++index; return { type: Token.Punctuator, value: ch1, range: [start, index] }; } throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); } // 7.8.3 Numeric Literals function scanNumericLiteral() { var number, start, ch; ch = source[index]; assert(isDecimalDigit(ch.charCodeAt(0)) || (ch === '.'), 'Numeric literal must start with a decimal digit or a decimal point'); start = index; number = ''; if (ch !== '.') { number = source[index++]; ch = source[index]; // Hex number starts with '0x'. // Octal number starts with '0'. if (number === '0') { // decimal number starts with '0' such as '09' is illegal. if (ch && isDecimalDigit(ch.charCodeAt(0))) { throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); } } while (isDecimalDigit(source.charCodeAt(index))) { number += source[index++]; } ch = source[index]; } if (ch === '.') { number += source[index++]; while (isDecimalDigit(source.charCodeAt(index))) { number += source[index++]; } ch = source[index]; } if (ch === 'e' || ch === 'E') { number += source[index++]; ch = source[index]; if (ch === '+' || ch === '-') { number += source[index++]; } if (isDecimalDigit(source.charCodeAt(index))) { while (isDecimalDigit(source.charCodeAt(index))) { number += source[index++]; } } else { throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); } } if (isIdentifierStart(source.charCodeAt(index))) { throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); } return { type: Token.NumericLiteral, value: parseFloat(number), range: [start, index] }; } // 7.8.4 String Literals function scanStringLiteral() { var str = '', quote, start, ch, octal = false; quote = source[index]; assert((quote === '\'' || quote === '"'), 'String literal must starts with a quote'); start = index; ++index; while (index < length) { ch = source[index++]; if (ch === quote) { quote = ''; break; } else if (ch === '\\') { ch = source[index++]; if (!ch || !isLineTerminator(ch.charCodeAt(0))) { switch (ch) { case 'n': str += '\n'; break; case 'r': str += '\r'; break; case 't': str += '\t'; break; case 'b': str += '\b'; break; case 'f': str += '\f'; break; case 'v': str += '\x0B'; break; default: str += ch; break; } } else { if (ch === '\r' && source[index] === '\n') { ++index; } } } else if (isLineTerminator(ch.charCodeAt(0))) { break; } else { str += ch; } } if (quote !== '') { throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); } return { type: Token.StringLiteral, value: str, octal: octal, range: [start, index] }; } function isIdentifierName(token) { return token.type === Token.Identifier || token.type === Token.Keyword || token.type === Token.BooleanLiteral || token.type === Token.NullLiteral; } function advance() { var ch; skipWhitespace(); if (index >= length) { return { type: Token.EOF, range: [index, index] }; } ch = source.charCodeAt(index); // Very common: ( and ) and ; if (ch === 40 || ch === 41 || ch === 58) { return scanPunctuator(); } // String literal starts with single quote (#39) or double quote (#34). if (ch === 39 || ch === 34) { return scanStringLiteral(); } if (isIdentifierStart(ch)) { return scanIdentifier(); } // Dot (.) char #46 can also start a floating-point number, hence the need // to check the next character. if (ch === 46) { if (isDecimalDigit(source.charCodeAt(index + 1))) { return scanNumericLiteral(); } return scanPunctuator(); } if (isDecimalDigit(ch)) { return scanNumericLiteral(); } return scanPunctuator(); } function lex() { var token; token = lookahead; index = token.range[1]; lookahead = advance(); index = token.range[1]; return token; } function peek() { var pos; pos = index; lookahead = advance(); index = pos; } // Throw an exception function throwError(token, messageFormat) { var error, args = Array.prototype.slice.call(arguments, 2), msg = messageFormat.replace( /%(\d)/g, function (whole, index) { assert(index < args.length, 'Message reference must be in range'); return args[index]; } ); error = new Error(msg); error.index = index; error.description = msg; throw error; } // Throw an exception because of the token. function throwUnexpected(token) { throwError(token, Messages.UnexpectedToken, token.value); } // Expect the next token to match the specified punctuator. // If not, an exception will be thrown. function expect(value) { var token = lex(); if (token.type !== Token.Punctuator || token.value !== value) { throwUnexpected(token); } } // Return true if the next token matches the specified punctuator. function match(value) { return lookahead.type === Token.Punctuator && lookahead.value === value; } // Return true if the next token matches the specified keyword function matchKeyword(keyword) { return lookahead.type === Token.Keyword && lookahead.value === keyword; } function consumeSemicolon() { // Catch the very common case first: immediately a semicolon (char #59). if (source.charCodeAt(index) === 59) { lex(); return; } skipWhitespace(); if (match(';')) { lex(); return; } if (lookahead.type !== Token.EOF && !match('}')) { throwUnexpected(lookahead); } } // 11.1.4 Array Initialiser function parseArrayInitialiser() { var elements = []; expect('['); while (!match(']')) { if (match(',')) { lex(); elements.push(null); } else { elements.push(parseExpression()); if (!match(']')) { expect(','); } } } expect(']'); return delegate.createArrayExpression(elements); } // 11.1.5 Object Initialiser function parseObjectPropertyKey() { var token; skipWhitespace(); token = lex(); // Note: This function is called only from parseObjectProperty(), where // EOF and Punctuator tokens are already filtered out. if (token.type === Token.StringLiteral || token.type === Token.NumericLiteral) { return delegate.createLiteral(token); } return delegate.createIdentifier(token.value); } function parseObjectProperty() { var token, key; token = lookahead; skipWhitespace(); if (token.type === Token.EOF || token.type === Token.Punctuator) { throwUnexpected(token); } key = parseObjectPropertyKey(); expect(':'); return delegate.createProperty('init', key, parseExpression()); } function parseObjectInitialiser() { var properties = []; expect('{'); while (!match('}')) { properties.push(parseObjectProperty()); if (!match('}')) { expect(','); } } expect('}'); return delegate.createObjectExpression(properties); } // 11.1.6 The Grouping Operator function parseGroupExpression() { var expr; expect('('); expr = parseExpression(); expect(')'); return expr; } // 11.1 Primary Expressions function parsePrimaryExpression() { var type, token, expr; if (match('(')) { return parseGroupExpression(); } type = lookahead.type; if (type === Token.Identifier) { expr = delegate.createIdentifier(lex().value); } else if (type === Token.StringLiteral || type === Token.NumericLiteral) { expr = delegate.createLiteral(lex()); } else if (type === Token.Keyword) { if (matchKeyword('this')) { lex(); expr = delegate.createThisExpression(); } } else if (type === Token.BooleanLiteral) { token = lex(); token.value = (token.value === 'true'); expr = delegate.createLiteral(token); } else if (type === Token.NullLiteral) { token = lex(); token.value = null; expr = delegate.createLiteral(token); } else if (match('[')) { expr = parseArrayInitialiser(); } else if (match('{')) { expr = parseObjectInitialiser(); } if (expr) { return expr; } throwUnexpected(lex()); } // 11.2 Left-Hand-Side Expressions function parseArguments() { var args = []; expect('('); if (!match(')')) { while (index < length) { args.push(parseExpression()); if (match(')')) { break; } expect(','); } } expect(')'); return args; } function parseNonComputedProperty() { var token; token = lex(); if (!isIdentifierName(token)) { throwUnexpected(token); } return delegate.createIdentifier(token.value); } function parseNonComputedMember() { expect('.'); return parseNonComputedProperty(); } function parseComputedMember() { var expr; expect('['); expr = parseExpression(); expect(']'); return expr; } function parseLeftHandSideExpression() { var expr, args, property; expr = parsePrimaryExpression(); while (true) { if (match('[')) { property = parseComputedMember(); expr = delegate.createMemberExpression('[', expr, property); } else if (match('.')) { property = parseNonComputedMember(); expr = delegate.createMemberExpression('.', expr, property); } else if (match('(')) { args = parseArguments(); expr = delegate.createCallExpression(expr, args); } else { break; } } return expr; } // 11.3 Postfix Expressions var parsePostfixExpression = parseLeftHandSideExpression; // 11.4 Unary Operators function parseUnaryExpression() { var token, expr; if (lookahead.type !== Token.Punctuator && lookahead.type !== Token.Keyword) { expr = parsePostfixExpression(); } else if (match('+') || match('-') || match('!')) { token = lex(); expr = parseUnaryExpression(); expr = delegate.createUnaryExpression(token.value, expr); } else if (matchKeyword('delete') || matchKeyword('void') || matchKeyword('typeof')) { throwError({}, Messages.UnexpectedToken); } else { expr = parsePostfixExpression(); } return expr; } function binaryPrecedence(token) { var prec = 0; if (token.type !== Token.Punctuator && token.type !== Token.Keyword) { return 0; } switch (token.value) { case '||': prec = 1; break; case '&&': prec = 2; break; case '==': case '!=': case '===': case '!==': prec = 6; break; case '<': case '>': case '<=': case '>=': case 'instanceof': prec = 7; break; case 'in': prec = 7; break; case '+': case '-': prec = 9; break; case '*': case '/': case '%': prec = 11; break; default: break; } return prec; } // 11.5 Multiplicative Operators // 11.6 Additive Operators // 11.7 Bitwise Shift Operators // 11.8 Relational Operators // 11.9 Equality Operators // 11.10 Binary Bitwise Operators // 11.11 Binary Logical Operators function parseBinaryExpression() { var expr, token, prec, stack, right, operator, left, i; left = parseUnaryExpression(); token = lookahead; prec = binaryPrecedence(token); if (prec === 0) { return left; } token.prec = prec; lex(); right = parseUnaryExpression(); stack = [left, token, right]; while ((prec = binaryPrecedence(lookahead)) > 0) { // Reduce: make a binary expression from the three topmost entries. while ((stack.length > 2) && (prec <= stack[stack.length - 2].prec)) { right = stack.pop(); operator = stack.pop().value; left = stack.pop(); expr = delegate.createBinaryExpression(operator, left, right); stack.push(expr); } // Shift. token = lex(); token.prec = prec; stack.push(token); expr = parseUnaryExpression(); stack.push(expr); } // Final reduce to clean-up the stack. i = stack.length - 1; expr = stack[i]; while (i > 1) { expr = delegate.createBinaryExpression(stack[i - 1].value, stack[i - 2], expr); i -= 2; } return expr; } // 11.12 Conditional Operator function parseConditionalExpression() { var expr, consequent, alternate; expr = parseBinaryExpression(); if (match('?')) { lex(); consequent = parseConditionalExpression(); expect(':'); alternate = parseConditionalExpression(); expr = delegate.createConditionalExpression(expr, consequent, alternate); } return expr; } // Simplification since we do not support AssignmentExpression. var parseExpression = parseConditionalExpression; // Polymer Syntax extensions // Filter :: // Identifier // Identifier "(" ")" // Identifier "(" FilterArguments ")" function parseFilter() { var identifier, args; identifier = lex(); if (identifier.type !== Token.Identifier) { throwUnexpected(identifier); } args = match('(') ? parseArguments() : []; return delegate.createFilter(identifier.value, args); } // Filters :: // "|" Filter // Filters "|" Filter function parseFilters() { while (match('|')) { lex(); parseFilter(); } } // TopLevel :: // LabelledExpressions // AsExpression // InExpression // FilterExpression // AsExpression :: // FilterExpression as Identifier // InExpression :: // Identifier, Identifier in FilterExpression // Identifier in FilterExpression // FilterExpression :: // Expression // Expression Filters function parseTopLevel() { skipWhitespace(); peek(); var expr = parseExpression(); if (expr) { if (lookahead.value === ',' || lookahead.value == 'in' && expr.type === Syntax.Identifier) { parseInExpression(expr); } else { parseFilters(); if (lookahead.value === 'as') { parseAsExpression(expr); } else { delegate.createTopLevel(expr); } } } if (lookahead.type !== Token.EOF) { throwUnexpected(lookahead); } } function parseAsExpression(expr) { lex(); // as var identifier = lex().value; delegate.createAsExpression(expr, identifier); } function parseInExpression(identifier) { var indexName; if (lookahead.value === ',') { lex(); if (lookahead.type !== Token.Identifier) throwUnexpected(lookahead); indexName = lex().value; } lex(); // in var expr = parseExpression(); parseFilters(); delegate.createInExpression(identifier.name, indexName, expr); } function parse(code, inDelegate) { delegate = inDelegate; source = code; index = 0; length = source.length; lookahead = null; state = { labelSet: {} }; return parseTopLevel(); } global.esprima = { parse: parse }; })(this); // Copyright (c) 2014 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 (function (global) { 'use strict'; function prepareBinding(expressionText, name, node, filterRegistry) { var expression; try { expression = getExpression(expressionText); if (expression.scopeIdent && (node.nodeType !== Node.ELEMENT_NODE || node.tagName !== 'TEMPLATE' || (name !== 'bind' && name !== 'repeat'))) { throw Error('as and in can only be used within <template bind/repeat>'); } } catch (ex) { console.error('Invalid expression syntax: ' + expressionText, ex); return; } return function(model, node, oneTime) { var binding = expression.getBinding(model, filterRegistry, oneTime); if (expression.scopeIdent && binding) { node.polymerExpressionScopeIdent_ = expression.scopeIdent; if (expression.indexIdent) node.polymerExpressionIndexIdent_ = expression.indexIdent; } return binding; } } // TODO(rafaelw): Implement simple LRU. var expressionParseCache = Object.create(null); function getExpression(expressionText) { var expression = expressionParseCache[expressionText]; if (!expression) { var delegate = new ASTDelegate(); esprima.parse(expressionText, delegate); expression = new Expression(delegate); expressionParseCache[expressionText] = expression; } return expression; } function Literal(value) { this.value = value; this.valueFn_ = undefined; } Literal.prototype = { valueFn: function() { if (!this.valueFn_) { var value = this.value; this.valueFn_ = function() { return value; } } return this.valueFn_; } } function IdentPath(name) { this.name = name; this.path = Path.get(name); } IdentPath.prototype = { valueFn: function() { if (!this.valueFn_) { var name = this.name; var path = this.path; this.valueFn_ = function(model, observer) { if (observer) observer.addPath(model, path); return path.getValueFrom(model); } } return this.valueFn_; }, setValue: function(model, newValue) { if (this.path.length == 1) model = findScope(model, this.path[0]); return this.path.setValueFrom(model, newValue); } }; function MemberExpression(object, property, accessor) { this.computed = accessor == '['; this.dynamicDeps = typeof object == 'function' || object.dynamicDeps || (this.computed && !(property instanceof Literal)); this.simplePath = !this.dynamicDeps && (property instanceof IdentPath || property instanceof Literal) && (object instanceof MemberExpression || object instanceof IdentPath); this.object = this.simplePath ? object : getFn(object); this.property = !this.computed || this.simplePath ? property : getFn(property); } MemberExpression.prototype = { get fullPath() { if (!this.fullPath_) { var parts = this.object instanceof MemberExpression ? this.object.fullPath.slice() : [this.object.name]; parts.push(this.property instanceof IdentPath ? this.property.name : this.property.value); this.fullPath_ = Path.get(parts); } return this.fullPath_; }, valueFn: function() { if (!this.valueFn_) { var object = this.object; if (this.simplePath) { var path = this.fullPath; this.valueFn_ = function(model, observer) { if (observer) observer.addPath(model, path); return path.getValueFrom(model); }; } else if (!this.computed) { var path = Path.get(this.property.name); this.valueFn_ = function(model, observer, filterRegistry) { var context = object(model, observer, filterRegistry); if (observer) observer.addPath(context, path); return path.getValueFrom(context); } } else { // Computed property. var property = this.property; this.valueFn_ = function(model, observer, filterRegistry) { var context = object(model, observer, filterRegistry); var propName = property(model, observer, filterRegistry); if (observer) observer.addPath(context, [propName]); return context ? context[propName] : undefined; }; } } return this.valueFn_; }, setValue: function(model, newValue) { if (this.simplePath) { this.fullPath.setValueFrom(model, newValue); return newValue; } var object = this.object(model); var propName = this.property instanceof IdentPath ? this.property.name : this.property(model); return object[propName] = newValue; } }; function Filter(name, args) { this.name = name; this.args = []; for (var i = 0; i < args.length; i++) { this.args[i] = getFn(args[i]); } } Filter.prototype = { transform: function(model, observer, filterRegistry, toModelDirection, initialArgs) { var context = model; var fn = context[this.name]; if (!fn) { fn = filterRegistry[this.name]; if (!fn) { console.error('Cannot find function or filter: ' + this.name); return; } } // If toModelDirection is falsey, then the "normal" (dom-bound) direction // is used. Otherwise, it looks for a 'toModel' property function on the // object. if (toModelDirection) { fn = fn.toModel; } else if (typeof fn.toDOM == 'function') { fn = fn.toDOM; } if (typeof fn != 'function') { console.error('Cannot find function or filter: ' + this.name); return; } var args = initialArgs || []; for (var i = 0; i < this.args.length; i++) { args.push(getFn(this.args[i])(model, observer, filterRegistry)); } return fn.apply(context, args); } }; function notImplemented() { throw Error('Not Implemented'); } var unaryOperators = { '+': function(v) { return +v; }, '-': function(v) { return -v; }, '!': function(v) { return !v; } }; var binaryOperators = { '+': function(l, r) { return l+r; }, '-': function(l, r) { return l-r; }, '*': function(l, r) { return l*r; }, '/': function(l, r) { return l/r; }, '%': function(l, r) { return l%r; }, '<': function(l, r) { return l<r; }, '>': function(l, r) { return l>r; }, '<=': function(l, r) { return l<=r; }, '>=': function(l, r) { return l>=r; }, '==': function(l, r) { return l==r; }, '!=': function(l, r) { return l!=r; }, '===': function(l, r) { return l===r; }, '!==': function(l, r) { return l!==r; }, '&&': function(l, r) { return l&&r; }, '||': function(l, r) { return l||r; }, }; function getFn(arg) { return typeof arg == 'function' ? arg : arg.valueFn(); } function ASTDelegate() { this.expression = null; this.filters = []; this.deps = {}; this.currentPath = undefined; this.scopeIdent = undefined; this.indexIdent = undefined; this.dynamicDeps = false; } ASTDelegate.prototype = { createUnaryExpression: function(op, argument) { if (!unaryOperators[op]) throw Error('Disallowed operator: ' + op); argument = getFn(argument); return function(model, observer, filterRegistry) { return unaryOperators[op](argument(model, observer, filterRegistry)); }; }, createBinaryExpression: function(op, left, right) { if (!binaryOperators[op]) throw Error('Disallowed operator: ' + op); left = getFn(left); right = getFn(right); switch (op) { case '||': this.dynamicDeps = true; return function(model, observer, filterRegistry) { return left(model, observer, filterRegistry) || right(model, observer, filterRegistry); }; case '&&': this.dynamicDeps = true; return function(model, observer, filterRegistry) { return left(model, observer, filterRegistry) && right(model, observer, filterRegistry); }; } return function(model, observer, filterRegistry) { return binaryOperators[op](left(model, observer, filterRegistry), right(model, observer, filterRegistry)); }; }, createConditionalExpression: function(test, consequent, alternate) { test = getFn(test); consequent = getFn(consequent); alternate = getFn(alternate); this.dynamicDeps = true; return function(model, observer, filterRegistry) { return test(model, observer, filterRegistry) ? consequent(model, observer, filterRegistry) : alternate(model, observer, filterRegistry); } }, createIdentifier: function(name) { var ident = new IdentPath(name); ident.type = 'Identifier'; return ident; }, createMemberExpression: function(accessor, object, property) { var ex = new MemberExpression(object, property, accessor); if (ex.dynamicDeps) this.dynamicDeps = true; return ex; }, createCallExpression: function(expression, args) { if (!(expression instanceof IdentPath)) throw Error('Only identifier function invocations are allowed'); var filter = new Filter(expression.name, args); return function(model, observer, filterRegistry) { return filter.transform(model, observer, filterRegistry, false); }; }, createLiteral: function(token) { return new Literal(token.value); }, createArrayExpression: function(elements) { for (var i = 0; i < elements.length; i++) elements[i] = getFn(elements[i]); return function(model, observer, filterRegistry) { var arr = [] for (var i = 0; i < elements.length; i++) arr.push(elements[i](model, observer, filterRegistry)); return arr; } }, createProperty: function(kind, key, value) { return { key: key instanceof IdentPath ? key.name : key.value, value: value }; }, createObjectExpression: function(properties) { for (var i = 0; i < properties.length; i++) properties[i].value = getFn(properties[i].value); return function(model, observer, filterRegistry) { var obj = {}; for (var i = 0; i < properties.length; i++) obj[properties[i].key] = properties[i].value(model, observer, filterRegistry); return obj; } }, createFilter: function(name, args) { this.filters.push(new Filter(name, args)); }, createAsExpression: function(expression, scopeIdent) { this.expression = expression; this.scopeIdent = scopeIdent; }, createInExpression: function(scopeIdent, indexIdent, expression) { this.expression = expression; this.scopeIdent = scopeIdent; this.indexIdent = indexIdent; }, createTopLevel: function(expression) { this.expression = expression; }, createThisExpression: notImplemented } function ConstantObservable(value) { this.value_ = value; } ConstantObservable.prototype = { open: function() { return this.value_; }, discardChanges: function() { return this.value_; }, deliver: function() {}, close: function() {}, } function Expression(delegate) { this.scopeIdent = delegate.scopeIdent; this.indexIdent = delegate.indexIdent; if (!delegate.expression) throw Error('No expression found.'); this.expression = delegate.expression; getFn(this.expression); // forces enumeration of path dependencies this.filters = delegate.filters; this.dynamicDeps = delegate.dynamicDeps; } Expression.prototype = { getBinding: function(model, filterRegistry, oneTime) { if (oneTime) return this.getValue(model, undefined, filterRegistry); var observer = new CompoundObserver(); // captures deps. var firstValue = this.getValue(model, observer, filterRegistry); var firstTime = true; var self = this; function valueFn() { // deps cannot have changed on first value retrieval. if (firstTime) { firstTime = false; return firstValue; } if (self.dynamicDeps) observer.startReset(); var value = self.getValue(model, self.dynamicDeps ? observer : undefined, filterRegistry); if (self.dynamicDeps) observer.finishReset(); return value; } function setValueFn(newValue) { self.setValue(model, newValue, filterRegistry); return newValue; } return new ObserverTransform(observer, valueFn, setValueFn, true); }, getValue: function(model, observer, filterRegistry) { var value = getFn(this.expression)(model, observer, filterRegistry); for (var i = 0; i < this.filters.length; i++) { value = this.filters[i].transform(model, observer, filterRegistry, false, [value]); } return value; }, setValue: function(model, newValue, filterRegistry) { var count = this.filters ? this.filters.length : 0; while (count-- > 0) { newValue = this.filters[count].transform(model, undefined, filterRegistry, true, [newValue]); } if (this.expression.setValue) return this.expression.setValue(model, newValue); } } /** * Converts a style property name to a css property name. For example: * "WebkitUserSelect" to "-webkit-user-select" */ function convertStylePropertyName(name) { return String(name).replace(/[A-Z]/g, function(c) { return '-' + c.toLowerCase(); }); } var parentScopeName = '@' + Math.random().toString(36).slice(2); // Single ident paths must bind directly to the appropriate scope object. // I.e. Pushed values in two-bindings need to be assigned to the actual model // object. function findScope(model, prop) { while (model[parentScopeName] && !Object.prototype.hasOwnProperty.call(model, prop)) { model = model[parentScopeName]; } return model; } function isLiteralExpression(pathString) { switch (pathString) { case '': return false; case 'false': case 'null': case 'true': return true; } if (!isNaN(Number(pathString))) return true; return false; }; function PolymerExpressions() {} PolymerExpressions.prototype = { // "built-in" filters styleObject: function(value) { var parts = []; for (var key in value) { parts.push(convertStylePropertyName(key) + ': ' + value[key]); } return parts.join('; '); }, tokenList: function(value) { var tokens = []; for (var key in value) { if (value[key]) tokens.push(key); } return tokens.join(' '); }, // binding delegate API prepareInstancePositionChanged: function(template) { var indexIdent = template.polymerExpressionIndexIdent_; if (!indexIdent) return; return function(templateInstance, index) { templateInstance.model[indexIdent] = index; }; }, prepareBinding: function(pathString, name, node) { var path = Path.get(pathString); if (!isLiteralExpression(pathString) && path.valid) { if (path.length == 1) { return function(model, node, oneTime) { if (oneTime) return path.getValueFrom(model); var scope = findScope(model, path[0]); return new PathObserver(scope, path); }; } return; // bail out early if pathString is simple path. } return prepareBinding(pathString, name, node, this); }, prepareInstanceModel: function(template) { var scopeName = template.polymerExpressionScopeIdent_; if (!scopeName) return; var parentScope = template.templateInstance ? template.templateInstance.model : template.model; var indexName = template.polymerExpressionIndexIdent_; return function(model) { return createScopeObject(parentScope, model, scopeName, indexName); }; } }; var createScopeObject = ('__proto__' in {}) ? function(parentScope, model, scopeName, indexName) { var scope = {}; scope[scopeName] = model; scope[indexName] = undefined; scope[parentScopeName] = parentScope; scope.__proto__ = parentScope; return scope; } : function(parentScope, model, scopeName, indexName) { var scope = Object.create(parentScope); Object.defineProperty(scope, scopeName, { value: model, configurable: true, writable: true }); Object.defineProperty(scope, indexName, { value: undefined, configurable: true, writable: true }); Object.defineProperty(scope, parentScopeName, { value: parentScope, configurable: true, writable: true }); return scope; }; global.PolymerExpressions = PolymerExpressions; PolymerExpressions.getExpression = getExpression; })(this); Polymer = { version: '0.5.5' }; // TODO(sorvell): this ensures Polymer is an object and not a function // Platform is currently defining it as a function to allow for async loading // of polymer; once we refine the loading process this likely goes away. if (typeof window.Polymer === 'function') { Polymer = {}; } (function(scope) { function withDependencies(task, depends) { depends = depends || []; if (!depends.map) { depends = [depends]; } return task.apply(this, depends.map(marshal)); } function module(name, dependsOrFactory, moduleFactory) { var module; switch (arguments.length) { case 0: return; case 1: module = null; break; case 2: // dependsOrFactory is `factory` in this case module = dependsOrFactory.apply(this); break; default: // dependsOrFactory is `depends` in this case module = withDependencies(moduleFactory, dependsOrFactory); break; } modules[name] = module; }; function marshal(name) { return modules[name]; } var modules = {}; function using(depends, task) { HTMLImports.whenImportsReady(function() { withDependencies(task, depends); }); }; // exports scope.marshal = marshal; // `module` confuses commonjs detectors scope.modularize = module; scope.using = using; })(window); /* Build only script. Ensures scripts needed for basic x-platform compatibility will be run when platform.js is not loaded. */ if (!window.WebComponents) { /* On supported platforms, platform.js is not needed. To retain compatibility with the polyfills, we stub out minimal functionality. */ if (!window.WebComponents) { WebComponents = { flush: function() {}, flags: {log: {}} }; Platform = WebComponents; CustomElements = { useNative: true, ready: true, takeRecords: function() {}, instanceof: function(obj, base) { return obj instanceof base; } }; HTMLImports = { useNative: true }; addEventListener('HTMLImportsLoaded', function() { document.dispatchEvent( new CustomEvent('WebComponentsReady', {bubbles: true}) ); }); // ShadowDOM ShadowDOMPolyfill = null; wrap = unwrap = function(n){ return n; }; } /* Create polyfill scope and feature detect native support. */ window.HTMLImports = window.HTMLImports || {flags:{}}; (function(scope) { /** Basic setup and simple module executer. We collect modules and then execute the code later, only if it's necessary for polyfilling. */ var IMPORT_LINK_TYPE = 'import'; var useNative = Boolean(IMPORT_LINK_TYPE in document.createElement('link')); /** Support `currentScript` on all browsers as `document._currentScript.` NOTE: We cannot polyfill `document.currentScript` because it's not possible both to override and maintain the ability to capture the native value. Therefore we choose to expose `_currentScript` both when native imports and the polyfill are in use. */ // NOTE: ShadowDOMPolyfill intrusion. var hasShadowDOMPolyfill = Boolean(window.ShadowDOMPolyfill); var wrap = function(node) { return hasShadowDOMPolyfill ? ShadowDOMPolyfill.wrapIfNeeded(node) : node; }; var rootDocument = wrap(document); var currentScriptDescriptor = { get: function() { var script = HTMLImports.currentScript || document.currentScript || // NOTE: only works when called in synchronously executing code. // readyState should check if `loading` but IE10 is // interactive when scripts run so we cheat. (document.readyState !== 'complete' ? document.scripts[document.scripts.length - 1] : null); return wrap(script); }, configurable: true }; Object.defineProperty(document, '_currentScript', currentScriptDescriptor); Object.defineProperty(rootDocument, '_currentScript', currentScriptDescriptor); /** Add support for the `HTMLImportsLoaded` event and the `HTMLImports.whenReady` method. This api is necessary because unlike the native implementation, script elements do not force imports to resolve. Instead, users should wrap code in either an `HTMLImportsLoaded` hander or after load time in an `HTMLImports.whenReady(callback)` call. NOTE: This module also supports these apis under the native implementation. Therefore, if this file is loaded, the same code can be used under both the polyfill and native implementation. */ var isIE = /Trident/.test(navigator.userAgent); // call a callback when all HTMLImports in the document at call time // (or at least document ready) have loaded. // 1. ensure the document is in a ready state (has dom), then // 2. watch for loading of imports and call callback when done function whenReady(callback, doc) { doc = doc || rootDocument; // if document is loading, wait and try again whenDocumentReady(function() { watchImportsLoad(callback, doc); }, doc); } // call the callback when the document is in a ready state (has dom) var requiredReadyState = isIE ? 'complete' : 'interactive'; var READY_EVENT = 'readystatechange'; function isDocumentReady(doc) { return (doc.readyState === 'complete' || doc.readyState === requiredReadyState); } // call <callback> when we ensure the document is in a ready state function whenDocumentReady(callback, doc) { if (!isDocumentReady(doc)) { var checkReady = function() { if (doc.readyState === 'complete' || doc.readyState === requiredReadyState) { doc.removeEventListener(READY_EVENT, checkReady); whenDocumentReady(callback, doc); } }; doc.addEventListener(READY_EVENT, checkReady); } else if (callback) { callback(); } } function markTargetLoaded(event) { event.target.__loaded = true; } // call <callback> when we ensure all imports have loaded function watchImportsLoad(callback, doc) { var imports = doc.querySelectorAll('link[rel=import]'); var loaded = 0, l = imports.length; function checkDone(d) { if ((loaded == l) && callback) { callback(); } } function loadedImport(e) { markTargetLoaded(e); loaded++; checkDone(); } if (l) { for (var i=0, imp; (i<l) && (imp=imports[i]); i++) { if (isImportLoaded(imp)) { loadedImport.call(imp, {target: imp}); } else { imp.addEventListener('load', loadedImport); imp.addEventListener('error', loadedImport); } } } else { checkDone(); } } // NOTE: test for native imports loading is based on explicitly watching // all imports (see below). // However, we cannot rely on this entirely without watching the entire document // for import links. For perf reasons, currently only head is watched. // Instead, we fallback to checking if the import property is available // and the document is not itself loading. function isImportLoaded(link) { return useNative ? link.__loaded || (link.import && link.import.readyState !== 'loading') : link.__importParsed; } // TODO(sorvell): Workaround for // https://www.w3.org/Bugs/Public/show_bug.cgi?id=25007, should be removed when // this bug is addressed. // (1) Install a mutation observer to see when HTMLImports have loaded // (2) if this script is run during document load it will watch any existing // imports for loading. // // NOTE: The workaround has restricted functionality: (1) it's only compatible // with imports that are added to document.head since the mutation observer // watches only head for perf reasons, (2) it requires this script // to run before any imports have completed loading. if (useNative) { new MutationObserver(function(mxns) { for (var i=0, l=mxns.length, m; (i < l) && (m=mxns[i]); i++) { if (m.addedNodes) { handleImports(m.addedNodes); } } }).observe(document.head, {childList: true}); function handleImports(nodes) { for (var i=0, l=nodes.length, n; (i<l) && (n=nodes[i]); i++) { if (isImport(n)) { handleImport(n); } } } function isImport(element) { return element.localName === 'link' && element.rel === 'import'; } function handleImport(element) { var loaded = element.import; if (loaded) { markTargetLoaded({target: element}); } else { element.addEventListener('load', markTargetLoaded); element.addEventListener('error', markTargetLoaded); } } // make sure to catch any imports that are in the process of loading // when this script is run. (function() { if (document.readyState === 'loading') { var imports = document.querySelectorAll('link[rel=import]'); for (var i=0, l=imports.length, imp; (i<l) && (imp=imports[i]); i++) { handleImport(imp); } } })(); } // Fire the 'HTMLImportsLoaded' event when imports in document at load time // have loaded. This event is required to simulate the script blocking // behavior of native imports. A main document script that needs to be sure // imports have loaded should wait for this event. whenReady(function() { HTMLImports.ready = true; HTMLImports.readyTime = new Date().getTime(); rootDocument.dispatchEvent( new CustomEvent('HTMLImportsLoaded', {bubbles: true}) ); }); // exports scope.IMPORT_LINK_TYPE = IMPORT_LINK_TYPE; scope.useNative = useNative; scope.rootDocument = rootDocument; scope.whenReady = whenReady; scope.isIE = isIE; })(HTMLImports); (function(scope) { // TODO(sorvell): It's desireable to provide a default stylesheet // that's convenient for styling unresolved elements, but // it's cumbersome to have to include this manually in every page. // It would make sense to put inside some HTMLImport but // the HTMLImports polyfill does not allow loading of stylesheets // that block rendering. Therefore this injection is tolerated here. var style = document.createElement('style'); style.textContent = '' + 'body {' + 'transition: opacity ease-in 0.2s;' + ' } \n' + 'body[unresolved] {' + 'opacity: 0; display: block; overflow: hidden;' + ' } \n' ; var head = document.querySelector('head'); head.insertBefore(style, head.firstChild); })(Platform); /* Build only script. Ensures scripts needed for basic x-platform compatibility will be run when platform.js is not loaded. */ } (function(global) { 'use strict'; var testingExposeCycleCount = global.testingExposeCycleCount; // Detect and do basic sanity checking on Object/Array.observe. function detectObjectObserve() { if (typeof Object.observe !== 'function' || typeof Array.observe !== 'function') { return false; } var records = []; function callback(recs) { records = recs; } var test = {}; var arr = []; Object.observe(test, callback); Array.observe(arr, callback); test.id = 1; test.id = 2; delete test.id; arr.push(1, 2); arr.length = 0; Object.deliverChangeRecords(callback); if (records.length !== 5) return false; if (records[0].type != 'add' || records[1].type != 'update' || records[2].type != 'delete' || records[3].type != 'splice' || records[4].type != 'splice') { return false; } Object.unobserve(test, callback); Array.unobserve(arr, callback); return true; } var hasObserve = detectObjectObserve(); function detectEval() { // Don't test for eval if we're running in a Chrome App environment. // We check for APIs set that only exist in a Chrome App context. if (typeof chrome !== 'undefined' && chrome.app && chrome.app.runtime) { return false; } // Firefox OS Apps do not allow eval. This feature detection is very hacky // but even if some other platform adds support for this function this code // will continue to work. if (typeof navigator != 'undefined' && navigator.getDeviceStorage) { return false; } try { var f = new Function('', 'return true;'); return f(); } catch (ex) { return false; } } var hasEval = detectEval(); function isIndex(s) { return +s === s >>> 0 && s !== ''; } function toNumber(s) { return +s; } function isObject(obj) { return obj === Object(obj); } var numberIsNaN = global.Number.isNaN || function(value) { return typeof value === 'number' && global.isNaN(value); } function areSameValue(left, right) { if (left === right) return left !== 0 || 1 / left === 1 / right; if (numberIsNaN(left) && numberIsNaN(right)) return true; return left !== left && right !== right; } var createObject = ('__proto__' in {}) ? function(obj) { return obj; } : function(obj) { var proto = obj.__proto__; if (!proto) return obj; var newObject = Object.create(proto); Object.getOwnPropertyNames(obj).forEach(function(name) { Object.defineProperty(newObject, name, Object.getOwnPropertyDescriptor(obj, name)); }); return newObject; }; var identStart = '[\$_a-zA-Z]'; var identPart = '[\$_a-zA-Z0-9]'; var identRegExp = new RegExp('^' + identStart + '+' + identPart + '*' + '$'); function getPathCharType(char) { if (char === undefined) return 'eof'; var code = char.charCodeAt(0); switch(code) { case 0x5B: // [ case 0x5D: // ] case 0x2E: // . case 0x22: // " case 0x27: // ' case 0x30: // 0 return char; case 0x5F: // _ case 0x24: // $ return 'ident'; case 0x20: // Space case 0x09: // Tab case 0x0A: // Newline case 0x0D: // Return case 0xA0: // No-break space case 0xFEFF: // Byte Order Mark case 0x2028: // Line Separator case 0x2029: // Paragraph Separator return 'ws'; } // a-z, A-Z if ((0x61 <= code && code <= 0x7A) || (0x41 <= code && code <= 0x5A)) return 'ident'; // 1-9 if (0x31 <= code && code <= 0x39) return 'number'; return 'else'; } var pathStateMachine = { 'beforePath': { 'ws': ['beforePath'], 'ident': ['inIdent', 'append'], '[': ['beforeElement'], 'eof': ['afterPath'] }, 'inPath': { 'ws': ['inPath'], '.': ['beforeIdent'], '[': ['beforeElement'], 'eof': ['afterPath'] }, 'beforeIdent': { 'ws': ['beforeIdent'], 'ident': ['inIdent', 'append'] }, 'inIdent': { 'ident': ['inIdent', 'append'], '0': ['inIdent', 'append'], 'number': ['inIdent', 'append'], 'ws': ['inPath', 'push'], '.': ['beforeIdent', 'push'], '[': ['beforeElement', 'push'], 'eof': ['afterPath', 'push'] }, 'beforeElement': { 'ws': ['beforeElement'], '0': ['afterZero', 'append'], 'number': ['inIndex', 'append'], "'": ['inSingleQuote', 'append', ''], '"': ['inDoubleQuote', 'append', ''] }, 'afterZero': { 'ws': ['afterElement', 'push'], ']': ['inPath', 'push'] }, 'inIndex': { '0': ['inIndex', 'append'], 'number': ['inIndex', 'append'], 'ws': ['afterElement'], ']': ['inPath', 'push'] }, 'inSingleQuote': { "'": ['afterElement'], 'eof': ['error'], 'else': ['inSingleQuote', 'append'] }, 'inDoubleQuote': { '"': ['afterElement'], 'eof': ['error'], 'else': ['inDoubleQuote', 'append'] }, 'afterElement': { 'ws': ['afterElement'], ']': ['inPath', 'push'] } } function noop() {} function parsePath(path) { var keys = []; var index = -1; var c, newChar, key, type, transition, action, typeMap, mode = 'beforePath'; var actions = { push: function() { if (key === undefined) return; keys.push(key); key = undefined; }, append: function() { if (key === undefined) key = newChar else key += newChar; } }; function maybeUnescapeQuote() { if (index >= path.length) return; var nextChar = path[index + 1]; if ((mode == 'inSingleQuote' && nextChar == "'") || (mode == 'inDoubleQuote' && nextChar == '"')) { index++; newChar = nextChar; actions.append(); return true; } } while (mode) { index++; c = path[index]; if (c == '\\' && maybeUnescapeQuote(mode)) continue; type = getPathCharType(c); typeMap = pathStateMachine[mode]; transition = typeMap[type] || typeMap['else'] || 'error'; if (transition == 'error') return; // parse error; mode = transition[0]; action = actions[transition[1]] || noop; newChar = transition[2] === undefined ? c : transition[2]; action(); if (mode === 'afterPath') { return keys; } } return; // parse error } function isIdent(s) { return identRegExp.test(s); } var constructorIsPrivate = {}; function Path(parts, privateToken) { if (privateToken !== constructorIsPrivate) throw Error('Use Path.get to retrieve path objects'); for (var i = 0; i < parts.length; i++) { this.push(String(parts[i])); } if (hasEval && this.length) { this.getValueFrom = this.compiledGetValueFromFn(); } } // TODO(rafaelw): Make simple LRU cache var pathCache = {}; function getPath(pathString) { if (pathString instanceof Path) return pathString; if (pathString == null || pathString.length == 0) pathString = ''; if (typeof pathString != 'string') { if (isIndex(pathString.length)) { // Constructed with array-like (pre-parsed) keys return new Path(pathString, constructorIsPrivate); } pathString = String(pathString); } var path = pathCache[pathString]; if (path) return path; var parts = parsePath(pathString); if (!parts) return invalidPath; var path = new Path(parts, constructorIsPrivate); pathCache[pathString] = path; return path; } Path.get = getPath; function formatAccessor(key) { if (isIndex(key)) { return '[' + key + ']'; } else { return '["' + key.replace(/"/g, '\\"') + '"]'; } } Path.prototype = createObject({ __proto__: [], valid: true, toString: function() { var pathString = ''; for (var i = 0; i < this.length; i++) { var key = this[i]; if (isIdent(key)) { pathString += i ? '.' + key : key; } else { pathString += formatAccessor(key); } } return pathString; }, getValueFrom: function(obj, directObserver) { for (var i = 0; i < this.length; i++) { if (obj == null) return; obj = obj[this[i]]; } return obj; }, iterateObjects: function(obj, observe) { for (var i = 0; i < this.length; i++) { if (i) obj = obj[this[i - 1]]; if (!isObject(obj)) return; observe(obj, this[i]); } }, compiledGetValueFromFn: function() { var str = ''; var pathString = 'obj'; str += 'if (obj != null'; var i = 0; var key; for (; i < (this.length - 1); i++) { key = this[i]; pathString += isIdent(key) ? '.' + key : formatAccessor(key); str += ' &&\n ' + pathString + ' != null'; } str += ')\n'; var key = this[i]; pathString += isIdent(key) ? '.' + key : formatAccessor(key); str += ' return ' + pathString + ';\nelse\n return undefined;'; return new Function('obj', str); }, setValueFrom: function(obj, value) { if (!this.length) return false; for (var i = 0; i < this.length - 1; i++) { if (!isObject(obj)) return false; obj = obj[this[i]]; } if (!isObject(obj)) return false; obj[this[i]] = value; return true; } }); var invalidPath = new Path('', constructorIsPrivate); invalidPath.valid = false; invalidPath.getValueFrom = invalidPath.setValueFrom = function() {}; var MAX_DIRTY_CHECK_CYCLES = 1000; function dirtyCheck(observer) { var cycles = 0; while (cycles < MAX_DIRTY_CHECK_CYCLES && observer.check_()) { cycles++; } if (testingExposeCycleCount) global.dirtyCheckCycleCount = cycles; return cycles > 0; } function objectIsEmpty(object) { for (var prop in object) return false; return true; } function diffIsEmpty(diff) { return objectIsEmpty(diff.added) && objectIsEmpty(diff.removed) && objectIsEmpty(diff.changed); } function diffObjectFromOldObject(object, oldObject) { var added = {}; var removed = {}; var changed = {}; for (var prop in oldObject) { var newValue = object[prop]; if (newValue !== undefined && newValue === oldObject[prop]) continue; if (!(prop in object)) { removed[prop] = undefined; continue; } if (newValue !== oldObject[prop]) changed[prop] = newValue; } for (var prop in object) { if (prop in oldObject) continue; added[prop] = object[prop]; } if (Array.isArray(object) && object.length !== oldObject.length) changed.length = object.length; return { added: added, removed: removed, changed: changed }; } var eomTasks = []; function runEOMTasks() { if (!eomTasks.length) return false; for (var i = 0; i < eomTasks.length; i++) { eomTasks[i](); } eomTasks.length = 0; return true; } var runEOM = hasObserve ? (function(){ return function(fn) { return Promise.resolve().then(fn); } })() : (function() { return function(fn) { eomTasks.push(fn); }; })(); var observedObjectCache = []; function newObservedObject() { var observer; var object; var discardRecords = false; var first = true; function callback(records) { if (observer && observer.state_ === OPENED && !discardRecords) observer.check_(records); } return { open: function(obs) { if (observer) throw Error('ObservedObject in use'); if (!first) Object.deliverChangeRecords(callback); observer = obs; first = false; }, observe: function(obj, arrayObserve) { object = obj; if (arrayObserve) Array.observe(object, callback); else Object.observe(object, callback); }, deliver: function(discard) { discardRecords = discard; Object.deliverChangeRecords(callback); discardRecords = false; }, close: function() { observer = undefined; Object.unobserve(object, callback); observedObjectCache.push(this); } }; } /* * The observedSet abstraction is a perf optimization which reduces the total * number of Object.observe observations of a set of objects. The idea is that * groups of Observers will have some object dependencies in common and this * observed set ensures that each object in the transitive closure of * dependencies is only observed once. The observedSet acts as a write barrier * such that whenever any change comes through, all Observers are checked for * changed values. * * Note that this optimization is explicitly moving work from setup-time to * change-time. * * TODO(rafaelw): Implement "garbage collection". In order to move work off * the critical path, when Observers are closed, their observed objects are * not Object.unobserve(d). As a result, it's possible that if the observedSet * is kept open, but some Observers have been closed, it could cause "leaks" * (prevent otherwise collectable objects from being collected). At some * point, we should implement incremental "gc" which keeps a list of * observedSets which may need clean-up and does small amounts of cleanup on a * timeout until all is clean. */ function getObservedObject(observer, object, arrayObserve) { var dir = observedObjectCache.pop() || newObservedObject(); dir.open(observer); dir.observe(object, arrayObserve); return dir; } var observedSetCache = []; function newObservedSet() { var observerCount = 0; var observers = []; var objects = []; var rootObj; var rootObjProps; function observe(obj, prop) { if (!obj) return; if (obj === rootObj) rootObjProps[prop] = true; if (objects.indexOf(obj) < 0) { objects.push(obj); Object.observe(obj, callback); } observe(Object.getPrototypeOf(obj), prop); } function allRootObjNonObservedProps(recs) { for (var i = 0; i < recs.length; i++) { var rec = recs[i]; if (rec.object !== rootObj || rootObjProps[rec.name] || rec.type === 'setPrototype') { return false; } } return true; } function callback(recs) { if (allRootObjNonObservedProps(recs)) return; var observer; for (var i = 0; i < observers.length; i++) { observer = observers[i]; if (observer.state_ == OPENED) { observer.iterateObjects_(observe); } } for (var i = 0; i < observers.length; i++) { observer = observers[i]; if (observer.state_ == OPENED) { observer.check_(); } } } var record = { objects: objects, get rootObject() { return rootObj; }, set rootObject(value) { rootObj = value; rootObjProps = {}; }, open: function(obs, object) { observers.push(obs); observerCount++; obs.iterateObjects_(observe); }, close: function(obs) { observerCount--; if (observerCount > 0) { return; } for (var i = 0; i < objects.length; i++) { Object.unobserve(objects[i], callback); Observer.unobservedCount++; } observers.length = 0; objects.length = 0; rootObj = undefined; rootObjProps = undefined; observedSetCache.push(this); if (lastObservedSet === this) lastObservedSet = null; }, }; return record; } var lastObservedSet; function getObservedSet(observer, obj) { if (!lastObservedSet || lastObservedSet.rootObject !== obj) { lastObservedSet = observedSetCache.pop() || newObservedSet(); lastObservedSet.rootObject = obj; } lastObservedSet.open(observer, obj); return lastObservedSet; } var UNOPENED = 0; var OPENED = 1; var CLOSED = 2; var RESETTING = 3; var nextObserverId = 1; function Observer() { this.state_ = UNOPENED; this.callback_ = undefined; this.target_ = undefined; // TODO(rafaelw): Should be WeakRef this.directObserver_ = undefined; this.value_ = undefined; this.id_ = nextObserverId++; } Observer.prototype = { open: function(callback, target) { if (this.state_ != UNOPENED) throw Error('Observer has already been opened.'); addToAll(this); this.callback_ = callback; this.target_ = target; this.connect_(); this.state_ = OPENED; return this.value_; }, close: function() { if (this.state_ != OPENED) return; removeFromAll(this); this.disconnect_(); this.value_ = undefined; this.callback_ = undefined; this.target_ = undefined; this.state_ = CLOSED; }, deliver: function() { if (this.state_ != OPENED) return; dirtyCheck(this); }, report_: function(changes) { try { this.callback_.apply(this.target_, changes); } catch (ex) { Observer._errorThrownDuringCallback = true; console.error('Exception caught during observer callback: ' + (ex.stack || ex)); } }, discardChanges: function() { this.check_(undefined, true); return this.value_; } } var collectObservers = !hasObserve; var allObservers; Observer._allObserversCount = 0; if (collectObservers) { allObservers = []; } function addToAll(observer) { Observer._allObserversCount++; if (!collectObservers) return; allObservers.push(observer); } function removeFromAll(observer) { Observer._allObserversCount--; } var runningMicrotaskCheckpoint = false; global.Platform = global.Platform || {}; global.Platform.performMicrotaskCheckpoint = function() { if (runningMicrotaskCheckpoint) return; if (!collectObservers) return; runningMicrotaskCheckpoint = true; var cycles = 0; var anyChanged, toCheck; do { cycles++; toCheck = allObservers; allObservers = []; anyChanged = false; for (var i = 0; i < toCheck.length; i++) { var observer = toCheck[i]; if (observer.state_ != OPENED) continue; if (observer.check_()) anyChanged = true; allObservers.push(observer); } if (runEOMTasks()) anyChanged = true; } while (cycles < MAX_DIRTY_CHECK_CYCLES && anyChanged); if (testingExposeCycleCount) global.dirtyCheckCycleCount = cycles; runningMicrotaskCheckpoint = false; }; if (collectObservers) { global.Platform.clearObservers = function() { allObservers = []; }; } function ObjectObserver(object) { Observer.call(this); this.value_ = object; this.oldObject_ = undefined; } ObjectObserver.prototype = createObject({ __proto__: Observer.prototype, arrayObserve: false, connect_: function(callback, target) { if (hasObserve) { this.directObserver_ = getObservedObject(this, this.value_, this.arrayObserve); } else { this.oldObject_ = this.copyObject(this.value_); } }, copyObject: function(object) { var copy = Array.isArray(object) ? [] : {}; for (var prop in object) { copy[prop] = object[prop]; }; if (Array.isArray(object)) copy.length = object.length; return copy; }, check_: function(changeRecords, skipChanges) { var diff; var oldValues; if (hasObserve) { if (!changeRecords) return false; oldValues = {}; diff = diffObjectFromChangeRecords(this.value_, changeRecords, oldValues); } else { oldValues = this.oldObject_; diff = diffObjectFromOldObject(this.value_, this.oldObject_); } if (diffIsEmpty(diff)) return false; if (!hasObserve) this.oldObject_ = this.copyObject(this.value_); this.report_([ diff.added || {}, diff.removed || {}, diff.changed || {}, function(property) { return oldValues[property]; } ]); return true; }, disconnect_: function() { if (hasObserve) { this.directObserver_.close(); this.directObserver_ = undefined; } else { this.oldObject_ = undefined; } }, deliver: function() { if (this.state_ != OPENED) return; if (hasObserve) this.directObserver_.deliver(false); else dirtyCheck(this); }, discardChanges: function() { if (this.directObserver_) this.directObserver_.deliver(true); else this.oldObject_ = this.copyObject(this.value_); return this.value_; } }); function ArrayObserver(array) { if (!Array.isArray(array)) throw Error('Provided object is not an Array'); ObjectObserver.call(this, array); } ArrayObserver.prototype = createObject({ __proto__: ObjectObserver.prototype, arrayObserve: true, copyObject: function(arr) { return arr.slice(); }, check_: function(changeRecords) { var splices; if (hasObserve) { if (!changeRecords) return false; splices = projectArraySplices(this.value_, changeRecords); } else { splices = calcSplices(this.value_, 0, this.value_.length, this.oldObject_, 0, this.oldObject_.length); } if (!splices || !splices.length) return false; if (!hasObserve) this.oldObject_ = this.copyObject(this.value_); this.report_([splices]); return true; } }); ArrayObserver.applySplices = function(previous, current, splices) { splices.forEach(function(splice) { var spliceArgs = [splice.index, splice.removed.length]; var addIndex = splice.index; while (addIndex < splice.index + splice.addedCount) { spliceArgs.push(current[addIndex]); addIndex++; } Array.prototype.splice.apply(previous, spliceArgs); }); }; function PathObserver(object, path) { Observer.call(this); this.object_ = object; this.path_ = getPath(path); this.directObserver_ = undefined; } PathObserver.prototype = createObject({ __proto__: Observer.prototype, get path() { return this.path_; }, connect_: function() { if (hasObserve) this.directObserver_ = getObservedSet(this, this.object_); this.check_(undefined, true); }, disconnect_: function() { this.value_ = undefined; if (this.directObserver_) { this.directObserver_.close(this); this.directObserver_ = undefined; } }, iterateObjects_: function(observe) { this.path_.iterateObjects(this.object_, observe); }, check_: function(changeRecords, skipChanges) { var oldValue = this.value_; this.value_ = this.path_.getValueFrom(this.object_); if (skipChanges || areSameValue(this.value_, oldValue)) return false; this.report_([this.value_, oldValue, this]); return true; }, setValue: function(newValue) { if (this.path_) this.path_.setValueFrom(this.object_, newValue); } }); function CompoundObserver(reportChangesOnOpen) { Observer.call(this); this.reportChangesOnOpen_ = reportChangesOnOpen; this.value_ = []; this.directObserver_ = undefined; this.observed_ = []; } var observerSentinel = {}; CompoundObserver.prototype = createObject({ __proto__: Observer.prototype, connect_: function() { if (hasObserve) { var object; var needsDirectObserver = false; for (var i = 0; i < this.observed_.length; i += 2) { object = this.observed_[i] if (object !== observerSentinel) { needsDirectObserver = true; break; } } if (needsDirectObserver) this.directObserver_ = getObservedSet(this, object); } this.check_(undefined, !this.reportChangesOnOpen_); }, disconnect_: function() { for (var i = 0; i < this.observed_.length; i += 2) { if (this.observed_[i] === observerSentinel) this.observed_[i + 1].close(); } this.observed_.length = 0; this.value_.length = 0; if (this.directObserver_) { this.directObserver_.close(this); this.directObserver_ = undefined; } }, addPath: function(object, path) { if (this.state_ != UNOPENED && this.state_ != RESETTING) throw Error('Cannot add paths once started.'); var path = getPath(path); this.observed_.push(object, path); if (!this.reportChangesOnOpen_) return; var index = this.observed_.length / 2 - 1; this.value_[index] = path.getValueFrom(object); }, addObserver: function(observer) { if (this.state_ != UNOPENED && this.state_ != RESETTING) throw Error('Cannot add observers once started.'); this.observed_.push(observerSentinel, observer); if (!this.reportChangesOnOpen_) return; var index = this.observed_.length / 2 - 1; this.value_[index] = observer.open(this.deliver, this); }, startReset: function() { if (this.state_ != OPENED) throw Error('Can only reset while open'); this.state_ = RESETTING; this.disconnect_(); }, finishReset: function() { if (this.state_ != RESETTING) throw Error('Can only finishReset after startReset'); this.state_ = OPENED; this.connect_(); return this.value_; }, iterateObjects_: function(observe) { var object; for (var i = 0; i < this.observed_.length; i += 2) { object = this.observed_[i] if (object !== observerSentinel) this.observed_[i + 1].iterateObjects(object, observe) } }, check_: function(changeRecords, skipChanges) { var oldValues; for (var i = 0; i < this.observed_.length; i += 2) { var object = this.observed_[i]; var path = this.observed_[i+1]; var value; if (object === observerSentinel) { var observable = path; value = this.state_ === UNOPENED ? observable.open(this.deliver, this) : observable.discardChanges(); } else { value = path.getValueFrom(object); } if (skipChanges) { this.value_[i / 2] = value; continue; } if (areSameValue(value, this.value_[i / 2])) continue; oldValues = oldValues || []; oldValues[i / 2] = this.value_[i / 2]; this.value_[i / 2] = value; } if (!oldValues) return false; // TODO(rafaelw): Having observed_ as the third callback arg here is // pretty lame API. Fix. this.report_([this.value_, oldValues, this.observed_]); return true; } }); function identFn(value) { return value; } function ObserverTransform(observable, getValueFn, setValueFn, dontPassThroughSet) { this.callback_ = undefined; this.target_ = undefined; this.value_ = undefined; this.observable_ = observable; this.getValueFn_ = getValueFn || identFn; this.setValueFn_ = setValueFn || identFn; // TODO(rafaelw): This is a temporary hack. PolymerExpressions needs this // at the moment because of a bug in it's dependency tracking. this.dontPassThroughSet_ = dontPassThroughSet; } ObserverTransform.prototype = { open: function(callback, target) { this.callback_ = callback; this.target_ = target; this.value_ = this.getValueFn_(this.observable_.open(this.observedCallback_, this)); return this.value_; }, observedCallback_: function(value) { value = this.getValueFn_(value); if (areSameValue(value, this.value_)) return; var oldValue = this.value_; this.value_ = value; this.callback_.call(this.target_, this.value_, oldValue); }, discardChanges: function() { this.value_ = this.getValueFn_(this.observable_.discardChanges()); return this.value_; }, deliver: function() { return this.observable_.deliver(); }, setValue: function(value) { value = this.setValueFn_(value); if (!this.dontPassThroughSet_ && this.observable_.setValue) return this.observable_.setValue(value); }, close: function() { if (this.observable_) this.observable_.close(); this.callback_ = undefined; this.target_ = undefined; this.observable_ = undefined; this.value_ = undefined; this.getValueFn_ = undefined; this.setValueFn_ = undefined; } } var expectedRecordTypes = { add: true, update: true, delete: true }; function diffObjectFromChangeRecords(object, changeRecords, oldValues) { var added = {}; var removed = {}; for (var i = 0; i < changeRecords.length; i++) { var record = changeRecords[i]; if (!expectedRecordTypes[record.type]) { console.error('Unknown changeRecord type: ' + record.type); console.error(record); continue; } if (!(record.name in oldValues)) oldValues[record.name] = record.oldValue; if (record.type == 'update') continue; if (record.type == 'add') { if (record.name in removed) delete removed[record.name]; else added[record.name] = true; continue; } // type = 'delete' if (record.name in added) { delete added[record.name]; delete oldValues[record.name]; } else { removed[record.name] = true; } } for (var prop in added) added[prop] = object[prop]; for (var prop in removed) removed[prop] = undefined; var changed = {}; for (var prop in oldValues) { if (prop in added || prop in removed) continue; var newValue = object[prop]; if (oldValues[prop] !== newValue) changed[prop] = newValue; } return { added: added, removed: removed, changed: changed }; } function newSplice(index, removed, addedCount) { return { index: index, removed: removed, addedCount: addedCount }; } var EDIT_LEAVE = 0; var EDIT_UPDATE = 1; var EDIT_ADD = 2; var EDIT_DELETE = 3; function ArraySplice() {} ArraySplice.prototype = { // Note: This function is *based* on the computation of the Levenshtein // "edit" distance. The one change is that "updates" are treated as two // edits - not one. With Array splices, an update is really a delete // followed by an add. By retaining this, we optimize for "keeping" the // maximum array items in the original array. For example: // // 'xxxx123' -> '123yyyy' // // With 1-edit updates, the shortest path would be just to update all seven // characters. With 2-edit updates, we delete 4, leave 3, and add 4. This // leaves the substring '123' intact. calcEditDistances: function(current, currentStart, currentEnd, old, oldStart, oldEnd) { // "Deletion" columns var rowCount = oldEnd - oldStart + 1; var columnCount = currentEnd - currentStart + 1; var distances = new Array(rowCount); // "Addition" rows. Initialize null column. for (var i = 0; i < rowCount; i++) { distances[i] = new Array(columnCount); distances[i][0] = i; } // Initialize null row for (var j = 0; j < columnCount; j++) distances[0][j] = j; for (var i = 1; i < rowCount; i++) { for (var j = 1; j < columnCount; j++) { if (this.equals(current[currentStart + j - 1], old[oldStart + i - 1])) distances[i][j] = distances[i - 1][j - 1]; else { var north = distances[i - 1][j] + 1; var west = distances[i][j - 1] + 1; distances[i][j] = north < west ? north : west; } } } return distances; }, // This starts at the final weight, and walks "backward" by finding // the minimum previous weight recursively until the origin of the weight // matrix. spliceOperationsFromEditDistances: function(distances) { var i = distances.length - 1; var j = distances[0].length - 1; var current = distances[i][j]; var edits = []; while (i > 0 || j > 0) { if (i == 0) { edits.push(EDIT_ADD); j--; continue; } if (j == 0) { edits.push(EDIT_DELETE); i--; continue; } var northWest = distances[i - 1][j - 1]; var west = distances[i - 1][j]; var north = distances[i][j - 1]; var min; if (west < north) min = west < northWest ? west : northWest; else min = north < northWest ? north : northWest; if (min == northWest) { if (northWest == current) { edits.push(EDIT_LEAVE); } else { edits.push(EDIT_UPDATE); current = northWest; } i--; j--; } else if (min == west) { edits.push(EDIT_DELETE); i--; current = west; } else { edits.push(EDIT_ADD); j--; current = north; } } edits.reverse(); return edits; }, /** * Splice Projection functions: * * A splice map is a representation of how a previous array of items * was transformed into a new array of items. Conceptually it is a list of * tuples of * * <index, removed, addedCount> * * which are kept in ascending index order of. The tuple represents that at * the |index|, |removed| sequence of items were removed, and counting forward * from |index|, |addedCount| items were added. */ /** * Lacking individual splice mutation information, the minimal set of * splices can be synthesized given the previous state and final state of an * array. The basic approach is to calculate the edit distance matrix and * choose the shortest path through it. * * Complexity: O(l * p) * l: The length of the current array * p: The length of the old array */ calcSplices: function(current, currentStart, currentEnd, old, oldStart, oldEnd) { var prefixCount = 0; var suffixCount = 0; var minLength = Math.min(currentEnd - currentStart, oldEnd - oldStart); if (currentStart == 0 && oldStart == 0) prefixCount = this.sharedPrefix(current, old, minLength); if (currentEnd == current.length && oldEnd == old.length) suffixCount = this.sharedSuffix(current, old, minLength - prefixCount); currentStart += prefixCount; oldStart += prefixCount; currentEnd -= suffixCount; oldEnd -= suffixCount; if (currentEnd - currentStart == 0 && oldEnd - oldStart == 0) return []; if (currentStart == currentEnd) { var splice = newSplice(currentStart, [], 0); while (oldStart < oldEnd) splice.removed.push(old[oldStart++]); return [ splice ]; } else if (oldStart == oldEnd) return [ newSplice(currentStart, [], currentEnd - currentStart) ]; var ops = this.spliceOperationsFromEditDistances( this.calcEditDistances(current, currentStart, currentEnd, old, oldStart, oldEnd)); var splice = undefined; var splices = []; var index = currentStart; var oldIndex = oldStart; for (var i = 0; i < ops.length; i++) { switch(ops[i]) { case EDIT_LEAVE: if (splice) { splices.push(splice); splice = undefined; } index++; oldIndex++; break; case EDIT_UPDATE: if (!splice) splice = newSplice(index, [], 0); splice.addedCount++; index++; splice.removed.push(old[oldIndex]); oldIndex++; break; case EDIT_ADD: if (!splice) splice = newSplice(index, [], 0); splice.addedCount++; index++; break; case EDIT_DELETE: if (!splice) splice = newSplice(index, [], 0); splice.removed.push(old[oldIndex]); oldIndex++; break; } } if (splice) { splices.push(splice); } return splices; }, sharedPrefix: function(current, old, searchLength) { for (var i = 0; i < searchLength; i++) if (!this.equals(current[i], old[i])) return i; return searchLength; }, sharedSuffix: function(current, old, searchLength) { var index1 = current.length; var index2 = old.length; var count = 0; while (count < searchLength && this.equals(current[--index1], old[--index2])) count++; return count; }, calculateSplices: function(current, previous) { return this.calcSplices(current, 0, current.length, previous, 0, previous.length); }, equals: function(currentValue, previousValue) { return currentValue === previousValue; } }; var arraySplice = new ArraySplice(); function calcSplices(current, currentStart, currentEnd, old, oldStart, oldEnd) { return arraySplice.calcSplices(current, currentStart, currentEnd, old, oldStart, oldEnd); } function intersect(start1, end1, start2, end2) { // Disjoint if (end1 < start2 || end2 < start1) return -1; // Adjacent if (end1 == start2 || end2 == start1) return 0; // Non-zero intersect, span1 first if (start1 < start2) { if (end1 < end2) return end1 - start2; // Overlap else return end2 - start2; // Contained } else { // Non-zero intersect, span2 first if (end2 < end1) return end2 - start1; // Overlap else return end1 - start1; // Contained } } function mergeSplice(splices, index, removed, addedCount) { var splice = newSplice(index, removed, addedCount); var inserted = false; var insertionOffset = 0; for (var i = 0; i < splices.length; i++) { var current = splices[i]; current.index += insertionOffset; if (inserted) continue; var intersectCount = intersect(splice.index, splice.index + splice.removed.length, current.index, current.index + current.addedCount); if (intersectCount >= 0) { // Merge the two splices splices.splice(i, 1); i--; insertionOffset -= current.addedCount - current.removed.length; splice.addedCount += current.addedCount - intersectCount; var deleteCount = splice.removed.length + current.removed.length - intersectCount; if (!splice.addedCount && !deleteCount) { // merged splice is a noop. discard. inserted = true; } else { var removed = current.removed; if (splice.index < current.index) { // some prefix of splice.removed is prepended to current.removed. var prepend = splice.removed.slice(0, current.index - splice.index); Array.prototype.push.apply(prepend, removed); removed = prepend; } if (splice.index + splice.removed.length > current.index + current.addedCount) { // some suffix of splice.removed is appended to current.removed. var append = splice.removed.slice(current.index + current.addedCount - splice.index); Array.prototype.push.apply(removed, append); } splice.removed = removed; if (current.index < splice.index) { splice.index = current.index; } } } else if (splice.index < current.index) { // Insert splice here. inserted = true; splices.splice(i, 0, splice); i++; var offset = splice.addedCount - splice.removed.length current.index += offset; insertionOffset += offset; } } if (!inserted) splices.push(splice); } function createInitialSplices(array, changeRecords) { var splices = []; for (var i = 0; i < changeRecords.length; i++) { var record = changeRecords[i]; switch(record.type) { case 'splice': mergeSplice(splices, record.index, record.removed.slice(), record.addedCount); break; case 'add': case 'update': case 'delete': if (!isIndex(record.name)) continue; var index = toNumber(record.name); if (index < 0) continue; mergeSplice(splices, index, [record.oldValue], 1); break; default: console.error('Unexpected record type: ' + JSON.stringify(record)); break; } } return splices; } function projectArraySplices(array, changeRecords) { var splices = []; createInitialSplices(array, changeRecords).forEach(function(splice) { if (splice.addedCount == 1 && splice.removed.length == 1) { if (splice.removed[0] !== array[splice.index]) splices.push(splice); return }; splices = splices.concat(calcSplices(array, splice.index, splice.index + splice.addedCount, splice.removed, 0, splice.removed.length)); }); return splices; } // Export the observe-js object for **Node.js**, with backwards-compatibility // for the old `require()` API. Also ensure `exports` is not a DOM Element. // If we're in the browser, export as a global object. var expose = global; if (typeof exports !== 'undefined' && !exports.nodeType) { if (typeof module !== 'undefined' && module.exports) { exports = module.exports; } expose = exports; } expose.Observer = Observer; expose.Observer.runEOM_ = runEOM; expose.Observer.observerSentinel_ = observerSentinel; // for testing. expose.Observer.hasObjectObserve = hasObserve; expose.ArrayObserver = ArrayObserver; expose.ArrayObserver.calculateSplices = function(current, previous) { return arraySplice.calculateSplices(current, previous); }; expose.ArraySplice = ArraySplice; expose.ObjectObserver = ObjectObserver; expose.PathObserver = PathObserver; expose.CompoundObserver = CompoundObserver; expose.Path = Path; expose.ObserverTransform = ObserverTransform; })(typeof global !== 'undefined' && global && typeof module !== 'undefined' && module ? global : this || window); // Copyright (c) 2014 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 (function(global) { 'use strict'; var filter = Array.prototype.filter.call.bind(Array.prototype.filter); function getTreeScope(node) { while (node.parentNode) { node = node.parentNode; } return typeof node.getElementById === 'function' ? node : null; } Node.prototype.bind = function(name, observable) { console.error('Unhandled binding to Node: ', this, name, observable); }; Node.prototype.bindFinished = function() {}; function updateBindings(node, name, binding) { var bindings = node.bindings_; if (!bindings) bindings = node.bindings_ = {}; if (bindings[name]) binding[name].close(); return bindings[name] = binding; } function returnBinding(node, name, binding) { return binding; } function sanitizeValue(value) { return value == null ? '' : value; } function updateText(node, value) { node.data = sanitizeValue(value); } function textBinding(node) { return function(value) { return updateText(node, value); }; } var maybeUpdateBindings = returnBinding; Object.defineProperty(Platform, 'enableBindingsReflection', { get: function() { return maybeUpdateBindings === updateBindings; }, set: function(enable) { maybeUpdateBindings = enable ? updateBindings : returnBinding; return enable; }, configurable: true }); Text.prototype.bind = function(name, value, oneTime) { if (name !== 'textContent') return Node.prototype.bind.call(this, name, value, oneTime); if (oneTime) return updateText(this, value); var observable = value; updateText(this, observable.open(textBinding(this))); return maybeUpdateBindings(this, name, observable); } function updateAttribute(el, name, conditional, value) { if (conditional) { if (value) el.setAttribute(name, ''); else el.removeAttribute(name); return; } el.setAttribute(name, sanitizeValue(value)); } function attributeBinding(el, name, conditional) { return function(value) { updateAttribute(el, name, conditional, value); }; } Element.prototype.bind = function(name, value, oneTime) { var conditional = name[name.length - 1] == '?'; if (conditional) { this.removeAttribute(name); name = name.slice(0, -1); } if (oneTime) return updateAttribute(this, name, conditional, value); var observable = value; updateAttribute(this, name, conditional, observable.open(attributeBinding(this, name, conditional))); return maybeUpdateBindings(this, name, observable); }; var checkboxEventType; (function() { // Attempt to feature-detect which event (change or click) is fired first // for checkboxes. var div = document.createElement('div'); var checkbox = div.appendChild(document.createElement('input')); checkbox.setAttribute('type', 'checkbox'); var first; var count = 0; checkbox.addEventListener('click', function(e) { count++; first = first || 'click'; }); checkbox.addEventListener('change', function() { count++; first = first || 'change'; }); var event = document.createEvent('MouseEvent'); event.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); checkbox.dispatchEvent(event); // WebKit/Blink don't fire the change event if the element is outside the // document, so assume 'change' for that case. checkboxEventType = count == 1 ? 'change' : first; })(); function getEventForInputType(element) { switch (element.type) { case 'checkbox': return checkboxEventType; case 'radio': case 'select-multiple': case 'select-one': return 'change'; case 'range': if (/Trident|MSIE/.test(navigator.userAgent)) return 'change'; default: return 'input'; } } function updateInput(input, property, value, santizeFn) { input[property] = (santizeFn || sanitizeValue)(value); } function inputBinding(input, property, santizeFn) { return function(value) { return updateInput(input, property, value, santizeFn); } } function noop() {} function bindInputEvent(input, property, observable, postEventFn) { var eventType = getEventForInputType(input); function eventHandler() { var isNum = property == 'value' && input.type == 'number'; observable.setValue(isNum ? input.valueAsNumber : input[property]); observable.discardChanges(); (postEventFn || noop)(input); Platform.performMicrotaskCheckpoint(); } input.addEventListener(eventType, eventHandler); return { close: function() { input.removeEventListener(eventType, eventHandler); observable.close(); }, observable_: observable } } function booleanSanitize(value) { return Boolean(value); } // |element| is assumed to be an HTMLInputElement with |type| == 'radio'. // Returns an array containing all radio buttons other than |element| that // have the same |name|, either in the form that |element| belongs to or, // if no form, in the document tree to which |element| belongs. // // This implementation is based upon the HTML spec definition of a // "radio button group": // http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state.html#radio-button-group // function getAssociatedRadioButtons(element) { if (element.form) { return filter(element.form.elements, function(el) { return el != element && el.tagName == 'INPUT' && el.type == 'radio' && el.name == element.name; }); } else { var treeScope = getTreeScope(element); if (!treeScope) return []; var radios = treeScope.querySelectorAll( 'input[type="radio"][name="' + element.name + '"]'); return filter(radios, function(el) { return el != element && !el.form; }); } } function checkedPostEvent(input) { // Only the radio button that is getting checked gets an event. We // therefore find all the associated radio buttons and update their // check binding manually. if (input.tagName === 'INPUT' && input.type === 'radio') { getAssociatedRadioButtons(input).forEach(function(radio) { var checkedBinding = radio.bindings_.checked; if (checkedBinding) { // Set the value directly to avoid an infinite call stack. checkedBinding.observable_.setValue(false); } }); } } HTMLInputElement.prototype.bind = function(name, value, oneTime) { if (name !== 'value' && name !== 'checked') return HTMLElement.prototype.bind.call(this, name, value, oneTime); this.removeAttribute(name); var sanitizeFn = name == 'checked' ? booleanSanitize : sanitizeValue; var postEventFn = name == 'checked' ? checkedPostEvent : noop; if (oneTime) return updateInput(this, name, value, sanitizeFn); var observable = value; var binding = bindInputEvent(this, name, observable, postEventFn); updateInput(this, name, observable.open(inputBinding(this, name, sanitizeFn)), sanitizeFn); // Checkboxes may need to update bindings of other checkboxes. return updateBindings(this, name, binding); } HTMLTextAreaElement.prototype.bind = function(name, value, oneTime) { if (name !== 'value') return HTMLElement.prototype.bind.call(this, name, value, oneTime); this.removeAttribute('value'); if (oneTime) return updateInput(this, 'value', value); var observable = value; var binding = bindInputEvent(this, 'value', observable); updateInput(this, 'value', observable.open(inputBinding(this, 'value', sanitizeValue))); return maybeUpdateBindings(this, name, binding); } function updateOption(option, value) { var parentNode = option.parentNode;; var select; var selectBinding; var oldValue; if (parentNode instanceof HTMLSelectElement && parentNode.bindings_ && parentNode.bindings_.value) { select = parentNode; selectBinding = select.bindings_.value; oldValue = select.value; } option.value = sanitizeValue(value); if (select && select.value != oldValue) { selectBinding.observable_.setValue(select.value); selectBinding.observable_.discardChanges(); Platform.performMicrotaskCheckpoint(); } } function optionBinding(option) { return function(value) { updateOption(option, value); } } HTMLOptionElement.prototype.bind = function(name, value, oneTime) { if (name !== 'value') return HTMLElement.prototype.bind.call(this, name, value, oneTime); this.removeAttribute('value'); if (oneTime) return updateOption(this, value); var observable = value; var binding = bindInputEvent(this, 'value', observable); updateOption(this, observable.open(optionBinding(this))); return maybeUpdateBindings(this, name, binding); } HTMLSelectElement.prototype.bind = function(name, value, oneTime) { if (name === 'selectedindex') name = 'selectedIndex'; if (name !== 'selectedIndex' && name !== 'value') return HTMLElement.prototype.bind.call(this, name, value, oneTime); this.removeAttribute(name); if (oneTime) return updateInput(this, name, value); var observable = value; var binding = bindInputEvent(this, name, observable); updateInput(this, name, observable.open(inputBinding(this, name))); // Option update events may need to access select bindings. return updateBindings(this, name, binding); } })(this); // Copyright (c) 2014 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 (function(global) { 'use strict'; function assert(v) { if (!v) throw new Error('Assertion failed'); } var forEach = Array.prototype.forEach.call.bind(Array.prototype.forEach); function getFragmentRoot(node) { var p; while (p = node.parentNode) { node = p; } return node; } function searchRefId(node, id) { if (!id) return; var ref; var selector = '#' + id; while (!ref) { node = getFragmentRoot(node); if (node.protoContent_) ref = node.protoContent_.querySelector(selector); else if (node.getElementById) ref = node.getElementById(id); if (ref || !node.templateCreator_) break node = node.templateCreator_; } return ref; } function getInstanceRoot(node) { while (node.parentNode) { node = node.parentNode; } return node.templateCreator_ ? node : null; } var Map; if (global.Map && typeof global.Map.prototype.forEach === 'function') { Map = global.Map; } else { Map = function() { this.keys = []; this.values = []; }; Map.prototype = { set: function(key, value) { var index = this.keys.indexOf(key); if (index < 0) { this.keys.push(key); this.values.push(value); } else { this.values[index] = value; } }, get: function(key) { var index = this.keys.indexOf(key); if (index < 0) return; return this.values[index]; }, delete: function(key, value) { var index = this.keys.indexOf(key); if (index < 0) return false; this.keys.splice(index, 1); this.values.splice(index, 1); return true; }, forEach: function(f, opt_this) { for (var i = 0; i < this.keys.length; i++) f.call(opt_this || this, this.values[i], this.keys[i], this); } }; } // JScript does not have __proto__. We wrap all object literals with // createObject which uses Object.create, Object.defineProperty and // Object.getOwnPropertyDescriptor to create a new object that does the exact // same thing. The main downside to this solution is that we have to extract // all those property descriptors for IE. var createObject = ('__proto__' in {}) ? function(obj) { return obj; } : function(obj) { var proto = obj.__proto__; if (!proto) return obj; var newObject = Object.create(proto); Object.getOwnPropertyNames(obj).forEach(function(name) { Object.defineProperty(newObject, name, Object.getOwnPropertyDescriptor(obj, name)); }); return newObject; }; // IE does not support have Document.prototype.contains. if (typeof document.contains != 'function') { Document.prototype.contains = function(node) { if (node === this || node.parentNode === this) return true; return this.documentElement.contains(node); } } var BIND = 'bind'; var REPEAT = 'repeat'; var IF = 'if'; var templateAttributeDirectives = { 'template': true, 'repeat': true, 'bind': true, 'ref': true, 'if': true }; var semanticTemplateElements = { 'THEAD': true, 'TBODY': true, 'TFOOT': true, 'TH': true, 'TR': true, 'TD': true, 'COLGROUP': true, 'COL': true, 'CAPTION': true, 'OPTION': true, 'OPTGROUP': true }; var hasTemplateElement = typeof HTMLTemplateElement !== 'undefined'; if (hasTemplateElement) { // TODO(rafaelw): Remove when fix for // https://codereview.chromium.org/164803002/ // makes it to Chrome release. (function() { var t = document.createElement('template'); var d = t.content.ownerDocument; var html = d.appendChild(d.createElement('html')); var head = html.appendChild(d.createElement('head')); var base = d.createElement('base'); base.href = document.baseURI; head.appendChild(base); })(); } var allTemplatesSelectors = 'template, ' + Object.keys(semanticTemplateElements).map(function(tagName) { return tagName.toLowerCase() + '[template]'; }).join(', '); function isSVGTemplate(el) { return el.tagName == 'template' && el.namespaceURI == 'http://www.w3.org/2000/svg'; } function isHTMLTemplate(el) { return el.tagName == 'TEMPLATE' && el.namespaceURI == 'http://www.w3.org/1999/xhtml'; } function isAttributeTemplate(el) { return Boolean(semanticTemplateElements[el.tagName] && el.hasAttribute('template')); } function isTemplate(el) { if (el.isTemplate_ === undefined) el.isTemplate_ = el.tagName == 'TEMPLATE' || isAttributeTemplate(el); return el.isTemplate_; } // FIXME: Observe templates being added/removed from documents // FIXME: Expose imperative API to decorate and observe templates in // "disconnected tress" (e.g. ShadowRoot) document.addEventListener('DOMContentLoaded', function(e) { bootstrapTemplatesRecursivelyFrom(document); // FIXME: Is this needed? Seems like it shouldn't be. Platform.performMicrotaskCheckpoint(); }, false); function forAllTemplatesFrom(node, fn) { var subTemplates = node.querySelectorAll(allTemplatesSelectors); if (isTemplate(node)) fn(node) forEach(subTemplates, fn); } function bootstrapTemplatesRecursivelyFrom(node) { function bootstrap(template) { if (!HTMLTemplateElement.decorate(template)) bootstrapTemplatesRecursivelyFrom(template.content); } forAllTemplatesFrom(node, bootstrap); } if (!hasTemplateElement) { /** * This represents a <template> element. * @constructor * @extends {HTMLElement} */ global.HTMLTemplateElement = function() { throw TypeError('Illegal constructor'); }; } var hasProto = '__proto__' in {}; function mixin(to, from) { Object.getOwnPropertyNames(from).forEach(function(name) { Object.defineProperty(to, name, Object.getOwnPropertyDescriptor(from, name)); }); } // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/templates/index.html#dfn-template-contents-owner function getOrCreateTemplateContentsOwner(template) { var doc = template.ownerDocument if (!doc.defaultView) return doc; var d = doc.templateContentsOwner_; if (!d) { // TODO(arv): This should either be a Document or HTMLDocument depending // on doc. d = doc.implementation.createHTMLDocument(''); while (d.lastChild) { d.removeChild(d.lastChild); } doc.templateContentsOwner_ = d; } return d; } function getTemplateStagingDocument(template) { if (!template.stagingDocument_) { var owner = template.ownerDocument; if (!owner.stagingDocument_) { owner.stagingDocument_ = owner.implementation.createHTMLDocument(''); owner.stagingDocument_.isStagingDocument = true; // TODO(rafaelw): Remove when fix for // https://codereview.chromium.org/164803002/ // makes it to Chrome release. var base = owner.stagingDocument_.createElement('base'); base.href = document.baseURI; owner.stagingDocument_.head.appendChild(base); owner.stagingDocument_.stagingDocument_ = owner.stagingDocument_; } template.stagingDocument_ = owner.stagingDocument_; } return template.stagingDocument_; } // For non-template browsers, the parser will disallow <template> in certain // locations, so we allow "attribute templates" which combine the template // element with the top-level container node of the content, e.g. // // <tr template repeat="{{ foo }}"" class="bar"><td>Bar</td></tr> // // becomes // // <template repeat="{{ foo }}"> // + #document-fragment // + <tr class="bar"> // + <td>Bar</td> // function extractTemplateFromAttributeTemplate(el) { var template = el.ownerDocument.createElement('template'); el.parentNode.insertBefore(template, el); var attribs = el.attributes; var count = attribs.length; while (count-- > 0) { var attrib = attribs[count]; if (templateAttributeDirectives[attrib.name]) { if (attrib.name !== 'template') template.setAttribute(attrib.name, attrib.value); el.removeAttribute(attrib.name); } } return template; } function extractTemplateFromSVGTemplate(el) { var template = el.ownerDocument.createElement('template'); el.parentNode.insertBefore(template, el); var attribs = el.attributes; var count = attribs.length; while (count-- > 0) { var attrib = attribs[count]; template.setAttribute(attrib.name, attrib.value); el.removeAttribute(attrib.name); } el.parentNode.removeChild(el); return template; } function liftNonNativeTemplateChildrenIntoContent(template, el, useRoot) { var content = template.content; if (useRoot) { content.appendChild(el); return; } var child; while (child = el.firstChild) { content.appendChild(child); } } var templateObserver; if (typeof MutationObserver == 'function') { templateObserver = new MutationObserver(function(records) { for (var i = 0; i < records.length; i++) { records[i].target.refChanged_(); } }); } /** * Ensures proper API and content model for template elements. * @param {HTMLTemplateElement} opt_instanceRef The template element which * |el| template element will return as the value of its ref(), and whose * content will be used as source when createInstance() is invoked. */ HTMLTemplateElement.decorate = function(el, opt_instanceRef) { if (el.templateIsDecorated_) return false; var templateElement = el; templateElement.templateIsDecorated_ = true; var isNativeHTMLTemplate = isHTMLTemplate(templateElement) && hasTemplateElement; var bootstrapContents = isNativeHTMLTemplate; var liftContents = !isNativeHTMLTemplate; var liftRoot = false; if (!isNativeHTMLTemplate) { if (isAttributeTemplate(templateElement)) { assert(!opt_instanceRef); templateElement = extractTemplateFromAttributeTemplate(el); templateElement.templateIsDecorated_ = true; isNativeHTMLTemplate = hasTemplateElement; liftRoot = true; } else if (isSVGTemplate(templateElement)) { templateElement = extractTemplateFromSVGTemplate(el); templateElement.templateIsDecorated_ = true; isNativeHTMLTemplate = hasTemplateElement; } } if (!isNativeHTMLTemplate) { fixTemplateElementPrototype(templateElement); var doc = getOrCreateTemplateContentsOwner(templateElement); templateElement.content_ = doc.createDocumentFragment(); } if (opt_instanceRef) { // template is contained within an instance, its direct content must be // empty templateElement.instanceRef_ = opt_instanceRef; } else if (liftContents) { liftNonNativeTemplateChildrenIntoContent(templateElement, el, liftRoot); } else if (bootstrapContents) { bootstrapTemplatesRecursivelyFrom(templateElement.content); } return true; }; // TODO(rafaelw): This used to decorate recursively all templates from a given // node. This happens by default on 'DOMContentLoaded', but may be needed // in subtrees not descendent from document (e.g. ShadowRoot). // Review whether this is the right public API. HTMLTemplateElement.bootstrap = bootstrapTemplatesRecursivelyFrom; var htmlElement = global.HTMLUnknownElement || HTMLElement; var contentDescriptor = { get: function() { return this.content_; }, enumerable: true, configurable: true }; if (!hasTemplateElement) { // Gecko is more picky with the prototype than WebKit. Make sure to use the // same prototype as created in the constructor. HTMLTemplateElement.prototype = Object.create(htmlElement.prototype); Object.defineProperty(HTMLTemplateElement.prototype, 'content', contentDescriptor); } function fixTemplateElementPrototype(el) { if (hasProto) el.__proto__ = HTMLTemplateElement.prototype; else mixin(el, HTMLTemplateElement.prototype); } function ensureSetModelScheduled(template) { if (!template.setModelFn_) { template.setModelFn_ = function() { template.setModelFnScheduled_ = false; var map = getBindings(template, template.delegate_ && template.delegate_.prepareBinding); processBindings(template, map, template.model_); }; } if (!template.setModelFnScheduled_) { template.setModelFnScheduled_ = true; Observer.runEOM_(template.setModelFn_); } } mixin(HTMLTemplateElement.prototype, { bind: function(name, value, oneTime) { if (name != 'ref') return Element.prototype.bind.call(this, name, value, oneTime); var self = this; var ref = oneTime ? value : value.open(function(ref) { self.setAttribute('ref', ref); self.refChanged_(); }); this.setAttribute('ref', ref); this.refChanged_(); if (oneTime) return; if (!this.bindings_) { this.bindings_ = { ref: value }; } else { this.bindings_.ref = value; } return value; }, processBindingDirectives_: function(directives) { if (this.iterator_) this.iterator_.closeDeps(); if (!directives.if && !directives.bind && !directives.repeat) { if (this.iterator_) { this.iterator_.close(); this.iterator_ = undefined; } return; } if (!this.iterator_) { this.iterator_ = new TemplateIterator(this); } this.iterator_.updateDependencies(directives, this.model_); if (templateObserver) { templateObserver.observe(this, { attributes: true, attributeFilter: ['ref'] }); } return this.iterator_; }, createInstance: function(model, bindingDelegate, delegate_) { if (bindingDelegate) delegate_ = this.newDelegate_(bindingDelegate); else if (!delegate_) delegate_ = this.delegate_; if (!this.refContent_) this.refContent_ = this.ref_.content; var content = this.refContent_; if (content.firstChild === null) return emptyInstance; var map = getInstanceBindingMap(content, delegate_); var stagingDocument = getTemplateStagingDocument(this); var instance = stagingDocument.createDocumentFragment(); instance.templateCreator_ = this; instance.protoContent_ = content; instance.bindings_ = []; instance.terminator_ = null; var instanceRecord = instance.templateInstance_ = { firstNode: null, lastNode: null, model: model }; var i = 0; var collectTerminator = false; for (var child = content.firstChild; child; child = child.nextSibling) { // The terminator of the instance is the clone of the last child of the // content. If the last child is an active template, it may produce // instances as a result of production, so simply collecting the last // child of the instance after it has finished producing may be wrong. if (child.nextSibling === null) collectTerminator = true; var clone = cloneAndBindInstance(child, instance, stagingDocument, map.children[i++], model, delegate_, instance.bindings_); clone.templateInstance_ = instanceRecord; if (collectTerminator) instance.terminator_ = clone; } instanceRecord.firstNode = instance.firstChild; instanceRecord.lastNode = instance.lastChild; instance.templateCreator_ = undefined; instance.protoContent_ = undefined; return instance; }, get model() { return this.model_; }, set model(model) { this.model_ = model; ensureSetModelScheduled(this); }, get bindingDelegate() { return this.delegate_ && this.delegate_.raw; }, refChanged_: function() { if (!this.iterator_ || this.refContent_ === this.ref_.content) return; this.refContent_ = undefined; this.iterator_.valueChanged(); this.iterator_.updateIteratedValue(this.iterator_.getUpdatedValue()); }, clear: function() { this.model_ = undefined; this.delegate_ = undefined; if (this.bindings_ && this.bindings_.ref) this.bindings_.ref.close() this.refContent_ = undefined; if (!this.iterator_) return; this.iterator_.valueChanged(); this.iterator_.close() this.iterator_ = undefined; }, setDelegate_: function(delegate) { this.delegate_ = delegate; this.bindingMap_ = undefined; if (this.iterator_) { this.iterator_.instancePositionChangedFn_ = undefined; this.iterator_.instanceModelFn_ = undefined; } }, newDelegate_: function(bindingDelegate) { if (!bindingDelegate) return; function delegateFn(name) { var fn = bindingDelegate && bindingDelegate[name]; if (typeof fn != 'function') return; return function() { return fn.apply(bindingDelegate, arguments); }; } return { bindingMaps: {}, raw: bindingDelegate, prepareBinding: delegateFn('prepareBinding'), prepareInstanceModel: delegateFn('prepareInstanceModel'), prepareInstancePositionChanged: delegateFn('prepareInstancePositionChanged') }; }, set bindingDelegate(bindingDelegate) { if (this.delegate_) { throw Error('Template must be cleared before a new bindingDelegate ' + 'can be assigned'); } this.setDelegate_(this.newDelegate_(bindingDelegate)); }, get ref_() { var ref = searchRefId(this, this.getAttribute('ref')); if (!ref) ref = this.instanceRef_; if (!ref) return this; var nextRef = ref.ref_; return nextRef ? nextRef : ref; } }); // Returns // a) undefined if there are no mustaches. // b) [TEXT, (ONE_TIME?, PATH, DELEGATE_FN, TEXT)+] if there is at least one mustache. function parseMustaches(s, name, node, prepareBindingFn) { if (!s || !s.length) return; var tokens; var length = s.length; var startIndex = 0, lastIndex = 0, endIndex = 0; var onlyOneTime = true; while (lastIndex < length) { var startIndex = s.indexOf('{{', lastIndex); var oneTimeStart = s.indexOf('[[', lastIndex); var oneTime = false; var terminator = '}}'; if (oneTimeStart >= 0 && (startIndex < 0 || oneTimeStart < startIndex)) { startIndex = oneTimeStart; oneTime = true; terminator = ']]'; } endIndex = startIndex < 0 ? -1 : s.indexOf(terminator, startIndex + 2); if (endIndex < 0) { if (!tokens) return; tokens.push(s.slice(lastIndex)); // TEXT break; } tokens = tokens || []; tokens.push(s.slice(lastIndex, startIndex)); // TEXT var pathString = s.slice(startIndex + 2, endIndex).trim(); tokens.push(oneTime); // ONE_TIME? onlyOneTime = onlyOneTime && oneTime; var delegateFn = prepareBindingFn && prepareBindingFn(pathString, name, node); // Don't try to parse the expression if there's a prepareBinding function if (delegateFn == null) { tokens.push(Path.get(pathString)); // PATH } else { tokens.push(null); } tokens.push(delegateFn); // DELEGATE_FN lastIndex = endIndex + 2; } if (lastIndex === length) tokens.push(''); // TEXT tokens.hasOnePath = tokens.length === 5; tokens.isSimplePath = tokens.hasOnePath && tokens[0] == '' && tokens[4] == ''; tokens.onlyOneTime = onlyOneTime; tokens.combinator = function(values) { var newValue = tokens[0]; for (var i = 1; i < tokens.length; i += 4) { var value = tokens.hasOnePath ? values : values[(i - 1) / 4]; if (value !== undefined) newValue += value; newValue += tokens[i + 3]; } return newValue; } return tokens; }; function processOneTimeBinding(name, tokens, node, model) { if (tokens.hasOnePath) { var delegateFn = tokens[3]; var value = delegateFn ? delegateFn(model, node, true) : tokens[2].getValueFrom(model); return tokens.isSimplePath ? value : tokens.combinator(value); } var values = []; for (var i = 1; i < tokens.length; i += 4) { var delegateFn = tokens[i + 2]; values[(i - 1) / 4] = delegateFn ? delegateFn(model, node) : tokens[i + 1].getValueFrom(model); } return tokens.combinator(values); } function processSinglePathBinding(name, tokens, node, model) { var delegateFn = tokens[3]; var observer = delegateFn ? delegateFn(model, node, false) : new PathObserver(model, tokens[2]); return tokens.isSimplePath ? observer : new ObserverTransform(observer, tokens.combinator); } function processBinding(name, tokens, node, model) { if (tokens.onlyOneTime) return processOneTimeBinding(name, tokens, node, model); if (tokens.hasOnePath) return processSinglePathBinding(name, tokens, node, model); var observer = new CompoundObserver(); for (var i = 1; i < tokens.length; i += 4) { var oneTime = tokens[i]; var delegateFn = tokens[i + 2]; if (delegateFn) { var value = delegateFn(model, node, oneTime); if (oneTime) observer.addPath(value) else observer.addObserver(value); continue; } var path = tokens[i + 1]; if (oneTime) observer.addPath(path.getValueFrom(model)) else observer.addPath(model, path); } return new ObserverTransform(observer, tokens.combinator); } function processBindings(node, bindings, model, instanceBindings) { for (var i = 0; i < bindings.length; i += 2) { var name = bindings[i] var tokens = bindings[i + 1]; var value = processBinding(name, tokens, node, model); var binding = node.bind(name, value, tokens.onlyOneTime); if (binding && instanceBindings) instanceBindings.push(binding); } node.bindFinished(); if (!bindings.isTemplate) return; node.model_ = model; var iter = node.processBindingDirectives_(bindings); if (instanceBindings && iter) instanceBindings.push(iter); } function parseWithDefault(el, name, prepareBindingFn) { var v = el.getAttribute(name); return parseMustaches(v == '' ? '{{}}' : v, name, el, prepareBindingFn); } function parseAttributeBindings(element, prepareBindingFn) { assert(element); var bindings = []; var ifFound = false; var bindFound = false; for (var i = 0; i < element.attributes.length; i++) { var attr = element.attributes[i]; var name = attr.name; var value = attr.value; // Allow bindings expressed in attributes to be prefixed with underbars. // We do this to allow correct semantics for browsers that don't implement // <template> where certain attributes might trigger side-effects -- and // for IE which sanitizes certain attributes, disallowing mustache // replacements in their text. while (name[0] === '_') { name = name.substring(1); } if (isTemplate(element) && (name === IF || name === BIND || name === REPEAT)) { continue; } var tokens = parseMustaches(value, name, element, prepareBindingFn); if (!tokens) continue; bindings.push(name, tokens); } if (isTemplate(element)) { bindings.isTemplate = true; bindings.if = parseWithDefault(element, IF, prepareBindingFn); bindings.bind = parseWithDefault(element, BIND, prepareBindingFn); bindings.repeat = parseWithDefault(element, REPEAT, prepareBindingFn); if (bindings.if && !bindings.bind && !bindings.repeat) bindings.bind = parseMustaches('{{}}', BIND, element, prepareBindingFn); } return bindings; } function getBindings(node, prepareBindingFn) { if (node.nodeType === Node.ELEMENT_NODE) return parseAttributeBindings(node, prepareBindingFn); if (node.nodeType === Node.TEXT_NODE) { var tokens = parseMustaches(node.data, 'textContent', node, prepareBindingFn); if (tokens) return ['textContent', tokens]; } return []; } function cloneAndBindInstance(node, parent, stagingDocument, bindings, model, delegate, instanceBindings, instanceRecord) { var clone = parent.appendChild(stagingDocument.importNode(node, false)); var i = 0; for (var child = node.firstChild; child; child = child.nextSibling) { cloneAndBindInstance(child, clone, stagingDocument, bindings.children[i++], model, delegate, instanceBindings); } if (bindings.isTemplate) { HTMLTemplateElement.decorate(clone, node); if (delegate) clone.setDelegate_(delegate); } processBindings(clone, bindings, model, instanceBindings); return clone; } function createInstanceBindingMap(node, prepareBindingFn) { var map = getBindings(node, prepareBindingFn); map.children = {}; var index = 0; for (var child = node.firstChild; child; child = child.nextSibling) { map.children[index++] = createInstanceBindingMap(child, prepareBindingFn); } return map; } var contentUidCounter = 1; // TODO(rafaelw): Setup a MutationObserver on content which clears the id // so that bindingMaps regenerate when the template.content changes. function getContentUid(content) { var id = content.id_; if (!id) id = content.id_ = contentUidCounter++; return id; } // Each delegate is associated with a set of bindingMaps, one for each // content which may be used by a template. The intent is that each binding // delegate gets the opportunity to prepare the instance (via the prepare* // delegate calls) once across all uses. // TODO(rafaelw): Separate out the parse map from the binding map. In the // current implementation, if two delegates need a binding map for the same // content, the second will have to reparse. function getInstanceBindingMap(content, delegate_) { var contentId = getContentUid(content); if (delegate_) { var map = delegate_.bindingMaps[contentId]; if (!map) { map = delegate_.bindingMaps[contentId] = createInstanceBindingMap(content, delegate_.prepareBinding) || []; } return map; } var map = content.bindingMap_; if (!map) { map = content.bindingMap_ = createInstanceBindingMap(content, undefined) || []; } return map; } Object.defineProperty(Node.prototype, 'templateInstance', { get: function() { var instance = this.templateInstance_; return instance ? instance : (this.parentNode ? this.parentNode.templateInstance : undefined); } }); var emptyInstance = document.createDocumentFragment(); emptyInstance.bindings_ = []; emptyInstance.terminator_ = null; function TemplateIterator(templateElement) { this.closed = false; this.templateElement_ = templateElement; this.instances = []; this.deps = undefined; this.iteratedValue = []; this.presentValue = undefined; this.arrayObserver = undefined; } TemplateIterator.prototype = { closeDeps: function() { var deps = this.deps; if (deps) { if (deps.ifOneTime === false) deps.ifValue.close(); if (deps.oneTime === false) deps.value.close(); } }, updateDependencies: function(directives, model) { this.closeDeps(); var deps = this.deps = {}; var template = this.templateElement_; var ifValue = true; if (directives.if) { deps.hasIf = true; deps.ifOneTime = directives.if.onlyOneTime; deps.ifValue = processBinding(IF, directives.if, template, model); ifValue = deps.ifValue; // oneTime if & predicate is false. nothing else to do. if (deps.ifOneTime && !ifValue) { this.valueChanged(); return; } if (!deps.ifOneTime) ifValue = ifValue.open(this.updateIfValue, this); } if (directives.repeat) { deps.repeat = true; deps.oneTime = directives.repeat.onlyOneTime; deps.value = processBinding(REPEAT, directives.repeat, template, model); } else { deps.repeat = false; deps.oneTime = directives.bind.onlyOneTime; deps.value = processBinding(BIND, directives.bind, template, model); } var value = deps.value; if (!deps.oneTime) value = value.open(this.updateIteratedValue, this); if (!ifValue) { this.valueChanged(); return; } this.updateValue(value); }, /** * Gets the updated value of the bind/repeat. This can potentially call * user code (if a bindingDelegate is set up) so we try to avoid it if we * already have the value in hand (from Observer.open). */ getUpdatedValue: function() { var value = this.deps.value; if (!this.deps.oneTime) value = value.discardChanges(); return value; }, updateIfValue: function(ifValue) { if (!ifValue) { this.valueChanged(); return; } this.updateValue(this.getUpdatedValue()); }, updateIteratedValue: function(value) { if (this.deps.hasIf) { var ifValue = this.deps.ifValue; if (!this.deps.ifOneTime) ifValue = ifValue.discardChanges(); if (!ifValue) { this.valueChanged(); return; } } this.updateValue(value); }, updateValue: function(value) { if (!this.deps.repeat) value = [value]; var observe = this.deps.repeat && !this.deps.oneTime && Array.isArray(value); this.valueChanged(value, observe); }, valueChanged: function(value, observeValue) { if (!Array.isArray(value)) value = []; if (value === this.iteratedValue) return; this.unobserve(); this.presentValue = value; if (observeValue) { this.arrayObserver = new ArrayObserver(this.presentValue); this.arrayObserver.open(this.handleSplices, this); } this.handleSplices(ArrayObserver.calculateSplices(this.presentValue, this.iteratedValue)); }, getLastInstanceNode: function(index) { if (index == -1) return this.templateElement_; var instance = this.instances[index]; var terminator = instance.terminator_; if (!terminator) return this.getLastInstanceNode(index - 1); if (terminator.nodeType !== Node.ELEMENT_NODE || this.templateElement_ === terminator) { return terminator; } var subtemplateIterator = terminator.iterator_; if (!subtemplateIterator) return terminator; return subtemplateIterator.getLastTemplateNode(); }, getLastTemplateNode: function() { return this.getLastInstanceNode(this.instances.length - 1); }, insertInstanceAt: function(index, fragment) { var previousInstanceLast = this.getLastInstanceNode(index - 1); var parent = this.templateElement_.parentNode; this.instances.splice(index, 0, fragment); parent.insertBefore(fragment, previousInstanceLast.nextSibling); }, extractInstanceAt: function(index) { var previousInstanceLast = this.getLastInstanceNode(index - 1); var lastNode = this.getLastInstanceNode(index); var parent = this.templateElement_.parentNode; var instance = this.instances.splice(index, 1)[0]; while (lastNode !== previousInstanceLast) { var node = previousInstanceLast.nextSibling; if (node == lastNode) lastNode = previousInstanceLast; instance.appendChild(parent.removeChild(node)); } return instance; }, getDelegateFn: function(fn) { fn = fn && fn(this.templateElement_); return typeof fn === 'function' ? fn : null; }, handleSplices: function(splices) { if (this.closed || !splices.length) return; var template = this.templateElement_; if (!template.parentNode) { this.close(); return; } ArrayObserver.applySplices(this.iteratedValue, this.presentValue, splices); var delegate = template.delegate_; if (this.instanceModelFn_ === undefined) { this.instanceModelFn_ = this.getDelegateFn(delegate && delegate.prepareInstanceModel); } if (this.instancePositionChangedFn_ === undefined) { this.instancePositionChangedFn_ = this.getDelegateFn(delegate && delegate.prepareInstancePositionChanged); } // Instance Removals var instanceCache = new Map; var removeDelta = 0; for (var i = 0; i < splices.length; i++) { var splice = splices[i]; var removed = splice.removed; for (var j = 0; j < removed.length; j++) { var model = removed[j]; var instance = this.extractInstanceAt(splice.index + removeDelta); if (instance !== emptyInstance) { instanceCache.set(model, instance); } } removeDelta -= splice.addedCount; } // Instance Insertions for (var i = 0; i < splices.length; i++) { var splice = splices[i]; var addIndex = splice.index; for (; addIndex < splice.index + splice.addedCount; addIndex++) { var model = this.iteratedValue[addIndex]; var instance = instanceCache.get(model); if (instance) { instanceCache.delete(model); } else { if (this.instanceModelFn_) { model = this.instanceModelFn_(model); } if (model === undefined) { instance = emptyInstance; } else { instance = template.createInstance(model, undefined, delegate); } } this.insertInstanceAt(addIndex, instance); } } instanceCache.forEach(function(instance) { this.closeInstanceBindings(instance); }, this); if (this.instancePositionChangedFn_) this.reportInstancesMoved(splices); }, reportInstanceMoved: function(index) { var instance = this.instances[index]; if (instance === emptyInstance) return; this.instancePositionChangedFn_(instance.templateInstance_, index); }, reportInstancesMoved: function(splices) { var index = 0; var offset = 0; for (var i = 0; i < splices.length; i++) { var splice = splices[i]; if (offset != 0) { while (index < splice.index) { this.reportInstanceMoved(index); index++; } } else { index = splice.index; } while (index < splice.index + splice.addedCount) { this.reportInstanceMoved(index); index++; } offset += splice.addedCount - splice.removed.length; } if (offset == 0) return; var length = this.instances.length; while (index < length) { this.reportInstanceMoved(index); index++; } }, closeInstanceBindings: function(instance) { var bindings = instance.bindings_; for (var i = 0; i < bindings.length; i++) { bindings[i].close(); } }, unobserve: function() { if (!this.arrayObserver) return; this.arrayObserver.close(); this.arrayObserver = undefined; }, close: function() { if (this.closed) return; this.unobserve(); for (var i = 0; i < this.instances.length; i++) { this.closeInstanceBindings(this.instances[i]); } this.instances.length = 0; this.closeDeps(); this.templateElement_.iterator_ = undefined; this.closed = true; } }; // Polyfill-specific API. HTMLTemplateElement.forAllTemplatesFrom_ = forAllTemplatesFrom; })(this); (function(scope) { 'use strict'; // feature detect for URL constructor var hasWorkingUrl = false; if (!scope.forceJURL) { try { var u = new URL('b', 'http://a'); u.pathname = 'c%20d'; hasWorkingUrl = u.href === 'http://a/c%20d'; } catch(e) {} } if (hasWorkingUrl) return; var relative = Object.create(null); relative['ftp'] = 21; relative['file'] = 0; relative['gopher'] = 70; relative['http'] = 80; relative['https'] = 443; relative['ws'] = 80; relative['wss'] = 443; var relativePathDotMapping = Object.create(null); relativePathDotMapping['%2e'] = '.'; relativePathDotMapping['.%2e'] = '..'; relativePathDotMapping['%2e.'] = '..'; relativePathDotMapping['%2e%2e'] = '..'; function isRelativeScheme(scheme) { return relative[scheme] !== undefined; } function invalid() { clear.call(this); this._isInvalid = true; } function IDNAToASCII(h) { if ('' == h) { invalid.call(this) } // XXX return h.toLowerCase() } function percentEscape(c) { var unicode = c.charCodeAt(0); if (unicode > 0x20 && unicode < 0x7F && // " # < > ? ` [0x22, 0x23, 0x3C, 0x3E, 0x3F, 0x60].indexOf(unicode) == -1 ) { return c; } return encodeURIComponent(c); } function percentEscapeQuery(c) { // XXX This actually needs to encode c using encoding and then // convert the bytes one-by-one. var unicode = c.charCodeAt(0); if (unicode > 0x20 && unicode < 0x7F && // " # < > ` (do not escape '?') [0x22, 0x23, 0x3C, 0x3E, 0x60].indexOf(unicode) == -1 ) { return c; } return encodeURIComponent(c); } var EOF = undefined, ALPHA = /[a-zA-Z]/, ALPHANUMERIC = /[a-zA-Z0-9\+\-\.]/; function parse(input, stateOverride, base) { function err(message) { errors.push(message) } var state = stateOverride || 'scheme start', cursor = 0, buffer = '', seenAt = false, seenBracket = false, errors = []; loop: while ((input[cursor - 1] != EOF || cursor == 0) && !this._isInvalid) { var c = input[cursor]; switch (state) { case 'scheme start': if (c && ALPHA.test(c)) { buffer += c.toLowerCase(); // ASCII-safe state = 'scheme'; } else if (!stateOverride) { buffer = ''; state = 'no scheme'; continue; } else { err('Invalid scheme.'); break loop; } break; case 'scheme': if (c && ALPHANUMERIC.test(c)) { buffer += c.toLowerCase(); // ASCII-safe } else if (':' == c) { this._scheme = buffer; buffer = ''; if (stateOverride) { break loop; } if (isRelativeScheme(this._scheme)) { this._isRelative = true; } if ('file' == this._scheme) { state = 'relative'; } else if (this._isRelative && base && base._scheme == this._scheme) { state = 'relative or authority'; } else if (this._isRelative) { state = 'authority first slash'; } else { state = 'scheme data'; } } else if (!stateOverride) { buffer = ''; cursor = 0; state = 'no scheme'; continue; } else if (EOF == c) { break loop; } else { err('Code point not allowed in scheme: ' + c) break loop; } break; case 'scheme data': if ('?' == c) { query = '?'; state = 'query'; } else if ('#' == c) { this._fragment = '#'; state = 'fragment'; } else { // XXX error handling if (EOF != c && '\t' != c && '\n' != c && '\r' != c) { this._schemeData += percentEscape(c); } } break; case 'no scheme': if (!base || !(isRelativeScheme(base._scheme))) { err('Missing scheme.'); invalid.call(this); } else { state = 'relative'; continue; } break; case 'relative or authority': if ('/' == c && '/' == input[cursor+1]) { state = 'authority ignore slashes'; } else { err('Expected /, got: ' + c); state = 'relative'; continue } break; case 'relative': this._isRelative = true; if ('file' != this._scheme) this._scheme = base._scheme; if (EOF == c) { this._host = base._host; this._port = base._port; this._path = base._path.slice(); this._query = base._query; break loop; } else if ('/' == c || '\\' == c) { if ('\\' == c) err('\\ is an invalid code point.'); state = 'relative slash'; } else if ('?' == c) { this._host = base._host; this._port = base._port; this._path = base._path.slice(); this._query = '?'; state = 'query'; } else if ('#' == c) { this._host = base._host; this._port = base._port; this._path = base._path.slice(); this._query = base._query; this._fragment = '#'; state = 'fragment'; } else { var nextC = input[cursor+1] var nextNextC = input[cursor+2] if ( 'file' != this._scheme || !ALPHA.test(c) || (nextC != ':' && nextC != '|') || (EOF != nextNextC && '/' != nextNextC && '\\' != nextNextC && '?' != nextNextC && '#' != nextNextC)) { this._host = base._host; this._port = base._port; this._path = base._path.slice(); this._path.pop(); } state = 'relative path'; continue; } break; case 'relative slash': if ('/' == c || '\\' == c) { if ('\\' == c) { err('\\ is an invalid code point.'); } if ('file' == this._scheme) { state = 'file host'; } else { state = 'authority ignore slashes'; } } else { if ('file' != this._scheme) { this._host = base._host; this._port = base._port; } state = 'relative path'; continue; } break; case 'authority first slash': if ('/' == c) { state = 'authority second slash'; } else { err("Expected '/', got: " + c); state = 'authority ignore slashes'; continue; } break; case 'authority second slash': state = 'authority ignore slashes'; if ('/' != c) { err("Expected '/', got: " + c); continue; } break; case 'authority ignore slashes': if ('/' != c && '\\' != c) { state = 'authority'; continue; } else { err('Expected authority, got: ' + c); } break; case 'authority': if ('@' == c) { if (seenAt) { err('@ already seen.'); buffer += '%40'; } seenAt = true; for (var i = 0; i < buffer.length; i++) { var cp = buffer[i]; if ('\t' == cp || '\n' == cp || '\r' == cp) { err('Invalid whitespace in authority.'); continue; } // XXX check URL code points if (':' == cp && null === this._password) { this._password = ''; continue; } var tempC = percentEscape(cp); (null !== this._password) ? this._password += tempC : this._username += tempC; } buffer = ''; } else if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c) { cursor -= buffer.length; buffer = ''; state = 'host'; continue; } else { buffer += c; } break; case 'file host': if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c) { if (buffer.length == 2 && ALPHA.test(buffer[0]) && (buffer[1] == ':' || buffer[1] == '|')) { state = 'relative path'; } else if (buffer.length == 0) { state = 'relative path start'; } else { this._host = IDNAToASCII.call(this, buffer); buffer = ''; state = 'relative path start'; } continue; } else if ('\t' == c || '\n' == c || '\r' == c) { err('Invalid whitespace in file host.'); } else { buffer += c; } break; case 'host': case 'hostname': if (':' == c && !seenBracket) { // XXX host parsing this._host = IDNAToASCII.call(this, buffer); buffer = ''; state = 'port'; if ('hostname' == stateOverride) { break loop; } } else if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c) { this._host = IDNAToASCII.call(this, buffer); buffer = ''; state = 'relative path start'; if (stateOverride) { break loop; } continue; } else if ('\t' != c && '\n' != c && '\r' != c) { if ('[' == c) { seenBracket = true; } else if (']' == c) { seenBracket = false; } buffer += c; } else { err('Invalid code point in host/hostname: ' + c); } break; case 'port': if (/[0-9]/.test(c)) { buffer += c; } else if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c || stateOverride) { if ('' != buffer) { var temp = parseInt(buffer, 10); if (temp != relative[this._scheme]) { this._port = temp + ''; } buffer = ''; } if (stateOverride) { break loop; } state = 'relative path start'; continue; } else if ('\t' == c || '\n' == c || '\r' == c) { err('Invalid code point in port: ' + c); } else { invalid.call(this); } break; case 'relative path start': if ('\\' == c) err("'\\' not allowed in path."); state = 'relative path'; if ('/' != c && '\\' != c) { continue; } break; case 'relative path': if (EOF == c || '/' == c || '\\' == c || (!stateOverride && ('?' == c || '#' == c))) { if ('\\' == c) { err('\\ not allowed in relative path.'); } var tmp; if (tmp = relativePathDotMapping[buffer.toLowerCase()]) { buffer = tmp; } if ('..' == buffer) { this._path.pop(); if ('/' != c && '\\' != c) { this._path.push(''); } } else if ('.' == buffer && '/' != c && '\\' != c) { this._path.push(''); } else if ('.' != buffer) { if ('file' == this._scheme && this._path.length == 0 && buffer.length == 2 && ALPHA.test(buffer[0]) && buffer[1] == '|') { buffer = buffer[0] + ':'; } this._path.push(buffer); } buffer = ''; if ('?' == c) { this._query = '?'; state = 'query'; } else if ('#' == c) { this._fragment = '#'; state = 'fragment'; } } else if ('\t' != c && '\n' != c && '\r' != c) { buffer += percentEscape(c); } break; case 'query': if (!stateOverride && '#' == c) { this._fragment = '#'; state = 'fragment'; } else if (EOF != c && '\t' != c && '\n' != c && '\r' != c) { this._query += percentEscapeQuery(c); } break; case 'fragment': if (EOF != c && '\t' != c && '\n' != c && '\r' != c) { this._fragment += c; } break; } cursor++; } } function clear() { this._scheme = ''; this._schemeData = ''; this._username = ''; this._password = null; this._host = ''; this._port = ''; this._path = []; this._query = ''; this._fragment = ''; this._isInvalid = false; this._isRelative = false; } // Does not process domain names or IP addresses. // Does not handle encoding for the query parameter. function jURL(url, base /* , encoding */) { if (base !== undefined && !(base instanceof jURL)) base = new jURL(String(base)); this._url = url; clear.call(this); var input = url.replace(/^[ \t\r\n\f]+|[ \t\r\n\f]+$/g, ''); // encoding = encoding || 'utf-8' parse.call(this, input, null, base); } jURL.prototype = { get href() { if (this._isInvalid) return this._url; var authority = ''; if ('' != this._username || null != this._password) { authority = this._username + (null != this._password ? ':' + this._password : '') + '@'; } return this.protocol + (this._isRelative ? '//' + authority + this.host : '') + this.pathname + this._query + this._fragment; }, set href(href) { clear.call(this); parse.call(this, href); }, get protocol() { return this._scheme + ':'; }, set protocol(protocol) { if (this._isInvalid) return; parse.call(this, protocol + ':', 'scheme start'); }, get host() { return this._isInvalid ? '' : this._port ? this._host + ':' + this._port : this._host; }, set host(host) { if (this._isInvalid || !this._isRelative) return; parse.call(this, host, 'host'); }, get hostname() { return this._host; }, set hostname(hostname) { if (this._isInvalid || !this._isRelative) return; parse.call(this, hostname, 'hostname'); }, get port() { return this._port; }, set port(port) { if (this._isInvalid || !this._isRelative) return; parse.call(this, port, 'port'); }, get pathname() { return this._isInvalid ? '' : this._isRelative ? '/' + this._path.join('/') : this._schemeData; }, set pathname(pathname) { if (this._isInvalid || !this._isRelative) return; this._path = []; parse.call(this, pathname, 'relative path start'); }, get search() { return this._isInvalid || !this._query || '?' == this._query ? '' : this._query; }, set search(search) { if (this._isInvalid || !this._isRelative) return; this._query = '?'; if ('?' == search[0]) search = search.slice(1); parse.call(this, search, 'query'); }, get hash() { return this._isInvalid || !this._fragment || '#' == this._fragment ? '' : this._fragment; }, set hash(hash) { if (this._isInvalid) return; this._fragment = '#'; if ('#' == hash[0]) hash = hash.slice(1); parse.call(this, hash, 'fragment'); }, get origin() { var host; if (this._isInvalid || !this._scheme) { return ''; } // javascript: Gecko returns String(""), WebKit/Blink String("null") // Gecko throws error for "data://" // data: Gecko returns "", Blink returns "data://", WebKit returns "null" // Gecko returns String("") for file: mailto: // WebKit/Blink returns String("SCHEME://") for file: mailto: switch (this._scheme) { case 'data': case 'file': case 'javascript': case 'mailto': return 'null'; } host = this.host; if (!host) { return ''; } return this._scheme + '://' + host; } }; // Copy over the static methods var OriginalURL = scope.URL; if (OriginalURL) { jURL.createObjectURL = function(blob) { // IE extension allows a second optional options argument. // http://msdn.microsoft.com/en-us/library/ie/hh772302(v=vs.85).aspx return OriginalURL.createObjectURL.apply(OriginalURL, arguments); }; jURL.revokeObjectURL = function(url) { OriginalURL.revokeObjectURL(url); }; } scope.URL = jURL; })(this); (function(scope) { var iterations = 0; var callbacks = []; var twiddle = document.createTextNode(''); function endOfMicrotask(callback) { twiddle.textContent = iterations++; callbacks.push(callback); } function atEndOfMicrotask() { while (callbacks.length) { callbacks.shift()(); } } new (window.MutationObserver || JsMutationObserver)(atEndOfMicrotask) .observe(twiddle, {characterData: true}) ; // exports scope.endOfMicrotask = endOfMicrotask; // bc Platform.endOfMicrotask = endOfMicrotask; })(Polymer); (function(scope) { /** * @class Polymer */ // imports var endOfMicrotask = scope.endOfMicrotask; // logging var log = window.WebComponents ? WebComponents.flags.log : {}; // inject style sheet var style = document.createElement('style'); style.textContent = 'template {display: none !important;} /* injected by platform.js */'; var head = document.querySelector('head'); head.insertBefore(style, head.firstChild); /** * Force any pending data changes to be observed before * the next task. Data changes are processed asynchronously but are guaranteed * to be processed, for example, before painting. This method should rarely be * needed. It does nothing when Object.observe is available; * when Object.observe is not available, Polymer automatically flushes data * changes approximately every 1/10 second. * Therefore, `flush` should only be used when a data mutation should be * observed sooner than this. * * @method flush */ // flush (with logging) var flushing; function flush() { if (!flushing) { flushing = true; endOfMicrotask(function() { flushing = false; log.data && console.group('flush'); Platform.performMicrotaskCheckpoint(); log.data && console.groupEnd(); }); } }; // polling dirty checker // flush periodically if platform does not have object observe. if (!Observer.hasObjectObserve) { var FLUSH_POLL_INTERVAL = 125; window.addEventListener('WebComponentsReady', function() { flush(); // watch document visiblity to toggle dirty-checking var visibilityHandler = function() { // only flush if the page is visibile if (document.visibilityState === 'hidden') { if (scope.flushPoll) { clearInterval(scope.flushPoll); } } else { scope.flushPoll = setInterval(flush, FLUSH_POLL_INTERVAL); } }; if (typeof document.visibilityState === 'string') { document.addEventListener('visibilitychange', visibilityHandler); } visibilityHandler(); }); } else { // make flush a no-op when we have Object.observe flush = function() {}; } if (window.CustomElements && !CustomElements.useNative) { var originalImportNode = Document.prototype.importNode; Document.prototype.importNode = function(node, deep) { var imported = originalImportNode.call(this, node, deep); CustomElements.upgradeAll(imported); return imported; }; } // exports scope.flush = flush; // bc Platform.flush = flush; })(window.Polymer); (function(scope) { var urlResolver = { resolveDom: function(root, url) { url = url || baseUrl(root); this.resolveAttributes(root, url); this.resolveStyles(root, url); // handle template.content var templates = root.querySelectorAll('template'); if (templates) { for (var i = 0, l = templates.length, t; (i < l) && (t = templates[i]); i++) { if (t.content) { this.resolveDom(t.content, url); } } } }, resolveTemplate: function(template) { this.resolveDom(template.content, baseUrl(template)); }, resolveStyles: function(root, url) { var styles = root.querySelectorAll('style'); if (styles) { for (var i = 0, l = styles.length, s; (i < l) && (s = styles[i]); i++) { this.resolveStyle(s, url); } } }, resolveStyle: function(style, url) { url = url || baseUrl(style); style.textContent = this.resolveCssText(style.textContent, url); }, resolveCssText: function(cssText, baseUrl, keepAbsolute) { cssText = replaceUrlsInCssText(cssText, baseUrl, keepAbsolute, CSS_URL_REGEXP); return replaceUrlsInCssText(cssText, baseUrl, keepAbsolute, CSS_IMPORT_REGEXP); }, resolveAttributes: function(root, url) { if (root.hasAttributes && root.hasAttributes()) { this.resolveElementAttributes(root, url); } // search for attributes that host urls var nodes = root && root.querySelectorAll(URL_ATTRS_SELECTOR); if (nodes) { for (var i = 0, l = nodes.length, n; (i < l) && (n = nodes[i]); i++) { this.resolveElementAttributes(n, url); } } }, resolveElementAttributes: function(node, url) { url = url || baseUrl(node); URL_ATTRS.forEach(function(v) { var attr = node.attributes[v]; var value = attr && attr.value; var replacement; if (value && value.search(URL_TEMPLATE_SEARCH) < 0) { if (v === 'style') { replacement = replaceUrlsInCssText(value, url, false, CSS_URL_REGEXP); } else { replacement = resolveRelativeUrl(url, value); } attr.value = replacement; } }); } }; var CSS_URL_REGEXP = /(url\()([^)]*)(\))/g; var CSS_IMPORT_REGEXP = /(@import[\s]+(?!url\())([^;]*)(;)/g; var URL_ATTRS = ['href', 'src', 'action', 'style', 'url']; var URL_ATTRS_SELECTOR = '[' + URL_ATTRS.join('],[') + ']'; var URL_TEMPLATE_SEARCH = '{{.*}}'; var URL_HASH = '#'; function baseUrl(node) { var u = new URL(node.ownerDocument.baseURI); u.search = ''; u.hash = ''; return u; } function replaceUrlsInCssText(cssText, baseUrl, keepAbsolute, regexp) { return cssText.replace(regexp, function(m, pre, url, post) { var urlPath = url.replace(/["']/g, ''); urlPath = resolveRelativeUrl(baseUrl, urlPath, keepAbsolute); return pre + '\'' + urlPath + '\'' + post; }); } function resolveRelativeUrl(baseUrl, url, keepAbsolute) { // do not resolve '/' absolute urls if (url && url[0] === '/') { return url; } // do not resolve '#' links, they are used for routing if (url && url[0] === '#') { return url; } var u = new URL(url, baseUrl); return keepAbsolute ? u.href : makeDocumentRelPath(u.href); } function makeDocumentRelPath(url) { var root = baseUrl(document.documentElement); var u = new URL(url, root); if (u.host === root.host && u.port === root.port && u.protocol === root.protocol) { return makeRelPath(root, u); } else { return url; } } // make a relative path from source to target function makeRelPath(sourceUrl, targetUrl) { var source = sourceUrl.pathname; var target = targetUrl.pathname; var s = source.split('/'); var t = target.split('/'); while (s.length && s[0] === t[0]){ s.shift(); t.shift(); } for (var i = 0, l = s.length - 1; i < l; i++) { t.unshift('..'); } // empty '#' is discarded but we need to preserve it. var hash = (targetUrl.href.slice(-1) === URL_HASH) ? URL_HASH : targetUrl.hash; return t.join('/') + targetUrl.search + hash; } // exports scope.urlResolver = urlResolver; })(Polymer); (function(scope) { var endOfMicrotask = Polymer.endOfMicrotask; // Generic url loader function Loader(regex) { this.cache = Object.create(null); this.map = Object.create(null); this.requests = 0; this.regex = regex; } Loader.prototype = { // TODO(dfreedm): there may be a better factoring here // extract absolute urls from the text (full of relative urls) extractUrls: function(text, base) { var matches = []; var matched, u; while ((matched = this.regex.exec(text))) { u = new URL(matched[1], base); matches.push({matched: matched[0], url: u.href}); } return matches; }, // take a text blob, a root url, and a callback and load all the urls found within the text // returns a map of absolute url to text process: function(text, root, callback) { var matches = this.extractUrls(text, root); // every call to process returns all the text this loader has ever received var done = callback.bind(null, this.map); this.fetch(matches, done); }, // build a mapping of url -> text from matches fetch: function(matches, callback) { var inflight = matches.length; // return early if there is no fetching to be done if (!inflight) { return callback(); } // wait for all subrequests to return var done = function() { if (--inflight === 0) { callback(); } }; // start fetching all subrequests var m, req, url; for (var i = 0; i < inflight; i++) { m = matches[i]; url = m.url; req = this.cache[url]; // if this url has already been requested, skip requesting it again if (!req) { req = this.xhr(url); req.match = m; this.cache[url] = req; } // wait for the request to process its subrequests req.wait(done); } }, handleXhr: function(request) { var match = request.match; var url = match.url; // handle errors with an empty string var response = request.response || request.responseText || ''; this.map[url] = response; this.fetch(this.extractUrls(response, url), request.resolve); }, xhr: function(url) { this.requests++; var request = new XMLHttpRequest(); request.open('GET', url, true); request.send(); request.onerror = request.onload = this.handleXhr.bind(this, request); // queue of tasks to run after XHR returns request.pending = []; request.resolve = function() { var pending = request.pending; for(var i = 0; i < pending.length; i++) { pending[i](); } request.pending = null; }; // if we have already resolved, pending is null, async call the callback request.wait = function(fn) { if (request.pending) { request.pending.push(fn); } else { endOfMicrotask(fn); } }; return request; } }; scope.Loader = Loader; })(Polymer); (function(scope) { var urlResolver = scope.urlResolver; var Loader = scope.Loader; function StyleResolver() { this.loader = new Loader(this.regex); } StyleResolver.prototype = { regex: /@import\s+(?:url)?["'\(]*([^'"\)]*)['"\)]*;/g, // Recursively replace @imports with the text at that url resolve: function(text, url, callback) { var done = function(map) { callback(this.flatten(text, url, map)); }.bind(this); this.loader.process(text, url, done); }, // resolve the textContent of a style node resolveNode: function(style, url, callback) { var text = style.textContent; var done = function(text) { style.textContent = text; callback(style); }; this.resolve(text, url, done); }, // flatten all the @imports to text flatten: function(text, base, map) { var matches = this.loader.extractUrls(text, base); var match, url, intermediate; for (var i = 0; i < matches.length; i++) { match = matches[i]; url = match.url; // resolve any css text to be relative to the importer, keep absolute url intermediate = urlResolver.resolveCssText(map[url], url, true); // flatten intermediate @imports intermediate = this.flatten(intermediate, base, map); text = text.replace(match.matched, intermediate); } return text; }, loadStyles: function(styles, base, callback) { var loaded=0, l = styles.length; // called in the context of the style function loadedStyle(style) { loaded++; if (loaded === l && callback) { callback(); } } for (var i=0, s; (i<l) && (s=styles[i]); i++) { this.resolveNode(s, base, loadedStyle); } } }; var styleResolver = new StyleResolver(); // exports scope.styleResolver = styleResolver; })(Polymer); (function(scope) { // copy own properties from 'api' to 'prototype, with name hinting for 'super' function extend(prototype, api) { if (prototype && api) { // use only own properties of 'api' Object.getOwnPropertyNames(api).forEach(function(n) { // acquire property descriptor var pd = Object.getOwnPropertyDescriptor(api, n); if (pd) { // clone property via descriptor Object.defineProperty(prototype, n, pd); // cache name-of-method for 'super' engine if (typeof pd.value == 'function') { // hint the 'super' engine pd.value.nom = n; } } }); } return prototype; } // mixin // copy all properties from inProps (et al) to inObj function mixin(inObj/*, inProps, inMoreProps, ...*/) { var obj = inObj || {}; for (var i = 1; i < arguments.length; i++) { var p = arguments[i]; try { for (var n in p) { copyProperty(n, p, obj); } } catch(x) { } } return obj; } // copy property inName from inSource object to inTarget object function copyProperty(inName, inSource, inTarget) { var pd = getPropertyDescriptor(inSource, inName); Object.defineProperty(inTarget, inName, pd); } // get property descriptor for inName on inObject, even if // inName exists on some link in inObject's prototype chain function getPropertyDescriptor(inObject, inName) { if (inObject) { var pd = Object.getOwnPropertyDescriptor(inObject, inName); return pd || getPropertyDescriptor(Object.getPrototypeOf(inObject), inName); } } // exports scope.extend = extend; scope.mixin = mixin; // for bc Platform.mixin = mixin; })(Polymer); (function(scope) { // usage // invoke cb.call(this) in 100ms, unless the job is re-registered, // which resets the timer // // this.myJob = this.job(this.myJob, cb, 100) // // returns a job handle which can be used to re-register a job var Job = function(inContext) { this.context = inContext; this.boundComplete = this.complete.bind(this) }; Job.prototype = { go: function(callback, wait) { this.callback = callback; var h; if (!wait) { h = requestAnimationFrame(this.boundComplete); this.handle = function() { cancelAnimationFrame(h); } } else { h = setTimeout(this.boundComplete, wait); this.handle = function() { clearTimeout(h); } } }, stop: function() { if (this.handle) { this.handle(); this.handle = null; } }, complete: function() { if (this.handle) { this.stop(); this.callback.call(this.context); } } }; function job(job, callback, wait) { if (job) { job.stop(); } else { job = new Job(this); } job.go(callback, wait); return job; } // exports scope.job = job; })(Polymer); (function(scope) { // dom polyfill, additions, and utility methods var registry = {}; HTMLElement.register = function(tag, prototype) { registry[tag] = prototype; }; // get prototype mapped to node <tag> HTMLElement.getPrototypeForTag = function(tag) { var prototype = !tag ? HTMLElement.prototype : registry[tag]; // TODO(sjmiles): creating <tag> is likely to have wasteful side-effects return prototype || Object.getPrototypeOf(document.createElement(tag)); }; // we have to flag propagation stoppage for the event dispatcher var originalStopPropagation = Event.prototype.stopPropagation; Event.prototype.stopPropagation = function() { this.cancelBubble = true; originalStopPropagation.apply(this, arguments); }; // polyfill DOMTokenList // * add/remove: allow these methods to take multiple classNames // * toggle: add a 2nd argument which forces the given state rather // than toggling. var add = DOMTokenList.prototype.add; var remove = DOMTokenList.prototype.remove; DOMTokenList.prototype.add = function() { for (var i = 0; i < arguments.length; i++) { add.call(this, arguments[i]); } }; DOMTokenList.prototype.remove = function() { for (var i = 0; i < arguments.length; i++) { remove.call(this, arguments[i]); } }; DOMTokenList.prototype.toggle = function(name, bool) { if (arguments.length == 1) { bool = !this.contains(name); } bool ? this.add(name) : this.remove(name); }; DOMTokenList.prototype.switch = function(oldName, newName) { oldName && this.remove(oldName); newName && this.add(newName); }; // add array() to NodeList, NamedNodeMap, HTMLCollection var ArraySlice = function() { return Array.prototype.slice.call(this); }; var namedNodeMap = (window.NamedNodeMap || window.MozNamedAttrMap || {}); NodeList.prototype.array = ArraySlice; namedNodeMap.prototype.array = ArraySlice; HTMLCollection.prototype.array = ArraySlice; // utility function createDOM(inTagOrNode, inHTML, inAttrs) { var dom = typeof inTagOrNode == 'string' ? document.createElement(inTagOrNode) : inTagOrNode.cloneNode(true); dom.innerHTML = inHTML; if (inAttrs) { for (var n in inAttrs) { dom.setAttribute(n, inAttrs[n]); } } return dom; } // exports scope.createDOM = createDOM; })(Polymer); (function(scope) { // super // `arrayOfArgs` is an optional array of args like one might pass // to `Function.apply` // TODO(sjmiles): // $super must be installed on an instance or prototype chain // as `super`, and invoked via `this`, e.g. // `this.super();` // will not work if function objects are not unique, for example, // when using mixins. // The memoization strategy assumes each function exists on only one // prototype chain i.e. we use the function object for memoizing) // perhaps we can bookkeep on the prototype itself instead function $super(arrayOfArgs) { // since we are thunking a method call, performance is important here: // memoize all lookups, once memoized the fast path calls no other // functions // // find the caller (cannot be `strict` because of 'caller') var caller = $super.caller; // memoized 'name of method' var nom = caller.nom; // memoized next implementation prototype var _super = caller._super; if (!_super) { if (!nom) { nom = caller.nom = nameInThis.call(this, caller); } if (!nom) { console.warn('called super() on a method not installed declaratively (has no .nom property)'); } // super prototype is either cached or we have to find it // by searching __proto__ (at the 'top') // invariant: because we cache _super on fn below, we never reach // here from inside a series of calls to super(), so it's ok to // start searching from the prototype of 'this' (at the 'top') // we must never memoize a null super for this reason _super = memoizeSuper(caller, nom, getPrototypeOf(this)); } // our super function var fn = _super[nom]; if (fn) { // memoize information so 'fn' can call 'super' if (!fn._super) { // must not memoize null, or we lose our invariant above memoizeSuper(fn, nom, _super); } // invoke the inherited method // if 'fn' is not function valued, this will throw return fn.apply(this, arrayOfArgs || []); } } function nameInThis(value) { var p = this.__proto__; while (p && p !== HTMLElement.prototype) { // TODO(sjmiles): getOwnPropertyNames is absurdly expensive var n$ = Object.getOwnPropertyNames(p); for (var i=0, l=n$.length, n; i<l && (n=n$[i]); i++) { var d = Object.getOwnPropertyDescriptor(p, n); if (typeof d.value === 'function' && d.value === value) { return n; } } p = p.__proto__; } } function memoizeSuper(method, name, proto) { // find and cache next prototype containing `name` // we need the prototype so we can do another lookup // from here var s = nextSuper(proto, name, method); if (s[name]) { // `s` is a prototype, the actual method is `s[name]` // tag super method with it's name for quicker lookups s[name].nom = name; } return method._super = s; } function nextSuper(proto, name, caller) { // look for an inherited prototype that implements name while (proto) { if ((proto[name] !== caller) && proto[name]) { return proto; } proto = getPrototypeOf(proto); } // must not return null, or we lose our invariant above // in this case, a super() call was invoked where no superclass // method exists // TODO(sjmiles): thow an exception? return Object; } // NOTE: In some platforms (IE10) the prototype chain is faked via // __proto__. Therefore, always get prototype via __proto__ instead of // the more standard Object.getPrototypeOf. function getPrototypeOf(prototype) { return prototype.__proto__; } // utility function to precompute name tags for functions // in a (unchained) prototype function hintSuper(prototype) { // tag functions with their prototype name to optimize // super call invocations for (var n in prototype) { var pd = Object.getOwnPropertyDescriptor(prototype, n); if (pd && typeof pd.value === 'function') { pd.value.nom = n; } } } // exports scope.super = $super; })(Polymer); (function(scope) { function noopHandler(value) { return value; } // helper for deserializing properties of various types to strings var typeHandlers = { string: noopHandler, 'undefined': noopHandler, date: function(value) { return new Date(Date.parse(value) || Date.now()); }, boolean: function(value) { if (value === '') { return true; } return value === 'false' ? false : !!value; }, number: function(value) { var n = parseFloat(value); // hex values like "0xFFFF" parseFloat as 0 if (n === 0) { n = parseInt(value); } return isNaN(n) ? value : n; // this code disabled because encoded values (like "0xFFFF") // do not round trip to their original format //return (String(floatVal) === value) ? floatVal : value; }, object: function(value, currentValue) { if (currentValue === null) { return value; } try { // If the string is an object, we can parse is with the JSON library. // include convenience replace for single-quotes. If the author omits // quotes altogether, parse will fail. return JSON.parse(value.replace(/'/g, '"')); } catch(e) { // The object isn't valid JSON, return the raw value return value; } }, // avoid deserialization of functions 'function': function(value, currentValue) { return currentValue; } }; function deserializeValue(value, currentValue) { // attempt to infer type from default value var inferredType = typeof currentValue; // invent 'date' type value for Date if (currentValue instanceof Date) { inferredType = 'date'; } // delegate deserialization via type string return typeHandlers[inferredType](value, currentValue); } // exports scope.deserializeValue = deserializeValue; })(Polymer); (function(scope) { // imports var extend = scope.extend; // module var api = {}; api.declaration = {}; api.instance = {}; api.publish = function(apis, prototype) { for (var n in apis) { extend(prototype, apis[n]); } }; // exports scope.api = api; })(Polymer); (function(scope) { /** * @class polymer-base */ var utils = { /** * Invokes a function asynchronously. The context of the callback * function is bound to 'this' automatically. Returns a handle which may * be passed to <a href="#cancelAsync">cancelAsync</a> to cancel the * asynchronous call. * * @method async * @param {Function|String} method * @param {any|Array} args * @param {number} timeout */ async: function(method, args, timeout) { // when polyfilling Object.observe, ensure changes // propagate before executing the async method Polymer.flush(); // second argument to `apply` must be an array args = (args && args.length) ? args : [args]; // function to invoke var fn = function() { (this[method] || method).apply(this, args); }.bind(this); // execute `fn` sooner or later var handle = timeout ? setTimeout(fn, timeout) : requestAnimationFrame(fn); // NOTE: switch on inverting handle to determine which time is used. return timeout ? handle : ~handle; }, /** * Cancels a pending callback that was scheduled via * <a href="#async">async</a>. * * @method cancelAsync * @param {handle} handle Handle of the `async` to cancel. */ cancelAsync: function(handle) { if (handle < 0) { cancelAnimationFrame(~handle); } else { clearTimeout(handle); } }, /** * Fire an event. * * @method fire * @returns {Object} event * @param {string} type An event name. * @param {any} detail * @param {Node} onNode Target node. * @param {Boolean} bubbles Set false to prevent bubbling, defaults to true * @param {Boolean} cancelable Set false to prevent cancellation, defaults to true */ fire: function(type, detail, onNode, bubbles, cancelable) { var node = onNode || this; var detail = detail === null || detail === undefined ? {} : detail; var event = new CustomEvent(type, { bubbles: bubbles !== undefined ? bubbles : true, cancelable: cancelable !== undefined ? cancelable : true, detail: detail }); node.dispatchEvent(event); return event; }, /** * Fire an event asynchronously. * * @method asyncFire * @param {string} type An event name. * @param detail * @param {Node} toNode Target node. */ asyncFire: function(/*inType, inDetail*/) { this.async("fire", arguments); }, /** * Remove class from old, add class to anew, if they exist. * * @param classFollows * @param anew A node. * @param old A node * @param className */ classFollows: function(anew, old, className) { if (old) { old.classList.remove(className); } if (anew) { anew.classList.add(className); } }, /** * Inject HTML which contains markup bound to this element into * a target element (replacing target element content). * * @param String html to inject * @param Element target element */ injectBoundHTML: function(html, element) { var template = document.createElement('template'); template.innerHTML = html; var fragment = this.instanceTemplate(template); if (element) { element.textContent = ''; element.appendChild(fragment); } return fragment; } }; // no-operation function for handy stubs var nop = function() {}; // null-object for handy stubs var nob = {}; // deprecated utils.asyncMethod = utils.async; // exports scope.api.instance.utils = utils; scope.nop = nop; scope.nob = nob; })(Polymer); (function(scope) { // imports var log = window.WebComponents ? WebComponents.flags.log : {}; var EVENT_PREFIX = 'on-'; // instance events api var events = { // read-only EVENT_PREFIX: EVENT_PREFIX, // event listeners on host addHostListeners: function() { var events = this.eventDelegates; log.events && (Object.keys(events).length > 0) && console.log('[%s] addHostListeners:', this.localName, events); // NOTE: host events look like bindings but really are not; // (1) we don't want the attribute to be set and (2) we want to support // multiple event listeners ('host' and 'instance') and Node.bind // by default supports 1 thing being bound. for (var type in events) { var methodName = events[type]; PolymerGestures.addEventListener(this, type, this.element.getEventHandler(this, this, methodName)); } }, // call 'method' or function method on 'obj' with 'args', if the method exists dispatchMethod: function(obj, method, args) { if (obj) { log.events && console.group('[%s] dispatch [%s]', obj.localName, method); var fn = typeof method === 'function' ? method : obj[method]; if (fn) { fn[args ? 'apply' : 'call'](obj, args); } log.events && console.groupEnd(); // NOTE: dirty check right after calling method to ensure // changes apply quickly; in a very complicated app using high // frequency events, this can be a perf concern; in this case, // imperative handlers can be used to avoid flushing. Polymer.flush(); } } }; // exports scope.api.instance.events = events; /** * @class Polymer */ /** * Add a gesture aware event handler to the given `node`. Can be used * in place of `element.addEventListener` and ensures gestures will function * as expected on mobile platforms. Please note that Polymer's declarative * event handlers include this functionality by default. * * @method addEventListener * @param {Node} node node on which to listen * @param {String} eventType name of the event * @param {Function} handlerFn event handler function * @param {Boolean} capture set to true to invoke event capturing * @type Function */ // alias PolymerGestures event listener logic scope.addEventListener = function(node, eventType, handlerFn, capture) { PolymerGestures.addEventListener(wrap(node), eventType, handlerFn, capture); }; /** * Remove a gesture aware event handler on the given `node`. To remove an * event listener, the exact same arguments are required that were passed * to `Polymer.addEventListener`. * * @method removeEventListener * @param {Node} node node on which to listen * @param {String} eventType name of the event * @param {Function} handlerFn event handler function * @param {Boolean} capture set to true to invoke event capturing * @type Function */ scope.removeEventListener = function(node, eventType, handlerFn, capture) { PolymerGestures.removeEventListener(wrap(node), eventType, handlerFn, capture); }; })(Polymer); (function(scope) { // instance api for attributes var attributes = { // copy attributes defined in the element declaration to the instance // e.g. <polymer-element name="x-foo" tabIndex="0"> tabIndex is copied // to the element instance here. copyInstanceAttributes: function () { var a$ = this._instanceAttributes; for (var k in a$) { if (!this.hasAttribute(k)) { this.setAttribute(k, a$[k]); } } }, // for each attribute on this, deserialize value to property as needed takeAttributes: function() { // if we have no publish lookup table, we have no attributes to take // TODO(sjmiles): ad hoc if (this._publishLC) { for (var i=0, a$=this.attributes, l=a$.length, a; (a=a$[i]) && i<l; i++) { this.attributeToProperty(a.name, a.value); } } }, // if attribute 'name' is mapped to a property, deserialize // 'value' into that property attributeToProperty: function(name, value) { // try to match this attribute to a property (attributes are // all lower-case, so this is case-insensitive search) var name = this.propertyForAttribute(name); if (name) { // filter out 'mustached' values, these are to be // replaced with bound-data and are not yet values // themselves if (value && value.search(scope.bindPattern) >= 0) { return; } // get original value var currentValue = this[name]; // deserialize Boolean or Number values from attribute var value = this.deserializeValue(value, currentValue); // only act if the value has changed if (value !== currentValue) { // install new value (has side-effects) this[name] = value; } } }, // return the published property matching name, or undefined propertyForAttribute: function(name) { var match = this._publishLC && this._publishLC[name]; return match; }, // convert representation of `stringValue` based on type of `currentValue` deserializeValue: function(stringValue, currentValue) { return scope.deserializeValue(stringValue, currentValue); }, // convert to a string value based on the type of `inferredType` serializeValue: function(value, inferredType) { if (inferredType === 'boolean') { return value ? '' : undefined; } else if (inferredType !== 'object' && inferredType !== 'function' && value !== undefined) { return value; } }, // serializes `name` property value and updates the corresponding attribute // note that reflection is opt-in. reflectPropertyToAttribute: function(name) { var inferredType = typeof this[name]; // try to intelligently serialize property value var serializedValue = this.serializeValue(this[name], inferredType); // boolean properties must reflect as boolean attributes if (serializedValue !== undefined) { this.setAttribute(name, serializedValue); // TODO(sorvell): we should remove attr for all properties // that have undefined serialization; however, we will need to // refine the attr reflection system to achieve this; pica, for example, // relies on having inferredType object properties not removed as // attrs. } else if (inferredType === 'boolean') { this.removeAttribute(name); } } }; // exports scope.api.instance.attributes = attributes; })(Polymer); (function(scope) { /** * @class polymer-base */ // imports var log = window.WebComponents ? WebComponents.flags.log : {}; // magic words var OBSERVE_SUFFIX = 'Changed'; // element api var empty = []; var updateRecord = { object: undefined, type: 'update', name: undefined, oldValue: undefined }; var numberIsNaN = Number.isNaN || function(value) { return typeof value === 'number' && isNaN(value); }; function areSameValue(left, right) { if (left === right) return left !== 0 || 1 / left === 1 / right; if (numberIsNaN(left) && numberIsNaN(right)) return true; return left !== left && right !== right; } // capture A's value if B's value is null or undefined, // otherwise use B's value function resolveBindingValue(oldValue, value) { if (value === undefined && oldValue === null) { return value; } return (value === null || value === undefined) ? oldValue : value; } var properties = { // creates a CompoundObserver to observe property changes // NOTE, this is only done there are any properties in the `observe` object createPropertyObserver: function() { var n$ = this._observeNames; if (n$ && n$.length) { var o = this._propertyObserver = new CompoundObserver(true); this.registerObserver(o); // TODO(sorvell): may not be kosher to access the value here (this[n]); // previously we looked at the descriptor on the prototype // this doesn't work for inheritance and not for accessors without // a value property for (var i=0, l=n$.length, n; (i<l) && (n=n$[i]); i++) { o.addPath(this, n); this.observeArrayValue(n, this[n], null); } } }, // start observing property changes openPropertyObserver: function() { if (this._propertyObserver) { this._propertyObserver.open(this.notifyPropertyChanges, this); } }, // handler for property changes; routes changes to observing methods // note: array valued properties are observed for array splices notifyPropertyChanges: function(newValues, oldValues, paths) { var name, method, called = {}; for (var i in oldValues) { // note: paths is of form [object, path, object, path] name = paths[2 * i + 1]; method = this.observe[name]; if (method) { var ov = oldValues[i], nv = newValues[i]; // observes the value if it is an array this.observeArrayValue(name, nv, ov); if (!called[method]) { // only invoke change method if one of ov or nv is not (undefined | null) if ((ov !== undefined && ov !== null) || (nv !== undefined && nv !== null)) { called[method] = true; // TODO(sorvell): call method with the set of values it's expecting; // e.g. 'foo bar': 'invalidate' expects the new and old values for // foo and bar. Currently we give only one of these and then // deliver all the arguments. this.invokeMethod(method, [ov, nv, arguments]); } } } } }, // call method iff it exists. invokeMethod: function(method, args) { var fn = this[method] || method; if (typeof fn === 'function') { fn.apply(this, args); } }, /** * Force any pending property changes to synchronously deliver to * handlers specified in the `observe` object. * Note, normally changes are processed at microtask time. * * @method deliverChanges */ deliverChanges: function() { if (this._propertyObserver) { this._propertyObserver.deliver(); } }, observeArrayValue: function(name, value, old) { // we only care if there are registered side-effects var callbackName = this.observe[name]; if (callbackName) { // if we are observing the previous value, stop if (Array.isArray(old)) { log.observe && console.log('[%s] observeArrayValue: unregister observer [%s]', this.localName, name); this.closeNamedObserver(name + '__array'); } // if the new value is an array, being observing it if (Array.isArray(value)) { log.observe && console.log('[%s] observeArrayValue: register observer [%s]', this.localName, name, value); var observer = new ArrayObserver(value); observer.open(function(splices) { this.invokeMethod(callbackName, [splices]); }, this); this.registerNamedObserver(name + '__array', observer); } } }, emitPropertyChangeRecord: function(name, value, oldValue) { var object = this; if (areSameValue(value, oldValue)) { return; } // invoke property change side effects this._propertyChanged(name, value, oldValue); // emit change record if (!Observer.hasObjectObserve) { return; } var notifier = this._objectNotifier; if (!notifier) { notifier = this._objectNotifier = Object.getNotifier(this); } updateRecord.object = this; updateRecord.name = name; updateRecord.oldValue = oldValue; notifier.notify(updateRecord); }, _propertyChanged: function(name, value, oldValue) { if (this.reflect[name]) { this.reflectPropertyToAttribute(name); } }, // creates a property binding (called via bind) to a published property. bindProperty: function(property, observable, oneTime) { if (oneTime) { this[property] = observable; return; } var computed = this.element.prototype.computed; // Binding an "out-only" value to a computed property. Note that // since this observer isn't opened, it doesn't need to be closed on // cleanup. if (computed && computed[property]) { var privateComputedBoundValue = property + 'ComputedBoundObservable_'; this[privateComputedBoundValue] = observable; return; } return this.bindToAccessor(property, observable, resolveBindingValue); }, // NOTE property `name` must be published. This makes it an accessor. bindToAccessor: function(name, observable, resolveFn) { var privateName = name + '_'; var privateObservable = name + 'Observable_'; // Present for properties which are computed and published and have a // bound value. var privateComputedBoundValue = name + 'ComputedBoundObservable_'; this[privateObservable] = observable; var oldValue = this[privateName]; // observable callback var self = this; function updateValue(value, oldValue) { self[privateName] = value; var setObserveable = self[privateComputedBoundValue]; if (setObserveable && typeof setObserveable.setValue == 'function') { setObserveable.setValue(value); } self.emitPropertyChangeRecord(name, value, oldValue); } // resolve initial value var value = observable.open(updateValue); if (resolveFn && !areSameValue(oldValue, value)) { var resolvedValue = resolveFn(oldValue, value); if (!areSameValue(value, resolvedValue)) { value = resolvedValue; if (observable.setValue) { observable.setValue(value); } } } updateValue(value, oldValue); // register and return observable var observer = { close: function() { observable.close(); self[privateObservable] = undefined; self[privateComputedBoundValue] = undefined; } }; this.registerObserver(observer); return observer; }, createComputedProperties: function() { if (!this._computedNames) { return; } for (var i = 0; i < this._computedNames.length; i++) { var name = this._computedNames[i]; var expressionText = this.computed[name]; try { var expression = PolymerExpressions.getExpression(expressionText); var observable = expression.getBinding(this, this.element.syntax); this.bindToAccessor(name, observable); } catch (ex) { console.error('Failed to create computed property', ex); } } }, // property bookkeeping registerObserver: function(observer) { if (!this._observers) { this._observers = [observer]; return; } this._observers.push(observer); }, closeObservers: function() { if (!this._observers) { return; } // observer array items are arrays of observers. var observers = this._observers; for (var i = 0; i < observers.length; i++) { var observer = observers[i]; if (observer && typeof observer.close == 'function') { observer.close(); } } this._observers = []; }, // bookkeeping observers for memory management registerNamedObserver: function(name, observer) { var o$ = this._namedObservers || (this._namedObservers = {}); o$[name] = observer; }, closeNamedObserver: function(name) { var o$ = this._namedObservers; if (o$ && o$[name]) { o$[name].close(); o$[name] = null; return true; } }, closeNamedObservers: function() { if (this._namedObservers) { for (var i in this._namedObservers) { this.closeNamedObserver(i); } this._namedObservers = {}; } } }; // logging var LOG_OBSERVE = '[%s] watching [%s]'; var LOG_OBSERVED = '[%s#%s] watch: [%s] now [%s] was [%s]'; var LOG_CHANGED = '[%s#%s] propertyChanged: [%s] now [%s] was [%s]'; // exports scope.api.instance.properties = properties; })(Polymer); (function(scope) { /** * @class polymer-base */ // imports var log = window.WebComponents ? WebComponents.flags.log : {}; // element api supporting mdv var mdv = { /** * Creates dom cloned from the given template, instantiating bindings * with this element as the template model and `PolymerExpressions` as the * binding delegate. * * @method instanceTemplate * @param {Template} template source template from which to create dom. */ instanceTemplate: function(template) { // ensure template is decorated (lets' things like <tr template ...> work) HTMLTemplateElement.decorate(template); // ensure a default bindingDelegate var syntax = this.syntax || (!template.bindingDelegate && this.element.syntax); var dom = template.createInstance(this, syntax); var observers = dom.bindings_; for (var i = 0; i < observers.length; i++) { this.registerObserver(observers[i]); } return dom; }, // Called by TemplateBinding/NodeBind to setup a binding to the given // property. It's overridden here to support property bindings // in addition to attribute bindings that are supported by default. bind: function(name, observable, oneTime) { var property = this.propertyForAttribute(name); if (!property) { // TODO(sjmiles): this mixin method must use the special form // of `super` installed by `mixinMethod` in declaration/prototype.js return this.mixinSuper(arguments); } else { // use n-way Polymer binding var observer = this.bindProperty(property, observable, oneTime); // NOTE: reflecting binding information is typically required only for // tooling. It has a performance cost so it's opt-in in Node.bind. if (Platform.enableBindingsReflection && observer) { observer.path = observable.path_; this._recordBinding(property, observer); } if (this.reflect[property]) { this.reflectPropertyToAttribute(property); } return observer; } }, _recordBinding: function(name, observer) { this.bindings_ = this.bindings_ || {}; this.bindings_[name] = observer; }, // Called by TemplateBinding when all bindings on an element have been // executed. This signals that all element inputs have been gathered // and it's safe to ready the element, create shadow-root and start // data-observation. bindFinished: function() { this.makeElementReady(); }, // called at detached time to signal that an element's bindings should be // cleaned up. This is done asynchronously so that users have the chance // to call `cancelUnbindAll` to prevent unbinding. asyncUnbindAll: function() { if (!this._unbound) { log.unbind && console.log('[%s] asyncUnbindAll', this.localName); this._unbindAllJob = this.job(this._unbindAllJob, this.unbindAll, 0); } }, /** * This method should rarely be used and only if * <a href="#cancelUnbindAll">`cancelUnbindAll`</a> has been called to * prevent element unbinding. In this case, the element's bindings will * not be automatically cleaned up and it cannot be garbage collected * by the system. If memory pressure is a concern or a * large amount of elements need to be managed in this way, `unbindAll` * can be called to deactivate the element's bindings and allow its * memory to be reclaimed. * * @method unbindAll */ unbindAll: function() { if (!this._unbound) { this.closeObservers(); this.closeNamedObservers(); this._unbound = true; } }, /** * Call in `detached` to prevent the element from unbinding when it is * detached from the dom. The element is unbound as a cleanup step that * allows its memory to be reclaimed. * If `cancelUnbindAll` is used, consider calling * <a href="#unbindAll">`unbindAll`</a> when the element is no longer * needed. This will allow its memory to be reclaimed. * * @method cancelUnbindAll */ cancelUnbindAll: function() { if (this._unbound) { log.unbind && console.warn('[%s] already unbound, cannot cancel unbindAll', this.localName); return; } log.unbind && console.log('[%s] cancelUnbindAll', this.localName); if (this._unbindAllJob) { this._unbindAllJob = this._unbindAllJob.stop(); } } }; function unbindNodeTree(node) { forNodeTree(node, _nodeUnbindAll); } function _nodeUnbindAll(node) { node.unbindAll(); } function forNodeTree(node, callback) { if (node) { callback(node); for (var child = node.firstChild; child; child = child.nextSibling) { forNodeTree(child, callback); } } } var mustachePattern = /\{\{([^{}]*)}}/; // exports scope.bindPattern = mustachePattern; scope.api.instance.mdv = mdv; })(Polymer); (function(scope) { /** * Common prototype for all Polymer Elements. * * @class polymer-base * @homepage polymer.github.io */ var base = { /** * Tags this object as the canonical Base prototype. * * @property PolymerBase * @type boolean * @default true */ PolymerBase: true, /** * Debounce signals. * * Call `job` to defer a named signal, and all subsequent matching signals, * until a wait time has elapsed with no new signal. * * debouncedClickAction: function(e) { * // processClick only when it's been 100ms since the last click * this.job('click', function() { * this.processClick; * }, 100); * } * * @method job * @param String {String} job A string identifier for the job to debounce. * @param Function {Function} callback A function that is called (with `this` context) when the wait time elapses. * @param Number {Number} wait Time in milliseconds (ms) after the last signal that must elapse before invoking `callback` * @type Handle */ job: function(job, callback, wait) { if (typeof job === 'string') { var n = '___' + job; this[n] = Polymer.job.call(this, this[n], callback, wait); } else { // TODO(sjmiles): suggest we deprecate this call signature return Polymer.job.call(this, job, callback, wait); } }, /** * Invoke a superclass method. * * Use `super()` to invoke the most recently overridden call to the * currently executing function. * * To pass arguments through, use the literal `arguments` as the parameter * to `super()`. * * nextPageAction: function(e) { * // invoke the superclass version of `nextPageAction` * this.super(arguments); * } * * To pass custom arguments, arrange them in an array. * * appendSerialNo: function(value, serial) { * // prefix the superclass serial number with our lot # before * // invoking the superlcass * return this.super([value, this.lotNo + serial]) * } * * @method super * @type Any * @param {args) An array of arguments to use when calling the superclass method, or null. */ super: Polymer.super, /** * Lifecycle method called when the element is instantiated. * * Override `created` to perform custom create-time tasks. No need to call * super-class `created` unless you are extending another Polymer element. * Created is called before the element creates `shadowRoot` or prepares * data-observation. * * @method created * @type void */ created: function() { }, /** * Lifecycle method called when the element has populated it's `shadowRoot`, * prepared data-observation, and made itself ready for API interaction. * * @method ready * @type void */ ready: function() { }, /** * Low-level lifecycle method called as part of standard Custom Elements * operation. Polymer implements this method to provide basic default * functionality. For custom create-time tasks, implement `created` * instead, which is called immediately after `createdCallback`. * * @method createdCallback */ createdCallback: function() { if (this.templateInstance && this.templateInstance.model) { console.warn('Attributes on ' + this.localName + ' were data bound ' + 'prior to Polymer upgrading the element. This may result in ' + 'incorrect binding types.'); } this.created(); this.prepareElement(); if (!this.ownerDocument.isStagingDocument) { this.makeElementReady(); } }, // system entry point, do not override prepareElement: function() { if (this._elementPrepared) { console.warn('Element already prepared', this.localName); return; } this._elementPrepared = true; // storage for shadowRoots info this.shadowRoots = {}; // install property observers this.createPropertyObserver(); this.openPropertyObserver(); // install boilerplate attributes this.copyInstanceAttributes(); // process input attributes this.takeAttributes(); // add event listeners this.addHostListeners(); }, // system entry point, do not override makeElementReady: function() { if (this._readied) { return; } this._readied = true; this.createComputedProperties(); this.parseDeclarations(this.__proto__); // NOTE: Support use of the `unresolved` attribute to help polyfill // custom elements' `:unresolved` feature. this.removeAttribute('unresolved'); // user entry point this.ready(); }, /** * Low-level lifecycle method called as part of standard Custom Elements * operation. Polymer implements this method to provide basic default * functionality. For custom tasks in your element, implement `attributeChanged` * instead, which is called immediately after `attributeChangedCallback`. * * @method attributeChangedCallback */ attributeChangedCallback: function(name, oldValue) { // TODO(sjmiles): adhoc filter if (name !== 'class' && name !== 'style') { this.attributeToProperty(name, this.getAttribute(name)); } if (this.attributeChanged) { this.attributeChanged.apply(this, arguments); } }, /** * Low-level lifecycle method called as part of standard Custom Elements * operation. Polymer implements this method to provide basic default * functionality. For custom create-time tasks, implement `attached` * instead, which is called immediately after `attachedCallback`. * * @method attachedCallback */ attachedCallback: function() { // when the element is attached, prevent it from unbinding. this.cancelUnbindAll(); // invoke user action if (this.attached) { this.attached(); } if (!this.hasBeenAttached) { this.hasBeenAttached = true; if (this.domReady) { this.async('domReady'); } } }, /** * Implement to access custom elements in dom descendants, ancestors, * or siblings. Because custom elements upgrade in document order, * elements accessed in `ready` or `attached` may not be upgraded. When * `domReady` is called, all registered custom elements are guaranteed * to have been upgraded. * * @method domReady */ /** * Low-level lifecycle method called as part of standard Custom Elements * operation. Polymer implements this method to provide basic default * functionality. For custom create-time tasks, implement `detached` * instead, which is called immediately after `detachedCallback`. * * @method detachedCallback */ detachedCallback: function() { if (!this.preventDispose) { this.asyncUnbindAll(); } // invoke user action if (this.detached) { this.detached(); } // TODO(sorvell): bc if (this.leftView) { this.leftView(); } }, /** * Walks the prototype-chain of this element and allows specific * classes a chance to process static declarations. * * In particular, each polymer-element has it's own `template`. * `parseDeclarations` is used to accumulate all element `template`s * from an inheritance chain. * * `parseDeclaration` static methods implemented in the chain are called * recursively, oldest first, with the `<polymer-element>` associated * with the current prototype passed as an argument. * * An element may override this method to customize shadow-root generation. * * @method parseDeclarations */ parseDeclarations: function(p) { if (p && p.element) { this.parseDeclarations(p.__proto__); p.parseDeclaration.call(this, p.element); } }, /** * Perform init-time actions based on static information in the * `<polymer-element>` instance argument. * * For example, the standard implementation locates the template associated * with the given `<polymer-element>` and stamps it into a shadow-root to * implement shadow inheritance. * * An element may override this method for custom behavior. * * @method parseDeclaration */ parseDeclaration: function(elementElement) { var template = this.fetchTemplate(elementElement); if (template) { var root = this.shadowFromTemplate(template); this.shadowRoots[elementElement.name] = root; } }, /** * Given a `<polymer-element>`, find an associated template (if any) to be * used for shadow-root generation. * * An element may override this method for custom behavior. * * @method fetchTemplate */ fetchTemplate: function(elementElement) { return elementElement.querySelector('template'); }, /** * Create a shadow-root in this host and stamp `template` as it's * content. * * An element may override this method for custom behavior. * * @method shadowFromTemplate */ shadowFromTemplate: function(template) { if (template) { // make a shadow root var root = this.createShadowRoot(); // stamp template // which includes parsing and applying MDV bindings before being // inserted (to avoid {{}} in attribute values). var dom = this.instanceTemplate(template); // append to shadow dom root.appendChild(dom); // perform post-construction initialization tasks on shadow root this.shadowRootReady(root, template); // return the created shadow root return root; } }, // utility function that stamps a <template> into light-dom lightFromTemplate: function(template, refNode) { if (template) { // TODO(sorvell): mark this element as an eventController so that // event listeners on bound nodes inside it will be called on it. // Note, the expectation here is that events on all descendants // should be handled by this element. this.eventController = this; // stamp template // which includes parsing and applying MDV bindings before being // inserted (to avoid {{}} in attribute values). var dom = this.instanceTemplate(template); // append to shadow dom if (refNode) { this.insertBefore(dom, refNode); } else { this.appendChild(dom); } // perform post-construction initialization tasks on ahem, light root this.shadowRootReady(this); // return the created shadow root return dom; } }, shadowRootReady: function(root) { // locate nodes with id and store references to them in this.$ hash this.marshalNodeReferences(root); }, // locate nodes with id and store references to them in this.$ hash marshalNodeReferences: function(root) { // establish $ instance variable var $ = this.$ = this.$ || {}; // populate $ from nodes with ID from the LOCAL tree if (root) { var n$ = root.querySelectorAll("[id]"); for (var i=0, l=n$.length, n; (i<l) && (n=n$[i]); i++) { $[n.id] = n; }; } }, /** * Register a one-time callback when a child-list or sub-tree mutation * occurs on node. * * For persistent callbacks, call onMutation from your listener. * * @method onMutation * @param Node {Node} node Node to watch for mutations. * @param Function {Function} listener Function to call on mutation. The function is invoked as `listener.call(this, observer, mutations);` where `observer` is the MutationObserver that triggered the notification, and `mutations` is the native mutation list. */ onMutation: function(node, listener) { var observer = new MutationObserver(function(mutations) { listener.call(this, observer, mutations); observer.disconnect(); }.bind(this)); observer.observe(node, {childList: true, subtree: true}); } }; /** * @class Polymer */ /** * Returns true if the object includes <a href="#polymer-base">polymer-base</a> in it's prototype chain. * * @method isBase * @param Object {Object} object Object to test. * @type Boolean */ function isBase(object) { return object.hasOwnProperty('PolymerBase') } // name a base constructor for dev tools /** * The Polymer base-class constructor. * * @property Base * @type Function */ function PolymerBase() {}; PolymerBase.prototype = base; base.constructor = PolymerBase; // exports scope.Base = PolymerBase; scope.isBase = isBase; scope.api.instance.base = base; })(Polymer); (function(scope) { // imports var log = window.WebComponents ? WebComponents.flags.log : {}; var hasShadowDOMPolyfill = window.ShadowDOMPolyfill; // magic words var STYLE_SCOPE_ATTRIBUTE = 'element'; var STYLE_CONTROLLER_SCOPE = 'controller'; var styles = { STYLE_SCOPE_ATTRIBUTE: STYLE_SCOPE_ATTRIBUTE, /** * Installs external stylesheets and <style> elements with the attribute * polymer-scope='controller' into the scope of element. This is intended * to be a called during custom element construction. */ installControllerStyles: function() { // apply controller styles, but only if they are not yet applied var scope = this.findStyleScope(); if (scope && !this.scopeHasNamedStyle(scope, this.localName)) { // allow inherited controller styles var proto = getPrototypeOf(this), cssText = ''; while (proto && proto.element) { cssText += proto.element.cssTextForScope(STYLE_CONTROLLER_SCOPE); proto = getPrototypeOf(proto); } if (cssText) { this.installScopeCssText(cssText, scope); } } }, installScopeStyle: function(style, name, scope) { var scope = scope || this.findStyleScope(), name = name || ''; if (scope && !this.scopeHasNamedStyle(scope, this.localName + name)) { var cssText = ''; if (style instanceof Array) { for (var i=0, l=style.length, s; (i<l) && (s=style[i]); i++) { cssText += s.textContent + '\n\n'; } } else { cssText = style.textContent; } this.installScopeCssText(cssText, scope, name); } }, installScopeCssText: function(cssText, scope, name) { scope = scope || this.findStyleScope(); name = name || ''; if (!scope) { return; } if (hasShadowDOMPolyfill) { cssText = shimCssText(cssText, scope.host); } var style = this.element.cssTextToScopeStyle(cssText, STYLE_CONTROLLER_SCOPE); Polymer.applyStyleToScope(style, scope); // cache that this style has been applied this.styleCacheForScope(scope)[this.localName + name] = true; }, findStyleScope: function(node) { // find the shadow root that contains this element var n = node || this; while (n.parentNode) { n = n.parentNode; } return n; }, scopeHasNamedStyle: function(scope, name) { var cache = this.styleCacheForScope(scope); return cache[name]; }, styleCacheForScope: function(scope) { if (hasShadowDOMPolyfill) { var scopeName = scope.host ? scope.host.localName : scope.localName; return polyfillScopeStyleCache[scopeName] || (polyfillScopeStyleCache[scopeName] = {}); } else { return scope._scopeStyles = (scope._scopeStyles || {}); } } }; var polyfillScopeStyleCache = {}; // NOTE: use raw prototype traversal so that we ensure correct traversal // on platforms where the protoype chain is simulated via __proto__ (IE10) function getPrototypeOf(prototype) { return prototype.__proto__; } function shimCssText(cssText, host) { var name = '', is = false; if (host) { name = host.localName; is = host.hasAttribute('is'); } var selector = WebComponents.ShadowCSS.makeScopeSelector(name, is); return WebComponents.ShadowCSS.shimCssText(cssText, selector); } // exports scope.api.instance.styles = styles; })(Polymer); (function(scope) { // imports var extend = scope.extend; var api = scope.api; // imperative implementation: Polymer() // specify an 'own' prototype for tag `name` function element(name, prototype) { if (typeof name !== 'string') { var script = prototype || document._currentScript; prototype = name; name = script && script.parentNode && script.parentNode.getAttribute ? script.parentNode.getAttribute('name') : ''; if (!name) { throw 'Element name could not be inferred.'; } } if (getRegisteredPrototype(name)) { throw 'Already registered (Polymer) prototype for element ' + name; } // cache the prototype registerPrototype(name, prototype); // notify the registrar waiting for 'name', if any notifyPrototype(name); } // async prototype source function waitingForPrototype(name, client) { waitPrototype[name] = client; } var waitPrototype = {}; function notifyPrototype(name) { if (waitPrototype[name]) { waitPrototype[name].registerWhenReady(); delete waitPrototype[name]; } } // utility and bookkeeping // maps tag names to prototypes, as registered with // Polymer. Prototypes associated with a tag name // using document.registerElement are available from // HTMLElement.getPrototypeForTag(). // If an element was fully registered by Polymer, then // Polymer.getRegisteredPrototype(name) === // HTMLElement.getPrototypeForTag(name) var prototypesByName = {}; function registerPrototype(name, prototype) { return prototypesByName[name] = prototype || {}; } function getRegisteredPrototype(name) { return prototypesByName[name]; } function instanceOfType(element, type) { if (typeof type !== 'string') { return false; } var proto = HTMLElement.getPrototypeForTag(type); var ctor = proto && proto.constructor; if (!ctor) { return false; } if (CustomElements.instanceof) { return CustomElements.instanceof(element, ctor); } return element instanceof ctor; } // exports scope.getRegisteredPrototype = getRegisteredPrototype; scope.waitingForPrototype = waitingForPrototype; scope.instanceOfType = instanceOfType; // namespace shenanigans so we can expose our scope on the registration // function // make window.Polymer reference `element()` window.Polymer = element; // TODO(sjmiles): find a way to do this that is less terrible // copy window.Polymer properties onto `element()` extend(Polymer, scope); // Under the HTMLImports polyfill, scripts in the main document // do not block on imports; we want to allow calls to Polymer in the main // document. WebComponents collects those calls until we can process them, which // we do here. if (WebComponents.consumeDeclarations) { WebComponents.consumeDeclarations(function(declarations) { if (declarations) { for (var i=0, l=declarations.length, d; (i<l) && (d=declarations[i]); i++) { element.apply(null, d); } } }); } })(Polymer); (function(scope) { /** * @class polymer-base */ /** * Resolve a url path to be relative to a `base` url. If unspecified, `base` * defaults to the element's ownerDocument url. Can be used to resolve * paths from element's in templates loaded in HTMLImports to be relative * to the document containing the element. Polymer automatically does this for * url attributes in element templates; however, if a url, for * example, contains a binding, then `resolvePath` can be used to ensure it is * relative to the element document. For example, in an element's template, * * <a href="{{resolvePath(path)}}">Resolved</a> * * @method resolvePath * @param {String} url Url path to resolve. * @param {String} base Optional base url against which to resolve, defaults * to the element's ownerDocument url. * returns {String} resolved url. */ var path = { resolveElementPaths: function(node) { Polymer.urlResolver.resolveDom(node); }, addResolvePathApi: function() { // let assetpath attribute modify the resolve path var assetPath = this.getAttribute('assetpath') || ''; var root = new URL(assetPath, this.ownerDocument.baseURI); this.prototype.resolvePath = function(urlPath, base) { var u = new URL(urlPath, base || root); return u.href; }; } }; // exports scope.api.declaration.path = path; })(Polymer); (function(scope) { // imports var log = window.WebComponents ? WebComponents.flags.log : {}; var api = scope.api.instance.styles; var STYLE_SCOPE_ATTRIBUTE = api.STYLE_SCOPE_ATTRIBUTE; var hasShadowDOMPolyfill = window.ShadowDOMPolyfill; // magic words var STYLE_SELECTOR = 'style'; var STYLE_LOADABLE_MATCH = '@import'; var SHEET_SELECTOR = 'link[rel=stylesheet]'; var STYLE_GLOBAL_SCOPE = 'global'; var SCOPE_ATTR = 'polymer-scope'; var styles = { // returns true if resources are loading loadStyles: function(callback) { var template = this.fetchTemplate(); var content = template && this.templateContent(); if (content) { this.convertSheetsToStyles(content); var styles = this.findLoadableStyles(content); if (styles.length) { var templateUrl = template.ownerDocument.baseURI; return Polymer.styleResolver.loadStyles(styles, templateUrl, callback); } } if (callback) { callback(); } }, convertSheetsToStyles: function(root) { var s$ = root.querySelectorAll(SHEET_SELECTOR); for (var i=0, l=s$.length, s, c; (i<l) && (s=s$[i]); i++) { c = createStyleElement(importRuleForSheet(s, this.ownerDocument.baseURI), this.ownerDocument); this.copySheetAttributes(c, s); s.parentNode.replaceChild(c, s); } }, copySheetAttributes: function(style, link) { for (var i=0, a$=link.attributes, l=a$.length, a; (a=a$[i]) && i<l; i++) { if (a.name !== 'rel' && a.name !== 'href') { style.setAttribute(a.name, a.value); } } }, findLoadableStyles: function(root) { var loadables = []; if (root) { var s$ = root.querySelectorAll(STYLE_SELECTOR); for (var i=0, l=s$.length, s; (i<l) && (s=s$[i]); i++) { if (s.textContent.match(STYLE_LOADABLE_MATCH)) { loadables.push(s); } } } return loadables; }, /** * Install external stylesheets loaded in <polymer-element> elements into the * element's template. * @param elementElement The <element> element to style. */ installSheets: function() { this.cacheSheets(); this.cacheStyles(); this.installLocalSheets(); this.installGlobalStyles(); }, /** * Remove all sheets from element and store for later use. */ cacheSheets: function() { this.sheets = this.findNodes(SHEET_SELECTOR); this.sheets.forEach(function(s) { if (s.parentNode) { s.parentNode.removeChild(s); } }); }, cacheStyles: function() { this.styles = this.findNodes(STYLE_SELECTOR + '[' + SCOPE_ATTR + ']'); this.styles.forEach(function(s) { if (s.parentNode) { s.parentNode.removeChild(s); } }); }, /** * Takes external stylesheets loaded in an <element> element and moves * their content into a <style> element inside the <element>'s template. * The sheet is then removed from the <element>. This is done only so * that if the element is loaded in the main document, the sheet does * not become active. * Note, ignores sheets with the attribute 'polymer-scope'. * @param elementElement The <element> element to style. */ installLocalSheets: function () { var sheets = this.sheets.filter(function(s) { return !s.hasAttribute(SCOPE_ATTR); }); var content = this.templateContent(); if (content) { var cssText = ''; sheets.forEach(function(sheet) { cssText += cssTextFromSheet(sheet) + '\n'; }); if (cssText) { var style = createStyleElement(cssText, this.ownerDocument); content.insertBefore(style, content.firstChild); } } }, findNodes: function(selector, matcher) { var nodes = this.querySelectorAll(selector).array(); var content = this.templateContent(); if (content) { var templateNodes = content.querySelectorAll(selector).array(); nodes = nodes.concat(templateNodes); } return matcher ? nodes.filter(matcher) : nodes; }, /** * Promotes external stylesheets and <style> elements with the attribute * polymer-scope='global' into global scope. * This is particularly useful for defining @keyframe rules which * currently do not function in scoped or shadow style elements. * (See wkb.ug/72462) * @param elementElement The <element> element to style. */ // TODO(sorvell): remove when wkb.ug/72462 is addressed. installGlobalStyles: function() { var style = this.styleForScope(STYLE_GLOBAL_SCOPE); applyStyleToScope(style, document.head); }, cssTextForScope: function(scopeDescriptor) { var cssText = ''; // handle stylesheets var selector = '[' + SCOPE_ATTR + '=' + scopeDescriptor + ']'; var matcher = function(s) { return matchesSelector(s, selector); }; var sheets = this.sheets.filter(matcher); sheets.forEach(function(sheet) { cssText += cssTextFromSheet(sheet) + '\n\n'; }); // handle cached style elements var styles = this.styles.filter(matcher); styles.forEach(function(style) { cssText += style.textContent + '\n\n'; }); return cssText; }, styleForScope: function(scopeDescriptor) { var cssText = this.cssTextForScope(scopeDescriptor); return this.cssTextToScopeStyle(cssText, scopeDescriptor); }, cssTextToScopeStyle: function(cssText, scopeDescriptor) { if (cssText) { var style = createStyleElement(cssText); style.setAttribute(STYLE_SCOPE_ATTRIBUTE, this.getAttribute('name') + '-' + scopeDescriptor); return style; } } }; function importRuleForSheet(sheet, baseUrl) { var href = new URL(sheet.getAttribute('href'), baseUrl).href; return '@import \'' + href + '\';'; } function applyStyleToScope(style, scope) { if (style) { if (scope === document) { scope = document.head; } if (hasShadowDOMPolyfill) { scope = document.head; } // TODO(sorvell): necessary for IE // see https://connect.microsoft.com/IE/feedback/details/790212/ // cloning-a-style-element-and-adding-to-document-produces // -unexpected-result#details // var clone = style.cloneNode(true); var clone = createStyleElement(style.textContent); var attr = style.getAttribute(STYLE_SCOPE_ATTRIBUTE); if (attr) { clone.setAttribute(STYLE_SCOPE_ATTRIBUTE, attr); } // TODO(sorvell): probably too brittle; try to figure out // where to put the element. var refNode = scope.firstElementChild; if (scope === document.head) { var selector = 'style[' + STYLE_SCOPE_ATTRIBUTE + ']'; var s$ = document.head.querySelectorAll(selector); if (s$.length) { refNode = s$[s$.length-1].nextElementSibling; } } scope.insertBefore(clone, refNode); } } function createStyleElement(cssText, scope) { scope = scope || document; scope = scope.createElement ? scope : scope.ownerDocument; var style = scope.createElement('style'); style.textContent = cssText; return style; } function cssTextFromSheet(sheet) { return (sheet && sheet.__resource) || ''; } function matchesSelector(node, inSelector) { if (matches) { return matches.call(node, inSelector); } } var p = HTMLElement.prototype; var matches = p.matches || p.matchesSelector || p.webkitMatchesSelector || p.mozMatchesSelector; // exports scope.api.declaration.styles = styles; scope.applyStyleToScope = applyStyleToScope; })(Polymer); (function(scope) { // imports var log = window.WebComponents ? WebComponents.flags.log : {}; var api = scope.api.instance.events; var EVENT_PREFIX = api.EVENT_PREFIX; var mixedCaseEventTypes = {}; [ 'webkitAnimationStart', 'webkitAnimationEnd', 'webkitTransitionEnd', 'DOMFocusOut', 'DOMFocusIn', 'DOMMouseScroll' ].forEach(function(e) { mixedCaseEventTypes[e.toLowerCase()] = e; }); // polymer-element declarative api: events feature var events = { parseHostEvents: function() { // our delegates map var delegates = this.prototype.eventDelegates; // extract data from attributes into delegates this.addAttributeDelegates(delegates); }, addAttributeDelegates: function(delegates) { // for each attribute for (var i=0, a; a=this.attributes[i]; i++) { // does it have magic marker identifying it as an event delegate? if (this.hasEventPrefix(a.name)) { // if so, add the info to delegates delegates[this.removeEventPrefix(a.name)] = a.value.replace('{{', '') .replace('}}', '').trim(); } } }, // starts with 'on-' hasEventPrefix: function (n) { return n && (n[0] === 'o') && (n[1] === 'n') && (n[2] === '-'); }, removeEventPrefix: function(n) { return n.slice(prefixLength); }, findController: function(node) { while (node.parentNode) { if (node.eventController) { return node.eventController; } node = node.parentNode; } return node.host; }, getEventHandler: function(controller, target, method) { var events = this; return function(e) { if (!controller || !controller.PolymerBase) { controller = events.findController(target); } var args = [e, e.detail, e.currentTarget]; controller.dispatchMethod(controller, method, args); }; }, prepareEventBinding: function(pathString, name, node) { if (!this.hasEventPrefix(name)) return; var eventType = this.removeEventPrefix(name); eventType = mixedCaseEventTypes[eventType] || eventType; var events = this; return function(model, node, oneTime) { var handler = events.getEventHandler(undefined, node, pathString); PolymerGestures.addEventListener(node, eventType, handler); if (oneTime) return; // TODO(rafaelw): This is really pointless work. Aside from the cost // of these allocations, NodeBind is going to setAttribute back to its // current value. Fixing this would mean changing the TemplateBinding // binding delegate API. function bindingValue() { return '{{ ' + pathString + ' }}'; } return { open: bindingValue, discardChanges: bindingValue, close: function() { PolymerGestures.removeEventListener(node, eventType, handler); } }; }; } }; var prefixLength = EVENT_PREFIX.length; // exports scope.api.declaration.events = events; })(Polymer); (function(scope) { // element api var observationBlacklist = ['attribute']; var properties = { inferObservers: function(prototype) { // called before prototype.observe is chained to inherited object var observe = prototype.observe, property; for (var n in prototype) { if (n.slice(-7) === 'Changed') { property = n.slice(0, -7); if (this.canObserveProperty(property)) { if (!observe) { observe = (prototype.observe = {}); } observe[property] = observe[property] || n; } } } }, canObserveProperty: function(property) { return (observationBlacklist.indexOf(property) < 0); }, explodeObservers: function(prototype) { // called before prototype.observe is chained to inherited object var o = prototype.observe; if (o) { var exploded = {}; for (var n in o) { var names = n.split(' '); for (var i=0, ni; ni=names[i]; i++) { exploded[ni] = o[n]; } } prototype.observe = exploded; } }, optimizePropertyMaps: function(prototype) { if (prototype.observe) { // construct name list var a = prototype._observeNames = []; for (var n in prototype.observe) { var names = n.split(' '); for (var i=0, ni; ni=names[i]; i++) { a.push(ni); } } } if (prototype.publish) { // construct name list var a = prototype._publishNames = []; for (var n in prototype.publish) { a.push(n); } } if (prototype.computed) { // construct name list var a = prototype._computedNames = []; for (var n in prototype.computed) { a.push(n); } } }, publishProperties: function(prototype, base) { // if we have any properties to publish var publish = prototype.publish; if (publish) { // transcribe `publish` entries onto own prototype this.requireProperties(publish, prototype, base); // warn and remove accessor names that are broken on some browsers this.filterInvalidAccessorNames(publish); // construct map of lower-cased property names prototype._publishLC = this.lowerCaseMap(publish); } var computed = prototype.computed; if (computed) { // warn and remove accessor names that are broken on some browsers this.filterInvalidAccessorNames(computed); } }, // Publishing/computing a property where the name might conflict with a // browser property is not currently supported to help users of Polymer // avoid browser bugs: // // https://code.google.com/p/chromium/issues/detail?id=43394 // https://bugs.webkit.org/show_bug.cgi?id=49739 // // We can lift this restriction when those bugs are fixed. filterInvalidAccessorNames: function(propertyNames) { for (var name in propertyNames) { // Check if the name is in our blacklist. if (this.propertyNameBlacklist[name]) { console.warn('Cannot define property "' + name + '" for element "' + this.name + '" because it has the same name as an HTMLElement ' + 'property, and not all browsers support overriding that. ' + 'Consider giving it a different name.'); // Remove the invalid accessor from the list. delete propertyNames[name]; } } }, // // `name: value` entries in the `publish` object may need to generate // matching properties on the prototype. // // Values that are objects may have a `reflect` property, which // signals that the value describes property control metadata. // In metadata objects, the prototype default value (if any) // is encoded in the `value` property. // // publish: { // foo: 5, // bar: {value: true, reflect: true}, // zot: {} // } // // `reflect` metadata property controls whether changes to the property // are reflected back to the attribute (default false). // // A value is stored on the prototype unless it's === `undefined`, // in which case the base chain is checked for a value. // If the basal value is also undefined, `null` is stored on the prototype. // // The reflection data is stored on another prototype object, `reflect` // which also can be specified directly. // // reflect: { // foo: true // } // requireProperties: function(propertyInfos, prototype, base) { // per-prototype storage for reflected properties prototype.reflect = prototype.reflect || {}; // ensure a prototype value for each property // and update the property's reflect to attribute status for (var n in propertyInfos) { var value = propertyInfos[n]; // value has metadata if it has a `reflect` property if (value && value.reflect !== undefined) { prototype.reflect[n] = Boolean(value.reflect); value = value.value; } // only set a value if one is specified if (value !== undefined) { prototype[n] = value; } } }, lowerCaseMap: function(properties) { var map = {}; for (var n in properties) { map[n.toLowerCase()] = n; } return map; }, createPropertyAccessor: function(name, ignoreWrites) { var proto = this.prototype; var privateName = name + '_'; var privateObservable = name + 'Observable_'; proto[privateName] = proto[name]; Object.defineProperty(proto, name, { get: function() { var observable = this[privateObservable]; if (observable) observable.deliver(); return this[privateName]; }, set: function(value) { if (ignoreWrites) { return this[privateName]; } var observable = this[privateObservable]; if (observable) { observable.setValue(value); return; } var oldValue = this[privateName]; this[privateName] = value; this.emitPropertyChangeRecord(name, value, oldValue); return value; }, configurable: true }); }, createPropertyAccessors: function(prototype) { var n$ = prototype._computedNames; if (n$ && n$.length) { for (var i=0, l=n$.length, n, fn; (i<l) && (n=n$[i]); i++) { this.createPropertyAccessor(n, true); } } var n$ = prototype._publishNames; if (n$ && n$.length) { for (var i=0, l=n$.length, n, fn; (i<l) && (n=n$[i]); i++) { // If the property is computed and published, the accessor is created // above. if (!prototype.computed || !prototype.computed[n]) { this.createPropertyAccessor(n); } } } }, // This list contains some property names that people commonly want to use, // but won't work because of Chrome/Safari bugs. It isn't an exhaustive // list. In particular it doesn't contain any property names found on // subtypes of HTMLElement (e.g. name, value). Rather it attempts to catch // some common cases. propertyNameBlacklist: { children: 1, 'class': 1, id: 1, hidden: 1, style: 1, title: 1, } }; // exports scope.api.declaration.properties = properties; })(Polymer); (function(scope) { // magic words var ATTRIBUTES_ATTRIBUTE = 'attributes'; var ATTRIBUTES_REGEX = /\s|,/; // attributes api var attributes = { inheritAttributesObjects: function(prototype) { // chain our lower-cased publish map to the inherited version this.inheritObject(prototype, 'publishLC'); // chain our instance attributes map to the inherited version this.inheritObject(prototype, '_instanceAttributes'); }, publishAttributes: function(prototype, base) { // merge names from 'attributes' attribute into the 'publish' object var attributes = this.getAttribute(ATTRIBUTES_ATTRIBUTE); if (attributes) { // create a `publish` object if needed. // the `publish` object is only relevant to this prototype, the // publishing logic in `declaration/properties.js` is responsible for // managing property values on the prototype chain. // TODO(sjmiles): the `publish` object is later chained to it's // ancestor object, presumably this is only for // reflection or other non-library uses. var publish = prototype.publish || (prototype.publish = {}); // names='a b c' or names='a,b,c' var names = attributes.split(ATTRIBUTES_REGEX); // record each name for publishing for (var i=0, l=names.length, n; i<l; i++) { // remove excess ws n = names[i].trim(); // looks weird, but causes n to exist on `publish` if it does not; // a more careful test would need expensive `in` operator if (n && publish[n] === undefined) { publish[n] = undefined; } } } }, // record clonable attributes from <element> accumulateInstanceAttributes: function() { // inherit instance attributes var clonable = this.prototype._instanceAttributes; // merge attributes from element var a$ = this.attributes; for (var i=0, l=a$.length, a; (i<l) && (a=a$[i]); i++) { if (this.isInstanceAttribute(a.name)) { clonable[a.name] = a.value; } } }, isInstanceAttribute: function(name) { return !this.blackList[name] && name.slice(0,3) !== 'on-'; }, // do not clone these attributes onto instances blackList: { name: 1, 'extends': 1, constructor: 1, noscript: 1, assetpath: 1, 'cache-csstext': 1 } }; // add ATTRIBUTES_ATTRIBUTE to the blacklist attributes.blackList[ATTRIBUTES_ATTRIBUTE] = 1; // exports scope.api.declaration.attributes = attributes; })(Polymer); (function(scope) { // imports var events = scope.api.declaration.events; var syntax = new PolymerExpressions(); var prepareBinding = syntax.prepareBinding; // Polymer takes a first crack at the binding to see if it's a declarative // event handler. syntax.prepareBinding = function(pathString, name, node) { return events.prepareEventBinding(pathString, name, node) || prepareBinding.call(syntax, pathString, name, node); }; // declaration api supporting mdv var mdv = { syntax: syntax, fetchTemplate: function() { return this.querySelector('template'); }, templateContent: function() { var template = this.fetchTemplate(); return template && template.content; }, installBindingDelegate: function(template) { if (template) { template.bindingDelegate = this.syntax; } } }; // exports scope.api.declaration.mdv = mdv; })(Polymer); (function(scope) { // imports var api = scope.api; var isBase = scope.isBase; var extend = scope.extend; var hasShadowDOMPolyfill = window.ShadowDOMPolyfill; // prototype api var prototype = { register: function(name, extendeeName) { // build prototype combining extendee, Polymer base, and named api this.buildPrototype(name, extendeeName); // register our custom element with the platform this.registerPrototype(name, extendeeName); // reference constructor in a global named by 'constructor' attribute this.publishConstructor(); }, buildPrototype: function(name, extendeeName) { // get our custom prototype (before chaining) var extension = scope.getRegisteredPrototype(name); // get basal prototype var base = this.generateBasePrototype(extendeeName); // implement declarative features this.desugarBeforeChaining(extension, base); // join prototypes this.prototype = this.chainPrototypes(extension, base); // more declarative features this.desugarAfterChaining(name, extendeeName); }, desugarBeforeChaining: function(prototype, base) { // back reference declaration element // TODO(sjmiles): replace `element` with `elementElement` or `declaration` prototype.element = this; // transcribe `attributes` declarations onto own prototype's `publish` this.publishAttributes(prototype, base); // `publish` properties to the prototype and to attribute watch this.publishProperties(prototype, base); // infer observers for `observe` list based on method names this.inferObservers(prototype); // desugar compound observer syntax, e.g. 'a b c' this.explodeObservers(prototype); }, chainPrototypes: function(prototype, base) { // chain various meta-data objects to inherited versions this.inheritMetaData(prototype, base); // chain custom api to inherited var chained = this.chainObject(prototype, base); // x-platform fixup ensurePrototypeTraversal(chained); return chained; }, inheritMetaData: function(prototype, base) { // chain observe object to inherited this.inheritObject('observe', prototype, base); // chain publish object to inherited this.inheritObject('publish', prototype, base); // chain reflect object to inherited this.inheritObject('reflect', prototype, base); // chain our lower-cased publish map to the inherited version this.inheritObject('_publishLC', prototype, base); // chain our instance attributes map to the inherited version this.inheritObject('_instanceAttributes', prototype, base); // chain our event delegates map to the inherited version this.inheritObject('eventDelegates', prototype, base); }, // implement various declarative features desugarAfterChaining: function(name, extendee) { // build side-chained lists to optimize iterations this.optimizePropertyMaps(this.prototype); this.createPropertyAccessors(this.prototype); // install mdv delegate on template this.installBindingDelegate(this.fetchTemplate()); // install external stylesheets as if they are inline this.installSheets(); // adjust any paths in dom from imports this.resolveElementPaths(this); // compile list of attributes to copy to instances this.accumulateInstanceAttributes(); // parse on-* delegates declared on `this` element this.parseHostEvents(); // // install a helper method this.resolvePath to aid in // setting resource urls. e.g. // this.$.image.src = this.resolvePath('images/foo.png') this.addResolvePathApi(); // under ShadowDOMPolyfill, transforms to approximate missing CSS features if (hasShadowDOMPolyfill) { WebComponents.ShadowCSS.shimStyling(this.templateContent(), name, extendee); } // allow custom element access to the declarative context if (this.prototype.registerCallback) { this.prototype.registerCallback(this); } }, // if a named constructor is requested in element, map a reference // to the constructor to the given symbol publishConstructor: function() { var symbol = this.getAttribute('constructor'); if (symbol) { window[symbol] = this.ctor; } }, // build prototype combining extendee, Polymer base, and named api generateBasePrototype: function(extnds) { var prototype = this.findBasePrototype(extnds); if (!prototype) { // create a prototype based on tag-name extension var prototype = HTMLElement.getPrototypeForTag(extnds); // insert base api in inheritance chain (if needed) prototype = this.ensureBaseApi(prototype); // memoize this base memoizedBases[extnds] = prototype; } return prototype; }, findBasePrototype: function(name) { return memoizedBases[name]; }, // install Polymer instance api into prototype chain, as needed ensureBaseApi: function(prototype) { if (prototype.PolymerBase) { return prototype; } var extended = Object.create(prototype); // we need a unique copy of base api for each base prototype // therefore we 'extend' here instead of simply chaining api.publish(api.instance, extended); // TODO(sjmiles): sharing methods across prototype chains is // not supported by 'super' implementation which optimizes // by memoizing prototype relationships. // Probably we should have a version of 'extend' that is // share-aware: it could study the text of each function, // look for usage of 'super', and wrap those functions in // closures. // As of now, there is only one problematic method, so // we just patch it manually. // To avoid re-entrancy problems, the special super method // installed is called `mixinSuper` and the mixin method // must use this method instead of the default `super`. this.mixinMethod(extended, prototype, api.instance.mdv, 'bind'); // return buffed-up prototype return extended; }, mixinMethod: function(extended, prototype, api, name) { var $super = function(args) { return prototype[name].apply(this, args); }; extended[name] = function() { this.mixinSuper = $super; return api[name].apply(this, arguments); } }, // ensure prototype[name] inherits from a prototype.prototype[name] inheritObject: function(name, prototype, base) { // require an object var source = prototype[name] || {}; // chain inherited properties onto a new object prototype[name] = this.chainObject(source, base[name]); }, // register 'prototype' to custom element 'name', store constructor registerPrototype: function(name, extendee) { var info = { prototype: this.prototype } // native element must be specified in extends var typeExtension = this.findTypeExtension(extendee); if (typeExtension) { info.extends = typeExtension; } // register the prototype with HTMLElement for name lookup HTMLElement.register(name, this.prototype); // register the custom type this.ctor = document.registerElement(name, info); }, findTypeExtension: function(name) { if (name && name.indexOf('-') < 0) { return name; } else { var p = this.findBasePrototype(name); if (p.element) { return this.findTypeExtension(p.element.extends); } } } }; // memoize base prototypes var memoizedBases = {}; // implementation of 'chainObject' depends on support for __proto__ if (Object.__proto__) { prototype.chainObject = function(object, inherited) { if (object && inherited && object !== inherited) { object.__proto__ = inherited; } return object; } } else { prototype.chainObject = function(object, inherited) { if (object && inherited && object !== inherited) { var chained = Object.create(inherited); object = extend(chained, object); } return object; } } // On platforms that do not support __proto__ (versions of IE), the prototype // chain of a custom element is simulated via installation of __proto__. // Although custom elements manages this, we install it here so it's // available during desugaring. function ensurePrototypeTraversal(prototype) { if (!Object.__proto__) { var ancestor = Object.getPrototypeOf(prototype); prototype.__proto__ = ancestor; if (isBase(ancestor)) { ancestor.__proto__ = Object.getPrototypeOf(ancestor); } } } // exports api.declaration.prototype = prototype; })(Polymer); (function(scope) { /* Elements are added to a registration queue so that they register in the proper order at the appropriate time. We do this for a few reasons: * to enable elements to load resources (like stylesheets) asynchronously. We need to do this until the platform provides an efficient alternative. One issue is that remote @import stylesheets are re-fetched whenever stamped into a shadowRoot. * to ensure elements loaded 'at the same time' (e.g. via some set of imports) are registered as a batch. This allows elements to be enured from upgrade ordering as long as they query the dom tree 1 task after upgrade (aka domReady). This is a performance tradeoff. On the one hand, elements that could register while imports are loading are prevented from doing so. On the other, grouping upgrades into a single task means less incremental work (for example style recalcs), Also, we can ensure the document is in a known state at the single quantum of time when elements upgrade. */ var queue = { // tell the queue to wait for an element to be ready wait: function(element) { if (!element.__queue) { element.__queue = {}; elements.push(element); } }, // enqueue an element to the next spot in the queue. enqueue: function(element, check, go) { var shouldAdd = element.__queue && !element.__queue.check; if (shouldAdd) { queueForElement(element).push(element); element.__queue.check = check; element.__queue.go = go; } return (this.indexOf(element) !== 0); }, indexOf: function(element) { var i = queueForElement(element).indexOf(element); if (i >= 0 && document.contains(element)) { i += (HTMLImports.useNative || HTMLImports.ready) ? importQueue.length : 1e9; } return i; }, // tell the queue an element is ready to be registered go: function(element) { var readied = this.remove(element); if (readied) { element.__queue.flushable = true; this.addToFlushQueue(readied); this.check(); } }, remove: function(element) { var i = this.indexOf(element); if (i !== 0) { //console.warn('queue order wrong', i); return; } return queueForElement(element).shift(); }, check: function() { // next var element = this.nextElement(); if (element) { element.__queue.check.call(element); } if (this.canReady()) { this.ready(); return true; } }, nextElement: function() { return nextQueued(); }, canReady: function() { return !this.waitToReady && this.isEmpty(); }, isEmpty: function() { for (var i=0, l=elements.length, e; (i<l) && (e=elements[i]); i++) { if (e.__queue && !e.__queue.flushable) { return; } } return true; }, addToFlushQueue: function(element) { flushQueue.push(element); }, flush: function() { // prevent re-entrance if (this.flushing) { return; } this.flushing = true; var element; while (flushQueue.length) { element = flushQueue.shift(); element.__queue.go.call(element); element.__queue = null; } this.flushing = false; }, ready: function() { // TODO(sorvell): As an optimization, turn off CE polyfill upgrading // while registering. This way we avoid having to upgrade each document // piecemeal per registration and can instead register all elements // and upgrade once in a batch. Without this optimization, upgrade time // degrades significantly when SD polyfill is used. This is mainly because // querying the document tree for elements is slow under the SD polyfill. var polyfillWasReady = CustomElements.ready; CustomElements.ready = false; this.flush(); if (!CustomElements.useNative) { CustomElements.upgradeDocumentTree(document); } CustomElements.ready = polyfillWasReady; Polymer.flush(); requestAnimationFrame(this.flushReadyCallbacks); }, addReadyCallback: function(callback) { if (callback) { readyCallbacks.push(callback); } }, flushReadyCallbacks: function() { if (readyCallbacks) { var fn; while (readyCallbacks.length) { fn = readyCallbacks.shift(); fn(); } } }, /** Returns a list of elements that have had polymer-elements created but are not yet ready to register. The list is an array of element definitions. */ waitingFor: function() { var e$ = []; for (var i=0, l=elements.length, e; (i<l) && (e=elements[i]); i++) { if (e.__queue && !e.__queue.flushable) { e$.push(e); } } return e$; }, waitToReady: true }; var elements = []; var flushQueue = []; var importQueue = []; var mainQueue = []; var readyCallbacks = []; function queueForElement(element) { return document.contains(element) ? mainQueue : importQueue; } function nextQueued() { return importQueue.length ? importQueue[0] : mainQueue[0]; } function whenReady(callback) { queue.waitToReady = true; Polymer.endOfMicrotask(function() { HTMLImports.whenReady(function() { queue.addReadyCallback(callback); queue.waitToReady = false; queue.check(); }); }); } /** Forces polymer to register any pending elements. Can be used to abort waiting for elements that are partially defined. @param timeout {Integer} Optional timeout in milliseconds */ function forceReady(timeout) { if (timeout === undefined) { queue.ready(); return; } var handle = setTimeout(function() { queue.ready(); }, timeout); Polymer.whenReady(function() { clearTimeout(handle); }); } // exports scope.elements = elements; scope.waitingFor = queue.waitingFor.bind(queue); scope.forceReady = forceReady; scope.queue = queue; scope.whenReady = scope.whenPolymerReady = whenReady; })(Polymer); (function(scope) { // imports var extend = scope.extend; var api = scope.api; var queue = scope.queue; var whenReady = scope.whenReady; var getRegisteredPrototype = scope.getRegisteredPrototype; var waitingForPrototype = scope.waitingForPrototype; // declarative implementation: <polymer-element> var prototype = extend(Object.create(HTMLElement.prototype), { createdCallback: function() { if (this.getAttribute('name')) { this.init(); } }, init: function() { // fetch declared values this.name = this.getAttribute('name'); this.extends = this.getAttribute('extends'); queue.wait(this); // initiate any async resource fetches this.loadResources(); // register when all constraints are met this.registerWhenReady(); }, // TODO(sorvell): we currently queue in the order the prototypes are // registered, but we should queue in the order that polymer-elements // are registered. We are currently blocked from doing this based on // crbug.com/395686. registerWhenReady: function() { if (this.registered || this.waitingForPrototype(this.name) || this.waitingForQueue() || this.waitingForResources()) { return; } queue.go(this); }, _register: function() { //console.log('registering', this.name); // warn if extending from a custom element not registered via Polymer if (isCustomTag(this.extends) && !isRegistered(this.extends)) { console.warn('%s is attempting to extend %s, an unregistered element ' + 'or one that was not registered with Polymer.', this.name, this.extends); } this.register(this.name, this.extends); this.registered = true; }, waitingForPrototype: function(name) { if (!getRegisteredPrototype(name)) { // then wait for a prototype waitingForPrototype(name, this); // emulate script if user is not supplying one this.handleNoScript(name); // prototype not ready yet return true; } }, handleNoScript: function(name) { // if explicitly marked as 'noscript' if (this.hasAttribute('noscript') && !this.noscript) { this.noscript = true; // imperative element registration Polymer(name); } }, waitingForResources: function() { return this._needsResources; }, // NOTE: Elements must be queued in proper order for inheritance/composition // dependency resolution. Previously this was enforced for inheritance, // and by rule for composition. It's now entirely by rule. waitingForQueue: function() { return queue.enqueue(this, this.registerWhenReady, this._register); }, loadResources: function() { this._needsResources = true; this.loadStyles(function() { this._needsResources = false; this.registerWhenReady(); }.bind(this)); } }); // semi-pluggable APIs // TODO(sjmiles): should be fully pluggable (aka decoupled, currently // the various plugins are allowed to depend on each other directly) api.publish(api.declaration, prototype); // utility and bookkeeping function isRegistered(name) { return Boolean(HTMLElement.getPrototypeForTag(name)); } function isCustomTag(name) { return (name && name.indexOf('-') >= 0); } // boot tasks whenReady(function() { document.body.removeAttribute('unresolved'); document.dispatchEvent( new CustomEvent('polymer-ready', {bubbles: true}) ); }); // register polymer-element with document document.registerElement('polymer-element', {prototype: prototype}); })(Polymer); (function(scope) { /** * @class Polymer */ var whenReady = scope.whenReady; /** * Loads the set of HTMLImports contained in `node`. Notifies when all * the imports have loaded by calling the `callback` function argument. * This method can be used to lazily load imports. For example, given a * template: * * <template> * <link rel="import" href="my-import1.html"> * <link rel="import" href="my-import2.html"> * </template> * * Polymer.importElements(template.content, function() { * console.log('imports lazily loaded'); * }); * * @method importElements * @param {Node} node Node containing the HTMLImports to load. * @param {Function} callback Callback called when all imports have loaded. */ function importElements(node, callback) { if (node) { document.head.appendChild(node); whenReady(callback); } else if (callback) { callback(); } } /** * Loads an HTMLImport for each url specified in the `urls` array. * Notifies when all the imports have loaded by calling the `callback` * function argument. This method can be used to lazily load imports. * For example, * * Polymer.import(['my-import1.html', 'my-import2.html'], function() { * console.log('imports lazily loaded'); * }); * * @method import * @param {Array} urls Array of urls to load as HTMLImports. * @param {Function} callback Callback called when all imports have loaded. */ function _import(urls, callback) { if (urls && urls.length) { var frag = document.createDocumentFragment(); for (var i=0, l=urls.length, url, link; (i<l) && (url=urls[i]); i++) { link = document.createElement('link'); link.rel = 'import'; link.href = url; frag.appendChild(link); } importElements(frag, callback); } else if (callback) { callback(); } } // exports scope.import = _import; scope.importElements = importElements; })(Polymer); /** * The `auto-binding` element extends the template element. It provides a quick * and easy way to do data binding without the need to setup a model. * The `auto-binding` element itself serves as the model and controller for the * elements it contains. Both data and event handlers can be bound. * * The `auto-binding` element acts just like a template that is bound to * a model. It stamps its content in the dom adjacent to itself. When the * content is stamped, the `template-bound` event is fired. * * Example: * * <template is="auto-binding"> * <div>Say something: <input value="{{value}}"></div> * <div>You said: {{value}}</div> * <button on-tap="{{buttonTap}}">Tap me!</button> * </template> * <script> * var template = document.querySelector('template'); * template.value = 'something'; * template.buttonTap = function() { * console.log('tap!'); * }; * </script> * * @module Polymer * @status stable */ (function() { var element = document.createElement('polymer-element'); element.setAttribute('name', 'auto-binding'); element.setAttribute('extends', 'template'); element.init(); Polymer('auto-binding', { createdCallback: function() { this.syntax = this.bindingDelegate = this.makeSyntax(); // delay stamping until polymer-ready so that auto-binding is not // required to load last. Polymer.whenPolymerReady(function() { this.model = this; this.setAttribute('bind', ''); // we don't bother with an explicit signal here, we could ust a MO // if necessary this.async(function() { // note: this will marshall *all* the elements in the parentNode // rather than just stamped ones. We'd need to use createInstance // to fix this or something else fancier. this.marshalNodeReferences(this.parentNode); // template stamping is asynchronous so stamping isn't complete // by polymer-ready; fire an event so users can use stamped elements this.fire('template-bound'); }); }.bind(this)); }, makeSyntax: function() { var events = Object.create(Polymer.api.declaration.events); var self = this; events.findController = function() { return self.model; }; var syntax = new PolymerExpressions(); var prepareBinding = syntax.prepareBinding; syntax.prepareBinding = function(pathString, name, node) { return events.prepareEventBinding(pathString, name, node) || prepareBinding.call(syntax, pathString, name, node); }; return syntax; } }); })();