11852 lines
336 KiB
JavaScript
11852 lines
336 KiB
JavaScript
/**
|
|
* @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.1
|
|
window.PolymerGestures = {};
|
|
|
|
(function(scope) {
|
|
var HAS_FULL_PATH = 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
|
|
HAS_FULL_PATH = 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 (HAS_FULL_PATH && 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 (HAS_FULL_PATH && 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 (HAS_FULL_PATH && 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];
|
|
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 = 0, 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 CURRENT_BUTTONS = 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 === document) {
|
|
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') {
|
|
CURRENT_BUTTONS |= bit;
|
|
} else if (type === 'mouseup') {
|
|
CURRENT_BUTTONS &= ~bit;
|
|
}
|
|
e.buttons = CURRENT_BUTTONS;
|
|
}
|
|
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) {
|
|
CURRENT_BUTTONS = 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 === document) {
|
|
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 === document) {
|
|
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 fn = filterRegistry[this.name];
|
|
var context = model;
|
|
if (fn) {
|
|
context = undefined;
|
|
} else {
|
|
fn = context[this.name];
|
|
if (!fn) {
|
|
console.error('Cannot find function or filter: ' + this.name);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If toModelDirection is falsey, then the "normal" (dom-bound) direction
|
|
// is used. Otherwise, it looks for a 'toModel' property function on the
|
|
// object.
|
|
if (toModelDirection) {
|
|
fn = fn.toModel;
|
|
} else if (typeof fn.toDOM == 'function') {
|
|
fn = fn.toDOM;
|
|
}
|
|
|
|
if (typeof fn != 'function') {
|
|
console.error('Cannot find function or filter: ' + this.name);
|
|
return;
|
|
}
|
|
|
|
var args = initialArgs || [];
|
|
for (var i = 0; i < this.args.length; i++) {
|
|
args.push(getFn(this.args[i])(model, observer, filterRegistry));
|
|
}
|
|
|
|
return fn.apply(context, args);
|
|
}
|
|
};
|
|
|
|
function notImplemented() { throw Error('Not Implemented'); }
|
|
|
|
var unaryOperators = {
|
|
'+': function(v) { return +v; },
|
|
'-': function(v) { return -v; },
|
|
'!': function(v) { return !v; }
|
|
};
|
|
|
|
var binaryOperators = {
|
|
'+': function(l, r) { return l+r; },
|
|
'-': function(l, r) { return l-r; },
|
|
'*': function(l, r) { return l*r; },
|
|
'/': function(l, r) { return l/r; },
|
|
'%': function(l, r) { return l%r; },
|
|
'<': function(l, r) { return l<r; },
|
|
'>': function(l, r) { return l>r; },
|
|
'<=': function(l, r) { return l<=r; },
|
|
'>=': function(l, r) { return l>=r; },
|
|
'==': function(l, r) { return l==r; },
|
|
'!=': function(l, r) { return l!=r; },
|
|
'===': function(l, r) { return l===r; },
|
|
'!==': function(l, r) { return l!==r; },
|
|
'&&': function(l, r) { return l&&r; },
|
|
'||': function(l, r) { return l||r; },
|
|
};
|
|
|
|
function getFn(arg) {
|
|
return typeof arg == 'function' ? arg : arg.valueFn();
|
|
}
|
|
|
|
function ASTDelegate() {
|
|
this.expression = null;
|
|
this.filters = [];
|
|
this.deps = {};
|
|
this.currentPath = undefined;
|
|
this.scopeIdent = undefined;
|
|
this.indexIdent = undefined;
|
|
this.dynamicDeps = false;
|
|
}
|
|
|
|
ASTDelegate.prototype = {
|
|
createUnaryExpression: function(op, argument) {
|
|
if (!unaryOperators[op])
|
|
throw Error('Disallowed operator: ' + op);
|
|
|
|
argument = getFn(argument);
|
|
|
|
return function(model, observer, filterRegistry) {
|
|
return unaryOperators[op](argument(model, observer, filterRegistry));
|
|
};
|
|
},
|
|
|
|
createBinaryExpression: function(op, left, right) {
|
|
if (!binaryOperators[op])
|
|
throw Error('Disallowed operator: ' + op);
|
|
|
|
left = getFn(left);
|
|
right = getFn(right);
|
|
|
|
switch (op) {
|
|
case '||':
|
|
this.dynamicDeps = true;
|
|
return function(model, observer, filterRegistry) {
|
|
return left(model, observer, filterRegistry) ||
|
|
right(model, observer, filterRegistry);
|
|
};
|
|
case '&&':
|
|
this.dynamicDeps = true;
|
|
return function(model, observer, filterRegistry) {
|
|
return left(model, observer, filterRegistry) &&
|
|
right(model, observer, filterRegistry);
|
|
};
|
|
}
|
|
|
|
return function(model, observer, filterRegistry) {
|
|
return binaryOperators[op](left(model, observer, filterRegistry),
|
|
right(model, observer, filterRegistry));
|
|
};
|
|
},
|
|
|
|
createConditionalExpression: function(test, consequent, alternate) {
|
|
test = getFn(test);
|
|
consequent = getFn(consequent);
|
|
alternate = getFn(alternate);
|
|
|
|
this.dynamicDeps = true;
|
|
|
|
return function(model, observer, filterRegistry) {
|
|
return test(model, observer, filterRegistry) ?
|
|
consequent(model, observer, filterRegistry) :
|
|
alternate(model, observer, filterRegistry);
|
|
}
|
|
},
|
|
|
|
createIdentifier: function(name) {
|
|
var ident = new IdentPath(name);
|
|
ident.type = 'Identifier';
|
|
return ident;
|
|
},
|
|
|
|
createMemberExpression: function(accessor, object, property) {
|
|
var ex = new MemberExpression(object, property, accessor);
|
|
if (ex.dynamicDeps)
|
|
this.dynamicDeps = true;
|
|
return ex;
|
|
},
|
|
|
|
createCallExpression: function(expression, args) {
|
|
if (!(expression instanceof IdentPath))
|
|
throw Error('Only identifier function invocations are allowed');
|
|
|
|
var filter = new Filter(expression.name, args);
|
|
|
|
return function(model, observer, filterRegistry) {
|
|
return filter.transform(model, observer, filterRegistry, false);
|
|
};
|
|
},
|
|
|
|
createLiteral: function(token) {
|
|
return new Literal(token.value);
|
|
},
|
|
|
|
createArrayExpression: function(elements) {
|
|
for (var i = 0; i < elements.length; i++)
|
|
elements[i] = getFn(elements[i]);
|
|
|
|
return function(model, observer, filterRegistry) {
|
|
var arr = []
|
|
for (var i = 0; i < elements.length; i++)
|
|
arr.push(elements[i](model, observer, filterRegistry));
|
|
return arr;
|
|
}
|
|
},
|
|
|
|
createProperty: function(kind, key, value) {
|
|
return {
|
|
key: key instanceof IdentPath ? key.name : key.value,
|
|
value: value
|
|
};
|
|
},
|
|
|
|
createObjectExpression: function(properties) {
|
|
for (var i = 0; i < properties.length; i++)
|
|
properties[i].value = getFn(properties[i].value);
|
|
|
|
return function(model, observer, filterRegistry) {
|
|
var obj = {};
|
|
for (var i = 0; i < properties.length; i++)
|
|
obj[properties[i].key] =
|
|
properties[i].value(model, observer, filterRegistry);
|
|
return obj;
|
|
}
|
|
},
|
|
|
|
createFilter: function(name, args) {
|
|
this.filters.push(new Filter(name, args));
|
|
},
|
|
|
|
createAsExpression: function(expression, scopeIdent) {
|
|
this.expression = expression;
|
|
this.scopeIdent = scopeIdent;
|
|
},
|
|
|
|
createInExpression: function(scopeIdent, indexIdent, expression) {
|
|
this.expression = expression;
|
|
this.scopeIdent = scopeIdent;
|
|
this.indexIdent = indexIdent;
|
|
},
|
|
|
|
createTopLevel: function(expression) {
|
|
this.expression = expression;
|
|
},
|
|
|
|
createThisExpression: notImplemented
|
|
}
|
|
|
|
function ConstantObservable(value) {
|
|
this.value_ = value;
|
|
}
|
|
|
|
ConstantObservable.prototype = {
|
|
open: function() { return this.value_; },
|
|
discardChanges: function() { return this.value_; },
|
|
deliver: function() {},
|
|
close: function() {},
|
|
}
|
|
|
|
function Expression(delegate) {
|
|
this.scopeIdent = delegate.scopeIdent;
|
|
this.indexIdent = delegate.indexIdent;
|
|
|
|
if (!delegate.expression)
|
|
throw Error('No expression found.');
|
|
|
|
this.expression = delegate.expression;
|
|
getFn(this.expression); // forces enumeration of path dependencies
|
|
|
|
this.filters = delegate.filters;
|
|
this.dynamicDeps = delegate.dynamicDeps;
|
|
}
|
|
|
|
Expression.prototype = {
|
|
getBinding: function(model, filterRegistry, oneTime) {
|
|
if (oneTime)
|
|
return this.getValue(model, undefined, filterRegistry);
|
|
|
|
var observer = new CompoundObserver();
|
|
// captures deps.
|
|
var firstValue = this.getValue(model, observer, filterRegistry);
|
|
var firstTime = true;
|
|
var self = this;
|
|
|
|
function valueFn() {
|
|
// deps cannot have changed on first value retrieval.
|
|
if (firstTime) {
|
|
firstTime = false;
|
|
return firstValue;
|
|
}
|
|
|
|
if (self.dynamicDeps)
|
|
observer.startReset();
|
|
|
|
var value = self.getValue(model,
|
|
self.dynamicDeps ? observer : undefined,
|
|
filterRegistry);
|
|
if (self.dynamicDeps)
|
|
observer.finishReset();
|
|
|
|
return value;
|
|
}
|
|
|
|
function setValueFn(newValue) {
|
|
self.setValue(model, newValue, filterRegistry);
|
|
return newValue;
|
|
}
|
|
|
|
return new ObserverTransform(observer, valueFn, setValueFn, true);
|
|
},
|
|
|
|
getValue: function(model, observer, filterRegistry) {
|
|
var value = getFn(this.expression)(model, observer, filterRegistry);
|
|
for (var i = 0; i < this.filters.length; i++) {
|
|
value = this.filters[i].transform(model, observer, filterRegistry,
|
|
false, [value]);
|
|
}
|
|
|
|
return value;
|
|
},
|
|
|
|
setValue: function(model, newValue, filterRegistry) {
|
|
var count = this.filters ? this.filters.length : 0;
|
|
while (count-- > 0) {
|
|
newValue = this.filters[count].transform(model, undefined,
|
|
filterRegistry, true, [newValue]);
|
|
}
|
|
|
|
if (this.expression.setValue)
|
|
return this.expression.setValue(model, newValue);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts a style property name to a css property name. For example:
|
|
* "WebkitUserSelect" to "-webkit-user-select"
|
|
*/
|
|
function convertStylePropertyName(name) {
|
|
return String(name).replace(/[A-Z]/g, function(c) {
|
|
return '-' + c.toLowerCase();
|
|
});
|
|
}
|
|
|
|
var parentScopeName = '@' + Math.random().toString(36).slice(2);
|
|
|
|
// Single ident paths must bind directly to the appropriate scope object.
|
|
// I.e. Pushed values in two-bindings need to be assigned to the actual model
|
|
// object.
|
|
function findScope(model, prop) {
|
|
while (model[parentScopeName] &&
|
|
!Object.prototype.hasOwnProperty.call(model, prop)) {
|
|
model = model[parentScopeName];
|
|
}
|
|
|
|
return model;
|
|
}
|
|
|
|
function isLiteralExpression(pathString) {
|
|
switch (pathString) {
|
|
case '':
|
|
return false;
|
|
|
|
case 'false':
|
|
case 'null':
|
|
case 'true':
|
|
return true;
|
|
}
|
|
|
|
if (!isNaN(Number(pathString)))
|
|
return true;
|
|
|
|
return false;
|
|
};
|
|
|
|
function PolymerExpressions() {}
|
|
|
|
PolymerExpressions.prototype = {
|
|
// "built-in" filters
|
|
styleObject: function(value) {
|
|
var parts = [];
|
|
for (var key in value) {
|
|
parts.push(convertStylePropertyName(key) + ': ' + value[key]);
|
|
}
|
|
return parts.join('; ');
|
|
},
|
|
|
|
tokenList: function(value) {
|
|
var tokens = [];
|
|
for (var key in value) {
|
|
if (value[key])
|
|
tokens.push(key);
|
|
}
|
|
return tokens.join(' ');
|
|
},
|
|
|
|
// binding delegate API
|
|
prepareInstancePositionChanged: function(template) {
|
|
var indexIdent = template.polymerExpressionIndexIdent_;
|
|
if (!indexIdent)
|
|
return;
|
|
|
|
return function(templateInstance, index) {
|
|
templateInstance.model[indexIdent] = index;
|
|
};
|
|
},
|
|
|
|
prepareBinding: function(pathString, name, node) {
|
|
var path = Path.get(pathString);
|
|
|
|
if (!isLiteralExpression(pathString) && path.valid) {
|
|
if (path.length == 1) {
|
|
return function(model, node, oneTime) {
|
|
if (oneTime)
|
|
return path.getValueFrom(model);
|
|
|
|
var scope = findScope(model, path[0]);
|
|
return new PathObserver(scope, path);
|
|
};
|
|
}
|
|
return; // bail out early if pathString is simple path.
|
|
}
|
|
|
|
return prepareBinding(pathString, name, node, this);
|
|
},
|
|
|
|
prepareInstanceModel: function(template) {
|
|
var scopeName = template.polymerExpressionScopeIdent_;
|
|
if (!scopeName)
|
|
return;
|
|
|
|
var parentScope = template.templateInstance ?
|
|
template.templateInstance.model :
|
|
template.model;
|
|
|
|
var indexName = template.polymerExpressionIndexIdent_;
|
|
|
|
return function(model) {
|
|
return createScopeObject(parentScope, model, scopeName, indexName);
|
|
};
|
|
}
|
|
};
|
|
|
|
var createScopeObject = ('__proto__' in {}) ?
|
|
function(parentScope, model, scopeName, indexName) {
|
|
var scope = {};
|
|
scope[scopeName] = model;
|
|
scope[indexName] = undefined;
|
|
scope[parentScopeName] = parentScope;
|
|
scope.__proto__ = parentScope;
|
|
return scope;
|
|
} :
|
|
function(parentScope, model, scopeName, indexName) {
|
|
var scope = Object.create(parentScope);
|
|
Object.defineProperty(scope, scopeName,
|
|
{ value: model, configurable: true, writable: true });
|
|
Object.defineProperty(scope, indexName,
|
|
{ value: undefined, configurable: true, writable: true });
|
|
Object.defineProperty(scope, parentScopeName,
|
|
{ value: parentScope, configurable: true, writable: true });
|
|
return scope;
|
|
};
|
|
|
|
global.PolymerExpressions = PolymerExpressions;
|
|
PolymerExpressions.getExpression = getExpression;
|
|
})(this);
|
|
|
|
Polymer = {
|
|
version: '0.5.1'
|
|
};
|
|
|
|
// 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. If we're in
|
|
// the browser, export as a global object.
|
|
|
|
var expose = global;
|
|
|
|
if (typeof exports !== 'undefined') {
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
expose = 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() {
|
|
observable.setValue(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
|
|
};
|
|
|
|
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');
|
|
hasWorkingUrl = u.href === 'http://a/b';
|
|
} 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;
|
|
}
|
|
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)
|
|
// e.g. to prevent <img src="images/{{icon}}"> from generating a 404.
|
|
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)
|
|
// e.g. to prevent <img src="images/{{icon}}"> from generating a 404.
|
|
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;
|
|
}
|
|
|
|
});
|
|
|
|
})();
|