Files
sdk/pkg/custom_element/lib/custom-elements.debug.js
T
blois@google.com 6def065afd Revert "Dart2js support for custom element lifecycle events"
This reverts commit 27729. Shadow DOM polyfill appears to have regressed some tests and some issues are coming up with the browser controller.

BUG=
R=jmesserly@google.com

Review URL: https://codereview.chromium.org//24251016

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@27730 260f80e4-7a28-3924-810f-c04153c831b5
2013-09-21 00:46:48 +00:00

1468 lines
43 KiB
JavaScript

// Copyright (c) 2012 The Polymer Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
(function() {
var scope = window.PolymerLoader = {};
var flags = {};
// convert url arguments to flags
if (!flags.noOpts) {
location.search.slice(1).split('&').forEach(function(o) {
o = o.split('=');
o[0] && (flags[o[0]] = o[1] || true);
});
}
// process global logFlags
parseLogFlags(flags);
function load(scopeName) {
// imports
var scope = window[scopeName];
var entryPointName = scope.entryPointName;
var processFlags = scope.processFlags;
// acquire attributes and base path from entry point
var entryPoint = findScript(entryPointName);
var base = entryPoint.basePath;
// acquire common flags
var flags = scope.flags;
// convert attributes to flags
var flags = PolymerLoader.flags;
for (var i=0, a; (a=entryPoint.attributes[i]); i++) {
if (a.name !== 'src') {
flags[a.name] = a.value || true;
}
}
// parse log flags into global
parseLogFlags(flags);
// exports
scope.basePath = base;
scope.flags = flags;
// process flags for dynamic dependencies
if (processFlags) {
processFlags.call(scope, flags);
}
// post-process imports
var modules = scope.modules || [];
var sheets = scope.sheets || [];
// write script tags for dependencies
modules.forEach(function(src) {
document.write('<script src="' + base + src + '"></script>');
});
// write link tags for styles
sheets.forEach(function(src) {
document.write('<link rel="stylesheet" href="' + base + src + '">');
});
}
// utility method
function findScript(fileName) {
var script = document.querySelector('script[src*="' + fileName + '"]');
var src = script.attributes.src.value;
script.basePath = src.slice(0, src.indexOf(fileName));
return script;
}
function parseLogFlags(flags) {
var logFlags = window.logFlags = window.logFlags || {};
if (flags.log) {
flags.log.split(',').forEach(function(f) {
logFlags[f] = true;
});
}
}
scope.flags = flags;
scope.load = load;
})();
window.CustomElements = {flags:{}};
// 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 counter = Date.now() % 1e9;
SideTable = function() {
this.name = '__st' + (Math.random() * 1e9 >>> 0) + (counter++ + '__');
};
SideTable.prototype = {
set: function(key, value) {
var entry = key[this.name];
if (entry && entry[0] === key)
entry[1] = value;
else
defineProperty(key, this.name, {value: [key, value], writable: true});
},
get: function(key) {
var entry;
return (entry = key[this.name]) && entry[0] === key ?
entry[1] : undefined;
},
delete: function(key) {
this.set(key, undefined);
}
}
})();
}
(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);
if (!window.MutationObserver) {
window.MutationObserver =
window.WebKitMutationObserver ||
window.JsMutationObserver;
if (!MutationObserver) {
throw new Error("no mutation observer support");
}
}
(function(scope){
var logFlags = window.logFlags || {};
// 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 all shadowRoots on a given node.
function forRoots(node, cb) {
var root = node.webkitShadowRoot;
while(root) {
forSubtree(root, cb);
root = root.olderShadowRoot;
}
}
// 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;
}
forRoots(e, cb);
});
forRoots(node, 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.enteredViewCallback || (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.enteredViewCallback) {
logFlags.dom && console.log('inserted:', element.localName);
element.enteredViewCallback();
}
}
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.leftViewCallback || (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.leftViewCallback) {
element.leftViewCallback();
}
}
}
}
function inDocument(element) {
var p = element;
var doc = window.ShadowDOMPolyfill &&
window.ShadowDOMPolyfill.wrapIfNeeded(document) || document;
while (p) {
if (p == doc) {
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);
// watch all unwatched roots...
var root = node.webkitShadowRoot;
while (root) {
watchRoot(root);
root = root.olderShadowRoot;
}
}
}
function watchRoot(root) {
if (!root.__watched) {
observe(root);
root.__watched = true;
}
}
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;
}
// 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.upgradeAll = addedNode;
scope.upgradeSubtree = addedSubtree;
scope.observeDocument = observeDocument;
scope.upgradeDocument = upgradeDocument;
scope.takeRecords = takeRecords;
})(window.CustomElements);
/**
* Implements `document.register`
* @module CustomElements
*/
/**
* Polyfilled extensions to the `document` object.
* @class Document
*/
(function(scope) {
// imports
if (!scope) {
scope = window.CustomElements = {flags:{}};
}
var flags = scope.flags;
// native document.register?
var hasNative = Boolean(document.register);
var useNative = !flags.register && hasNative;
if (useNative) {
// stub
var nop = function() {};
// exports
scope.registry = {};
scope.upgradeElement = nop;
scope.watchShadow = nop;
scope.upgrade = nop;
scope.upgradeAll = nop;
scope.upgradeSubtree = nop;
scope.observeDocument = nop;
scope.upgradeDocument = nop;
scope.takeRecords = 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 `options.prototype` or `options.lifecycle` with the latter taking
* precedence.
*
* @method register
* @param {String} name The tag name to register. Must include a dash ('-'),
* for example 'x-component'.
* @param {Object} options
* @param {String} [options.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} options.prototype The prototype to use for the new
* element. The prototype must inherit from HTMLElement.
* @param {Object} [options.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(name, options) {
//console.warn('document.register("' + name + '", ', options, ')');
// construct a defintion out of options
// TODO(sjmiles): probably should clone options instead of mutating it
var definition = options || {};
if (!name) {
// TODO(sjmiles): replace with more appropriate error (EricB can probably
// offer guidance)
throw new Error('document.register: first argument `name` must not be empty');
}
if (name.indexOf('-') < 0) {
// TODO(sjmiles): replace with more appropriate error (EricB can probably
// offer guidance)
throw new Error('document.register: first argument (\'name\') must contain a dash (\'-\'). Argument provided was \'' + String(name) + '\'.');
}
// record name
definition.name = name;
// 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 (EricB 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 attributeChanged callback
overrideAttributeApi(definition.prototype);
// 7.1.5: Register the DEFINITION with DOCUMENT
registerDefinition(name, 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;
// force our .constructor to be our actual constructor
definition.prototype.constructor = definition.ctor;
// if initial parsing is complete
if (scope.ready) {
// upgrade any pre-existing nodes of this type
scope.upgradeAll(document);
}
return definition.ctor;
}
function ancestry(extnds) {
var extendee = registry[extnds];
if (extendee) {
return ancestry(extendee.extends).concat([extendee]);
}
return [];
}
function resolveTagName(definition) {
// if we are explicitly extending something, that thing is our
// baseTag, unless it represents a custom component
var baseTag = definition.extends;
// if our ancestry includes custom components, we only have a
// baseTag if one of them does
for (var i=0, a; (a=definition.ancestry[i]); i++) {
baseTag = a.is && a.tag;
}
// our tag is our baseTag, if it exists, and otherwise just our name
definition.tag = baseTag || definition.name;
if (baseTag) {
// if there is a base tag, use secondary 'is' specifier
definition.is = definition.name;
}
}
function resolvePrototypeChain(definition) {
// if we don't support __proto__ we need to locate the native level
// prototype for precise mixing in
if (!Object.__proto__) {
// default prototype
var nativePrototype = HTMLElement.prototype;
// work out prototype when using type-extension
if (definition.is) {
var inst = document.createElement(definition.tag);
nativePrototype = Object.getPrototypeOf(inst);
}
// ensure __proto__ reference is installed at each point on the prototype
// chain.
// NOTE: On platforms without __proto__, a mixin strategy is used instead
// of prototype swizzling. In this case, this generated __proto__ provides
// limited support for prototype traversal.
var proto = definition.prototype, ancestor;
while (proto && (proto !== nativePrototype)) {
var ancestor = Object.getPrototypeOf(proto);
proto.__proto__ = ancestor;
proto = ancestor;
}
}
// cache this in case of mixin
definition.native = nativePrototype;
}
// SECTION 4
function instantiate(definition) {
// 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(definition.tag), definition);
}
function upgrade(element, definition) {
// some definitions specify an 'is' attribute
if (definition.is) {
element.setAttribute('is', definition.is);
}
// make 'element' implement definition.prototype
implement(element, definition);
// flag as upgraded
element.__upgraded__ = true;
// there should never be a shadow root on element at this point
// we require child nodes be upgraded before `created`
scope.upgradeSubtree(element);
// lifecycle management
created(element);
// OUTPUT
return element;
}
function implement(element, definition) {
// prototype swizzling is best
if (Object.__proto__) {
element.__proto__ = definition.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(element, definition.prototype, definition.native);
element.__proto__ = definition.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 created(element) {
// invoke createdCallback
if (element.createdCallback) {
element.createdCallback();
}
}
// attribute watching
function overrideAttributeApi(prototype) {
// overrides to implement callbacks
// TODO(sjmiles): should support access via .attributes NamedNodeMap
// TODO(sjmiles): preserves user defined overrides, if any
var setAttribute = prototype.setAttribute;
prototype.setAttribute = function(name, value) {
changeAttribute.call(this, name, value, setAttribute);
}
var removeAttribute = prototype.removeAttribute;
prototype.removeAttribute = function(name, value) {
changeAttribute.call(this, name, value, removeAttribute);
}
}
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(name, definition) {
if (registry[name]) {
throw new Error('Cannot register a tag more than once');
}
registry[name] = definition;
}
function generateConstructor(definition) {
return function() {
return instantiate(definition);
};
}
function createElement(tag, typeExtension) {
var definition = registry[typeExtension || tag];
if (definition) {
if (tag == definition.tag && typeExtension == definition.is) {
return new definition.ctor();
}
// Handle empty string for type extension.
if (!typeExtension && !definition.is) {
return new definition.ctor();
}
}
if (typeExtension) {
var element = createElement(tag);
element.setAttribute('is', typeExtension);
return element;
}
var element = domCreateElement(tag);
// Custom tags should be HTMLElements even if not upgraded.
if (tag.indexOf('-') >= 0) {
implement(element, HTMLElement);
}
return element;
}
function upgradeElement(element) {
if (!element.__upgraded__ && (element.nodeType === Node.ELEMENT_NODE)) {
var is = element.getAttribute('is');
var definition = registry[is || element.localName];
if (definition) {
if (is && definition.tag == element.localName) {
return upgrade(element, definition);
} else if (!is && !definition.extends) {
return upgrade(element, definition);
}
}
}
}
function cloneNode(deep) {
// call original clone
var n = domCloneNode.call(this, deep);
// upgrade the element and subtree
scope.upgradeAll(n);
// return the clone
return n;
}
// capture native createElement before we override it
var domCreateElement = document.createElement.bind(document);
// capture native cloneNode before we override it
var domCloneNode = Node.prototype.cloneNode;
// exports
document.register = register;
document.createElement = createElement; // override
Node.prototype.cloneNode = cloneNode; // 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} element The element to upgrade.
* @return {Element} The upgraded element.
*/
scope.upgrade = upgradeElement;
}
scope.hasNative = hasNative;
scope.useNative = useNative;
})(window.CustomElements);
(function() {
// import
var IMPORT_LINK_TYPE = window.HTMLImports ? HTMLImports.IMPORT_LINK_TYPE : 'none';
// highlander object for parsing a document tree
var parser = {
selectors: [
'link[rel=' + IMPORT_LINK_TYPE + ']'
],
map: {
link: 'parseLink'
},
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(parser.selectors);
// for each parsable node type, call the mapped parsing method
forEach(elts, function(e) {
parser[parser.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(linkElt) {
// imports
if (isDocumentLink(linkElt)) {
this.parseImport(linkElt);
}
},
parseImport: function(linkElt) {
if (linkElt.content) {
parser.parse(linkElt.content);
}
}
};
function isDocumentLink(inElt) {
return (inElt.localName === 'link'
&& inElt.getAttribute('rel') === IMPORT_LINK_TYPE);
}
var forEach = Array.prototype.forEach.call.bind(Array.prototype.forEach);
// exports
CustomElements.parser = parser;
})();
(function(){
// bootstrap parsing
function bootstrap() {
// parse document
CustomElements.parser.parse(document);
// one more pass before register is 'live'
CustomElements.upgradeDocument(document);
// choose async
var async = window.Platform && Platform.endOfMicrotask ?
Platform.endOfMicrotask :
setTimeout;
async(function() {
// set internal 'ready' flag, now document.register will trigger
// synchronous upgrades
CustomElements.ready = true;
// capture blunt profiling data
CustomElements.readyTime = Date.now();
if (window.HTMLImports) {
CustomElements.elapsed = CustomElements.readyTime - HTMLImports.readyTime;
}
// notify the system that we are bootstrapped
document.body.dispatchEvent(
new CustomEvent('WebComponentsReady', {bubbles: true})
);
});
}
// CustomEvent shim for IE
if (typeof window.CustomEvent !== 'function') {
window.CustomEvent = function(inType) {
var e = document.createEvent('HTMLEvents');
e.initEvent(inType, true, true);
return e;
};
}
if (document.readyState === 'complete') {
bootstrap();
} else {
var loadEvent = window.HTMLImports ? 'HTMLImportsLoaded' : 'DOMContentLoaded';
window.addEventListener(loadEvent, bootstrap);
}
})();