fdc29a9ed5
fixes an issue in the previous package, it was missing MutationObservers which it expects included in the binary generated by Grunt. This did not happen because the sources are pulled in from a relative path, rather than being included in the repository, and there was unfortunately no error issued. R=blois@google.com Review URL: https://codereview.chromium.org//153063003 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@32263 260f80e4-7a28-3924-810f-c04153c831b5
1527 lines
47 KiB
JavaScript
1527 lines
47 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.
|
|
if (typeof WeakMap === 'undefined') {
|
|
(function() {
|
|
var defineProperty = Object.defineProperty;
|
|
var counter = Date.now() % 1e9;
|
|
|
|
var WeakMap = function() {
|
|
this.name = '__st' + (Math.random() * 1e9 >>> 0) + (counter++ + '__');
|
|
};
|
|
|
|
WeakMap.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);
|
|
}
|
|
};
|
|
|
|
window.WeakMap = WeakMap;
|
|
})();
|
|
}
|
|
|
|
(function(global) {
|
|
|
|
var registrationsTable = new WeakMap();
|
|
|
|
// 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;
|
|
|
|
if (!global.MutationObserver)
|
|
global.MutationObserver = JsMutationObserver;
|
|
|
|
|
|
})(this);
|
|
|
|
window.CustomElements = window.CustomElements || {flags:{}};
|
|
(function(scope){
|
|
|
|
var logFlags = window.logFlags || {};
|
|
var IMPORT_LINK_TYPE = window.HTMLImports ? HTMLImports.IMPORT_LINK_TYPE : 'none';
|
|
|
|
// 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.shadowRoot;
|
|
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(sorvell): on platforms without MutationObserver, mutations may not be
|
|
// reliable and therefore attached/detached are not reliable.
|
|
// To make these callbacks less likely to fail, we defer all inserts and removes
|
|
// to give a chance for elements to be inserted into dom.
|
|
// This ensures attachedCallback fires for elements that are created and
|
|
// immediately added to dom.
|
|
var hasPolyfillMutations = (!window.MutationObserver ||
|
|
(window.MutationObserver === window.JsMutationObserver));
|
|
scope.hasPolyfillMutations = hasPolyfillMutations;
|
|
|
|
var isPendingMutations = false;
|
|
var pendingMutations = [];
|
|
function deferMutation(fn) {
|
|
pendingMutations.push(fn);
|
|
if (!isPendingMutations) {
|
|
isPendingMutations = true;
|
|
var async = (window.Platform && window.Platform.endOfMicrotask) ||
|
|
setTimeout;
|
|
async(takeMutations);
|
|
}
|
|
}
|
|
|
|
function takeMutations() {
|
|
isPendingMutations = false;
|
|
var $p = pendingMutations;
|
|
for (var i=0, l=$p.length, p; (i<l) && (p=$p[i]); i++) {
|
|
p();
|
|
}
|
|
pendingMutations = [];
|
|
}
|
|
|
|
function inserted(element) {
|
|
if (hasPolyfillMutations) {
|
|
deferMutation(function() {
|
|
_inserted(element);
|
|
});
|
|
} else {
|
|
_inserted(element);
|
|
}
|
|
}
|
|
|
|
// 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.attachedCallback || element.detachedCallback || (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.attachedCallback) {
|
|
logFlags.dom && console.log('inserted:', element.localName);
|
|
element.attachedCallback();
|
|
}
|
|
}
|
|
logFlags.dom && console.groupEnd();
|
|
}
|
|
}
|
|
|
|
function removedNode(node) {
|
|
removed(node);
|
|
forSubtree(node, function(e) {
|
|
removed(e);
|
|
});
|
|
}
|
|
|
|
function removed(element) {
|
|
if (hasPolyfillMutations) {
|
|
deferMutation(function() {
|
|
_removed(element);
|
|
});
|
|
} else {
|
|
_removed(element);
|
|
}
|
|
}
|
|
|
|
function _removed(element) {
|
|
// TODO(sjmiles): temporary: do work on all custom elements so we can track
|
|
// behavior even when callbacks not defined
|
|
if (element.attachedCallback || element.detachedCallback || (element.__upgraded__ && logFlags.dom)) {
|
|
logFlags.dom && console.group('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.detachedCallback) {
|
|
element.detachedCallback();
|
|
}
|
|
}
|
|
logFlags.dom && console.groupEnd();
|
|
}
|
|
}
|
|
|
|
// SD polyfill intrustion due mainly to the fact that 'document'
|
|
// is not entirely wrapped
|
|
function wrapIfNeeded(node) {
|
|
return window.ShadowDOMPolyfill ? ShadowDOMPolyfill.wrapIfNeeded(node)
|
|
: node;
|
|
}
|
|
|
|
function inDocument(element) {
|
|
var p = element;
|
|
var doc = wrapIfNeeded(document);
|
|
while (p) {
|
|
if (p == doc) {
|
|
return true;
|
|
}
|
|
p = p.parentNode || p.host;
|
|
}
|
|
}
|
|
|
|
function watchShadow(node) {
|
|
if (node.shadowRoot && !node.shadowRoot.__watched) {
|
|
logFlags.dom && console.log('watching shadow-root for: ', node.localName);
|
|
// watch all unwatched roots...
|
|
var root = node.shadowRoot;
|
|
while (root) {
|
|
watchRoot(root);
|
|
root = root.olderShadowRoot;
|
|
}
|
|
}
|
|
}
|
|
|
|
function watchRoot(root) {
|
|
if (!root.__watched) {
|
|
observe(root);
|
|
root.__watched = 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 (!n.localName) {
|
|
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 (!n.localName) {
|
|
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());
|
|
takeMutations();
|
|
}
|
|
|
|
var forEach = Array.prototype.forEach.call.bind(Array.prototype.forEach);
|
|
|
|
function observe(inRoot) {
|
|
observer.observe(inRoot, {childList: true, subtree: true});
|
|
}
|
|
|
|
function observeDocument(doc) {
|
|
observe(doc);
|
|
}
|
|
|
|
function upgradeDocument(doc) {
|
|
logFlags.dom && console.group('upgradeDocument: ', (doc.baseURI).split('/').pop());
|
|
addedNode(doc);
|
|
logFlags.dom && console.groupEnd();
|
|
}
|
|
|
|
function upgradeDocumentTree(doc) {
|
|
doc = wrapIfNeeded(doc);
|
|
upgradeDocument(doc);
|
|
//console.log('upgradeDocumentTree: ', (doc.baseURI).split('/').pop());
|
|
// upgrade contained imported documents
|
|
var imports = doc.querySelectorAll('link[rel=' + IMPORT_LINK_TYPE + ']');
|
|
for (var i=0, l=imports.length, n; (i<l) && (n=imports[i]); i++) {
|
|
if (n.import && n.import.__parsed) {
|
|
upgradeDocumentTree(n.import);
|
|
}
|
|
}
|
|
}
|
|
|
|
// exports
|
|
scope.IMPORT_LINK_TYPE = IMPORT_LINK_TYPE;
|
|
scope.watchShadow = watchShadow;
|
|
scope.upgradeDocumentTree = upgradeDocumentTree;
|
|
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.registerElement?
|
|
|
|
var hasNative = Boolean(document.registerElement);
|
|
// TODO(sorvell): See https://github.com/Polymer/polymer/issues/399
|
|
// we'll address this by defaulting to CE polyfill in the presence of the SD
|
|
// polyfill. This will avoid spamming excess attached/detached callbacks.
|
|
// If there is a compelling need to run CE native with SD polyfill,
|
|
// we'll need to fix this issue.
|
|
var useNative = !flags.register && hasNative && !window.ShadowDOMPolyfill;
|
|
|
|
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.upgradeDocumentTree = 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.registerElement("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.registerElement("' + 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.registerElement: 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.registerElement: first argument (\'name\') must contain a dash (\'-\'). Argument provided was \'' + String(name) + '\'.');
|
|
}
|
|
// elements may only be registered once
|
|
if (getRegisteredDefinition(name)) {
|
|
throw new Error('DuplicateDefinitionError: a type with name \'' + String(name) + '\' is already registered');
|
|
}
|
|
// 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');
|
|
}
|
|
// record name
|
|
definition.__name = name.toLowerCase();
|
|
// 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(definition.__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 || scope.performedInitialDocumentUpgrade) {
|
|
// upgrade any pre-existing nodes of this type
|
|
scope.upgradeDocumentTree(document);
|
|
}
|
|
return definition.ctor;
|
|
}
|
|
|
|
function ancestry(extnds) {
|
|
var extendee = getRegisteredDefinition(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);
|
|
}
|
|
// remove 'unresolved' attr, which is a standin for :unresolved.
|
|
element.removeAttribute('unresolved');
|
|
// make 'element' implement definition.prototype
|
|
implement(element, definition);
|
|
// flag as upgraded
|
|
element.__upgraded__ = true;
|
|
// lifecycle management
|
|
created(element);
|
|
// there should never be a shadow root on element at this point
|
|
// we require child nodes be upgraded before `created`
|
|
scope.upgradeSubtree(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);
|
|
|
|
// Dart note: make sure we pick up the right constructor.
|
|
// dart2js depends on this for dart:mirrors caching to work.
|
|
// See tests/html/custom/mirrors_test.dart
|
|
element.constructor = definition.prototype.constructor;
|
|
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
|
|
if (prototype.setAttribute._polyfilled) {
|
|
return;
|
|
}
|
|
var setAttribute = prototype.setAttribute;
|
|
prototype.setAttribute = function(name, value) {
|
|
changeAttribute.call(this, name, value, setAttribute);
|
|
}
|
|
var removeAttribute = prototype.removeAttribute;
|
|
prototype.removeAttribute = function(name) {
|
|
changeAttribute.call(this, name, null, removeAttribute);
|
|
}
|
|
prototype.setAttribute._polyfilled = true;
|
|
}
|
|
|
|
// https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/custom/
|
|
// index.html#dfn-attribute-changed-callback
|
|
function changeAttribute(name, value, operation) {
|
|
var oldValue = this.getAttribute(name);
|
|
operation.apply(this, arguments);
|
|
var newValue = this.getAttribute(name);
|
|
if (this.attributeChangedCallback
|
|
&& (newValue !== oldValue)) {
|
|
this.attributeChangedCallback(name, oldValue, newValue);
|
|
}
|
|
}
|
|
|
|
// element registry (maps tag names to definitions)
|
|
|
|
var registry = {};
|
|
|
|
function getRegisteredDefinition(name) {
|
|
if (name) {
|
|
return registry[name.toLowerCase()];
|
|
}
|
|
}
|
|
|
|
function registerDefinition(name, definition) {
|
|
if (registry[name]) {
|
|
throw new Error('a type with that name is already registered.');
|
|
}
|
|
registry[name] = definition;
|
|
}
|
|
|
|
function generateConstructor(definition) {
|
|
return function() {
|
|
return instantiate(definition);
|
|
};
|
|
}
|
|
|
|
function createElement(tag, typeExtension) {
|
|
// TODO(sjmiles): ignore 'tag' when using 'typeExtension', we could
|
|
// error check it, or perhaps there should only ever be one argument
|
|
var definition = getRegisteredDefinition(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.registerElement = 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;
|
|
}
|
|
|
|
// bc
|
|
document.register = document.registerElement;
|
|
|
|
scope.hasNative = hasNative;
|
|
scope.useNative = useNative;
|
|
|
|
})(window.CustomElements);
|
|
|
|
(function(scope) {
|
|
|
|
// import
|
|
|
|
var IMPORT_LINK_TYPE = scope.IMPORT_LINK_TYPE;
|
|
|
|
// 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.import) {
|
|
parser.parse(linkElt.import);
|
|
}
|
|
}
|
|
};
|
|
|
|
function isDocumentLink(inElt) {
|
|
return (inElt.localName === 'link'
|
|
&& inElt.getAttribute('rel') === IMPORT_LINK_TYPE);
|
|
}
|
|
|
|
var forEach = Array.prototype.forEach.call.bind(Array.prototype.forEach);
|
|
|
|
// exports
|
|
|
|
scope.parser = parser;
|
|
scope.IMPORT_LINK_TYPE = IMPORT_LINK_TYPE;
|
|
|
|
})(window.CustomElements);
|
|
(function(scope){
|
|
|
|
// bootstrap parsing
|
|
function bootstrap() {
|
|
// parse document
|
|
CustomElements.parser.parse(document);
|
|
// one more pass before register is 'live'
|
|
CustomElements.upgradeDocument(document);
|
|
CustomElements.performedInitialDocumentUpgrade = true;
|
|
// choose async
|
|
var async = window.Platform && Platform.endOfMicrotask ?
|
|
Platform.endOfMicrotask :
|
|
setTimeout;
|
|
async(function() {
|
|
// set internal 'ready' flag, now document.registerElement 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.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;
|
|
};
|
|
}
|
|
|
|
// When loading at readyState complete time (or via flag), boot custom elements
|
|
// immediately.
|
|
// If relevant, HTMLImports must already be loaded.
|
|
if (document.readyState === 'complete' || scope.flags.eager) {
|
|
bootstrap();
|
|
// When loading at readyState interactive time, bootstrap only if HTMLImports
|
|
// are not pending. Also avoid IE as the semantics of this state are unreliable.
|
|
} else if (document.readyState === 'interactive' && !window.attachEvent &&
|
|
(!window.HTMLImports || window.HTMLImports.ready)) {
|
|
bootstrap();
|
|
// When loading at other readyStates, wait for the appropriate DOM event to
|
|
// bootstrap.
|
|
} else {
|
|
var loadEvent = window.HTMLImports && !HTMLImports.ready ?
|
|
'HTMLImportsLoaded' : document.readyState == 'loading' ?
|
|
'DOMContentLoaded' : 'load';
|
|
window.addEventListener(loadEvent, bootstrap);
|
|
}
|
|
|
|
})(window.CustomElements);
|
|
|
|
(function() {
|
|
// Patch to allow custom element and shadow dom to work together, from:
|
|
// https://github.com/Polymer/platform-dev/blob/60ece8c323c5d9325cbfdfd6e8cd180d4f38a3bc/src/patches-shadowdom-polyfill.js
|
|
// include .host reference
|
|
if (HTMLElement.prototype.createShadowRoot) {
|
|
var originalCreateShadowRoot = HTMLElement.prototype.createShadowRoot;
|
|
HTMLElement.prototype.createShadowRoot = function() {
|
|
var root = originalCreateShadowRoot.call(this);
|
|
root.host = this;
|
|
CustomElements.watchShadow(this);
|
|
return root;
|
|
}
|
|
}
|
|
|
|
|
|
// Patch to allow custom elements and shadow dom to work together, from:
|
|
// https://github.com/Polymer/platform-dev/blob/2bb9c56d90f9ac19c2e65cdad368668aff514f14/src/patches-custom-elements.js
|
|
if (window.ShadowDOMPolyfill) {
|
|
|
|
// 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](window.ShadowDOMPolyfill.wrapIfNeeded(inNode));
|
|
};
|
|
});
|
|
|
|
}
|
|
|
|
// Patch to make importNode work.
|
|
// https://github.com/Polymer/platform-dev/blob/64a92f273462f04a84abbe2f054294f2b62dbcd6/src/patches-mdv.js
|
|
if (window.CustomElements && !CustomElements.useNative) {
|
|
var originalImportNode = Document.prototype.importNode;
|
|
Document.prototype.importNode = function(node, deep) {
|
|
var imported = originalImportNode.call(this, node, deep);
|
|
CustomElements.upgradeAll(imported);
|
|
return imported;
|
|
}
|
|
}
|
|
|
|
})();
|