diff --git a/index.js b/index.js index 66cdc6f..b5b1f30 100644 --- a/index.js +++ b/index.js @@ -1,586 +1,596 @@ -var MutationObserver = window.MutationObserver - || window.WebKitMutationObserver - || window.MozMutationObserver; -/* - * Copyright 2012 The Polymer Authors. All rights reserved. - * Use of this source code is goverened by a BSD-style - * license that can be found in the LICENSE file. - */ +(function() { -var WeakMap = window.WeakMap; + // silently exit in non-browser environment + if(typeof window === "undefined") { + return; + } -if (typeof WeakMap === 'undefined') { - var defineProperty = Object.defineProperty; - var counter = Date.now() % 1e9; + var MutationObserver = window.MutationObserver + || window.WebKitMutationObserver + || window.MozMutationObserver; + + /* + * Copyright 2012 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + + var WeakMap = window.WeakMap; + + if (typeof WeakMap === 'undefined') { + var defineProperty = Object.defineProperty; + var counter = Date.now() % 1e9; + + 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}); + return this; + }, + get: function(key) { + var entry; + return (entry = key[this.name]) && entry[0] === key ? + entry[1] : undefined; + }, + 'delete': function(key) { + var entry = key[this.name]; + if (!entry) return false; + var hasValue = entry[0] === key; + entry[0] = entry[1] = undefined; + return hasValue; + }, + has: function(key) { + var entry = key[this.name]; + if (!entry) return false; + return entry[0] === key; + } + }; + } - WeakMap = function() { - this.name = '__st' + (Math.random() * 1e9 >>> 0) + (counter++ + '__'); - }; + 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, '*'); + }; + } - 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}); - return this; - }, - get: function(key) { - var entry; - return (entry = key[this.name]) && entry[0] === key ? - entry[1] : undefined; - }, - 'delete': function(key) { - var entry = key[this.name]; - if (!entry) return false; - var hasValue = entry[0] === key; - entry[0] = entry[1] = undefined; - return hasValue; - }, - has: function(key) { - var entry = key[this.name]; - if (!entry) return false; - return entry[0] === key; - } - }; -} - -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(); - }); + // 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); } - }); - 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); - } - } + + function wrapIfNeeded(node) { + return window.ShadowDOMPolyfill && + window.ShadowDOMPolyfill.wrapIfNeeded(node) || + node; } -} -var uidCounter = 0; + function dispatchCallbacks() { + // http://dom.spec.whatwg.org/#mutation-observers -/** - * 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; -} + isScheduled = false; // Used to allow a new setImmediate call above. -JsMutationObserver.prototype = { - observe: function(target, options) { - target = wrapIfNeeded(target); + var observers = scheduledObservers; + scheduledObservers = []; + // Sort observers based on their creation UID (incremental). + observers.sort(function(o1, o2) { + return o1.uid_ - o2.uid_; + }); - // 1.1 - if (!options.childList && !options.attributes && !options.characterData || + var anyNonEmpty = false; + observers.forEach(function(observer) { - // 1.2 - options.attributeOldValue && !options.attributes || + // 2.1, 2.2 + var queue = observer.takeRecords(); + // 2.3. Remove all transient registered observers whose observer is mo. + removeTransientObserversFor(observer); - // 1.3 - options.attributeFilter && options.attributeFilter.length && - !options.attributes || + // 2.4 + if (queue.length) { + observer.callback_(queue, observer); + anyNonEmpty = true; + } + }); - // 1.4 - options.characterDataOldValue && !options.characterData) { + // 3. + if (anyNonEmpty) + dispatchCallbacks(); + } - throw new SyntaxError(); - } + 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); - 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; + 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); + } } } + } - // 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); - } + 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; + } - registration.addListeners(); - }, + JsMutationObserver.prototype = { + observe: function(target, options) { + target = wrapIfNeeded(target); - disconnect: function() { - this.nodes_.forEach(function(node) { - var registrations = registrationsTable.get(node); + // 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++) { - var registration = registrations[i]; - if (registration.observer === this) { + if (registrations[i].observer === this) { + registration = registrations[i]; registration.removeListeners(); - registrations.splice(i, 1); - // Each node can only have one registered observer associated with - // this observer. + registration.options = options; 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; + // 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); } - } else { - scheduleCallback(this.observer); - } - records[length] = record; - }, + 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_ = []; + }, - addListeners: function() { - this.addListeners_(this.target); - }, + takeRecords: function() { + var copyOfRecords = this.records_; + this.records_ = []; + return copyOfRecords; + } + }; - addListeners_: function(node) { - var options = this.options; - if (options.attributes) - node.addEventListener('DOMAttrModified', this, true); + /** + * @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; + } - if (options.characterData) - node.addEventListener('DOMCharacterDataModified', this, true); + 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; + }; - if (options.childList) - node.addEventListener('DOMNodeInserted', this, true); + // We keep track of the two (possibly one) records used in a single mutation. + var currentRecord, recordWithOldValue; - if (options.childList || options.subtree) - node.addEventListener('DOMNodeRemoved', this, true); - }, + /** + * 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; + } - removeListeners: function() { - this.removeListeners_(this.target); - }, + function clearRecords() { + currentRecord = recordWithOldValue = undefined; + } - removeListeners_: function(node) { - var options = this.options; - if (options.attributes) - node.removeEventListener('DOMAttrModified', this, true); + /** + * @param {MutationRecord} record + * @return {boolean} Whether the record represents a record from the current + * mutation event. + */ + function recordRepresentsCurrentMutation(record) { + return record === recordWithOldValue || record === currentRecord; + } - if (options.characterData) - node.removeEventListener('DOMCharacterDataModified', this, true); + /** + * 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; - if (options.childList) - node.removeEventListener('DOMNodeInserted', this, true); + // 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; - if (options.childList || options.subtree) - node.removeEventListener('DOMNodeRemoved', this, true); - }, + return null; + } /** - * Adds a transient observer on node. The transient observer gets removed - * next time we deliver the change records. - * @param {Node} node + * Class used to represent a registered observer. + * @param {MutationObserver} observer + * @param {Node} target + * @param {MutationObserverInit} options + * @constructor */ - 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; + function Registration(observer, target, options) { + this.observer = observer; + this.target = target; + this.options = options; 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; + 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); } - }, 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 = null; - if (!(typeof MutationEvent !== 'undefined' && e.attrChange === MutationEvent.ADDITION)) - oldValue = 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; - }); + records[length] = record; + }, + + addListeners: function() { + this.addListeners_(this.target); + }, - break; + addListeners_: function(node) { + var options = this.options; + if (options.attributes) + node.addEventListener('DOMAttrModified', this, true); - case 'DOMCharacterDataModified': - // http://dom.spec.whatwg.org/#concept-mo-queue-characterdata - var target = e.target; + if (options.characterData) + node.addEventListener('DOMCharacterDataModified', this, true); - // 1. - var record = getRecord('characterData', target); + if (options.childList) + node.addEventListener('DOMNodeInserted', this, true); - // 2. - var oldValue = e.prevValue; + if (options.childList || options.subtree) + node.addEventListener('DOMNodeRemoved', this, true); + }, + removeListeners: function() { + this.removeListeners_(this.target); + }, - forEachAncestorAndObserverEnqueueRecord(target, function(options) { - // 3.1, 4.2 - if (!options.characterData) - return; + removeListeners_: function(node) { + var options = this.options; + if (options.attributes) + node.removeEventListener('DOMAttrModified', this, true); - // 3.2, 4.3 - if (options.characterDataOldValue) - return getRecordWithOldValue(oldValue); + if (options.characterData) + node.removeEventListener('DOMCharacterDataModified', this, true); - // 3.3, 4.4 - return record; - }); + 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 = []); - 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]; + // 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; + } } - 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; - }); + }, 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 = null; + if (!(typeof MutationEvent !== 'undefined' && e.attrChange === MutationEvent.ADDITION)) + oldValue = 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(); } + }; - clearRecords(); + if (!MutationObserver) { + MutationObserver = JsMutationObserver; } -}; -if (!MutationObserver) { - MutationObserver = JsMutationObserver; -} + module.exports = MutationObserver; -module.exports = MutationObserver; +})()