Created
May 22, 2013 20:13
-
-
Save pennyfx/5630529 to your computer and use it in GitHub Desktop.
latest platform polyfill
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2012 The Polymer Authors. All rights reserved. | |
* Use of this source code is goverened by a BSD-style | |
* license that can be found in the LICENSE file. | |
*/ | |
// SideTable is a weak map where possible. If WeakMap is not available the | |
// association is stored as an expando property. | |
var SideTable; | |
// TODO(arv): WeakMap does not allow for Node etc to be keys in Firefox | |
if (typeof WeakMap !== 'undefined' && navigator.userAgent.indexOf('Firefox/') < 0) { | |
SideTable = WeakMap; | |
} else { | |
(function() { | |
var defineProperty = Object.defineProperty; | |
var hasOwnProperty = Object.hasOwnProperty; | |
var counter = new Date().getTime() % 1e9; | |
SideTable = function() { | |
this.name = '__st' + (Math.random() * 1e9 >>> 0) + (counter++ + '__'); | |
}; | |
SideTable.prototype = { | |
set: function(key, value) { | |
defineProperty(key, this.name, {value: value, writable: true}); | |
}, | |
get: function(key) { | |
return hasOwnProperty.call(key, this.name) ? key[this.name] : undefined; | |
}, | |
delete: function(key) { | |
this.set(key, undefined); | |
} | |
} | |
})(); | |
} | |
window.Platform = {}; | |
var logFlags = {}; | |
/* | |
* Copyright 2013 The Polymer Authors. All rights reserved. | |
* Use of this source code is governed by a BSD-style | |
* license that can be found in the LICENSE file. | |
*/ | |
(function() { | |
// poor man's adapter for template.content on various platform scenarios | |
window.templateContent = window.templateContent || function(inTemplate) { | |
return inTemplate.content; | |
}; | |
// so we can call wrap/unwrap without testing for ShadowDOMPolyfill | |
window.wrap = window.unwrap = function(n){ | |
return n; | |
} | |
window.createShadowRoot = function(inElement) { | |
return inElement.webkitCreateShadowRoot(); | |
}; | |
window.templateContent = function(inTemplate) { | |
// if MDV exists, it may need to boostrap this template to reveal content | |
if (window.HTMLTemplateElement && HTMLTemplateElement.bootstrap) { | |
HTMLTemplateElement.bootstrap(inTemplate); | |
} | |
// fallback when there is no Shadow DOM polyfill, no MDV polyfill, and no | |
// native template support | |
if (!inTemplate.content && !inTemplate._content) { | |
var frag = document.createDocumentFragment(); | |
while (inTemplate.firstChild) { | |
frag.appendChild(inTemplate.firstChild); | |
} | |
inTemplate._content = frag; | |
} | |
return inTemplate.content || inTemplate._content; | |
}; | |
})(); | |
/* | |
* Copyright 2013 The Polymer Authors. All rights reserved. | |
* Use of this source code is governed by a BSD-style | |
* license that can be found in the LICENSE file. | |
*/ | |
(function(scope) { | |
// Old versions of iOS do not have bind. | |
if (!Function.prototype.bind) { | |
Function.prototype.bind = function(scope) { | |
var self = this; | |
var args = Array.prototype.slice.call(arguments, 1); | |
return function() { | |
var args2 = args.slice(); | |
args2.push.apply(args2, arguments); | |
return self.apply(scope, args2); | |
}; | |
}; | |
} | |
// namespace an import from CustomElements | |
// TODO(sjmiles): clean up this global | |
scope.mixin = window.mixin; | |
})(window.Platform); | |
// Copyright 2011 Google Inc. | |
// | |
// Licensed under the Apache License, Version 2.0 (the "License"); | |
// you may not use this file except in compliance with the License. | |
// You may obtain a copy of the License at | |
// | |
// http://www.apache.org/licenses/LICENSE-2.0 | |
// | |
// Unless required by applicable law or agreed to in writing, software | |
// distributed under the License is distributed on an "AS IS" BASIS, | |
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
// See the License for the specific language governing permissions and | |
// limitations under the License. | |
(function(scope) { | |
'use strict'; | |
// 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); | |
}; | |
// make forEach work on NodeList | |
NodeList.prototype.forEach = function(cb, context) { | |
Array.prototype.slice.call(this).forEach(cb, context); | |
}; | |
HTMLCollection.prototype.forEach = function(cb, context) { | |
Array.prototype.slice.call(this).forEach(cb, context); | |
}; | |
// polyfill performance.now | |
if (!window.performance) { | |
var start = Date.now(); | |
// only at millisecond precision | |
window.performance = {now: function(){ return Date.now() - start }}; | |
} | |
// polyfill for requestAnimationFrame | |
if (!window.requestAnimationFrame) { | |
window.requestAnimationFrame = (function() { | |
var nativeRaf = window.webkitRequestAnimationFrame || | |
window.mozRequestAnimationFrame; | |
return nativeRaf ? | |
function(callback) { | |
return nativeRaf(function() { | |
callback(performance.now()); | |
}); | |
} : | |
function( callback ){ | |
return window.setTimeout(callback, 1000 / 60); | |
}; | |
})(); | |
} | |
if (!window.cancelAnimationFrame) { | |
window.cancelAnimationFrame = (function() { | |
return window.webkitCancelAnimationFrame || | |
window.mozCancelAnimationFrame || | |
function(id) { | |
clearTimeout(id); | |
}; | |
})(); | |
} | |
// 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; | |
})(window.Platform); | |
/* | |
* Copyright 2013 The Polymer Authors. All rights reserved. | |
* Use of this source code is governed by a BSD-style | |
* license that can be found in the LICENSE file. | |
*/ | |
// poor man's adapter for template.content on various platform scenarios | |
window.templateContent = window.templateContent || function(inTemplate) { | |
return inTemplate.content; | |
}; | |
/* | |
* Copyright 2013 The Polymer Authors. All rights reserved. | |
* Use of this source code is governed by a BSD-style | |
* license that can be found in the LICENSE file. | |
*/ | |
(function(scope) { | |
if (!scope) { | |
scope = window.HTMLImports = {flags:{}}; | |
} | |
var IMPORT_LINK_TYPE = 'import'; | |
// highlander object represents a primary document (the argument to 'parse') | |
// at the root of a tree of documents | |
var importer = { | |
documents: {}, | |
cache: {}, | |
preloadSelectors: [ | |
'link[rel=' + IMPORT_LINK_TYPE + ']', | |
'script[src]', | |
'link[rel=stylesheet]' | |
].join(','), | |
load: function(inDocument, inNext) { | |
// construct a loader instance | |
loader = new Loader(importer.loaded, inNext); | |
// alias the loader cache (for debugging) | |
loader.cache = importer.cache; | |
// add nodes from document into loader queue | |
importer.preload(inDocument); | |
}, | |
preload: function(inDocument) { | |
// all preloadable nodes in inDocument | |
var nodes = inDocument.querySelectorAll(importer.preloadSelectors); | |
// only load imports from the main document | |
// TODO(sjmiles): do this by altering the selector list instead | |
if (inDocument === document) { | |
nodes = Array.prototype.filter.call(nodes, function(n) { | |
return isDocumentLink(n); | |
}); | |
} | |
// add these nodes to loader's queue | |
loader.addNodes(nodes); | |
}, | |
loaded: function(inUrl, inElt, inResource) { | |
if (isDocumentLink(inElt)) { | |
var document = importer.documents[inUrl]; | |
// if we've never seen a document at this url | |
if (!document) { | |
// generate an HTMLDocument from data | |
document = makeDocument(inResource, inUrl); | |
// resolve resource paths relative to host document | |
path.resolvePathsInHTML(document); | |
// cache document | |
importer.documents[inUrl] = document; | |
// add nodes from this document to the loader queue | |
importer.preload(document); | |
} | |
// store document resource | |
inElt.content = inElt.__resource = document; | |
} else { | |
inElt.__resource = inResource; | |
// resolve stylesheet resource paths relative to host document | |
if (isStylesheetLink(inElt)) { | |
path.resolvePathsInStylesheet(inElt); | |
} | |
} | |
} | |
}; | |
function isDocumentLink(inElt) { | |
return isLinkRel(inElt, IMPORT_LINK_TYPE); | |
} | |
function isStylesheetLink(inElt) { | |
return isLinkRel(inElt, 'stylesheet'); | |
} | |
function isLinkRel(inElt, inRel) { | |
return (inElt.localName === 'link' && inElt.getAttribute('rel') === inRel); | |
} | |
function inMainDocument(inElt) { | |
return inElt.ownerDocument === document || | |
// TODO(sjmiles): ShadowDOMPolyfill intrusion | |
inElt.ownerDocument.impl === document; | |
} | |
function makeDocument(inHTML, inUrl) { | |
// create a new HTML document | |
var doc = document.implementation.createHTMLDocument(IMPORT_LINK_TYPE); | |
// cache the new document's source url | |
doc._URL = inUrl; | |
// establish a relative path via <base> | |
var base = doc.createElement('base'); | |
base.setAttribute('href', document.baseURI); | |
doc.head.appendChild(base); | |
// install html | |
doc.body.innerHTML = inHTML; | |
return doc; | |
} | |
var loader; | |
var Loader = function(inOnLoad, inOnComplete) { | |
this.onload = inOnLoad; | |
this.oncomplete = inOnComplete; | |
this.inflight = 0; | |
this.pending = {}; | |
this.cache = {}; | |
}; | |
Loader.prototype = { | |
addNodes: function(inNodes) { | |
// number of transactions to complete | |
this.inflight += inNodes.length; | |
// commence transactions | |
forEach(inNodes, this.require, this); | |
// anything to do? | |
this.checkDone(); | |
}, | |
require: function(inElt) { | |
var url = path.nodeUrl(inElt); | |
// TODO(sjmiles): ad-hoc | |
inElt.__nodeUrl = url; | |
// deduplication | |
if (!this.dedupe(url, inElt)) { | |
// fetch this resource | |
this.fetch(url, inElt); | |
} | |
}, | |
dedupe: function(inUrl, inElt) { | |
if (this.pending[inUrl]) { | |
// add to list of nodes waiting for inUrl | |
this.pending[inUrl].push(inElt); | |
// don't need fetch | |
return true; | |
} | |
if (this.cache[inUrl]) { | |
// complete load using cache data | |
this.onload(inUrl, inElt, loader.cache[inUrl]); | |
// finished this transaction | |
this.tail(); | |
// don't need fetch | |
return true; | |
} | |
// first node waiting for inUrl | |
this.pending[inUrl] = [inElt]; | |
// need fetch (not a dupe) | |
return false; | |
}, | |
fetch: function(inUrl, inElt) { | |
xhr.load(inUrl, function(err, resource) { | |
this.receive(inUrl, inElt, err, resource); | |
}.bind(this)); | |
}, | |
receive: function(inUrl, inElt, inErr, inResource) { | |
if (!inErr) { | |
loader.cache[inUrl] = inResource; | |
} | |
loader.pending[inUrl].forEach(function(e) { | |
if (!inErr) { | |
this.onload(inUrl, e, inResource); | |
} | |
this.tail(); | |
}, this); | |
loader.pending[inUrl] = null; | |
}, | |
tail: function() { | |
--this.inflight; | |
this.checkDone(); | |
}, | |
checkDone: function() { | |
if (!this.inflight) { | |
this.oncomplete(); | |
} | |
} | |
}; | |
var path = { | |
nodeUrl: function(inNode) { | |
return path.resolveUrl(path.getDocumentUrl(document), path.hrefOrSrc(inNode)); | |
}, | |
hrefOrSrc: function(inNode) { | |
return inNode.getAttribute("href") || inNode.getAttribute("src"); | |
}, | |
documentUrlFromNode: function(inNode) { | |
var url = path.getDocumentUrl(inNode.ownerDocument); | |
// take only the left side if there is a # | |
url = url.split('#')[0]; | |
return url; | |
}, | |
getDocumentUrl: function(inDocument) { | |
return inDocument && | |
// TODO(sjmiles): ShadowDOMPolyfill intrusion | |
(inDocument._URL || (inDocument.impl && inDocument.impl._URL) | |
|| inDocument.baseURI || inDocument.URL) | |
|| ''; | |
}, | |
resolveUrl: function(inBaseUrl, inUrl, inRelativeToDocument) { | |
if (this.isAbsUrl(inUrl)) { | |
return inUrl; | |
} | |
var url = this.compressUrl(this.urlToPath(inBaseUrl) + inUrl); | |
if (inRelativeToDocument) { | |
url = path.makeRelPath(path.getDocumentUrl(document), url); | |
} | |
return url; | |
}, | |
isAbsUrl: function(inUrl) { | |
return /(^data:)|(^http[s]?:)|(^\/)/.test(inUrl); | |
}, | |
urlToPath: function(inBaseUrl) { | |
var parts = inBaseUrl.split("/"); | |
parts.pop(); | |
parts.push(''); | |
return parts.join("/"); | |
}, | |
compressUrl: function(inUrl) { | |
var parts = inUrl.split("/"); | |
for (var i=0, p; i<parts.length; i++) { | |
p = parts[i]; | |
if (p === "..") { | |
parts.splice(i-1, 2); | |
i -= 2; | |
} | |
} | |
return parts.join("/"); | |
}, | |
// make a relative path from source to target | |
makeRelPath: function(inSource, inTarget) { | |
var s, t; | |
s = this.compressUrl(inSource).split("/"); | |
t = this.compressUrl(inTarget).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(".."); | |
} | |
var r = t.join("/"); | |
return r; | |
}, | |
resolvePathsInHTML: function(inRoot) { | |
var docUrl = path.documentUrlFromNode(inRoot.body); | |
// TODO(sorvell): MDV Polyfill Intrusion | |
if (window.HTMLTemplateElement && HTMLTemplateElement.bootstrap) { | |
HTMLTemplateElement.bootstrap(inRoot); | |
} | |
var node = inRoot.body; | |
path._resolvePathsInHTML(node, docUrl); | |
}, | |
_resolvePathsInHTML: function(inRoot, inUrl) { | |
path.resolveAttributes(inRoot, inUrl); | |
path.resolveStyleElts(inRoot, inUrl); | |
// handle templates, if supported | |
if (window.templateContent) { | |
var templates = inRoot.querySelectorAll('template'); | |
if (templates) { | |
forEach(templates, function(t) { | |
path._resolvePathsInHTML(templateContent(t), inUrl); | |
}); | |
} | |
} | |
}, | |
resolvePathsInStylesheet: function(inSheet) { | |
var docUrl = path.nodeUrl(inSheet); | |
inSheet.__resource = path.resolveCssText(inSheet.__resource, docUrl); | |
}, | |
resolveStyleElts: function(inRoot, inUrl) { | |
var styles = inRoot.querySelectorAll('style'); | |
if (styles) { | |
forEach(styles, function(style) { | |
style.textContent = path.resolveCssText(style.textContent, inUrl); | |
}); | |
} | |
}, | |
resolveCssText: function(inCssText, inBaseUrl) { | |
return inCssText.replace(/url\([^)]*\)/g, function(inMatch) { | |
// find the url path, ignore quotes in url string | |
var urlPath = inMatch.replace(/["']/g, "").slice(4, -1); | |
urlPath = path.resolveUrl(inBaseUrl, urlPath, true); | |
return "url(" + urlPath + ")"; | |
}); | |
}, | |
resolveAttributes: function(inRoot, inUrl) { | |
// search for attributes that host urls | |
var nodes = inRoot && inRoot.querySelectorAll(URL_ATTRS_SELECTOR); | |
if (nodes) { | |
forEach(nodes, function(n) { | |
this.resolveNodeAttributes(n, inUrl); | |
}, this); | |
} | |
}, | |
resolveNodeAttributes: function(inNode, inUrl) { | |
URL_ATTRS.forEach(function(v) { | |
var attr = inNode.attributes[v]; | |
if (attr && attr.value && | |
(attr.value.search(URL_TEMPLATE_SEARCH) < 0)) { | |
var urlPath = path.resolveUrl(inUrl, attr.value, true); | |
attr.value = urlPath; | |
} | |
}); | |
} | |
}; | |
var URL_ATTRS = ['href', 'src', 'action']; | |
var URL_ATTRS_SELECTOR = '[' + URL_ATTRS.join('],[') + ']'; | |
var URL_TEMPLATE_SEARCH = '{{.*}}'; | |
var xhr = { | |
async: true, | |
ok: function(inRequest) { | |
return (inRequest.status >= 200 && inRequest.status < 300) | |
|| (inRequest.status === 304); | |
}, | |
load: function(url, next, nextContext) { | |
var request = new XMLHttpRequest(); | |
if (scope.flags.debug || scope.flags.bust) { | |
url += '?' + Math.random(); | |
} | |
request.open('GET', url, xhr.async); | |
request.addEventListener('readystatechange', function(e) { | |
if (request.readyState === 4) { | |
next.call(nextContext, !xhr.ok(request) && request, | |
request.response, url); | |
} | |
}); | |
request.send(); | |
} | |
}; | |
var forEach = Array.prototype.forEach.call.bind(Array.prototype.forEach); | |
// exports | |
scope.importer = importer; | |
scope.getDocumentUrl = path.getDocumentUrl; | |
// bootstrap | |
// IE shim for CustomEvent | |
if (typeof window.CustomEvent !== 'function') { | |
window.CustomEvent = function(inType) { | |
var e = document.createEvent('HTMLEvents'); | |
e.initEvent(inType, true, true); | |
return e; | |
}; | |
} | |
window.addEventListener('load', function() { | |
// preload document resource trees | |
importer.load(document, function() { | |
// TODO(sjmiles): ShadowDOM polyfill pollution | |
var doc = window.ShadowDOMPolyfill ? ShadowDOMPolyfill.wrap(document) | |
: document; | |
HTMLImports.readyTime = new Date().getTime(); | |
// send HTMLImportsLoaded when finished | |
doc.body.dispatchEvent( | |
new CustomEvent('HTMLImportsLoaded', {bubbles: true}) | |
); | |
}); | |
}); | |
})(window.HTMLImports); | |
/* | |
* Copyright 2012 The Polymer Authors. All rights reserved. | |
* Use of this source code is goverened by a BSD-style | |
* license that can be found in the LICENSE file. | |
*/ | |
(function(global) { | |
var registrationsTable = new SideTable(); | |
// We use setImmediate or postMessage for our future callback. | |
var setImmediate = window.msSetImmediate; | |
// Use post message to emulate setImmediate. | |
if (!setImmediate) { | |
var setImmediateQueue = []; | |
var sentinel = String(Math.random()); | |
window.addEventListener('message', function(e) { | |
if (e.data === sentinel) { | |
var queue = setImmediateQueue; | |
setImmediateQueue = []; | |
queue.forEach(function(func) { | |
func(); | |
}); | |
} | |
}); | |
setImmediate = function(func) { | |
setImmediateQueue.push(func); | |
window.postMessage(sentinel, '*'); | |
}; | |
} | |
// This is used to ensure that we never schedule 2 callas to setImmediate | |
var isScheduled = false; | |
// Keep track of observers that needs to be notified next time. | |
var scheduledObservers = []; | |
/** | |
* Schedules |dispatchCallback| to be called in the future. | |
* @param {MutationObserver} observer | |
*/ | |
function scheduleCallback(observer) { | |
scheduledObservers.push(observer); | |
if (!isScheduled) { | |
isScheduled = true; | |
setImmediate(dispatchCallbacks); | |
} | |
} | |
function wrapIfNeeded(node) { | |
return window.ShadowDOMPolyfill && | |
window.ShadowDOMPolyfill.wrapIfNeeded(node) || | |
node; | |
} | |
function dispatchCallbacks() { | |
// http://dom.spec.whatwg.org/#mutation-observers | |
isScheduled = false; // Used to allow a new setImmediate call above. | |
var observers = scheduledObservers; | |
scheduledObservers = []; | |
// Sort observers based on their creation UID (incremental). | |
observers.sort(function(o1, o2) { | |
return o1.uid_ - o2.uid_; | |
}); | |
var anyNonEmpty = false; | |
observers.forEach(function(observer) { | |
// 2.1, 2.2 | |
var queue = observer.takeRecords(); | |
// 2.3. Remove all transient registered observers whose observer is mo. | |
removeTransientObserversFor(observer); | |
// 2.4 | |
if (queue.length) { | |
observer.callback_(queue, observer); | |
anyNonEmpty = true; | |
} | |
}); | |
// 3. | |
if (anyNonEmpty) | |
dispatchCallbacks(); | |
} | |
function removeTransientObserversFor(observer) { | |
observer.nodes_.forEach(function(node) { | |
var registrations = registrationsTable.get(node); | |
if (!registrations) | |
return; | |
registrations.forEach(function(registration) { | |
if (registration.observer === observer) | |
registration.removeTransientObservers(); | |
}); | |
}); | |
} | |
/** | |
* This function is used for the "For each registered observer observer (with | |
* observer's options as options) in target's list of registered observers, | |
* run these substeps:" and the "For each ancestor ancestor of target, and for | |
* each registered observer observer (with options options) in ancestor's list | |
* of registered observers, run these substeps:" part of the algorithms. The | |
* |options.subtree| is checked to ensure that the callback is called | |
* correctly. | |
* | |
* @param {Node} target | |
* @param {function(MutationObserverInit):MutationRecord} callback | |
*/ | |
function forEachAncestorAndObserverEnqueueRecord(target, callback) { | |
for (var node = target; node; node = node.parentNode) { | |
var registrations = registrationsTable.get(node); | |
if (registrations) { | |
for (var j = 0; j < registrations.length; j++) { | |
var registration = registrations[j]; | |
var options = registration.options; | |
// Only target ignores subtree. | |
if (node !== target && !options.subtree) | |
continue; | |
var record = callback(options); | |
if (record) | |
registration.enqueue(record); | |
} | |
} | |
} | |
} | |
var uidCounter = 0; | |
/** | |
* The class that maps to the DOM MutationObserver interface. | |
* @param {Function} callback. | |
* @constructor | |
*/ | |
function JsMutationObserver(callback) { | |
this.callback_ = callback; | |
this.nodes_ = []; | |
this.records_ = []; | |
this.uid_ = ++uidCounter; | |
} | |
JsMutationObserver.prototype = { | |
observe: function(target, options) { | |
target = wrapIfNeeded(target); | |
// 1.1 | |
if (!options.childList && !options.attributes && !options.characterData || | |
// 1.2 | |
options.attributeOldValue && !options.attributes || | |
// 1.3 | |
options.attributeFilter && options.attributeFilter.length && | |
!options.attributes || | |
// 1.4 | |
options.characterDataOldValue && !options.characterData) { | |
throw new SyntaxError(); | |
} | |
var registrations = registrationsTable.get(target); | |
if (!registrations) | |
registrationsTable.set(target, registrations = []); | |
// 2 | |
// If target's list of registered observers already includes a registered | |
// observer associated with the context object, replace that registered | |
// observer's options with options. | |
var registration; | |
for (var i = 0; i < registrations.length; i++) { | |
if (registrations[i].observer === this) { | |
registration = registrations[i]; | |
registration.removeListeners(); | |
registration.options = options; | |
break; | |
} | |
} | |
// 3. | |
// Otherwise, add a new registered observer to target's list of registered | |
// observers with the context object as the observer and options as the | |
// options, and add target to context object's list of nodes on which it | |
// is registered. | |
if (!registration) { | |
registration = new Registration(this, target, options); | |
registrations.push(registration); | |
this.nodes_.push(target); | |
} | |
registration.addListeners(); | |
}, | |
disconnect: function() { | |
this.nodes_.forEach(function(node) { | |
var registrations = registrationsTable.get(node); | |
for (var i = 0; i < registrations.length; i++) { | |
var registration = registrations[i]; | |
if (registration.observer === this) { | |
registration.removeListeners(); | |
registrations.splice(i, 1); | |
// Each node can only have one registered observer associated with | |
// this observer. | |
break; | |
} | |
} | |
}, this); | |
this.records_ = []; | |
}, | |
takeRecords: function() { | |
var copyOfRecords = this.records_; | |
this.records_ = []; | |
return copyOfRecords; | |
} | |
}; | |
/** | |
* @param {string} type | |
* @param {Node} target | |
* @constructor | |
*/ | |
function MutationRecord(type, target) { | |
this.type = type; | |
this.target = target; | |
this.addedNodes = []; | |
this.removedNodes = []; | |
this.previousSibling = null; | |
this.nextSibling = null; | |
this.attributeName = null; | |
this.attributeNamespace = null; | |
this.oldValue = null; | |
} | |
function copyMutationRecord(original) { | |
var record = new MutationRecord(original.type, original.target); | |
record.addedNodes = original.addedNodes.slice(); | |
record.removedNodes = original.removedNodes.slice(); | |
record.previousSibling = original.previousSibling; | |
record.nextSibling = original.nextSibling; | |
record.attributeName = original.attributeName; | |
record.attributeNamespace = original.attributeNamespace; | |
record.oldValue = original.oldValue; | |
return record; | |
}; | |
// We keep track of the two (possibly one) records used in a single mutation. | |
var currentRecord, recordWithOldValue; | |
/** | |
* Creates a record without |oldValue| and caches it as |currentRecord| for | |
* later use. | |
* @param {string} oldValue | |
* @return {MutationRecord} | |
*/ | |
function getRecord(type, target) { | |
return currentRecord = new MutationRecord(type, target); | |
} | |
/** | |
* Gets or creates a record with |oldValue| based in the |currentRecord| | |
* @param {string} oldValue | |
* @return {MutationRecord} | |
*/ | |
function getRecordWithOldValue(oldValue) { | |
if (recordWithOldValue) | |
return recordWithOldValue; | |
recordWithOldValue = copyMutationRecord(currentRecord); | |
recordWithOldValue.oldValue = oldValue; | |
return recordWithOldValue; | |
} | |
function clearRecords() { | |
currentRecord = recordWithOldValue = undefined; | |
} | |
/** | |
* @param {MutationRecord} record | |
* @return {boolean} Whether the record represents a record from the current | |
* mutation event. | |
*/ | |
function recordRepresentsCurrentMutation(record) { | |
return record === recordWithOldValue || record === currentRecord; | |
} | |
/** | |
* Selects which record, if any, to replace the last record in the queue. | |
* This returns |null| if no record should be replaced. | |
* | |
* @param {MutationRecord} lastRecord | |
* @param {MutationRecord} newRecord | |
* @param {MutationRecord} | |
*/ | |
function selectRecord(lastRecord, newRecord) { | |
if (lastRecord === newRecord) | |
return lastRecord; | |
// Check if the the record we are adding represents the same record. If | |
// so, we keep the one with the oldValue in it. | |
if (recordWithOldValue && recordRepresentsCurrentMutation(lastRecord)) | |
return recordWithOldValue; | |
return null; | |
} | |
/** | |
* Class used to represent a registered observer. | |
* @param {MutationObserver} observer | |
* @param {Node} target | |
* @param {MutationObserverInit} options | |
* @constructor | |
*/ | |
function Registration(observer, target, options) { | |
this.observer = observer; | |
this.target = target; | |
this.options = options; | |
this.transientObservedNodes = []; | |
} | |
Registration.prototype = { | |
enqueue: function(record) { | |
var records = this.observer.records_; | |
var length = records.length; | |
// There are cases where we replace the last record with the new record. | |
// For example if the record represents the same mutation we need to use | |
// the one with the oldValue. If we get same record (this can happen as we | |
// walk up the tree) we ignore the new record. | |
if (records.length > 0) { | |
var lastRecord = records[length - 1]; | |
var recordToReplaceLast = selectRecord(lastRecord, record); | |
if (recordToReplaceLast) { | |
records[length - 1] = recordToReplaceLast; | |
return; | |
} | |
} else { | |
scheduleCallback(this.observer); | |
} | |
records[length] = record; | |
}, | |
addListeners: function() { | |
this.addListeners_(this.target); | |
}, | |
addListeners_: function(node) { | |
var options = this.options; | |
if (options.attributes) | |
node.addEventListener('DOMAttrModified', this, true); | |
if (options.characterData) | |
node.addEventListener('DOMCharacterDataModified', this, true); | |
if (options.childList) | |
node.addEventListener('DOMNodeInserted', this, true); | |
if (options.childList || options.subtree) | |
node.addEventListener('DOMNodeRemoved', this, true); | |
}, | |
removeListeners: function() { | |
this.removeListeners_(this.target); | |
}, | |
removeListeners_: function(node) { | |
var options = this.options; | |
if (options.attributes) | |
node.removeEventListener('DOMAttrModified', this, true); | |
if (options.characterData) | |
node.removeEventListener('DOMCharacterDataModified', this, true); | |
if (options.childList) | |
node.removeEventListener('DOMNodeInserted', this, true); | |
if (options.childList || options.subtree) | |
node.removeEventListener('DOMNodeRemoved', this, true); | |
}, | |
/** | |
* Adds a transient observer on node. The transient observer gets removed | |
* next time we deliver the change records. | |
* @param {Node} node | |
*/ | |
addTransientObserver: function(node) { | |
// Don't add transient observers on the target itself. We already have all | |
// the required listeners set up on the target. | |
if (node === this.target) | |
return; | |
this.addListeners_(node); | |
this.transientObservedNodes.push(node); | |
var registrations = registrationsTable.get(node); | |
if (!registrations) | |
registrationsTable.set(node, registrations = []); | |
// We know that registrations does not contain this because we already | |
// checked if node === this.target. | |
registrations.push(this); | |
}, | |
removeTransientObservers: function() { | |
var transientObservedNodes = this.transientObservedNodes; | |
this.transientObservedNodes = []; | |
transientObservedNodes.forEach(function(node) { | |
// Transient observers are never added to the target. | |
this.removeListeners_(node); | |
var registrations = registrationsTable.get(node); | |
for (var i = 0; i < registrations.length; i++) { | |
if (registrations[i] === this) { | |
registrations.splice(i, 1); | |
// Each node can only have one registered observer associated with | |
// this observer. | |
break; | |
} | |
} | |
}, this); | |
}, | |
handleEvent: function(e) { | |
// Stop propagation since we are managing the propagation manually. | |
// This means that other mutation events on the page will not work | |
// correctly but that is by design. | |
e.stopImmediatePropagation(); | |
switch (e.type) { | |
case 'DOMAttrModified': | |
// http://dom.spec.whatwg.org/#concept-mo-queue-attributes | |
var name = e.attrName; | |
var namespace = e.relatedNode.namespaceURI; | |
var target = e.target; | |
// 1. | |
var record = new getRecord('attributes', target); | |
record.attributeName = name; | |
record.attributeNamespace = namespace; | |
// 2. | |
var oldValue = | |
e.attrChange === MutationEvent.ADDITION ? null : e.prevValue; | |
forEachAncestorAndObserverEnqueueRecord(target, function(options) { | |
// 3.1, 4.2 | |
if (!options.attributes) | |
return; | |
// 3.2, 4.3 | |
if (options.attributeFilter && options.attributeFilter.length && | |
options.attributeFilter.indexOf(name) === -1 && | |
options.attributeFilter.indexOf(namespace) === -1) { | |
return; | |
} | |
// 3.3, 4.4 | |
if (options.attributeOldValue) | |
return getRecordWithOldValue(oldValue); | |
// 3.4, 4.5 | |
return record; | |
}); | |
break; | |
case 'DOMCharacterDataModified': | |
// http://dom.spec.whatwg.org/#concept-mo-queue-characterdata | |
var target = e.target; | |
// 1. | |
var record = getRecord('characterData', target); | |
// 2. | |
var oldValue = e.prevValue; | |
forEachAncestorAndObserverEnqueueRecord(target, function(options) { | |
// 3.1, 4.2 | |
if (!options.characterData) | |
return; | |
// 3.2, 4.3 | |
if (options.characterDataOldValue) | |
return getRecordWithOldValue(oldValue); | |
// 3.3, 4.4 | |
return record; | |
}); | |
break; | |
case 'DOMNodeRemoved': | |
this.addTransientObserver(e.target); | |
// Fall through. | |
case 'DOMNodeInserted': | |
// http://dom.spec.whatwg.org/#concept-mo-queue-childlist | |
var target = e.relatedNode; | |
var changedNode = e.target; | |
var addedNodes, removedNodes; | |
if (e.type === 'DOMNodeInserted') { | |
addedNodes = [changedNode]; | |
removedNodes = []; | |
} else { | |
addedNodes = []; | |
removedNodes = [changedNode]; | |
} | |
var previousSibling = changedNode.previousSibling; | |
var nextSibling = changedNode.nextSibling; | |
// 1. | |
var record = getRecord('childList', target); | |
record.addedNodes = addedNodes; | |
record.removedNodes = removedNodes; | |
record.previousSibling = previousSibling; | |
record.nextSibling = nextSibling; | |
forEachAncestorAndObserverEnqueueRecord(target, function(options) { | |
// 2.1, 3.2 | |
if (!options.childList) | |
return; | |
// 2.2, 3.3 | |
return record; | |
}); | |
} | |
clearRecords(); | |
} | |
}; | |
global.JsMutationObserver = JsMutationObserver; | |
})(this); | |
/* | |
* Copyright 2013 The Polymer Authors. All rights reserved. | |
* Use of this source code is governed by a BSD-style | |
* license that can be found in the LICENSE file. | |
*/ | |
if (!window.MutationObserver) { | |
window.MutationObserver = | |
window.WebKitMutationObserver || | |
window.JsMutationObserver; | |
if (!MutationObserver) { | |
throw new Error("no mutation observer support"); | |
} | |
} | |
/* | |
* Copyright 2013 The Polymer Authors. All rights reserved. | |
* Use of this source code is governed by a BSD-style | |
* license that can be found in the LICENSE file. | |
*/ | |
/** | |
* Implements `document.register` | |
* @module CustomElements | |
*/ | |
/** | |
* Polyfilled extensions to the `document` object. | |
* @class Document | |
*/ | |
(function(scope) { | |
if (!scope) { | |
scope = window.CustomElements = {flags:{}}; | |
} | |
// native document.register? | |
scope.hasNative = (document.webkitRegister || document.register) && scope.flags.register === 'native'; | |
if (scope.hasNative) { | |
// normalize | |
document.register = document.register || document.webkitRegister; | |
var nop = function() {}; | |
// exports | |
scope.registry = {}; | |
scope.upgradeElement = nop; | |
} else { | |
/** | |
* Registers a custom tag name with the document. | |
* | |
* When a registered element is created, a `readyCallback` method is called | |
* in the scope of the element. The `readyCallback` method can be specified on | |
* either `inOptions.prototype` or `inOptions.lifecycle` with the latter taking | |
* precedence. | |
* | |
* @method register | |
* @param {String} inName The tag name to register. Must include a dash ('-'), | |
* for example 'x-component'. | |
* @param {Object} inOptions | |
* @param {String} [inOptions.extends] | |
* (_off spec_) Tag name of an element to extend (or blank for a new | |
* element). This parameter is not part of the specification, but instead | |
* is a hint for the polyfill because the extendee is difficult to infer. | |
* Remember that the input prototype must chain to the extended element's | |
* prototype (or HTMLElement.prototype) regardless of the value of | |
* `extends`. | |
* @param {Object} inOptions.prototype The prototype to use for the new | |
* element. The prototype must inherit from HTMLElement. | |
* @param {Object} [inOptions.lifecycle] | |
* Callbacks that fire at important phases in the life of the custom | |
* element. | |
* | |
* @example | |
* FancyButton = document.register("fancy-button", { | |
* extends: 'button', | |
* prototype: Object.create(HTMLButtonElement.prototype, { | |
* readyCallback: { | |
* value: function() { | |
* console.log("a fancy-button was created", | |
* } | |
* } | |
* }) | |
* }); | |
* @return {Function} Constructor for the newly registered type. | |
*/ | |
function register(inName, inOptions) { | |
//console.warn('document.register("' + inName + '", ', inOptions, ')'); | |
// construct a defintion out of options | |
// TODO(sjmiles): probably should clone inOptions instead of mutating it | |
var definition = inOptions || {}; | |
if (!inName) { | |
// TODO(sjmiles): replace with more appropriate error (Erik can probably | |
// offer guidance) | |
throw new Error('Name argument must not be empty'); | |
} | |
// record name | |
definition.name = inName; | |
// must have a prototype, default to an extension of HTMLElement | |
// TODO(sjmiles): probably should throw if no prototype, check spec | |
if (!definition.prototype) { | |
// TODO(sjmiles): replace with more appropriate error (Erik can probably | |
// offer guidance) | |
throw new Error('Options missing required prototype property'); | |
} | |
// ensure a lifecycle object so we don't have to null test it | |
definition.lifecycle = definition.lifecycle || {}; | |
// build a list of ancestral custom elements (for native base detection) | |
// TODO(sjmiles): we used to need to store this, but current code only | |
// uses it in 'resolveTagName': it should probably be inlined | |
definition.ancestry = ancestry(definition.extends); | |
// extensions of native specializations of HTMLElement require localName | |
// to remain native, and use secondary 'is' specifier for extension type | |
resolveTagName(definition); | |
// some platforms require modifications to the user-supplied prototype | |
// chain | |
resolvePrototypeChain(definition); | |
// overrides to implement callbacks | |
// TODO(sjmiles): should support access via .attributes NamedNodeMap | |
definition.prototype.setAttribute = setAttribute; | |
definition.prototype.removeAttribute = removeAttribute; | |
// 7.1.5: Register the DEFINITION with DOCUMENT | |
registerDefinition(inName, definition); | |
// 7.1.7. Run custom element constructor generation algorithm with PROTOTYPE | |
// 7.1.8. Return the output of the previous step. | |
definition.ctor = generateConstructor(definition); | |
definition.ctor.prototype = definition.prototype; | |
// if initial parsing is complete | |
if (scope.ready) { | |
// upgrade any pre-existing nodes of this type | |
scope.upgradeAll(document); | |
} | |
return definition.ctor; | |
} | |
function ancestry(inExtends) { | |
var extendee = registry[inExtends]; | |
if (extendee) { | |
return ancestry(extendee.extends).concat([extendee]); | |
} | |
return []; | |
} | |
function resolveTagName(inDefinition) { | |
// if we are explicitly extending something, that thing is our | |
// baseTag, unless it represents a custom component | |
var baseTag = inDefinition.extends; | |
// if our ancestry includes custom components, we only have a | |
// baseTag if one of them does | |
for (var i=0, a; (a=inDefinition.ancestry[i]); i++) { | |
baseTag = a.is && a.tag; | |
} | |
// our tag is our baseTag, if it exists, and otherwise just our name | |
inDefinition.tag = baseTag || inDefinition.name; | |
if (baseTag) { | |
// if there is a base tag, use secondary 'is' specifier | |
inDefinition.is = inDefinition.name; | |
} | |
} | |
function resolvePrototypeChain(inDefinition) { | |
// if we don't support __proto__ we need to locate the native level | |
// prototype for precise mixing in | |
if (!Object.__proto__) { | |
if (inDefinition.is) { | |
// for non-trivial extensions, work out both prototypes | |
var inst = document.createElement(inDefinition.tag); | |
var native = Object.getPrototypeOf(inst); | |
} else { | |
// otherwise, use the default | |
native = HTMLElement.prototype; | |
} | |
} | |
// cache this in case of mixin | |
inDefinition.native = native; | |
} | |
// SECTION 4 | |
function instantiate(inDefinition) { | |
// 4.a.1. Create a new object that implements PROTOTYPE | |
// 4.a.2. Let ELEMENT by this new object | |
// | |
// the custom element instantiation algorithm must also ensure that the | |
// output is a valid DOM element with the proper wrapper in place. | |
// | |
return upgrade(domCreateElement(inDefinition.tag), inDefinition); | |
} | |
function upgrade(inElement, inDefinition) { | |
// some definitions specify an 'is' attribute | |
if (inDefinition.is) { | |
inElement.setAttribute('is', inDefinition.is); | |
} | |
// make 'element' implement inDefinition.prototype | |
implement(inElement, inDefinition); | |
// flag as upgraded | |
inElement.__upgraded__ = true; | |
// there should never be a shadow root on inElement at this point | |
// we require child nodes be upgraded before ready | |
scope.upgradeSubtree(inElement); | |
// lifecycle management | |
ready(inElement); | |
// OUTPUT | |
return inElement; | |
} | |
function implement(inElement, inDefinition) { | |
// prototype swizzling is best | |
if (Object.__proto__) { | |
inElement.__proto__ = inDefinition.prototype; | |
} else { | |
// where above we can re-acquire inPrototype via | |
// getPrototypeOf(Element), we cannot do so when | |
// we use mixin, so we install a magic reference | |
customMixin(inElement, inDefinition.prototype, inDefinition.native); | |
inElement.__proto__ = inDefinition.prototype; | |
} | |
} | |
function customMixin(inTarget, inSrc, inNative) { | |
// TODO(sjmiles): 'used' allows us to only copy the 'youngest' version of | |
// any property. This set should be precalculated. We also need to | |
// consider this for supporting 'super'. | |
var used = {}; | |
// start with inSrc | |
var p = inSrc; | |
// sometimes the default is HTMLUnknownElement.prototype instead of | |
// HTMLElement.prototype, so we add a test | |
// the idea is to avoid mixing in native prototypes, so adding | |
// the second test is WLOG | |
while (p !== inNative && p !== HTMLUnknownElement.prototype) { | |
var keys = Object.getOwnPropertyNames(p); | |
for (var i=0, k; k=keys[i]; i++) { | |
if (!used[k]) { | |
Object.defineProperty(inTarget, k, | |
Object.getOwnPropertyDescriptor(p, k)); | |
used[k] = 1; | |
} | |
} | |
p = Object.getPrototypeOf(p); | |
} | |
} | |
function ready(inElement) { | |
// invoke readyCallback | |
if (inElement.readyCallback) { | |
inElement.readyCallback(); | |
} | |
} | |
// attribute watching | |
var originalSetAttribute = HTMLElement.prototype.setAttribute; | |
var originalRemoveAttribute = HTMLElement.prototype.removeAttribute; | |
function setAttribute(name, value) { | |
changeAttribute.call(this, name, value, originalSetAttribute); | |
} | |
function removeAttribute(name, value) { | |
changeAttribute.call(this, name, value, originalRemoveAttribute); | |
} | |
function changeAttribute(name, value, operation) { | |
var oldValue = this.getAttribute(name); | |
operation.apply(this, arguments); | |
if (this.attributeChangedCallback | |
&& (this.getAttribute(name) !== oldValue)) { | |
this.attributeChangedCallback(name, oldValue); | |
} | |
} | |
// element registry (maps tag names to definitions) | |
var registry = {}; | |
function registerDefinition(inName, inDefinition) { | |
registry[inName] = inDefinition; | |
} | |
function generateConstructor(inDefinition) { | |
return function() { | |
return instantiate(inDefinition); | |
}; | |
} | |
function createElement(inTag) { | |
var definition = registry[inTag]; | |
if (definition) { | |
return new definition.ctor(); | |
} | |
return domCreateElement(inTag); | |
} | |
function upgradeElement(inElement) { | |
if (!inElement.__upgraded__ && (inElement.nodeType === Node.ELEMENT_NODE)) { | |
var type = inElement.getAttribute('is') || inElement.localName; | |
var definition = registry[type]; | |
return definition && upgrade(inElement, definition); | |
} | |
} | |
// capture native createElement before we override it | |
var domCreateElement = document.createElement.bind(document); | |
// exports | |
document.register = register; | |
document.createElement = createElement; // override | |
scope.registry = registry; | |
/** | |
* Upgrade an element to a custom element. Upgrading an element | |
* causes the custom prototype to be applied, an `is` attribute | |
* to be attached (as needed), and invocation of the `readyCallback`. | |
* `upgrade` does nothing if the element is already upgraded, or | |
* if it matches no registered custom tag name. | |
* | |
* @method ugprade | |
* @param {Element} inElement The element to upgrade. | |
* @return {Element} The upgraded element. | |
*/ | |
scope.upgrade = upgradeElement; | |
} | |
})(window.CustomElements); | |
/* | |
Copyright 2013 The Polymer Authors. All rights reserved. | |
Use of this source code is governed by a BSD-style | |
license that can be found in the LICENSE file. | |
*/ | |
(function(scope){ | |
/* | |
if (HTMLElement.prototype.webkitShadowRoot) { | |
Object.defineProperty(HTMLElement.prototype, 'shadowRoot', { | |
get: function() { | |
return this.webkitShadowRoot; | |
} | |
}; | |
} | |
*/ | |
// walk the subtree rooted at node, applying 'find(element, data)' function | |
// to each element | |
// if 'find' returns true for 'element', do not search element's subtree | |
function findAll(node, find, data) { | |
var e = node.firstElementChild; | |
if (!e) { | |
e = node.firstChild; | |
while (e && e.nodeType !== Node.ELEMENT_NODE) { | |
e = e.nextSibling; | |
} | |
} | |
while (e) { | |
if (find(e, data) !== true) { | |
findAll(e, find, data); | |
} | |
e = e.nextElementSibling; | |
} | |
return null; | |
} | |
// walk the subtree rooted at node, including descent into shadow-roots, | |
// applying 'cb' to each element | |
function forSubtree(node, cb) { | |
//logFlags.dom && node.childNodes && node.childNodes.length && console.group('subTree: ', node); | |
findAll(node, function(e) { | |
if (cb(e)) { | |
return true; | |
} | |
if (e.webkitShadowRoot) { | |
forSubtree(e.webkitShadowRoot, cb); | |
} | |
}); | |
if (node.webkitShadowRoot) { | |
forSubtree(node.webkitShadowRoot, cb); | |
} | |
//logFlags.dom && node.childNodes && node.childNodes.length && console.groupEnd(); | |
} | |
// manage lifecycle on added node | |
function added(node) { | |
if (upgrade(node)) { | |
insertedNode(node); | |
return true; | |
} | |
inserted(node); | |
} | |
// manage lifecycle on added node's subtree only | |
function addedSubtree(node) { | |
forSubtree(node, function(e) { | |
if (added(e)) { | |
return true; | |
} | |
}); | |
} | |
// manage lifecycle on added node and it's subtree | |
function addedNode(node) { | |
return added(node) || addedSubtree(node); | |
} | |
// upgrade custom elements at node, if applicable | |
function upgrade(node) { | |
if (!node.__upgraded__ && node.nodeType === Node.ELEMENT_NODE) { | |
var type = node.getAttribute('is') || node.localName; | |
var definition = scope.registry[type]; | |
if (definition) { | |
logFlags.dom && console.group('upgrade:', node.localName); | |
scope.upgrade(node); | |
logFlags.dom && console.groupEnd(); | |
return true; | |
} | |
} | |
} | |
function insertedNode(node) { | |
inserted(node); | |
if (inDocument(node)) { | |
forSubtree(node, function(e) { | |
inserted(e); | |
}); | |
} | |
} | |
// TODO(sjmiles): if there are descents into trees that can never have inDocument(*) true, fix this | |
function inserted(element) { | |
// TODO(sjmiles): it's possible we were inserted and removed in the space | |
// of one microtask, in which case we won't be 'inDocument' here | |
// But there are other cases where we are testing for inserted without | |
// specific knowledge of mutations, and must test 'inDocument' to determine | |
// whether to call inserted | |
// If we can factor these cases into separate code paths we can have | |
// better diagnostics. | |
// TODO(sjmiles): when logging, do work on all custom elements so we can | |
// track behavior even when callbacks not defined | |
//console.log('inserted: ', element.localName); | |
if (element.insertedCallback || (element.__upgraded__ && logFlags.dom)) { | |
logFlags.dom && console.group('inserted:', element.localName); | |
if (inDocument(element)) { | |
element.__inserted = (element.__inserted || 0) + 1; | |
// if we are in a 'removed' state, bluntly adjust to an 'inserted' state | |
if (element.__inserted < 1) { | |
element.__inserted = 1; | |
} | |
// if we are 'over inserted', squelch the callback | |
if (element.__inserted > 1) { | |
logFlags.dom && console.warn('inserted:', element.localName, | |
'insert/remove count:', element.__inserted) | |
} else if (element.insertedCallback) { | |
logFlags.dom && console.log('inserted:', element.localName); | |
element.insertedCallback(); | |
} | |
} | |
logFlags.dom && console.groupEnd(); | |
} | |
} | |
function removedNode(node) { | |
removed(node); | |
forSubtree(node, function(e) { | |
removed(e); | |
}); | |
} | |
function removed(element) { | |
// TODO(sjmiles): temporary: do work on all custom elements so we can track | |
// behavior even when callbacks not defined | |
if (element.removedCallback || (element.__upgraded__ && logFlags.dom)) { | |
logFlags.dom && console.log('removed:', element.localName); | |
if (!inDocument(element)) { | |
element.__inserted = (element.__inserted || 0) - 1; | |
// if we are in a 'inserted' state, bluntly adjust to an 'removed' state | |
if (element.__inserted > 0) { | |
element.__inserted = 0; | |
} | |
// if we are 'over removed', squelch the callback | |
if (element.__inserted < 0) { | |
logFlags.dom && console.warn('removed:', element.localName, | |
'insert/remove count:', element.__inserted) | |
} else if (element.removedCallback) { | |
element.removedCallback(); | |
} | |
} | |
} | |
} | |
function inDocument(element) { | |
var p = element; | |
while (p) { | |
if (p == element.ownerDocument) { | |
return true; | |
} | |
p = p.parentNode || p.host; | |
} | |
} | |
function watchShadow(node) { | |
if (node.webkitShadowRoot && !node.webkitShadowRoot.__watched) { | |
logFlags.dom && console.log('watching shadow-root for: ', node.localName); | |
observe(node.webkitShadowRoot); | |
node.webkitShadowRoot.__watched = true; | |
} | |
} | |
function watchAllShadows(node) { | |
watchShadow(node); | |
forSubtree(node, function(e) { | |
watchShadow(node); | |
}); | |
} | |
function filter(inNode) { | |
switch (inNode.localName) { | |
case 'style': | |
case 'script': | |
case 'template': | |
case undefined: | |
return true; | |
} | |
} | |
function handler(mutations) { | |
// | |
if (logFlags.dom) { | |
var mx = mutations[0]; | |
if (mx && mx.type === 'childList' && mx.addedNodes) { | |
if (mx.addedNodes) { | |
var d = mx.addedNodes[0]; | |
while (d && d !== document && !d.host) { | |
d = d.parentNode; | |
} | |
var u = d && (d.URL || d._URL || (d.host && d.host.localName)) || ''; | |
u = u.split('/?').shift().split('/').pop(); | |
} | |
} | |
console.group('mutations (%d) [%s]', mutations.length, u || ''); | |
} | |
// | |
mutations.forEach(function(mx) { | |
//logFlags.dom && console.group('mutation'); | |
if (mx.type === 'childList') { | |
forEach(mx.addedNodes, function(n) { | |
//logFlags.dom && console.log(n.localName); | |
if (filter(n)) { | |
return; | |
} | |
// watch shadow-roots on nodes that have had them attached manually | |
// TODO(sjmiles): remove if createShadowRoot is overridden | |
// TODO(sjmiles): removed as an optimization, manual shadow roots | |
// must be watched explicitly | |
//watchAllShadows(n); | |
// nodes added may need lifecycle management | |
addedNode(n); | |
}); | |
// removed nodes may need lifecycle management | |
forEach(mx.removedNodes, function(n) { | |
//logFlags.dom && console.log(n.localName); | |
if (filter(n)) { | |
return; | |
} | |
removedNode(n); | |
}); | |
} | |
//logFlags.dom && console.groupEnd(); | |
}); | |
logFlags.dom && console.groupEnd(); | |
}; | |
var observer = new MutationObserver(handler); | |
function takeRecords() { | |
// TODO(sjmiles): ask Raf why we have to call handler ourselves | |
handler(observer.takeRecords()); | |
} | |
var forEach = Array.prototype.forEach.call.bind(Array.prototype.forEach); | |
function observe(inRoot) { | |
observer.observe(inRoot, {childList: true, subtree: true}); | |
} | |
function observeDocument(document) { | |
observe(document); | |
} | |
function upgradeDocument(document) { | |
logFlags.dom && console.group('upgradeDocument: ', (document.URL || document._URL || '').split('/').pop()); | |
addedNode(document); | |
logFlags.dom && console.groupEnd(); | |
} | |
// exports | |
scope.watchShadow = watchShadow; | |
scope.watchAllShadows = watchAllShadows; | |
scope.upgradeAll = addedNode; | |
scope.upgradeSubtree = addedSubtree; | |
scope.observeDocument = observeDocument; | |
scope.upgradeDocument = upgradeDocument; | |
scope.takeRecords = takeRecords; | |
})(window.CustomElements); | |
/* | |
* Copyright 2013 The Polymer Authors. All rights reserved. | |
* Use of this source code is governed by a BSD-style | |
* license that can be found in the LICENSE file. | |
*/ | |
(function(){ | |
var HTMLElementElement = function(inElement) { | |
inElement.register = HTMLElementElement.prototype.register; | |
parseElementElement(inElement); | |
return inElement; | |
}; | |
HTMLElementElement.prototype = { | |
register: function(inMore) { | |
if (inMore) { | |
this.options.lifecycle = inMore.lifecycle; | |
if (inMore.prototype) { | |
mixin(this.options.prototype, inMore.prototype); | |
} | |
} | |
} | |
}; | |
function parseElementElement(inElement) { | |
// options to glean from inElement attributes | |
var options = { | |
name: '', | |
extends: null | |
}; | |
// glean them | |
takeAttributes(inElement, options); | |
// default base | |
var base = HTMLElement.prototype; | |
// optional specified base | |
if (options.extends) { | |
// build an instance of options.extends | |
var archetype = document.createElement(options.extends); | |
// acquire the prototype | |
// TODO(sjmiles): __proto__ may be hinted by the custom element | |
// system on platforms that don't support native __proto__ | |
// on those platforms the API is mixed into archetype and the | |
// effective base is not archetype's real prototype | |
base = archetype.__proto__ || Object.getPrototypeOf(archetype); | |
} | |
// extend base | |
options.prototype = Object.create(base); | |
// install options | |
inElement.options = options; | |
// locate user script | |
var script = inElement.querySelector('script,scripts'); | |
if (script) { | |
// execute user script in 'inElement' context | |
executeComponentScript(script.textContent, inElement, options.name); | |
}; | |
// register our new element | |
var ctor = document.register(options.name, options); | |
inElement.ctor = ctor; | |
// store optional constructor reference | |
var refName = inElement.getAttribute('constructor'); | |
if (refName) { | |
window[refName] = ctor; | |
} | |
} | |
// each property in inDictionary takes a value | |
// from the matching attribute in inElement, if any | |
function takeAttributes(inElement, inDictionary) { | |
for (var n in inDictionary) { | |
var a = inElement.attributes[n]; | |
if (a) { | |
inDictionary[n] = a.value; | |
} | |
} | |
} | |
// invoke inScript in inContext scope | |
function executeComponentScript(inScript, inContext, inName) { | |
// set (highlander) context | |
context = inContext; | |
// source location | |
var owner = context.ownerDocument; | |
var url = (owner._URL || owner.URL || owner.impl | |
&& (owner.impl._URL || owner.impl.URL)); | |
// ensure the component has a unique source map so it can be debugged | |
// if the name matches the filename part of the owning document's url, | |
// use this, otherwise, add ":<name>" to the document url. | |
var match = url.match(/.*\/([^.]*)[.]?.*$/); | |
if (match) { | |
var name = match[1]; | |
url += name != inName ? ':' + inName : ''; | |
} | |
// compose script | |
var code = "__componentScript('" | |
+ inName | |
+ "', function(){" | |
+ inScript | |
+ "});" | |
+ "\n//@ sourceURL=" + url + "\n" | |
; | |
// inject script | |
eval(code); | |
} | |
var context; | |
// global necessary for script injection | |
window.__componentScript = function(inName, inFunc) { | |
inFunc.call(context); | |
}; | |
// utility | |
// 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 | |
window.HTMLElementElement = HTMLElementElement; | |
// TODO(sjmiles): completely ad-hoc, used by Polymer.register | |
window.mixin = mixin; | |
})(); | |
/* | |
* Copyright 2013 The Polymer Authors. All rights reserved. | |
* Use of this source code is governed by a BSD-style | |
* license that can be found in the LICENSE file. | |
*/ | |
(function() { | |
var IMPORT_LINK_TYPE = 'import'; | |
// highlander object for parsing a document tree | |
var componentParser = { | |
selectors: [ | |
'link[rel=' + IMPORT_LINK_TYPE + ']', | |
'link[rel=stylesheet]', | |
'script[src]', | |
'script', | |
'style', | |
'element' | |
], | |
map: { | |
link: 'parseLink', | |
script: 'parseScript', | |
element: 'parseElement', | |
style: 'parseStyle' | |
}, | |
parse: function(inDocument) { | |
if (!inDocument.__parsed) { | |
// only parse once | |
inDocument.__parsed = true; | |
// all parsable elements in inDocument (depth-first pre-order traversal) | |
var elts = inDocument.querySelectorAll(cp.selectors); | |
// for each parsable node type, call the mapped parsing method | |
forEach(elts, function(e) { | |
//console.log(map[e.localName] + ":", path.nodeUrl(e)); | |
cp[cp.map[e.localName]](e); | |
}); | |
// upgrade all upgradeable static elements, anything dynamically | |
// created should be caught by observer | |
CustomElements.upgradeDocument(inDocument); | |
// observe document for dom changes | |
CustomElements.observeDocument(inDocument); | |
} | |
}, | |
parseLink: function(inLinkElt) { | |
// imports | |
if (isDocumentLink(inLinkElt)) { | |
if (inLinkElt.content) { | |
cp.parse(inLinkElt.content); | |
} | |
} else if (!inMainDocument(inLinkElt) | |
&& inLinkElt.parentNode | |
&& !isElementElementChild(inLinkElt)) { | |
document.head.appendChild(inLinkElt); | |
} | |
}, | |
parseScript: function(inScriptElt) { | |
// ignore scripts in primary document, they are already loaded | |
if (inMainDocument(inScriptElt)) { | |
return; | |
} | |
// ignore scripts inside <element> | |
if (isElementElementChild(inScriptElt)) { | |
return; | |
} | |
// otherwise, evaluate now | |
var code = inScriptElt.__resource || inScriptElt.textContent; | |
if (code) { | |
code += "\n//@ sourceURL=" + inScriptElt.__nodeUrl + "\n"; | |
eval.call(window, code); | |
} | |
}, | |
parseStyle: function(inStyleElt) { | |
if (!inMainDocument(inStyleElt) && !isElementElementChild(inStyleElt)) { | |
document.querySelector('head').appendChild(inStyleElt); | |
} | |
}, | |
parseElement: function(inElementElt) { | |
new HTMLElementElement(inElementElt); | |
} | |
}; | |
var cp = componentParser; | |
function inMainDocument(inElt) { | |
return inElt.ownerDocument === document || | |
// TODO(sjmiles): ShadowDOMPolyfill intrusion | |
inElt.ownerDocument.impl === document; | |
} | |
function isDocumentLink(inElt) { | |
return (inElt.localName === 'link' | |
&& inElt.getAttribute('rel') === IMPORT_LINK_TYPE); | |
} | |
function isElementElementChild(inElt) { | |
if (inElt.parentNode && inElt.parentNode.localName === 'element') { | |
return true; | |
} | |
} | |
var forEach = Array.prototype.forEach.call.bind(Array.prototype.forEach); | |
// exports | |
CustomElements.parser = componentParser; | |
})(); | |
/* | |
* Copyright 2013 The Polymer Authors. All rights reserved. | |
* Use of this source code is governed by a BSD-style | |
* license that can be found in the LICENSE file. | |
*/ | |
(function(){ | |
// bootstrap parsing | |
// IE shim for CustomEvent | |
if (typeof window.CustomEvent !== 'function') { | |
window.CustomEvent = function(inType) { | |
var e = document.createEvent('HTMLEvents'); | |
e.initEvent(inType, true, true); | |
return e; | |
}; | |
} | |
function bootstrap() { | |
// go async so call stack can unwind | |
setTimeout(function() { | |
// parse document | |
CustomElements.parser.parse(document); | |
// set internal flag | |
CustomElements.ready = true; | |
CustomElements.readyTime = new Date().getTime(); | |
if (window.HTMLImports) { | |
CustomElements.elapsed = CustomElements.readyTime - HTMLImports.readyTime; | |
} | |
// notify system | |
document.body.dispatchEvent( | |
new CustomEvent('WebComponentsReady', {bubbles: true}) | |
); | |
}, 0); | |
} | |
// TODO(sjmiles): 'window' has no wrappability under ShadowDOM polyfill, so | |
// we are forced to split into two versions | |
if (window.HTMLImports) { | |
document.addEventListener('HTMLImportsLoaded', bootstrap); | |
} else { | |
window.addEventListener('load', bootstrap); | |
} | |
})(); | |
/* | |
* Copyright 2013 The Polymer Authors. All rights reserved. | |
* Use of this source code is governed by a BSD-style | |
* license that can be found in the LICENSE file. | |
*/ | |
(function() { | |
// inject style sheet | |
document.write('<style>element {display: none;} /* injected by platform.js */</style>'); | |
if (window.ShadowDOMPolyfill) { | |
function nop() {}; | |
// disable shadow dom watching | |
CustomElements.watchShadow = nop; | |
CustomElements.watchAllShadows = nop; | |
// ensure wrapped inputs for these functions | |
var fns = ['upgradeAll', 'upgradeSubtree', 'observeDocument', | |
'upgradeDocument']; | |
// cache originals | |
var original = {}; | |
fns.forEach(function(fn) { | |
original[fn] = CustomElements[fn]; | |
}); | |
// override | |
fns.forEach(function(fn) { | |
CustomElements[fn] = function(inNode) { | |
return original[fn](wrap(inNode)); | |
}; | |
}); | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment