diff --git a/build.json b/build.json
index c959cec..e3bea05 100644
--- a/build.json
+++ b/build.json
@@ -2,6 +2,8 @@
"../observe-js/src/observe.js",
"../WeakMap/weakmap.js",
"src/wrappers.js",
+ "src/microtask.js",
+ "src/MutationObserver.js",
"src/wrappers/events.js",
"src/wrappers/NodeList.js",
"src/wrappers/Node.js",
@@ -27,7 +29,6 @@
"src/wrappers/elements-with-form-property.js",
"src/wrappers/Document.js",
"src/wrappers/Window.js",
- "src/wrappers/MutationObserver.js",
"src/wrappers/Range.js",
"src/wrappers/override-constructors.js"
]
diff --git a/shadowdom.js b/shadowdom.js
index 52e44dc..3808328 100644
--- a/shadowdom.js
+++ b/shadowdom.js
@@ -18,6 +18,8 @@
'../observe-js/src/observe.js',
'../WeakMap/weakmap.js',
'src/wrappers.js',
+ 'src/microtask.js',
+ 'src/MutationObserver.js',
'src/wrappers/events.js',
'src/wrappers/NodeList.js',
'src/wrappers/Node.js',
@@ -43,7 +45,6 @@
'src/wrappers/elements-with-form-property.js',
'src/wrappers/Document.js',
'src/wrappers/Window.js',
- 'src/wrappers/MutationObserver.js',
'src/wrappers/Range.js',
'src/wrappers/override-constructors.js'
].forEach(function(src) {
diff --git a/src/MutationObserver.js b/src/MutationObserver.js
new file mode 100644
index 0000000..a060172
--- /dev/null
+++ b/src/MutationObserver.js
@@ -0,0 +1,372 @@
+/*
+ * Copyright 2013 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(scope) {
+ 'use strict';
+
+ var setEndOfMicrotask = scope.setEndOfMicrotask
+ var wrapIfNeeded = scope.wrapIfNeeded
+ var wrappers = scope.wrappers;
+
+ var registrationsTable = new WeakMap();
+ var globalMutationObservers = [];
+ var isScheduled = false;
+
+ function scheduleCallback(observer) {
+ if (isScheduled)
+ return;
+ setEndOfMicrotask(notifyObservers);
+ isScheduled = true;
+ }
+
+ // http://dom.spec.whatwg.org/#mutation-observers
+ function notifyObservers() {
+ isScheduled = false;
+
+ do {
+ var notifyList = globalMutationObservers.slice();
+ var anyNonEmpty = false;
+ for (var i = 0; i < notifyList.length; i++) {
+ var mo = notifyList[i];
+ var queue = mo.takeRecords();
+ removeTransientObserversFor(mo);
+ if (queue.length) {
+ mo.callback_(queue, mo);
+ anyNonEmpty = true;
+ }
+ }
+ } while (anyNonEmpty);
+ }
+
+ /**
+ * @param {string} type
+ * @param {Node} target
+ * @constructor
+ */
+ function MutationRecord(type, target) {
+ this.type = type;
+ this.target = target;
+ this.addedNodes = new wrappers.NodeList();
+ this.removedNodes = new wrappers.NodeList();
+ this.previousSibling = null;
+ this.nextSibling = null;
+ this.attributeName = null;
+ this.attributeNamespace = null;
+ this.oldValue = null;
+ }
+
+ /**
+ * Registers transient observers to ancestor and its ancesors for the node
+ * which was removed.
+ * @param {!Node} ancestor
+ * @param {!Node} node
+ */
+ function registerTransientObservers(ancestor, node) {
+ for (; ancestor; ancestor = ancestor.parentNode) {
+ var registrations = registrationsTable.get(ancestor);
+ if (!registrations)
+ continue;
+ for (var i = 0; i < registrations.length; i++) {
+ var registration = registrations[i];
+ if (registration.options.subtree)
+ registration.addTransientObserver(node);
+ }
+ }
+ }
+
+ function removeTransientObserversFor(observer) {
+ for (var i = 0; i < observer.nodes_.length; i++) {
+ var node = observer.nodes_[i];
+ var registrations = registrationsTable.get(node);
+ if (!registrations)
+ return;
+ for (var j = 0; j < registrations.length; j++) {
+ var registration = registrations[j];
+ if (registration.observer === observer)
+ registration.removeTransientObservers();
+ }
+ }
+ }
+
+ // http://dom.spec.whatwg.org/#queue-a-mutation-record
+ function enqueueMutation(target, type, data) {
+ // 1.
+ var interestedObservers = Object.create(null);
+ var associatedStrings = Object.create(null);
+
+ // 2.
+ for (var node = target; node; node = node.parentNode) {
+ // 3.
+ var registrations = registrationsTable.get(node);
+ if (!registrations)
+ continue;
+ for (var j = 0; j < registrations.length; j++) {
+ var registration = registrations[j];
+ var options = registration.options;
+ // 1.
+ if (node !== target && !options.subtree)
+ continue;
+
+ // 2.
+ if (type === 'attributes' && !options.attributes)
+ continue;
+
+ // 3. If type is "attributes", options's attributeFilter is present, and
+ // either options's attributeFilter does not contain name or namespace
+ // is non-null, continue.
+ if (type === 'attributes' && options.attributeFilter &&
+ (data.namespace !== null ||
+ options.attributeFilter.indexOf(data.name) === -1)) {
+ continue;
+ }
+
+ // 4.
+ if (type === 'characterData' && !options.characterData)
+ continue;
+
+ // 5.
+ if (type === 'childList' && !options.childList)
+ continue;
+
+ // 6.
+ var observer = registration.observer;
+ interestedObservers[observer.uid_] = observer;
+
+ // 7. If either type is "attributes" and options's attributeOldValue is
+ // true, or type is "characterData" and options's characterDataOldValue
+ // is true, set the paired string of registered observer's observer in
+ // interested observers to oldValue.
+ if (type === 'attributes' && options.attributeOldValue ||
+ type === 'characterData' && options.characterDataOldValue) {
+ associatedStrings[observer.uid_] = data.oldValue;
+ }
+ }
+ }
+
+ var anyRecordsEnqueued = false;
+
+ // 4.
+ for (var uid in interestedObservers) {
+ var observer = interestedObservers[uid];
+ var record = new MutationRecord(type, target);
+
+ // 2.
+ if ('name' in data && 'namespace' in data) {
+ record.attributeName = data.name;
+ record.attributeNamespace = data.namespace;
+ }
+
+ // 3.
+ if (data.addedNodes)
+ record.addedNodes = data.addedNodes;
+
+ // 4.
+ if (data.removedNodes)
+ record.removedNodes = data.removedNodes;
+
+ // 5.
+ if (data.previousSibling)
+ record.previousSibling = data.previousSibling;
+
+ // 6.
+ if (data.nextSibling)
+ record.nextSibling = data.nextSibling;
+
+ // 7.
+ if (associatedStrings[uid] !== undefined)
+ record.oldValue = associatedStrings[uid];
+
+ // 8.
+ observer.records_.push(record);
+
+ anyRecordsEnqueued = true;
+ }
+
+ if (anyRecordsEnqueued)
+ scheduleCallback();
+ }
+
+ var slice = Array.prototype.slice;
+
+ /**
+ * @param {!Object} options
+ * @constructor
+ */
+ function MutationObserverOptions(options) {
+ this.childList = !!options.childList;
+ this.subtree = !!options.subtree;
+
+ // 1. If either options' attributeOldValue or attributeFilter is present
+ // and options' attributes is omitted, set options' attributes to true.
+ if (!('attributes' in options) &&
+ ('attributeOldValue' in options || 'attributeFilter' in options)) {
+ this.attributes = true;
+ } else {
+ this.attributes = !!options.attributes;
+ }
+
+ // 2. If options' characterDataOldValue is present and options'
+ // characterData is omitted, set options' characterData to true.
+ if ('characterDataOldValue' in options && !('characterData' in options))
+ this.characterData = true;
+ else
+ this.characterData = !!options.characterData;
+
+ // 3. & 4.
+ if (!this.attributes &&
+ (options.attributeOldValue || 'attributeFilter' in options) ||
+ // 5.
+ !this.characterData && options.characterDataOldValue) {
+ throw new TypeError();
+ }
+
+ this.characterData = !!options.characterData;
+ this.attributeOldValue = !!options.attributeOldValue;
+ this.characterDataOldValue = !!options.characterDataOldValue;
+ if ('attributeFilter' in options) {
+ if (options.attributeFilter == null ||
+ typeof options.attributeFilter !== 'object') {
+ throw new TypeError();
+ }
+ this.attributeFilter = slice.call(options.attributeFilter);
+ } else {
+ this.attributeFilter = null;
+ }
+ }
+
+ var uidCounter = 0;
+
+ /**
+ * The class that maps to the DOM MutationObserver interface.
+ * @param {Function} callback.
+ * @constructor
+ */
+ function MutationObserver(callback) {
+ this.callback_ = callback;
+ this.nodes_ = [];
+ this.records_ = [];
+ this.uid_ = ++uidCounter;
+
+ // This will leak. There is no way to implement this without WeakRefs :'(
+ globalMutationObservers.push(this);
+ }
+
+ MutationObserver.prototype = {
+ // http://dom.spec.whatwg.org/#dom-mutationobserver-observe
+ observe: function(target, options) {
+ target = wrapIfNeeded(target);
+
+ var newOptions = new MutationObserverOptions(options);
+
+ // 6.
+ var registration;
+ var registrations = registrationsTable.get(target);
+ if (!registrations)
+ registrationsTable.set(target, registrations = []);
+
+ for (var i = 0; i < registrations.length; i++) {
+ if (registrations[i].observer === this) {
+ registration = registrations[i];
+ // 6.1.
+ registration.removeTransientObservers();
+ // 6.2.
+ registration.options = newOptions;
+ }
+ }
+
+ // 7.
+ if (!registration) {
+ registration = new Registration(this, target, newOptions);
+ registrations.push(registration);
+ this.nodes_.push(target);
+ }
+ },
+
+ // http://dom.spec.whatwg.org/#dom-mutationobserver-disconnect
+ 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) {
+ 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;
+ }
+ };
+
+ /**
+ * Class used to represent a registered observer.
+ * @param {MutationObserver} observer
+ * @param {Node} target
+ * @param {MutationObserverOptions} options
+ * @constructor
+ */
+ function Registration(observer, target, options) {
+ this.observer = observer;
+ this.target = target;
+ this.options = options;
+ this.transientObservedNodes = [];
+ }
+
+ Registration.prototype = {
+ /**
+ * 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.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 = [];
+
+ for (var i = 0; i < transientObservedNodes.length; i++) {
+ var node = transientObservedNodes[i];
+ var registrations = registrationsTable.get(node);
+ for (var j = 0; j < registrations.length; j++) {
+ if (registrations[j] === this) {
+ registrations.splice(j, 1);
+ // Each node can only have one registered observer associated with
+ // this observer.
+ break;
+ }
+ }
+ }
+ }
+ };
+
+ scope.enqueueMutation = enqueueMutation;
+ scope.registerTransientObservers = registerTransientObservers;
+ scope.wrappers.MutationObserver = MutationObserver;
+ scope.wrappers.MutationRecord = MutationRecord;
+
+})(window.ShadowDOMPolyfill);
diff --git a/src/ShadowRenderer.js b/src/ShadowRenderer.js
index 36a9b8c..56afed3 100644
--- a/src/ShadowRenderer.js
+++ b/src/ShadowRenderer.js
@@ -632,8 +632,8 @@
return getDistributedChildNodes(this);
};
- HTMLShadowElement.prototype.nodeWasAdded_ =
- HTMLContentElement.prototype.nodeWasAdded_ = function() {
+ HTMLShadowElement.prototype.nodeIsInserted_ =
+ HTMLContentElement.prototype.nodeIsInserted_ = function() {
// Invalidate old renderer if any.
this.invalidateShadowRenderer();
diff --git a/src/microtask.js b/src/microtask.js
new file mode 100644
index 0000000..c0e35b7
--- /dev/null
+++ b/src/microtask.js
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2013 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(context) {
+ 'use strict';
+
+ var OriginalMutationObserver = window.MutationObserver;
+ var callbacks = [];
+ var pending = false;
+ var timerFunc;
+
+ function handle() {
+ pending = false;
+ var copies = callbacks.slice(0);
+ callbacks = [];
+ for (var i = 0; i < copies.length; i++) {
+ (0, copies[i])();
+ }
+ }
+
+ if (OriginalMutationObserver) {
+ var counter = 1;
+ var observer = new OriginalMutationObserver(handle);
+ var textNode = document.createTextNode(counter);
+ observer.observe(textNode, {characterData: true});
+
+ timerFunc = function() {
+ counter = (counter + 1) % 2;
+ textNode.data = counter;
+ };
+
+ } else {
+ timerFunc = window.setImmediate || window.setTimeout;
+ }
+
+ function setEndOfMicrotask(func) {
+ callbacks.push(func);
+ if (pending)
+ return;
+ pending = true;
+ timerFunc(handle, 0);
+ }
+
+ context.setEndOfMicrotask = setEndOfMicrotask;
+
+})(window.ShadowDOMPolyfill);
diff --git a/src/wrappers/CharacterData.js b/src/wrappers/CharacterData.js
index 146b9bd..e2655ff 100644
--- a/src/wrappers/CharacterData.js
+++ b/src/wrappers/CharacterData.js
@@ -7,6 +7,7 @@
var ChildNodeInterface = scope.ChildNodeInterface;
var Node = scope.wrappers.Node;
+ var enqueueMutation = scope.enqueueMutation;
var mixin = scope.mixin;
var registerWrapper = scope.registerWrapper;
@@ -22,6 +23,16 @@
},
set textContent(value) {
this.data = value;
+ },
+ get data() {
+ return this.impl.data;
+ },
+ set data(value) {
+ var oldValue = this.impl.data;
+ enqueueMutation(this, 'characterData', {
+ oldValue: oldValue
+ });
+ this.impl.data = value;
}
});
diff --git a/src/wrappers/Element.js b/src/wrappers/Element.js
index 02e0ce0..5e76c0d 100644
--- a/src/wrappers/Element.js
+++ b/src/wrappers/Element.js
@@ -11,6 +11,7 @@
var ParentNodeInterface = scope.ParentNodeInterface;
var SelectorsInterface = scope.SelectorsInterface;
var addWrapNodeListMethod = scope.addWrapNodeListMethod;
+ var enqueueMutation = scope.enqueueMutation;
var mixin = scope.mixin;
var oneOf = scope.oneOf;
var registerWrapper = scope.registerWrapper;
@@ -38,6 +39,17 @@
renderer.invalidate();
}
+ function enqueAttributeChange(element, name, oldValue) {
+ // This is not fully spec compliant. We should use localName (which might
+ // have a different case than name) and the namespace (which requires us
+ // to get the Attr object).
+ enqueueMutation(element, 'attributes', {
+ name: name,
+ namespace: null,
+ oldValue: oldValue
+ });
+ }
+
function Element(node) {
Node.call(this, node);
}
@@ -58,12 +70,16 @@
},
setAttribute: function(name, value) {
+ var oldValue = this.impl.getAttribute(name);
this.impl.setAttribute(name, value);
+ enqueAttributeChange(this, name, oldValue);
invalidateRendererBasedOnAttribute(this, name);
},
removeAttribute: function(name) {
+ var oldValue = this.impl.getAttribute(name);
this.impl.removeAttribute(name);
+ enqueAttributeChange(this, name, oldValue);
invalidateRendererBasedOnAttribute(this, name);
},
diff --git a/src/wrappers/HTMLElement.js b/src/wrappers/HTMLElement.js
index 1bae2cc..7fc1f91 100644
--- a/src/wrappers/HTMLElement.js
+++ b/src/wrappers/HTMLElement.js
@@ -7,8 +7,12 @@
var Element = scope.wrappers.Element;
var defineGetter = scope.defineGetter;
+ var enqueueMutation = scope.enqueueMutation;
var mixin = scope.mixin;
+ var nodesWereAdded = scope.nodesWereAdded;
+ var nodesWereRemoved = scope.nodesWereRemoved;
var registerWrapper = scope.registerWrapper;
+ var snapshotNodeList = scope.snapshotNodeList;
var unwrap = scope.unwrap;
var wrap = scope.wrap;
@@ -110,10 +114,21 @@
return getInnerHTML(this);
},
set innerHTML(value) {
+ var removedNodes = snapshotNodeList(this.childNodes);
+
if (this.invalidateShadowRenderer())
setInnerHTML(this, value, this.tagName);
else
this.impl.innerHTML = value;
+ var addedNodes = snapshotNodeList(this.childNodes);
+
+ enqueueMutation(this, 'childList', {
+ addedNodes: addedNodes,
+ removedNodes: removedNodes
+ });
+
+ nodesWereRemoved(removedNodes);
+ nodesWereAdded(addedNodes);
},
get outerHTML() {
diff --git a/src/wrappers/MutationObserver.js b/src/wrappers/MutationObserver.js
deleted file mode 100644
index ee10d95..0000000
--- a/src/wrappers/MutationObserver.js
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright 2013 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(scope) {
- 'use strict';
-
- var defineGetter = scope.defineGetter;
- var defineWrapGetter = scope.defineWrapGetter;
- var registerWrapper = scope.registerWrapper;
- var unwrapIfNeeded = scope.unwrapIfNeeded;
- var wrapNodeList = scope.wrapNodeList;
- var wrappers = scope.wrappers;
-
- var OriginalMutationObserver = window.MutationObserver ||
- window.WebKitMutationObserver;
-
- if (!OriginalMutationObserver)
- return;
-
- var OriginalMutationRecord = window.MutationRecord;
-
- function MutationRecord(impl) {
- this.impl = impl;
- }
-
- MutationRecord.prototype = {
- get addedNodes() {
- return wrapNodeList(this.impl.addedNodes);
- },
- get removedNodes() {
- return wrapNodeList(this.impl.removedNodes);
- }
- };
-
- ['target', 'previousSibling', 'nextSibling'].forEach(function(name) {
- defineWrapGetter(MutationRecord, name);
- });
-
- // WebKit/Blink treats these as instance properties so we override
- [
- 'type',
- 'attributeName',
- 'attributeNamespace',
- 'oldValue'
- ].forEach(function(name) {
- defineGetter(MutationRecord, name, function() {
- return this.impl[name];
- });
- });
-
- if (OriginalMutationRecord)
- registerWrapper(OriginalMutationRecord, MutationRecord);
-
- function wrapRecord(record) {
- return new MutationRecord(record);
- }
-
- function wrapRecords(records) {
- return records.map(wrapRecord);
- }
-
- function MutationObserver(callback) {
- var self = this;
- this.impl = new OriginalMutationObserver(function(mutations, observer) {
- callback.call(self, wrapRecords(mutations), self);
- });
- }
-
- var OriginalNode = window.Node;
-
- MutationObserver.prototype = {
- observe: function(target, options) {
- this.impl.observe(unwrapIfNeeded(target), options);
- },
- disconnect: function() {
- this.impl.disconnect();
- },
- takeRecords: function() {
- return wrapRecords(this.impl.takeRecords());
- }
- };
-
- scope.wrappers.MutationObserver = MutationObserver;
- scope.wrappers.MutationRecord = MutationRecord;
-
-})(window.ShadowDOMPolyfill);
diff --git a/src/wrappers/Node.js b/src/wrappers/Node.js
index 55d981d..85ed559 100644
--- a/src/wrappers/Node.js
+++ b/src/wrappers/Node.js
@@ -7,9 +7,11 @@
var EventTarget = scope.wrappers.EventTarget;
var NodeList = scope.wrappers.NodeList;
- var defineWrapGetter = scope.defineWrapGetter;
var assert = scope.assert;
+ var defineWrapGetter = scope.defineWrapGetter;
+ var enqueueMutation = scope.enqueueMutation;
var mixin = scope.mixin;
+ var registerTransientObservers = scope.registerTransientObservers;
var registerWrapper = scope.registerWrapper;
var unwrap = scope.unwrap;
var wrap = scope.wrap;
@@ -63,23 +65,46 @@
}
function collectNodesNoNeedToUpdatePointers(node) {
+ var nodes = new NodeList();
if (node instanceof DocumentFragment) {
var nodes = [];
var i = 0;
for (var child = node.firstChild; child; child = child.nextSibling) {
nodes[i++] = child;
}
+ nodes.length = i;
return nodes;
}
- return [node];
+ nodes[0] = node;
+ nodes.length = 1;
+ return nodes;
+ }
+
+ function snapshotNodeList(nodeList) {
+ // NodeLists are not live at the moment so just return the same object.
+ return nodeList;
+ }
+
+ // http://dom.spec.whatwg.org/#node-is-inserted
+ function nodeWasAdded(node) {
+ node.nodeIsInserted_();
}
function nodesWereAdded(nodes) {
for (var i = 0; i < nodes.length; i++) {
- nodes[i].nodeWasAdded_();
+ nodeWasAdded(nodes[i]);
}
}
+ // http://dom.spec.whatwg.org/#node-is-removed
+ function nodeWasRemoved(node) {
+ // Nothing at this point in time.
+ }
+
+ function nodesWereRemoved(nodes) {
+ // Nothing at this point in time.
+ }
+
function ensureSameOwnerDocument(parent, child) {
var ownerDoc = parent.nodeType === Node.DOCUMENT_NODE ?
parent : parent.ownerDocument;
@@ -148,6 +173,27 @@
return p && p.invalidateShadowRenderer();
}
+ /**
+ * Called before node is inserted into a node to enqueue its removal from its
+ * old parent.
+ * @param {!Node} node The node that is about to be removed.
+ * @param {!NodeList} nodes The collected nodes.
+ */
+ function enqueueRemovalForInsertedNodes(node, nodes) {
+ var parent;
+ if (node instanceof DocumentFragment) {
+ enqueueMutation(node, 'childList', {
+ removedNodes: nodes
+ });
+ } else if (parent = node.parentNode) {
+ enqueueMutation(parent, 'childList', {
+ removedNodes: nodes,
+ previousSibling: node.previousSibling,
+ nextSibling: node.nextSibling
+ });
+ }
+ }
+
var OriginalNode = window.Node;
/**
@@ -222,69 +268,58 @@
Node.prototype = Object.create(EventTarget.prototype);
mixin(Node.prototype, {
appendChild: function(childWrapper) {
- assertIsNodeWrapper(childWrapper);
-
- var nodes;
-
- if (this.invalidateShadowRenderer() || invalidateParent(childWrapper)) {
- var previousNode = this.lastChild;
- var nextNode = null;
- nodes = collectNodes(childWrapper, this, previousNode, nextNode);
-
- this.lastChild_ = nodes[nodes.length - 1];
- if (!previousNode)
- this.firstChild_ = nodes[0];
-
- originalAppendChild.call(this.impl, unwrapNodesForInsertion(this, nodes));
- } else {
- nodes = collectNodesNoNeedToUpdatePointers(childWrapper)
- ensureSameOwnerDocument(this, childWrapper);
- originalAppendChild.call(this.impl, unwrap(childWrapper));
- }
-
- nodesWereAdded(nodes);
-
- return childWrapper;
+ return this.insertBefore(childWrapper, null);
},
insertBefore: function(childWrapper, refWrapper) {
- // TODO(arv): Unify with appendChild
- if (!refWrapper)
- return this.appendChild(childWrapper);
-
assertIsNodeWrapper(childWrapper);
- assertIsNodeWrapper(refWrapper);
- assert(refWrapper.parentNode === this);
+
+ refWrapper = refWrapper || null;
+ refWrapper && assertIsNodeWrapper(refWrapper);
+ refWrapper && assert(refWrapper.parentNode === this);
var nodes;
+ var previousNode =
+ refWrapper ? refWrapper.previousSibling : this.lastChild;
- if (this.invalidateShadowRenderer() || invalidateParent(childWrapper)) {
- var previousNode = refWrapper.previousSibling;
- var nextNode = refWrapper;
- nodes = collectNodes(childWrapper, this, previousNode, nextNode);
+ var useNative = !this.invalidateShadowRenderer() &&
+ !invalidateParent(childWrapper);
- if (this.firstChild === refWrapper)
+ if (useNative)
+ nodes = collectNodesNoNeedToUpdatePointers(childWrapper);
+ else
+ nodes = collectNodes(childWrapper, this, previousNode, refWrapper);
+
+ enqueueRemovalForInsertedNodes(childWrapper, nodes);
+
+ if (useNative) {
+ ensureSameOwnerDocument(this, childWrapper);
+ originalInsertBefore.call(this.impl, unwrap(childWrapper),
+ unwrap(refWrapper));
+ } else {
+ if (!previousNode)
this.firstChild_ = nodes[0];
+ if (!refWrapper)
+ this.lastChild_ = nodes[nodes.length - 1];
- // insertBefore refWrapper no matter what the parent is?
var refNode = unwrap(refWrapper);
- var parentNode = refNode.parentNode;
+ var parentNode = refNode ? refNode.parentNode : this.impl;
+ // insertBefore refWrapper no matter what the parent is?
if (parentNode) {
- originalInsertBefore.call(
- parentNode,
- unwrapNodesForInsertion(this, nodes),
- refNode);
+ originalInsertBefore.call(parentNode,
+ unwrapNodesForInsertion(this, nodes), refNode);
} else {
adoptNodesIfNeeded(this, nodes);
}
- } else {
- nodes = collectNodesNoNeedToUpdatePointers(childWrapper);
- ensureSameOwnerDocument(this, childWrapper);
- originalInsertBefore.call(this.impl, unwrap(childWrapper),
- unwrap(refWrapper));
}
+ enqueueMutation(this, 'childList', {
+ addedNodes: nodes,
+ nextSibling: refWrapper,
+ previousSibling: previousNode
+ });
+
nodesWereAdded(nodes);
return childWrapper;
@@ -310,15 +345,15 @@
}
var childNode = unwrap(childWrapper);
- if (this.invalidateShadowRenderer()) {
+ var childWrapperNextSibling = childWrapper.nextSibling;
+ var childWrapperPreviousSibling = childWrapper.previousSibling;
+ if (this.invalidateShadowRenderer()) {
// We need to remove the real node from the DOM before updating the
// pointers. This is so that that mutation event is dispatched before
// the pointers have changed.
var thisFirstChild = this.firstChild;
var thisLastChild = this.lastChild;
- var childWrapperNextSibling = childWrapper.nextSibling;
- var childWrapperPreviousSibling = childWrapper.previousSibling;
var parentNode = childNode.parentNode;
if (parentNode)
@@ -341,6 +376,14 @@
removeChildOriginalHelper(this.impl, childNode);
}
+ enqueueMutation(this, 'childList', {
+ removedNodes: [childWrapper],
+ nextSibling: childWrapperNextSibling,
+ previousSibling: childWrapperPreviousSibling
+ });
+
+ registerTransientObservers(this, childWrapper);
+
return childWrapper;
},
@@ -354,16 +397,24 @@
}
var oldChildNode = unwrap(oldChildWrapper);
+ var nextNode = oldChildWrapper.nextSibling;
+ var previousNode = oldChildWrapper.previousSibling;
var nodes;
- if (this.invalidateShadowRenderer() ||
- invalidateParent(newChildWrapper)) {
- var previousNode = oldChildWrapper.previousSibling;
- var nextNode = oldChildWrapper.nextSibling;
+ var useNative = !this.invalidateShadowRenderer() &&
+ !invalidateParent(newChildWrapper);
+
+ if (useNative) {
+ nodes = collectNodesNoNeedToUpdatePointers(newChildWrapper);
+ } else {
if (nextNode === newChildWrapper)
nextNode = newChildWrapper.nextSibling;
nodes = collectNodes(newChildWrapper, this, previousNode, nextNode);
+ }
+
+ enqueueRemovalForInsertedNodes(newChildWrapper, nodes);
+ if (!useNative) {
if (this.firstChild === oldChildWrapper)
this.firstChild_ = nodes[0];
if (this.lastChild === oldChildWrapper)
@@ -380,25 +431,32 @@
oldChildNode);
}
} else {
- nodes = collectNodesNoNeedToUpdatePointers(newChildWrapper);
ensureSameOwnerDocument(this, newChildWrapper);
originalReplaceChild.call(this.impl, unwrap(newChildWrapper),
oldChildNode);
}
+ enqueueMutation(this, 'childList', {
+ addedNodes: nodes,
+ removedNodes: [oldChildWrapper],
+ nextSibling: nextNode,
+ previousSibling: previousNode
+ });
+
+ nodeWasRemoved(oldChildWrapper);
nodesWereAdded(nodes);
return oldChildWrapper;
},
/**
- * Called after a node was added. Subclasses override this to invalidate
+ * Called after a node was inserted. Subclasses override this to invalidate
* the renderer as needed.
* @private
*/
- nodeWasAdded_: function() {
+ nodeIsInserted_: function() {
for (var child = this.firstChild; child; child = child.nextSibling) {
- child.nodeWasAdded_();
+ child.nodeIsInserted_();
}
},
@@ -455,6 +513,8 @@
return s;
},
set textContent(textContent) {
+ var removedNodes = snapshotNodeList(this.childNodes);
+
if (this.invalidateShadowRenderer()) {
removeAllChildNodes(this);
if (textContent !== '') {
@@ -464,6 +524,16 @@
} else {
this.impl.textContent = textContent;
}
+
+ var addedNodes = snapshotNodeList(this.childNodes);
+
+ enqueueMutation(this, 'childList', {
+ addedNodes: addedNodes,
+ removedNodes: removedNodes
+ });
+
+ nodesWereRemoved(removedNodes);
+ nodesWereAdded(addedNodes);
},
get childNodes() {
@@ -522,6 +592,11 @@
delete Node.prototype.querySelectorAll;
Node.prototype = mixin(Object.create(EventTarget.prototype), Node.prototype);
+ scope.nodeWasAdded = nodeWasAdded;
+ scope.nodeWasRemoved = nodeWasRemoved;
+ scope.nodesWereAdded = nodesWereAdded;
+ scope.nodesWereRemoved = nodesWereRemoved;
+ scope.snapshotNodeList = snapshotNodeList;
scope.wrappers.Node = Node;
})(window.ShadowDOMPolyfill);
diff --git a/test/js/MutationObserver.js b/test/js/MutationObserver.js
index 5f44a38..670e127 100644
--- a/test/js/MutationObserver.js
+++ b/test/js/MutationObserver.js
@@ -209,7 +209,7 @@ suite('MutationObserver', function() {
subtree: true
});
- document.body.setAttribute('a', newValue());
+ wrap(document).body.setAttribute('a', newValue());
});
test('observe document.body', function(done) {
@@ -230,7 +230,7 @@ suite('MutationObserver', function() {
attributes: true
});
- document.body.setAttribute('a', newValue());
+ wrap(document.body).setAttribute('a', newValue());
});
test('observe document.head', function(done) {
@@ -251,7 +251,7 @@ suite('MutationObserver', function() {
attributes: true
});
- document.head.setAttribute('a', newValue());
+ wrap(document.head).setAttribute('a', newValue());
});
test('observe text node', function(done) {
diff --git a/test/js/MutationObserver/attributes.js b/test/js/MutationObserver/attributes.js
new file mode 100644
index 0000000..0bc5ae8
--- /dev/null
+++ b/test/js/MutationObserver/attributes.js
@@ -0,0 +1,280 @@
+/*
+ * Copyright 2012 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.
+ */
+
+suite('MutationObserver', function() {
+
+ suite('attributes', function() {
+
+ test('attr', function() {
+ var div = document.createElement('div');
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ attributes: true
+ });
+ div.setAttribute('a', 'A');
+ div.setAttribute('a', 'B');
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 2);
+
+ expectMutationRecord(records[0], {
+ type: 'attributes',
+ target: div,
+ attributeName: 'a',
+ attributeNamespace: null
+ });
+ expectMutationRecord(records[1], {
+ type: 'attributes',
+ target: div,
+ attributeName: 'a',
+ attributeNamespace: null
+ });
+ });
+
+ test('attr with oldValue', function() {
+ var div = document.createElement('div');
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ attributes: true,
+ attributeOldValue: true
+ });
+ div.setAttribute('a', 'A');
+ div.setAttribute('a', 'B');
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 2);
+
+ expectMutationRecord(records[0], {
+ type: 'attributes',
+ target: div,
+ attributeName: 'a',
+ attributeNamespace: null,
+ oldValue: null
+ });
+ expectMutationRecord(records[1], {
+ type: 'attributes',
+ target: div,
+ attributeName: 'a',
+ attributeNamespace: null,
+ oldValue: 'A'
+ });
+ });
+
+ test('attr change in subtree should not genereate a record', function() {
+ var div = document.createElement('div');
+ var child = div.appendChild(document.createElement('div'));
+
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ attributes: true
+ });
+ child.setAttribute('a', 'A');
+ child.setAttribute('a', 'B');
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 0);
+ });
+
+ test('attr change, subtree', function() {
+ var div = document.createElement('div');
+ var child = div.appendChild(document.createElement('div'));
+
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ attributes: true,
+ subtree: true
+ });
+ child.setAttribute('a', 'A');
+ child.setAttribute('a', 'B');
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 2);
+
+ expectMutationRecord(records[0], {
+ type: 'attributes',
+ target: child,
+ attributeName: 'a'
+ });
+ expectMutationRecord(records[1], {
+ type: 'attributes',
+ target: child,
+ attributeName: 'a'
+ });
+ });
+
+
+ test('multiple observers on same target', function() {
+ var div = document.createElement('div');
+ var observer1 = new MutationObserver(function() {});
+ observer1.observe(div, {
+ attributes: true,
+ attributeOldValue: true
+ });
+ var observer2 = new MutationObserver(function() {});
+ observer2.observe(div, {
+ attributes: true,
+ attributeFilter: ['b']
+ });
+
+ div.setAttribute('a', 'A');
+ div.setAttribute('a', 'A2');
+ div.setAttribute('b', 'B');
+
+ var records = observer1.takeRecords();
+ assert.equal(records.length, 3);
+
+ expectMutationRecord(records[0], {
+ type: 'attributes',
+ target: div,
+ attributeName: 'a'
+ });
+ expectMutationRecord(records[1], {
+ type: 'attributes',
+ target: div,
+ attributeName: 'a',
+ oldValue: 'A'
+ });
+ expectMutationRecord(records[2], {
+ type: 'attributes',
+ target: div,
+ attributeName: 'b'
+ });
+
+ records = observer2.takeRecords();
+ assert.equal(records.length, 1);
+
+ expectMutationRecord(records[0], {
+ type: 'attributes',
+ target: div,
+ attributeName: 'b'
+ });
+ });
+
+ test('observer observes on different target', function() {
+ var div = document.createElement('div');
+ var child = div.appendChild(document.createElement('div'));
+
+ var observer = new MutationObserver(function() {});
+ observer.observe(child, {
+ attributes: true
+ });
+ observer.observe(div, {
+ attributes: true,
+ subtree: true,
+ attributeOldValue: true
+ });
+
+ child.setAttribute('a', 'A');
+ child.setAttribute('a', 'A2');
+ child.setAttribute('b', 'B');
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 3);
+
+ expectMutationRecord(records[0], {
+ type: 'attributes',
+ target: child,
+ attributeName: 'a'
+ });
+ expectMutationRecord(records[1], {
+ type: 'attributes',
+ target: child,
+ attributeName: 'a',
+ oldValue: 'A'
+ });
+ expectMutationRecord(records[2], {
+ type: 'attributes',
+ target: child,
+ attributeName: 'b'
+ });
+ });
+
+ test('observing on the same node should update the options', function() {
+ var div = document.createElement('div');
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ attributes: true,
+ attributeFilter: ['a']
+ });
+ observer.observe(div, {
+ attributes: true,
+ attributeFilter: ['b']
+ });
+
+ div.setAttribute('a', 'A');
+ div.setAttribute('b', 'B');
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 1);
+
+ expectMutationRecord(records[0], {
+ type: 'attributes',
+ target: div,
+ attributeName: 'b'
+ });
+ });
+
+ test('disconnect should stop all events and empty the records', function() {
+ var div = document.createElement('div');
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ attributes: true,
+ });
+
+ div.setAttribute('a', 'A');
+
+ observer.disconnect();
+ var records = observer.takeRecords();
+ assert.equal(records.length, 0);
+
+ div.setAttribute('b', 'B');
+
+ records = observer.takeRecords();
+ assert.equal(records.length, 0);
+ });
+
+ test('disconnect should not affect other observers', function() {
+ var div = document.createElement('div');
+ var observer1 = new MutationObserver(function() {});
+ observer1.observe(div, {
+ attributes: true,
+ });
+ var observer2 = new MutationObserver(function() {});
+ observer2.observe(div, {
+ attributes: true,
+ });
+
+ div.setAttribute('a', 'A');
+
+ observer1.disconnect();
+ var records1 = observer1.takeRecords();
+ assert.equal(records1.length, 0);
+
+ var records2 = observer2.takeRecords();
+ assert.equal(records2.length, 1);
+ expectMutationRecord(records2[0], {
+ type: 'attributes',
+ target: div,
+ attributeName: 'a'
+ });
+
+ div.setAttribute('b', 'B');
+
+ records1 = observer1.takeRecords();
+ assert.equal(records1.length, 0);
+
+ records2 = observer2.takeRecords();
+ assert.equal(records2.length, 1);
+ expectMutationRecord(records2[0], {
+ type: 'attributes',
+ target: div,
+ attributeName: 'b'
+ });
+ });
+
+ });
+
+});
\ No newline at end of file
diff --git a/test/js/MutationObserver/callback.js b/test/js/MutationObserver/callback.js
new file mode 100644
index 0000000..7f84844
--- /dev/null
+++ b/test/js/MutationObserver/callback.js
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2012 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.
+ */
+
+suite('MutationObserver', function() {
+
+ suite('callback', function() {
+
+ test('One observer, two attribute changes', function(done) {
+ var div = document.createElement('div');
+ var observer = new MutationObserver(function(records) {
+ assert.equal(records.length, 2);
+
+ expectMutationRecord(records[0], {
+ type: 'attributes',
+ target: div,
+ attributeName: 'a',
+ attributeNamespace: null
+ });
+ expectMutationRecord(records[1], {
+ type: 'attributes',
+ target: div,
+ attributeName: 'a',
+ attributeNamespace: null
+ });
+
+ done();
+ });
+
+ observer.observe(div, {
+ attributes: true
+ });
+
+ div.setAttribute('a', 'A');
+ div.setAttribute('a', 'B');
+ });
+
+ test('nested changes', function(done) {
+ var div = document.createElement('div');
+ var i = 0;
+ var observer = new MutationObserver(function(records) {
+ assert.equal(records.length, 1);
+
+ if (i === 0) {
+ expectMutationRecord(records[0], {
+ type: 'attributes',
+ target: div,
+ attributeName: 'a',
+ attributeNamespace: null
+ });
+ div.setAttribute('b', 'B');
+ i++;
+ } else {
+ expectMutationRecord(records[0], {
+ type: 'attributes',
+ target: div,
+ attributeName: 'b',
+ attributeNamespace: null
+ });
+
+ done();
+ }
+ });
+
+ observer.observe(div, {
+ attributes: true
+ });
+
+ div.setAttribute('a', 'A');
+ });
+
+ });
+
+});
\ No newline at end of file
diff --git a/test/js/MutationObserver/characterData.js b/test/js/MutationObserver/characterData.js
new file mode 100644
index 0000000..cc462b2
--- /dev/null
+++ b/test/js/MutationObserver/characterData.js
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2012 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.
+ */
+
+suite('MutationObserver', function() {
+
+ suite('characterData', function() {
+
+ test('characterData', function() {
+ var text = document.createTextNode('abc');
+ var observer = new MutationObserver(function() {});
+ observer.observe(text, {
+ characterData: true
+ });
+ text.data = 'def';
+ text.data = 'ghi';
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 2);
+
+ expectMutationRecord(records[0], {
+ type: 'characterData',
+ target: text
+ });
+ expectMutationRecord(records[1], {
+ type: 'characterData',
+ target: text
+ });
+ });
+
+ test('characterData with old value', function() {
+ var text = document.createTextNode('abc');
+ var observer = new MutationObserver(function() {});
+ observer.observe(text, {
+ characterData: true,
+ characterDataOldValue: true
+ });
+ text.data = 'def';
+ text.data = 'ghi';
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 2);
+
+ expectMutationRecord(records[0], {
+ type: 'characterData',
+ target: text,
+ oldValue: 'abc'
+ });
+ expectMutationRecord(records[1], {
+ type: 'characterData',
+ target: text,
+ oldValue: 'def'
+ });
+ });
+
+ test('characterData change in subtree should not generate a record',
+ function() {
+ var div = document.createElement('div');
+ var text = div.appendChild(document.createTextNode('abc'));
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ characterData: true
+ });
+ text.data = 'def';
+ text.data = 'ghi';
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 0);
+ });
+
+ test('characterData change in subtree',
+ function() {
+ var div = document.createElement('div');
+ var text = div.appendChild(document.createTextNode('abc'));
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ characterData: true,
+ subtree: true
+ });
+ text.data = 'def';
+ text.data = 'ghi';
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 2);
+
+ expectMutationRecord(records[0], {
+ type: 'characterData',
+ target: text
+ });
+ expectMutationRecord(records[1], {
+ type: 'characterData',
+ target: text
+ });
+ });
+
+ });
+
+});
\ No newline at end of file
diff --git a/test/js/MutationObserver/childList.js b/test/js/MutationObserver/childList.js
new file mode 100644
index 0000000..7b3b440
--- /dev/null
+++ b/test/js/MutationObserver/childList.js
@@ -0,0 +1,556 @@
+/*
+ * Copyright 2013 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.
+ */
+
+suite('MutationObserver', function() {
+
+ suite('childList', function() {
+
+ var NodeList = ShadowDOMPolyfill.wrappers.NodeList;
+
+ function makeNodeList(/* ...args */) {
+ var nodeList = new NodeList;
+ for (var i = 0; i < arguments.length; i++) {
+ nodeList[i] = arguments[i];
+ }
+ nodeList.length = i;
+ return nodeList;
+ }
+
+ test('appendChild', function() {
+ var div = document.createElement('div');
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ childList: true
+ });
+ var a = document.createElement('a');
+ var b = document.createElement('b');
+
+ div.appendChild(a);
+ div.appendChild(b);
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 2);
+
+ expectMutationRecord(records[0], {
+ type: 'childList',
+ target: div,
+ addedNodes: [a]
+ });
+
+ expectMutationRecord(records[1], {
+ type: 'childList',
+ target: div,
+ addedNodes: [b],
+ previousSibling: a
+ });
+ });
+
+ test('insertBefore', function() {
+ var div = document.createElement('div');
+ var a = document.createElement('a');
+ var b = document.createElement('b');
+ var c = document.createElement('c');
+ div.appendChild(a);
+
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ childList: true
+ });
+
+ div.insertBefore(b, a);
+ div.insertBefore(c, a);
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 2);
+
+ expectMutationRecord(records[0], {
+ type: 'childList',
+ target: div,
+ addedNodes: [b],
+ nextSibling: a
+ });
+
+ expectMutationRecord(records[1], {
+ type: 'childList',
+ target: div,
+ addedNodes: [c],
+ nextSibling: a,
+ previousSibling: b
+ });
+ });
+
+ test('replaceChild', function() {
+ var div = document.createElement('div');
+ var a = document.createElement('a');
+ var b = document.createElement('b');
+ div.appendChild(a);
+
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ childList: true
+ });
+
+ div.replaceChild(b, a);
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 1);
+
+ expectMutationRecord(records[0], {
+ type: 'childList',
+ target: div,
+ addedNodes: [b],
+ removedNodes: [a]
+ });
+ });
+
+ test('removeChild', function() {
+ var div = document.createElement('div');
+ var a = div.appendChild(document.createElement('a'));
+ var b = div.appendChild(document.createElement('b'));
+ var c = div.appendChild(document.createElement('c'));
+
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ childList: true
+ });
+
+ div.removeChild(b);
+ div.removeChild(a);
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 2);
+
+ expectMutationRecord(records[0], {
+ type: 'childList',
+ target: div,
+ removedNodes: [b],
+ nextSibling: c,
+ previousSibling: a
+ });
+
+ expectMutationRecord(records[1], {
+ type: 'childList',
+ target: div,
+ removedNodes: [a],
+ nextSibling: c
+ });
+ });
+
+ test('Direct children', function() {
+ var div = document.createElement('div');
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ childList: true
+ });
+ var a = document.createElement('a');
+ var b = document.createElement('b');
+
+ div.appendChild(a);
+ div.insertBefore(b, a);
+ div.removeChild(b);
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 3);
+
+ expectMutationRecord(records[0], {
+ type: 'childList',
+ target: div,
+ addedNodes: [a]
+ });
+
+ expectMutationRecord(records[1], {
+ type: 'childList',
+ target: div,
+ nextSibling: a,
+ addedNodes: [b]
+ });
+
+ expectMutationRecord(records[2], {
+ type: 'childList',
+ target: div,
+ nextSibling: a,
+ removedNodes: [b]
+ });
+ });
+
+ test('subtree', function() {
+ var div = document.createElement('div');
+ var child = div.appendChild(document.createElement('div'));
+ var observer = new MutationObserver(function() {});
+ observer.observe(child, {
+ childList: true
+ });
+ var a = document.createTextNode('a');
+ var b = document.createTextNode('b');
+
+ child.appendChild(a);
+ child.insertBefore(b, a);
+ child.removeChild(b);
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 3);
+
+ expectMutationRecord(records[0], {
+ type: 'childList',
+ target: child,
+ addedNodes: [a]
+ });
+
+ expectMutationRecord(records[1], {
+ type: 'childList',
+ target: child,
+ nextSibling: a,
+ addedNodes: [b]
+ });
+
+ expectMutationRecord(records[2], {
+ type: 'childList',
+ target: child,
+ nextSibling: a,
+ removedNodes: [b]
+ });
+ });
+
+ test('both direct and subtree', function() {
+ var div = document.createElement('div');
+ var child = div.appendChild(document.createElement('div'));
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ childList: true,
+ subtree: true
+ });
+ observer.observe(child, {
+ childList: true
+ });
+
+ var a = document.createTextNode('a');
+ var b = document.createTextNode('b');
+
+ child.appendChild(a);
+ div.appendChild(b);
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 2);
+
+ expectMutationRecord(records[0], {
+ type: 'childList',
+ target: child,
+ addedNodes: [a]
+ });
+
+ expectMutationRecord(records[1], {
+ type: 'childList',
+ target: div,
+ addedNodes: [b],
+ previousSibling: child
+ });
+ });
+
+ test('Append multiple at once at the end', function() {
+ var div = document.createElement('div');
+ var a = div.appendChild(document.createTextNode('a'));
+
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ childList: true
+ });
+
+ var df = document.createDocumentFragment();
+ var b = df.appendChild(document.createTextNode('b'));
+ var c = df.appendChild(document.createTextNode('c'));
+ var d = df.appendChild(document.createTextNode('d'));
+
+ div.appendChild(df);
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 1);
+ expectMutationRecord(records[0], {
+ type: 'childList',
+ target: div,
+ addedNodes: makeNodeList(b, c, d),
+ removedNodes: makeNodeList(),
+ previousSibling: a,
+ nextSibling: null
+ });
+ });
+
+ test('Append multiple at once at the front', function() {
+ var div = document.createElement('div');
+ var a = div.appendChild(document.createTextNode('a'));
+
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ childList: true
+ });
+
+ var df = document.createDocumentFragment();
+ var b = df.appendChild(document.createTextNode('b'));
+ var c = df.appendChild(document.createTextNode('c'));
+ var d = df.appendChild(document.createTextNode('d'));
+
+ div.insertBefore(df, a);
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 1);
+ expectMutationRecord(records[0], {
+ type: 'childList',
+ target: div,
+ addedNodes: makeNodeList(b, c, d),
+ removedNodes: makeNodeList(),
+ previousSibling: null,
+ nextSibling: a
+ });
+ });
+
+ test('Append multiple at once in the middle', function() {
+ var div = document.createElement('div');
+ var a = div.appendChild(document.createTextNode('a'));
+ var b = div.appendChild(document.createTextNode('b'));
+
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ childList: true
+ });
+
+ var df = document.createDocumentFragment();
+ var c = df.appendChild(document.createTextNode('c'));
+ var d = df.appendChild(document.createTextNode('d'));
+
+ div.insertBefore(df, b);
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 1);
+ expectMutationRecord(records[0], {
+ type: 'childList',
+ target: div,
+ addedNodes: makeNodeList(c, d),
+ removedNodes: makeNodeList(),
+ previousSibling: a,
+ nextSibling: b
+ });
+ });
+
+ test('Remove all children using innerHTML', function() {
+ var div = document.createElement('div');
+ var a = div.appendChild(document.createTextNode('a'));
+ var b = div.appendChild(document.createTextNode('b'));
+ var c = div.appendChild(document.createTextNode('c'));
+
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ childList: true
+ });
+
+ div.innerHTML = '';
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 1);
+ expectMutationRecord(records[0], {
+ type: 'childList',
+ target: div,
+ addedNodes: makeNodeList(),
+ removedNodes: makeNodeList(a, b, c),
+ previousSibling: null,
+ nextSibling: null
+ });
+ });
+
+ test('Replace all children using innerHTML', function() {
+ var div = document.createElement('div');
+ var a = div.appendChild(document.createTextNode('a'));
+ var b = div.appendChild(document.createTextNode('b'));
+
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ childList: true
+ });
+
+ div.innerHTML = '';
+ var c = div.firstChild;
+ var d = div.lastChild;
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 1);
+ expectMutationRecord(records[0], {
+ type: 'childList',
+ target: div,
+ addedNodes: makeNodeList(c, d),
+ removedNodes: makeNodeList(a, b),
+ previousSibling: null,
+ nextSibling: null
+ });
+ });
+
+ test('Remove all children using textContent', function() {
+ var div = document.createElement('div');
+ var a = div.appendChild(document.createTextNode('a'));
+ var b = div.appendChild(document.createTextNode('b'));
+ var c = div.appendChild(document.createTextNode('c'));
+
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ childList: true
+ });
+
+ div.textContent = '';
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 1);
+ expectMutationRecord(records[0], {
+ type: 'childList',
+ target: div,
+ addedNodes: makeNodeList(),
+ removedNodes: makeNodeList(a, b, c),
+ previousSibling: null,
+ nextSibling: null
+ });
+ });
+
+ test('Replace all children using textContent', function() {
+ var div = document.createElement('div');
+ var a = div.appendChild(document.createTextNode('a'));
+ var b = div.appendChild(document.createTextNode('b'));
+
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ childList: true
+ });
+
+ div.textContent = 'text';
+ var text = div.firstChild;
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 1);
+ expectMutationRecord(records[0], {
+ type: 'childList',
+ target: div,
+ addedNodes: makeNodeList(text),
+ removedNodes: makeNodeList(a, b),
+ previousSibling: null,
+ nextSibling: null
+ });
+ });
+
+ test('appendChild removal', function() {
+ var a = document.createElement('a');
+ var b = document.createElement('b');
+ var c = document.createElement('c');
+
+ a.appendChild(c);
+
+ var observerA = new MutationObserver(function() {});
+ observerA.observe(a, {
+ childList: true
+ });
+
+ var observerB = new MutationObserver(function() {});
+ observerB.observe(b, {
+ childList: true
+ });
+
+ b.appendChild(c);
+
+ var recordsA = observerA.takeRecords();
+
+ assert.equal(recordsA.length, 1);
+ expectMutationRecord(recordsA[0], {
+ type: 'childList',
+ target: a,
+ removedNodes: [c]
+ });
+
+ var recordsB = observerB.takeRecords();
+ assert.equal(recordsB.length, 1);
+ expectMutationRecord(recordsB[0], {
+ type: 'childList',
+ target: b,
+ addedNodes: [c]
+ });
+ });
+
+ test('insertBefore removal', function() {
+ var a = document.createElement('a');
+ var b = document.createElement('b');
+ var c = document.createElement('c');
+ var d = document.createElement('d');
+ var e = document.createElement('e');
+
+ a.appendChild(c);
+ a.appendChild(d);
+ b.appendChild(e);
+
+ var observerA = new MutationObserver(function() {});
+ observerA.observe(a, {
+ childList: true
+ });
+
+ var observerB = new MutationObserver(function() {});
+ observerB.observe(b, {
+ childList: true
+ });
+
+ b.insertBefore(d, e);
+
+ var recordsA = observerA.takeRecords();
+
+ assert.equal(recordsA.length, 1);
+ expectMutationRecord(recordsA[0], {
+ type: 'childList',
+ target: a,
+ removedNodes: [d],
+ previousSibling: c
+ });
+
+ var recordsB = observerB.takeRecords();
+ assert.equal(recordsB.length, 1);
+ expectMutationRecord(recordsB[0], {
+ type: 'childList',
+ target: b,
+ addedNodes: [d],
+ nextSibling: e
+ });
+ });
+
+ test('appendChild removal document fragment', function() {
+ var df = document.createDocumentFragment();
+ var b = document.createElement('b');
+ var c = document.createElement('c');
+
+ df.appendChild(c);
+
+ var observerDf = new MutationObserver(function() {});
+ observerDf.observe(df, {
+ childList: true
+ });
+
+ var observerB = new MutationObserver(function() {});
+ observerB.observe(b, {
+ childList: true
+ });
+
+ b.appendChild(df);
+
+ var recordsDf = observerDf.takeRecords();
+
+ assert.equal(recordsDf.length, 1);
+ expectMutationRecord(recordsDf[0], {
+ type: 'childList',
+ target: df,
+ removedNodes: [c]
+ });
+
+ var recordsB = observerB.takeRecords();
+ assert.equal(recordsB.length, 1);
+ expectMutationRecord(recordsB[0], {
+ type: 'childList',
+ target: b,
+ addedNodes: [c]
+ });
+ });
+
+ });
+
+});
diff --git a/test/js/MutationObserver/mixed.js b/test/js/MutationObserver/mixed.js
new file mode 100644
index 0000000..afc75a9
--- /dev/null
+++ b/test/js/MutationObserver/mixed.js
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2012 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.
+ */
+
+suite('MutationObserver', function() {
+
+ suite('mixed', function() {
+
+ test('attr and characterData', function() {
+ var div = document.createElement('div');
+ var text = div.appendChild(document.createTextNode('text'));
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ attributes: true,
+ characterData: true,
+ subtree: true
+ });
+ div.setAttribute('a', 'A');
+ div.firstChild.data = 'changed';
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 2);
+
+ expectMutationRecord(records[0], {
+ type: 'attributes',
+ target: div,
+ attributeName: 'a',
+ attributeNamespace: null
+ });
+ expectMutationRecord(records[1], {
+ type: 'characterData',
+ target: div.firstChild
+ });
+ });
+
+ });
+
+});
\ No newline at end of file
diff --git a/test/js/MutationObserver/options.js b/test/js/MutationObserver/options.js
new file mode 100644
index 0000000..bea4c5d
--- /dev/null
+++ b/test/js/MutationObserver/options.js
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2012 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.
+ */
+
+suite('MutationObserver', function() {
+
+ suite('options', function() {
+
+ test('attributeOldValue and attributes', function() {
+ var div = document.createElement('div');
+ var observer = new MutationObserver(function() {});
+
+ assert.throws(function() {
+ observer.observe(div, {
+ attributeOldValue: true,
+ attributes: false
+ });
+ }, TypeError);
+
+ observer.observe(div, {
+ attributeOldValue: true,
+ });
+
+ observer.observe(div, {
+ attributeOldValue: false,
+ attributes: false
+ });
+
+ observer.observe(div, {
+ attributeOldValue: false,
+ attributes: true
+ });
+
+ observer.observe(div, {
+ attributeOldValue: true,
+ attributes: true
+ });
+ });
+
+ test('attributeFilter and attributes', function() {
+ var div = document.createElement('div');
+ var observer = new MutationObserver(function() {});
+
+ assert.throws(function() {
+ observer.observe(div, {
+ attributeFilter: ['name'],
+ attributes: false
+ });
+ }, TypeError);
+
+ observer.observe(div, {
+ attributeFilter: ['name'],
+ });
+
+ assert.throws(function() {
+ observer.observe(div, {
+ attributeFilter: null,
+ });
+ }, TypeError);
+
+ observer.observe(div, {
+ attributeFilter: ['name'],
+ attributes: true
+ });
+
+ observer.observe(div, {
+ attributes: false
+ });
+
+ observer.observe(div, {
+ attributes: true
+ });
+ });
+
+ test('characterDataOldValue and characterData', function() {
+ var div = document.createElement('div');
+ var observer = new MutationObserver(function() {});
+
+ assert.throws(function() {
+ observer.observe(div, {
+ characterDataOldValue: true,
+ characterData: false
+ });
+ }, TypeError);
+
+ observer.observe(div, {
+ characterDataOldValue: true
+ });
+
+ observer.observe(div, {
+ characterDataOldValue: false,
+ characterData: false
+ });
+
+ observer.observe(div, {
+ characterDataOldValue: false,
+ characterData: true
+ });
+
+ observer.observe(div, {
+ characterDataOldValue: true,
+ characterData: true
+ });
+ });
+
+ });
+
+});
\ No newline at end of file
diff --git a/test/js/MutationObserver/transient.js b/test/js/MutationObserver/transient.js
new file mode 100644
index 0000000..1f85cdd
--- /dev/null
+++ b/test/js/MutationObserver/transient.js
@@ -0,0 +1,278 @@
+/*
+ * Copyright 2012 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.
+ */
+
+suite('MutationObserver', function() {
+
+ suite('transient', function() {
+
+ test('attr', function() {
+ var div = document.createElement('div');
+ var child = div.appendChild(document.createElement('div'));
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ attributes: true,
+ subtree: true
+ });
+ div.removeChild(child);
+ child.setAttribute('a', 'A');
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 1);
+
+ expectMutationRecord(records[0], {
+ type: 'attributes',
+ target: child,
+ attributeName: 'a',
+ attributeNamespace: null
+ });
+
+ child.setAttribute('b', 'B');
+
+ records = observer.takeRecords();
+ assert.equal(records.length, 1);
+
+ expectMutationRecord(records[0], {
+ type: 'attributes',
+ target: child,
+ attributeName: 'b',
+ attributeNamespace: null
+ });
+ });
+
+ test('attr callback', function(cont) {
+ var div = document.createElement('div');
+ var child = div.appendChild(document.createElement('div'));
+ var i = 0;
+ var observer = new MutationObserver(function(records) {
+ i++;
+ if (i > 1)
+ expect().fail();
+
+ assert.equal(records.length, 1);
+
+ expectMutationRecord(records[0], {
+ type: 'attributes',
+ target: child,
+ attributeName: 'a',
+ attributeNamespace: null
+ });
+
+ // The transient observers are removed before the callback is called.
+ child.setAttribute('b', 'B');
+ records = observer.takeRecords();
+ assert.equal(records.length, 0);
+
+ cont();
+ });
+
+ observer.observe(div, {
+ attributes: true,
+ subtree: true
+ });
+
+ div.removeChild(child);
+ child.setAttribute('a', 'A');
+ });
+
+ test('attr, make sure transient gets removed', function(cont) {
+ var div = document.createElement('div');
+ var child = div.appendChild(document.createElement('div'));
+ var i = 0;
+ var observer = new MutationObserver(function(records) {
+ i++;
+ if (i > 1)
+ expect().fail();
+
+ assert.equal(records.length, 1);
+
+ expectMutationRecord(records[0], {
+ type: 'attributes',
+ target: child,
+ attributeName: 'a',
+ attributeNamespace: null
+ });
+
+ step2();
+ });
+
+ observer.observe(div, {
+ attributes: true,
+ subtree: true
+ });
+
+ div.removeChild(child);
+ child.setAttribute('a', 'A');
+
+ function step2() {
+ var div2 = document.createElement('div');
+ var observer2 = new MutationObserver(function(records) {
+ i++;
+ if (i > 2)
+ expect().fail();
+
+ assert.equal(records.length, 1);
+
+ expectMutationRecord(records[0], {
+ type: 'attributes',
+ target: child,
+ attributeName: 'b',
+ attributeNamespace: null
+ });
+
+ cont();
+ });
+
+ observer2.observe(div2, {
+ attributes: true,
+ subtree: true,
+ });
+
+ div2.appendChild(child);
+ child.setAttribute('b', 'B');
+ }
+ });
+
+ test('characterData', function() {
+ var div = document.createElement('div');
+ var child = div.appendChild(document.createTextNode('text'));
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ characterData: true,
+ subtree: true
+ });
+ div.removeChild(child);
+ child.data = 'changed';
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 1);
+
+ expectMutationRecord(records[0], {
+ type: 'characterData',
+ target: child
+ });
+
+ child.data += ' again';
+
+ records = observer.takeRecords();
+ assert.equal(records.length, 1);
+
+ expectMutationRecord(records[0], {
+ type: 'characterData',
+ target: child
+ });
+ });
+
+ test('characterData callback', function(cont) {
+ var div = document.createElement('div');
+ var child = div.appendChild(document.createTextNode('text'));
+ var i = 0;
+ var observer = new MutationObserver(function(records) {
+ i++;
+ if (i > 1)
+ expect().fail();
+
+ assert.equal(records.length, 1);
+
+ expectMutationRecord(records[0], {
+ type: 'characterData',
+ target: child
+ });
+
+ // The transient observers are removed before the callback is called.
+ child.data += ' again';
+ records = observer.takeRecords();
+ assert.equal(records.length, 0);
+
+ cont();
+ });
+ observer.observe(div, {
+ characterData: true,
+ subtree: true
+ });
+ div.removeChild(child);
+ child.data = 'changed';
+ });
+
+ test('childList', function() {
+ var div = document.createElement('div');
+ var child = div.appendChild(document.createElement('div'));
+ var observer = new MutationObserver(function() {});
+ observer.observe(div, {
+ childList: true,
+ subtree: true
+ });
+ div.removeChild(child);
+ var grandChild = child.appendChild(document.createElement('span'));
+
+ var records = observer.takeRecords();
+ assert.equal(records.length, 2);
+
+ expectMutationRecord(records[0], {
+ type: 'childList',
+ target: div,
+ removedNodes: [child]
+ });
+
+ expectMutationRecord(records[1], {
+ type: 'childList',
+ target: child,
+ addedNodes: [grandChild]
+ });
+
+ child.removeChild(grandChild);
+
+ records = observer.takeRecords();
+ assert.equal(records.length, 1);
+
+ expectMutationRecord(records[0], {
+ type: 'childList',
+ target: child,
+ removedNodes: [grandChild]
+ });
+ });
+
+ test('childList callback', function(cont) {
+ var div = document.createElement('div');
+ var child = div.appendChild(document.createElement('div'));
+ var i = 0;
+ var observer = new MutationObserver(function(records) {
+ i++;
+ if (i > 1)
+ expect().fail();
+
+ assert.equal(records.length, 2);
+
+ expectMutationRecord(records[0], {
+ type: 'childList',
+ target: div,
+ removedNodes: [child]
+ });
+
+ expectMutationRecord(records[1], {
+ type: 'childList',
+ target: child,
+ addedNodes: [grandChild]
+ });
+
+ // The transient observers are removed before the callback is called.
+ child.removeChild(grandChild);
+
+ records = observer.takeRecords();
+ assert.equal(records.length, 0);
+
+ cont();
+ });
+ observer.observe(div, {
+ childList: true,
+ subtree: true
+ });
+ div.removeChild(child);
+ var grandChild = child.appendChild(document.createElement('span'));
+ });
+
+ });
+
+});
\ No newline at end of file
diff --git a/test/js/microtask.js b/test/js/microtask.js
new file mode 100644
index 0000000..179057b
--- /dev/null
+++ b/test/js/microtask.js
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2013 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.
+ */
+
+suite('Microtask', function() {
+
+ var setEndOfMicrotask = ShadowDOMPolyfill.setEndOfMicrotask;
+
+ test('single', function(done) {
+ setEndOfMicrotask(done);
+ });
+
+ test('multiple', function(done) {
+ var count = 0;
+ setEndOfMicrotask(function() {
+ count++;
+ assert.equal(2, count);
+ });
+ setEndOfMicrotask(function() {
+ count++;
+ assert.equal(3, count);
+ });
+ setEndOfMicrotask(function() {
+ count++;
+ assert.equal(4, count);
+ done();
+ });
+ count++;
+ });
+
+ test('nested', function(done) {
+ var count = 0;
+ setEndOfMicrotask(function() {
+ assert.equal(1, count);
+ setEndOfMicrotask(function() {
+ assert.equal(2, count);
+ done();
+ });
+ count++;
+ });
+ count++;
+ });
+
+});
diff --git a/test/test.main.js b/test/test.main.js
index 3f2da9a..08662f6 100644
--- a/test/test.main.js
+++ b/test/test.main.js
@@ -44,6 +44,29 @@ function assertArrayEqual(a, b, msg) {
assert.equal(a.length, b.length, msg);
}
+function expectMutationRecord(record, expected) {
+ assert.equal(record.type,
+ expected.type === undefined ? null : expected.type);
+ assert.equal(record.target,
+ expected.target === undefined ? null : expected.target);
+ assertArrayEqual(record.addedNodes,
+ expected.addedNodes === undefined ? [] : expected.addedNodes);
+ assertArrayEqual(record.removedNodes,
+ expected.removedNodes === undefined ? [] : expected.removedNodes);
+ assert.equal(record.previousSibling,
+ expected.previousSibling === undefined ?
+ null : expected.previousSibling);
+ assert.equal(record.nextSibling,
+ expected.nextSibling === undefined ? null : expected.nextSibling);
+ assert.equal(record.attributeName,
+ expected.attributeName === undefined ? null : expected.attributeName);
+ assert.equal(record.attributeNamespace,
+ expected.attributeNamespace === undefined ?
+ null : expected.attributeNamespace);
+ assert.equal(record.oldValue,
+ expected.oldValue === undefined ? null : expected.oldValue);
+}
+
mocha.setup({
ui: 'tdd',
globals: ['console', 'getInterface']
@@ -76,14 +99,22 @@ var modules = [
'HTMLTemplateElement.js',
'HTMLTextAreaElement.js',
'MutationObserver.js',
+ 'MutationObserver/attributes.js',
+ 'MutationObserver/callback.js',
+ 'MutationObserver/characterData.js',
+ 'MutationObserver/childList.js',
+ 'MutationObserver/mixed.js',
+ 'MutationObserver/transient.js',
+ 'MutationObserver/options.js',
'Node.js',
'ParentNodeInterface.js',
'ShadowRoot.js',
'Text.js',
- 'Window.js',
'Range.js',
+ 'Window.js',
'custom-element.js',
'events.js',
+ 'microtask.js',
'paralleltrees.js',
'reprojection.js',
'rerender.js',