// ==UserScript==
// @name mutation-observer
// @version 0.1.0
// @description Simple wrapper for using DOM mutation events
// ==/UserScript==
// Copyright 2011 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
var __extends = this.__extends || function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
__.prototype = b.prototype;
d.prototype = new __();
};
var MutationObserverCtor;
if (typeof WebKitMutationObserver !== 'undefined')
MutationObserverCtor = WebKitMutationObserver;
else
MutationObserverCtor = MutationObserver;
if (MutationObserverCtor === undefined) {
console.error('DOM Mutation Observers are required.');
console.error('https://developer.mozilla.org/en-US/docs/DOM/MutationObserver');
throw Error('DOM Mutation Observers are required');
}
var NodeMap = (function () {
function NodeMap() {
this.nodes = [];
this.values = [];
}
NodeMap.prototype.isIndex = function (s) {
return +s === s >>> 0;
};
NodeMap.prototype.nodeId = function (node) {
var id = node[NodeMap.ID_PROP];
if (!id)
id = node[NodeMap.ID_PROP] = NodeMap.nextId_++;
return id;
};
NodeMap.prototype.set = function (node, value) {
var id = this.nodeId(node);
this.nodes[id] = node;
this.values[id] = value;
};
NodeMap.prototype.get = function (node) {
var id = this.nodeId(node);
return this.values[id];
};
NodeMap.prototype.has = function (node) {
return this.nodeId(node) in this.nodes;
};
NodeMap.prototype.delete = function (node) {
var id = this.nodeId(node);
delete this.nodes[id];
this.values[id] = undefined;
};
NodeMap.prototype.keys = function () {
var nodes = [];
for (var id in this.nodes) {
if (!this.isIndex(id))
continue;
nodes.push(this.nodes[id]);
}
return nodes;
};
NodeMap.ID_PROP = '__mutation_summary_node_map_id__';
NodeMap.nextId_ = 1;
return NodeMap;
})();
/**
* var reachableMatchableProduct = [
* // STAYED_OUT, ENTERED, STAYED_IN, EXITED
* [ STAYED_OUT, STAYED_OUT, STAYED_OUT, STAYED_OUT ], // STAYED_OUT
* [ STAYED_OUT, ENTERED, ENTERED, STAYED_OUT ], // ENTERED
* [ STAYED_OUT, ENTERED, STAYED_IN, EXITED ], // STAYED_IN
* [ STAYED_OUT, STAYED_OUT, EXITED, EXITED ] // EXITED
* ];
*/
var Movement;
(function (Movement) {
Movement[Movement["STAYED_OUT"] = 0] = "STAYED_OUT";
Movement[Movement["ENTERED"] = 1] = "ENTERED";
Movement[Movement["STAYED_IN"] = 2] = "STAYED_IN";
Movement[Movement["REPARENTED"] = 3] = "REPARENTED";
Movement[Movement["REORDERED"] = 4] = "REORDERED";
Movement[Movement["EXITED"] = 5] = "EXITED";
})(Movement || (Movement = {}));
function enteredOrExited(changeType) {
return changeType === 1 /* ENTERED */ || changeType === 5 /* EXITED */;
}
var NodeChange = (function () {
function NodeChange(node, childList, attributes, characterData, oldParentNode, added, attributeOldValues, characterDataOldValue) {
if (typeof childList === "undefined") { childList = false; }
if (typeof attributes === "undefined") { attributes = false; }
if (typeof characterData === "undefined") { characterData = false; }
if (typeof oldParentNode === "undefined") { oldParentNode = null; }
if (typeof added === "undefined") { added = false; }
if (typeof attributeOldValues === "undefined") { attributeOldValues = null; }
if (typeof characterDataOldValue === "undefined") { characterDataOldValue = null; }
this.node = node;
this.childList = childList;
this.attributes = attributes;
this.characterData = characterData;
this.oldParentNode = oldParentNode;
this.added = added;
this.attributeOldValues = attributeOldValues;
this.characterDataOldValue = characterDataOldValue;
this.isCaseInsensitive = this.node.nodeType === Node.ELEMENT_NODE && this.node instanceof HTMLElement && this.node.ownerDocument instanceof HTMLDocument;
}
NodeChange.prototype.getAttributeOldValue = function (name) {
if (!this.attributeOldValues)
return undefined;
if (this.isCaseInsensitive)
name = name.toLowerCase();
return this.attributeOldValues[name];
};
NodeChange.prototype.getAttributeNamesMutated = function () {
var names = [];
if (!this.attributeOldValues)
return names;
for (var name in this.attributeOldValues) {
names.push(name);
}
return names;
};
NodeChange.prototype.attributeMutated = function (name, oldValue) {
this.attributes = true;
this.attributeOldValues = this.attributeOldValues || {};
if (name in this.attributeOldValues)
return;
this.attributeOldValues[name] = oldValue;
};
NodeChange.prototype.characterDataMutated = function (oldValue) {
if (this.characterData)
return;
this.characterData = true;
this.characterDataOldValue = oldValue;
};
// Note: is it possible to receive a removal followed by a removal. This
// can occur if the removed node is added to an non-observed node, that
// node is added to the observed area, and then the node removed from
// it.
NodeChange.prototype.removedFromParent = function (parent) {
this.childList = true;
if (this.added || this.oldParentNode)
this.added = false;
else
this.oldParentNode = parent;
};
NodeChange.prototype.insertedIntoParent = function () {
this.childList = true;
this.added = true;
};
// An node's oldParent is
// -its present parent, if its parentNode was not changed.
// -null if the first thing that happened to it was an add.
// -the node it was removed from if the first thing that happened to it
// was a remove.
NodeChange.prototype.getOldParent = function () {
if (this.childList) {
if (this.oldParentNode)
return this.oldParentNode;
if (this.added)
return null;
}
return this.node.parentNode;
};
return NodeChange;
})();
var ChildListChange = (function () {
function ChildListChange() {
this.added = new NodeMap();
this.removed = new NodeMap();
this.maybeMoved = new NodeMap();
this.oldPrevious = new NodeMap();
this.moved = undefined;
}
return ChildListChange;
})();
var TreeChanges = (function (_super) {
__extends(TreeChanges, _super);
function TreeChanges(rootNode, mutations) {
_super.call(this);
this.rootNode = rootNode;
this.reachableCache = undefined;
this.wasReachableCache = undefined;
this.anyParentsChanged = false;
this.anyAttributesChanged = false;
this.anyCharacterDataChanged = false;
for (var m = 0; m < mutations.length; m++) {
var mutation = mutations[m];
switch (mutation.type) {
case 'childList':
this.anyParentsChanged = true;
for (var i = 0; i < mutation.removedNodes.length; i++) {
var node = mutation.removedNodes[i];
this.getChange(node).removedFromParent(mutation.target);
}
for (var i = 0; i < mutation.addedNodes.length; i++) {
var node = mutation.addedNodes[i];
this.getChange(node).insertedIntoParent();
}
break;
case 'attributes':
this.anyAttributesChanged = true;
var change = this.getChange(mutation.target);
change.attributeMutated(mutation.attributeName, mutation.oldValue);
break;
case 'characterData':
this.anyCharacterDataChanged = true;
var change = this.getChange(mutation.target);
change.characterDataMutated(mutation.oldValue);
break;
}
}
}
TreeChanges.prototype.getChange = function (node) {
var change = this.get(node);
if (!change) {
change = new NodeChange(node);
this.set(node, change);
}
return change;
};
TreeChanges.prototype.getOldParent = function (node) {
var change = this.get(node);
return change ? change.getOldParent() : node.parentNode;
};
TreeChanges.prototype.getIsReachable = function (node) {
if (node === this.rootNode)
return true;
if (!node)
return false;
this.reachableCache = this.reachableCache || new NodeMap();
var isReachable = this.reachableCache.get(node);
if (isReachable === undefined) {
isReachable = this.getIsReachable(node.parentNode);
this.reachableCache.set(node, isReachable);
}
return isReachable;
};
// A node wasReachable if its oldParent wasReachable.
TreeChanges.prototype.getWasReachable = function (node) {
if (node === this.rootNode)
return true;
if (!node)
return false;
this.wasReachableCache = this.wasReachableCache || new NodeMap();
var wasReachable = this.wasReachableCache.get(node);
if (wasReachable === undefined) {
wasReachable = this.getWasReachable(this.getOldParent(node));
this.wasReachableCache.set(node, wasReachable);
}
return wasReachable;
};
TreeChanges.prototype.reachabilityChange = function (node) {
if (this.getIsReachable(node)) {
return this.getWasReachable(node) ? 2 /* STAYED_IN */ : 1 /* ENTERED */;
}
return this.getWasReachable(node) ? 5 /* EXITED */ : 0 /* STAYED_OUT */;
};
return TreeChanges;
})(NodeMap);
var MutationProjection = (function () {
// TOOD(any)
function MutationProjection(rootNode, mutations, selectors, calcReordered, calcOldPreviousSibling) {
this.rootNode = rootNode;
this.mutations = mutations;
this.selectors = selectors;
this.calcReordered = calcReordered;
this.calcOldPreviousSibling = calcOldPreviousSibling;
this.treeChanges = new TreeChanges(rootNode, mutations);
this.entered = [];
this.exited = [];
this.stayedIn = new NodeMap();
this.visited = new NodeMap();
this.childListChangeMap = undefined;
this.characterDataOnly = undefined;
this.matchCache = undefined;
this.processMutations();
}
MutationProjection.prototype.processMutations = function () {
if (!this.treeChanges.anyParentsChanged && !this.treeChanges.anyAttributesChanged)
return;
var changedNodes = this.treeChanges.keys();
for (var i = 0; i < changedNodes.length; i++) {
this.visitNode(changedNodes[i], undefined);
}
};
MutationProjection.prototype.visitNode = function (node, parentReachable) {
if (this.visited.has(node))
return;
this.visited.set(node, true);
var change = this.treeChanges.get(node);
var reachable = parentReachable;
// node inherits its parent's reachability change unless
// its parentNode was mutated.
if ((change && change.childList) || reachable == undefined)
reachable = this.treeChanges.reachabilityChange(node);
if (reachable === 0 /* STAYED_OUT */)
return;
// Cache match results for sub-patterns.
this.matchabilityChange(node);
if (reachable === 1 /* ENTERED */) {
this.entered.push(node);
} else if (reachable === 5 /* EXITED */) {
this.exited.push(node);
this.ensureHasOldPreviousSiblingIfNeeded(node);
} else if (reachable === 2 /* STAYED_IN */) {
var movement = 2 /* STAYED_IN */;
if (change && change.childList) {
if (change.oldParentNode !== node.parentNode) {
movement = 3 /* REPARENTED */;
this.ensureHasOldPreviousSiblingIfNeeded(node);
} else if (this.calcReordered && this.wasReordered(node)) {
movement = 4 /* REORDERED */;
}
}
this.stayedIn.set(node, movement);
}
if (reachable === 2 /* STAYED_IN */)
return;
for (var child = node.firstChild; child; child = child.nextSibling) {
this.visitNode(child, reachable);
}
};
MutationProjection.prototype.ensureHasOldPreviousSiblingIfNeeded = function (node) {
if (!this.calcOldPreviousSibling)
return;
this.processChildlistChanges();
var parentNode = node.parentNode;
var nodeChange = this.treeChanges.get(node);
if (nodeChange && nodeChange.oldParentNode)
parentNode = nodeChange.oldParentNode;
var change = this.childListChangeMap.get(parentNode);
if (!change) {
change = new ChildListChange();
this.childListChangeMap.set(parentNode, change);
}
if (!change.oldPrevious.has(node)) {
change.oldPrevious.set(node, node.previousSibling);
}
};
MutationProjection.prototype.getChanged = function (summary, selectors, characterDataOnly) {
this.selectors = selectors;
this.characterDataOnly = characterDataOnly;
for (var i = 0; i < this.entered.length; i++) {
var node = this.entered[i];
var matchable = this.matchabilityChange(node);
if (matchable === 1 /* ENTERED */ || matchable === 2 /* STAYED_IN */)
summary.added.push(node);
}
var stayedInNodes = this.stayedIn.keys();
for (var i = 0; i < stayedInNodes.length; i++) {
var node = stayedInNodes[i];
var matchable = this.matchabilityChange(node);
if (matchable === 1 /* ENTERED */) {
summary.added.push(node);
} else if (matchable === 5 /* EXITED */) {
summary.removed.push(node);
} else if (matchable === 2 /* STAYED_IN */ && (summary.reparented || summary.reordered)) {
var movement = this.stayedIn.get(node);
if (summary.reparented && movement === 3 /* REPARENTED */)
summary.reparented.push(node);
else if (summary.reordered && movement === 4 /* REORDERED */)
summary.reordered.push(node);
}
}
for (var i = 0; i < this.exited.length; i++) {
var node = this.exited[i];
var matchable = this.matchabilityChange(node);
if (matchable === 5 /* EXITED */ || matchable === 2 /* STAYED_IN */)
summary.removed.push(node);
}
};
MutationProjection.prototype.getOldParentNode = function (node) {
var change = this.treeChanges.get(node);
if (change && change.childList)
return change.oldParentNode ? change.oldParentNode : null;
var reachabilityChange = this.treeChanges.reachabilityChange(node);
if (reachabilityChange === 0 /* STAYED_OUT */ || reachabilityChange === 1 /* ENTERED */)
throw Error('getOldParentNode requested on invalid node.');
return node.parentNode;
};
MutationProjection.prototype.getOldPreviousSibling = function (node) {
var parentNode = node.parentNode;
var nodeChange = this.treeChanges.get(node);
if (nodeChange && nodeChange.oldParentNode)
parentNode = nodeChange.oldParentNode;
var change = this.childListChangeMap.get(parentNode);
if (!change)
throw Error('getOldPreviousSibling requested on invalid node.');
return change.oldPrevious.get(node);
};
MutationProjection.prototype.getOldAttribute = function (element, attrName) {
var change = this.treeChanges.get(element);
if (!change || !change.attributes)
throw Error('getOldAttribute requested on invalid node.');
var value = change.getAttributeOldValue(attrName);
if (value === undefined)
throw Error('getOldAttribute requested for unchanged attribute name.');
return value;
};
MutationProjection.prototype.attributeChangedNodes = function (includeAttributes) {
if (!this.treeChanges.anyAttributesChanged)
return {};
var attributeFilter;
var caseInsensitiveFilter;
if (includeAttributes) {
attributeFilter = {};
caseInsensitiveFilter = {};
for (var i = 0; i < includeAttributes.length; i++) {
var attrName = includeAttributes[i];
attributeFilter[attrName] = true;
caseInsensitiveFilter[attrName.toLowerCase()] = attrName;
}
}
var result = {};
var nodes = this.treeChanges.keys();
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
var change = this.treeChanges.get(node);
if (!change.attributes)
continue;
if (2 /* STAYED_IN */ !== this.treeChanges.reachabilityChange(node) || 2 /* STAYED_IN */ !== this.matchabilityChange(node)) {
continue;
}
var element = node;
var changedAttrNames = change.getAttributeNamesMutated();
for (var j = 0; j < changedAttrNames.length; j++) {
var attrName = changedAttrNames[j];
if (attributeFilter && !attributeFilter[attrName] && !(change.isCaseInsensitive && caseInsensitiveFilter[attrName])) {
continue;
}
var oldValue = change.getAttributeOldValue(attrName);
if (oldValue === element.getAttribute(attrName))
continue;
if (caseInsensitiveFilter && change.isCaseInsensitive)
attrName = caseInsensitiveFilter[attrName];
result[attrName] = result[attrName] || [];
result[attrName].push(element);
}
}
return result;
};
MutationProjection.prototype.getOldCharacterData = function (node) {
var change = this.treeChanges.get(node);
if (!change || !change.characterData)
throw Error('getOldCharacterData requested on invalid node.');
return change.characterDataOldValue;
};
MutationProjection.prototype.getCharacterDataChanged = function () {
if (!this.treeChanges.anyCharacterDataChanged)
return [];
var nodes = this.treeChanges.keys();
var result = [];
for (var i = 0; i < nodes.length; i++) {
var target = nodes[i];
if (2 /* STAYED_IN */ !== this.treeChanges.reachabilityChange(target))
continue;
var change = this.treeChanges.get(target);
if (!change.characterData || target.textContent == change.characterDataOldValue)
continue;
result.push(target);
}
return result;
};
MutationProjection.prototype.computeMatchabilityChange = function (selector, el) {
if (!this.matchCache)
this.matchCache = [];
if (!this.matchCache[selector.uid])
this.matchCache[selector.uid] = new NodeMap();
var cache = this.matchCache[selector.uid];
var result = cache.get(el);
if (result === undefined) {
result = selector.matchabilityChange(el, this.treeChanges.get(el));
cache.set(el, result);
}
return result;
};
MutationProjection.prototype.matchabilityChange = function (node) {
var _this = this;
// TODO(rafaelw): Include PI, CDATA?
// Only include text nodes.
if (this.characterDataOnly) {
switch (node.nodeType) {
case Node.COMMENT_NODE:
case Node.TEXT_NODE:
return 2 /* STAYED_IN */;
default:
return 0 /* STAYED_OUT */;
}
}
// No element filter. Include all nodes.
if (!this.selectors)
return 2 /* STAYED_IN */;
// Element filter. Exclude non-elements.
if (node.nodeType !== Node.ELEMENT_NODE)
return 0 /* STAYED_OUT */;
var el = node;
var matchChanges = this.selectors.map(function (selector) {
return _this.computeMatchabilityChange(selector, el);
});
var accum = 0 /* STAYED_OUT */;
var i = 0;
while (accum !== 2 /* STAYED_IN */ && i < matchChanges.length) {
switch (matchChanges[i]) {
case 2 /* STAYED_IN */:
accum = 2 /* STAYED_IN */;
break;
case 1 /* ENTERED */:
if (accum === 5 /* EXITED */)
accum = 2 /* STAYED_IN */;
else
accum = 1 /* ENTERED */;
break;
case 5 /* EXITED */:
if (accum === 1 /* ENTERED */)
accum = 2 /* STAYED_IN */;
else
accum = 5 /* EXITED */;
break;
}
i++;
}
return accum;
};
MutationProjection.prototype.getChildlistChange = function (el) {
var change = this.childListChangeMap.get(el);
if (!change) {
change = new ChildListChange();
this.childListChangeMap.set(el, change);
}
return change;
};
MutationProjection.prototype.processChildlistChanges = function () {
if (this.childListChangeMap)
return;
this.childListChangeMap = new NodeMap();
for (var i = 0; i < this.mutations.length; i++) {
var mutation = this.mutations[i];
if (mutation.type != 'childList')
continue;
if (this.treeChanges.reachabilityChange(mutation.target) !== 2 /* STAYED_IN */ && !this.calcOldPreviousSibling)
continue;
var change = this.getChildlistChange(mutation.target);
var oldPrevious = mutation.previousSibling;
function recordOldPrevious(node, previous) {
if (!node || change.oldPrevious.has(node) || change.added.has(node) || change.maybeMoved.has(node))
return;
if (previous && (change.added.has(previous) || change.maybeMoved.has(previous)))
return;
change.oldPrevious.set(node, previous);
}
for (var j = 0; j < mutation.removedNodes.length; j++) {
var node = mutation.removedNodes[j];
recordOldPrevious(node, oldPrevious);
if (change.added.has(node)) {
change.added.delete(node);
} else {
change.removed.set(node, true);
change.maybeMoved.delete(node);
}
oldPrevious = node;
}
recordOldPrevious(mutation.nextSibling, oldPrevious);
for (var j = 0; j < mutation.addedNodes.length; j++) {
var node = mutation.addedNodes[j];
if (change.removed.has(node)) {
change.removed.delete(node);
change.maybeMoved.set(node, true);
} else {
change.added.set(node, true);
}
}
}
};
MutationProjection.prototype.wasReordered = function (node) {
if (!this.treeChanges.anyParentsChanged)
return false;
this.processChildlistChanges();
var parentNode = node.parentNode;
var nodeChange = this.treeChanges.get(node);
if (nodeChange && nodeChange.oldParentNode)
parentNode = nodeChange.oldParentNode;
var change = this.childListChangeMap.get(parentNode);
if (!change)
return false;
if (change.moved)
return change.moved.get(node);
change.moved = new NodeMap();
var pendingMoveDecision = new NodeMap();
function isMoved(node) {
if (!node)
return false;
if (!change.maybeMoved.has(node))
return false;
var didMove = change.moved.get(node);
if (didMove !== undefined)
return didMove;
if (pendingMoveDecision.has(node)) {
didMove = true;
} else {
pendingMoveDecision.set(node, true);
didMove = getPrevious(node) !== getOldPrevious(node);
}
if (pendingMoveDecision.has(node)) {
pendingMoveDecision.delete(node);
change.moved.set(node, didMove);
} else {
didMove = change.moved.get(node);
}
return didMove;
}
var oldPreviousCache = new NodeMap();
function getOldPrevious(node) {
var oldPrevious = oldPreviousCache.get(node);
if (oldPrevious !== undefined)
return oldPrevious;
oldPrevious = change.oldPrevious.get(node);
while (oldPrevious && (change.removed.has(oldPrevious) || isMoved(oldPrevious))) {
oldPrevious = getOldPrevious(oldPrevious);
}
if (oldPrevious === undefined)
oldPrevious = node.previousSibling;
oldPreviousCache.set(node, oldPrevious);
return oldPrevious;
}
var previousCache = new NodeMap();
function getPrevious(node) {
if (previousCache.has(node))
return previousCache.get(node);
var previous = node.previousSibling;
while (previous && (change.added.has(previous) || isMoved(previous)))
previous = previous.previousSibling;
previousCache.set(node, previous);
return previous;
}
change.maybeMoved.keys().forEach(isMoved);
return change.moved.get(node);
};
return MutationProjection;
})();
var Summary = (function () {
function Summary(projection, query) {
var _this = this;
this.projection = projection;
this.added = [];
this.removed = [];
this.reparented = query.all || query.element ? [] : undefined;
this.reordered = query.all ? [] : undefined;
projection.getChanged(this, query.elementFilter, query.characterData);
if (query.all || query.attribute || query.attributeList) {
var filter = query.attribute ? [query.attribute] : query.attributeList;
var attributeChanged = projection.attributeChangedNodes(filter);
if (query.attribute) {
this.valueChanged = attributeChanged[query.attribute] || [];
} else {
this.attributeChanged = attributeChanged;
if (query.attributeList) {
query.attributeList.forEach(function (attrName) {
if (!_this.attributeChanged.hasOwnProperty(attrName))
_this.attributeChanged[attrName] = [];
});
}
}
}
if (query.all || query.characterData) {
var characterDataChanged = projection.getCharacterDataChanged();
if (query.characterData)
this.valueChanged = characterDataChanged;
else
this.characterDataChanged = characterDataChanged;
}
if (this.reordered)
this.getOldPreviousSibling = projection.getOldPreviousSibling.bind(projection);
}
Summary.prototype.getOldParentNode = function (node) {
return this.projection.getOldParentNode(node);
};
Summary.prototype.getOldAttribute = function (node, name) {
return this.projection.getOldAttribute(node, name);
};
Summary.prototype.getOldCharacterData = function (node) {
return this.projection.getOldCharacterData(node);
};
Summary.prototype.getOldPreviousSibling = function (node) {
return this.projection.getOldPreviousSibling(node);
};
return Summary;
})();
// TODO(rafaelw): Allow ':' and '.' as valid name characters.
var validNameInitialChar = /[a-zA-Z_]+/;
var validNameNonInitialChar = /[a-zA-Z0-9_\-]+/;
// TODO(rafaelw): Consider allowing backslash in the attrValue.
// TODO(rafaelw): There's got a to be way to represent this state machine
// more compactly???
function escapeQuotes(value) {
return '"' + value.replace(/"/, '\\\"') + '"';
}
var Qualifier = (function () {
function Qualifier() {
}
Qualifier.prototype.matches = function (oldValue) {
if (oldValue === null)
return false;
if (this.attrValue === undefined)
return true;
if (!this.contains)
return this.attrValue == oldValue;
var tokens = oldValue.split(' ');
for (var i = 0; i < tokens.length; i++) {
if (this.attrValue === tokens[i])
return true;
}
return false;
};
Qualifier.prototype.toString = function () {
if (this.attrName === 'class' && this.contains)
return '.' + this.attrValue;
if (this.attrName === 'id' && !this.contains)
return '#' + this.attrValue;
if (this.contains)
return '[' + this.attrName + '~=' + escapeQuotes(this.attrValue) + ']';
if ('attrValue' in this)
return '[' + this.attrName + '=' + escapeQuotes(this.attrValue) + ']';
return '[' + this.attrName + ']';
};
return Qualifier;
})();
var Selector = (function () {
function Selector() {
this.uid = Selector.nextUid++;
this.qualifiers = [];
}
Object.defineProperty(Selector.prototype, "caseInsensitiveTagName", {
get: function () {
return this.tagName.toUpperCase();
},
enumerable: true,
configurable: true
});
Object.defineProperty(Selector.prototype, "selectorString", {
get: function () {
return this.tagName + this.qualifiers.join('');
},
enumerable: true,
configurable: true
});
Selector.prototype.isMatching = function (el) {
return el[Selector.matchesSelector](this.selectorString);
};
Selector.prototype.wasMatching = function (el, change, isMatching) {
if (!change || !change.attributes)
return isMatching;
var tagName = change.isCaseInsensitive ? this.caseInsensitiveTagName : this.tagName;
if (tagName !== '*' && tagName !== el.tagName)
return false;
var attributeOldValues = [];
var anyChanged = false;
for (var i = 0; i < this.qualifiers.length; i++) {
var qualifier = this.qualifiers[i];
var oldValue = change.getAttributeOldValue(qualifier.attrName);
attributeOldValues.push(oldValue);
anyChanged = anyChanged || (oldValue !== undefined);
}
if (!anyChanged)
return isMatching;
for (var i = 0; i < this.qualifiers.length; i++) {
var qualifier = this.qualifiers[i];
var oldValue = attributeOldValues[i];
if (oldValue === undefined)
oldValue = el.getAttribute(qualifier.attrName);
if (!qualifier.matches(oldValue))
return false;
}
return true;
};
Selector.prototype.matchabilityChange = function (el, change) {
var isMatching = this.isMatching(el);
if (isMatching)
return this.wasMatching(el, change, isMatching) ? 2 /* STAYED_IN */ : 1 /* ENTERED */;
else
return this.wasMatching(el, change, isMatching) ? 5 /* EXITED */ : 0 /* STAYED_OUT */;
};
Selector.parseSelectors = function (input) {
var selectors = [];
var currentSelector;
var currentQualifier;
function newSelector() {
if (currentSelector) {
if (currentQualifier) {
currentSelector.qualifiers.push(currentQualifier);
currentQualifier = undefined;
}
selectors.push(currentSelector);
}
currentSelector = new Selector();
}
function newQualifier() {
if (currentQualifier)
currentSelector.qualifiers.push(currentQualifier);
currentQualifier = new Qualifier();
}
var WHITESPACE = /\s/;
var valueQuoteChar;
var SYNTAX_ERROR = 'Invalid or unsupported selector syntax.';
var SELECTOR = 1;
var TAG_NAME = 2;
var QUALIFIER = 3;
var QUALIFIER_NAME_FIRST_CHAR = 4;
var QUALIFIER_NAME = 5;
var ATTR_NAME_FIRST_CHAR = 6;
var ATTR_NAME = 7;
var EQUIV_OR_ATTR_QUAL_END = 8;
var EQUAL = 9;
var ATTR_QUAL_END = 10;
var VALUE_FIRST_CHAR = 11;
var VALUE = 12;
var QUOTED_VALUE = 13;
var SELECTOR_SEPARATOR = 14;
var state = SELECTOR;
var i = 0;
while (i < input.length) {
var c = input[i++];
switch (state) {
case SELECTOR:
if (c.match(validNameInitialChar)) {
newSelector();
currentSelector.tagName = c;
state = TAG_NAME;
break;
}
if (c == '*') {
newSelector();
currentSelector.tagName = '*';
state = QUALIFIER;
break;
}
if (c == '.') {
newSelector();
newQualifier();
currentSelector.tagName = '*';
currentQualifier.attrName = 'class';
currentQualifier.contains = true;
state = QUALIFIER_NAME_FIRST_CHAR;
break;
}
if (c == '#') {
newSelector();
newQualifier();
currentSelector.tagName = '*';
currentQualifier.attrName = 'id';
state = QUALIFIER_NAME_FIRST_CHAR;
break;
}
if (c == '[') {
newSelector();
newQualifier();
currentSelector.tagName = '*';
currentQualifier.attrName = '';
state = ATTR_NAME_FIRST_CHAR;
break;
}
if (c.match(WHITESPACE))
break;
throw Error(SYNTAX_ERROR);
case TAG_NAME:
if (c.match(validNameNonInitialChar)) {
currentSelector.tagName += c;
break;
}
if (c == '.') {
newQualifier();
currentQualifier.attrName = 'class';
currentQualifier.contains = true;
state = QUALIFIER_NAME_FIRST_CHAR;
break;
}
if (c == '#') {
newQualifier();
currentQualifier.attrName = 'id';
state = QUALIFIER_NAME_FIRST_CHAR;
break;
}
if (c == '[') {
newQualifier();
currentQualifier.attrName = '';
state = ATTR_NAME_FIRST_CHAR;
break;
}
if (c.match(WHITESPACE)) {
state = SELECTOR_SEPARATOR;
break;
}
if (c == ',') {
state = SELECTOR;
break;
}
throw Error(SYNTAX_ERROR);
case QUALIFIER:
if (c == '.') {
newQualifier();
currentQualifier.attrName = 'class';
currentQualifier.contains = true;
state = QUALIFIER_NAME_FIRST_CHAR;
break;
}
if (c == '#') {
newQualifier();
currentQualifier.attrName = 'id';
state = QUALIFIER_NAME_FIRST_CHAR;
break;
}
if (c == '[') {
newQualifier();
currentQualifier.attrName = '';
state = ATTR_NAME_FIRST_CHAR;
break;
}
if (c.match(WHITESPACE)) {
state = SELECTOR_SEPARATOR;
break;
}
if (c == ',') {
state = SELECTOR;
break;
}
throw Error(SYNTAX_ERROR);
case QUALIFIER_NAME_FIRST_CHAR:
if (c.match(validNameInitialChar)) {
currentQualifier.attrValue = c;
state = QUALIFIER_NAME;
break;
}
throw Error(SYNTAX_ERROR);
case QUALIFIER_NAME:
if (c.match(validNameNonInitialChar)) {
currentQualifier.attrValue += c;
break;
}
if (c == '.') {
newQualifier();
currentQualifier.attrName = 'class';
currentQualifier.contains = true;
state = QUALIFIER_NAME_FIRST_CHAR;
break;
}
if (c == '#') {
newQualifier();
currentQualifier.attrName = 'id';
state = QUALIFIER_NAME_FIRST_CHAR;
break;
}
if (c == '[') {
newQualifier();
state = ATTR_NAME_FIRST_CHAR;
break;
}
if (c.match(WHITESPACE)) {
state = SELECTOR_SEPARATOR;
break;
}
if (c == ',') {
state = SELECTOR;
break;
}
throw Error(SYNTAX_ERROR);
case ATTR_NAME_FIRST_CHAR:
if (c.match(validNameInitialChar)) {
currentQualifier.attrName = c;
state = ATTR_NAME;
break;
}
if (c.match(WHITESPACE))
break;
throw Error(SYNTAX_ERROR);
case ATTR_NAME:
if (c.match(validNameNonInitialChar)) {
currentQualifier.attrName += c;
break;
}
if (c.match(WHITESPACE)) {
state = EQUIV_OR_ATTR_QUAL_END;
break;
}
if (c == '~') {
currentQualifier.contains = true;
state = EQUAL;
break;
}
if (c == '=') {
currentQualifier.attrValue = '';
state = VALUE_FIRST_CHAR;
break;
}
if (c == ']') {
state = QUALIFIER;
break;
}
throw Error(SYNTAX_ERROR);
case EQUIV_OR_ATTR_QUAL_END:
if (c == '~') {
currentQualifier.contains = true;
state = EQUAL;
break;
}
if (c == '=') {
currentQualifier.attrValue = '';
state = VALUE_FIRST_CHAR;
break;
}
if (c == ']') {
state = QUALIFIER;
break;
}
if (c.match(WHITESPACE))
break;
throw Error(SYNTAX_ERROR);
case EQUAL:
if (c == '=') {
currentQualifier.attrValue = '';
state = VALUE_FIRST_CHAR;
break;
}
throw Error(SYNTAX_ERROR);
case ATTR_QUAL_END:
if (c == ']') {
state = QUALIFIER;
break;
}
if (c.match(WHITESPACE))
break;
throw Error(SYNTAX_ERROR);
case VALUE_FIRST_CHAR:
if (c.match(WHITESPACE))
break;
if (c == '"' || c == "'") {
valueQuoteChar = c;
state = QUOTED_VALUE;
break;
}
currentQualifier.attrValue += c;
state = VALUE;
break;
case VALUE:
if (c.match(WHITESPACE)) {
state = ATTR_QUAL_END;
break;
}
if (c == ']') {
state = QUALIFIER;
break;
}
if (c == "'" || c == '"')
throw Error(SYNTAX_ERROR);
currentQualifier.attrValue += c;
break;
case QUOTED_VALUE:
if (c == valueQuoteChar) {
state = ATTR_QUAL_END;
break;
}
currentQualifier.attrValue += c;
break;
case SELECTOR_SEPARATOR:
if (c.match(WHITESPACE))
break;
if (c == ',') {
state = SELECTOR;
break;
}
throw Error(SYNTAX_ERROR);
}
}
switch (state) {
case SELECTOR:
case TAG_NAME:
case QUALIFIER:
case QUALIFIER_NAME:
case SELECTOR_SEPARATOR:
// Valid end states.
newSelector();
break;
default:
throw Error(SYNTAX_ERROR);
}
if (!selectors.length)
throw Error(SYNTAX_ERROR);
return selectors;
};
Selector.nextUid = 1;
Selector.matchesSelector = (function () {
var element = document.createElement('div');
if (typeof element['webkitMatchesSelector'] === 'function')
return 'webkitMatchesSelector';
if (typeof element['mozMatchesSelector'] === 'function')
return 'mozMatchesSelector';
if (typeof element['msMatchesSelector'] === 'function')
return 'msMatchesSelector';
return 'matchesSelector';
})();
return Selector;
})();
var attributeFilterPattern = /^([a-zA-Z:_]+[a-zA-Z0-9_\-:\.]*)$/;
function validateAttribute(attribute) {
if (typeof attribute != 'string')
throw Error('Invalid request opion. attribute must be a non-zero length string.');
attribute = attribute.trim();
if (!attribute)
throw Error('Invalid request opion. attribute must be a non-zero length string.');
if (!attribute.match(attributeFilterPattern))
throw Error('Invalid request option. invalid attribute name: ' + attribute);
return attribute;
}
function validateElementAttributes(attribs) {
if (!attribs.trim().length)
throw Error('Invalid request option: elementAttributes must contain at least one attribute.');
var lowerAttributes = {};
var attributes = {};
var tokens = attribs.split(/\s+/);
for (var i = 0; i < tokens.length; i++) {
var name = tokens[i];
if (!name)
continue;
var name = validateAttribute(name);
var nameLower = name.toLowerCase();
if (lowerAttributes[nameLower])
throw Error('Invalid request option: observing multiple case variations of the same attribute is not supported.');
attributes[name] = true;
lowerAttributes[nameLower] = true;
}
return Object.keys(attributes);
}
function elementFilterAttributes(selectors) {
var attributes = {};
selectors.forEach(function (selector) {
selector.qualifiers.forEach(function (qualifier) {
attributes[qualifier.attrName] = true;
});
});
return Object.keys(attributes);
}
var MutationSummary = (function () {
function MutationSummary(opts) {
var _this = this;
this.connected = false;
this.options = MutationSummary.validateOptions(opts);
this.observerOptions = MutationSummary.createObserverOptions(this.options.queries);
this.root = this.options.rootNode;
this.callback = this.options.callback;
this.elementFilter = Array.prototype.concat.apply([], this.options.queries.map(function (query) {
return query.elementFilter ? query.elementFilter : [];
}));
if (!this.elementFilter.length)
this.elementFilter = undefined;
this.calcReordered = this.options.queries.some(function (query) {
return query.all;
});
this.queryValidators = []; // TODO(rafaelw): Shouldn't always define this.
if (MutationSummary.createQueryValidator) {
this.queryValidators = this.options.queries.map(function (query) {
return MutationSummary.createQueryValidator(_this.root, query);
});
}
this.observer = new MutationObserverCtor(function (mutations) {
_this.observerCallback(mutations);
});
this.reconnect();
}
MutationSummary.createObserverOptions = function (queries) {
var observerOptions = {
childList: true,
subtree: true
};
var attributeFilter;
function observeAttributes(attributes) {
if (observerOptions.attributes && !attributeFilter)
return;
observerOptions.attributes = true;
observerOptions.attributeOldValue = true;
if (!attributes) {
// observe all.
attributeFilter = undefined;
return;
}
// add to observed.
attributeFilter = attributeFilter || {};
attributes.forEach(function (attribute) {
attributeFilter[attribute] = true;
attributeFilter[attribute.toLowerCase()] = true;
});
}
queries.forEach(function (query) {
if (query.characterData) {
observerOptions.characterData = true;
observerOptions.characterDataOldValue = true;
return;
}
if (query.all) {
observeAttributes();
observerOptions.characterData = true;
observerOptions.characterDataOldValue = true;
return;
}
if (query.attribute) {
observeAttributes([query.attribute.trim()]);
return;
}
var attributes = elementFilterAttributes(query.elementFilter).concat(query.attributeList || []);
if (attributes.length)
observeAttributes(attributes);
});
if (attributeFilter)
observerOptions.attributeFilter = Object.keys(attributeFilter);
return observerOptions;
};
MutationSummary.validateOptions = function (options) {
for (var prop in options) {
if (!(prop in MutationSummary.optionKeys))
throw Error('Invalid option: ' + prop);
}
if (typeof options.callback !== 'function')
throw Error('Invalid options: callback is required and must be a function');
if (!options.queries || !options.queries.length)
throw Error('Invalid options: queries must contain at least one query request object.');
var opts = {
callback: options.callback,
rootNode: options.rootNode || document,
observeOwnChanges: !!options.observeOwnChanges,
oldPreviousSibling: !!options.oldPreviousSibling,
queries: []
};
for (var i = 0; i < options.queries.length; i++) {
var request = options.queries[i];
// all
if (request.all) {
if (Object.keys(request).length > 1)
throw Error('Invalid request option. all has no options.');
opts.queries.push({ all: true });
continue;
}
// attribute
if ('attribute' in request) {
var query = {
attribute: validateAttribute(request.attribute)
};
query.elementFilter = Selector.parseSelectors('*[' + query.attribute + ']');
if (Object.keys(request).length > 1)
throw Error('Invalid request option. attribute has no options.');
opts.queries.push(query);
continue;
}
// element
if ('element' in request) {
var requestOptionCount = Object.keys(request).length;
var query = {
element: request.element,
elementFilter: Selector.parseSelectors(request.element)
};
if (request.hasOwnProperty('elementAttributes')) {
query.attributeList = validateElementAttributes(request.elementAttributes);
requestOptionCount--;
}
if (requestOptionCount > 1)
throw Error('Invalid request option. element only allows elementAttributes option.');
opts.queries.push(query);
continue;
}
// characterData
if (request.characterData) {
if (Object.keys(request).length > 1)
throw Error('Invalid request option. characterData has no options.');
opts.queries.push({ characterData: true });
continue;
}
throw Error('Invalid request option. Unknown query request.');
}
return opts;
};
MutationSummary.prototype.createSummaries = function (mutations) {
if (!mutations || !mutations.length)
return [];
var projection = new MutationProjection(this.root, mutations, this.elementFilter, this.calcReordered, this.options.oldPreviousSibling);
var summaries = [];
for (var i = 0; i < this.options.queries.length; i++) {
summaries.push(new Summary(projection, this.options.queries[i]));
}
return summaries;
};
MutationSummary.prototype.checkpointQueryValidators = function () {
this.queryValidators.forEach(function (validator) {
if (validator)
validator.recordPreviousState();
});
};
MutationSummary.prototype.runQueryValidators = function (summaries) {
this.queryValidators.forEach(function (validator, index) {
if (validator)
validator.validate(summaries[index]);
});
};
MutationSummary.prototype.changesToReport = function (summaries) {
return summaries.some(function (summary) {
var summaryProps = [
'added', 'removed', 'reordered', 'reparented',
'valueChanged', 'characterDataChanged'];
if (summaryProps.some(function (prop) {
return summary[prop] && summary[prop].length;
}))
return true;
if (summary.attributeChanged) {
var attrNames = Object.keys(summary.attributeChanged);
var attrsChanged = attrNames.some(function (attrName) {
return !!summary.attributeChanged[attrName].length;
});
if (attrsChanged)
return true;
}
return false;
});
};
MutationSummary.prototype.observerCallback = function (mutations) {
if (!this.options.observeOwnChanges)
this.observer.disconnect();
var summaries = this.createSummaries(mutations);
this.runQueryValidators(summaries);
if (this.options.observeOwnChanges)
this.checkpointQueryValidators();
if (this.changesToReport(summaries))
this.callback(summaries);
// disconnect() may have been called during the callback.
if (!this.options.observeOwnChanges && this.connected) {
this.checkpointQueryValidators();
this.observer.observe(this.root, this.observerOptions);
}
};
MutationSummary.prototype.reconnect = function () {
if (this.connected)
throw Error('Already connected');
this.observer.observe(this.root, this.observerOptions);
this.connected = true;
this.checkpointQueryValidators();
};
MutationSummary.prototype.takeSummaries = function () {
if (!this.connected)
throw Error('Not connected');
var summaries = this.createSummaries(this.observer.takeRecords());
return this.changesToReport(summaries) ? summaries : undefined;
};
MutationSummary.prototype.disconnect = function () {
var summaries = this.takeSummaries();
this.observer.disconnect();
this.connected = false;
return summaries;
};
MutationSummary.NodeMap = NodeMap;
MutationSummary.parseElementFilter = Selector.parseSelectors;
MutationSummary.optionKeys = {
'callback': true,
'queries': true,
'rootNode': true,
'oldPreviousSibling': true,
'observeOwnChanges': true
};
return MutationSummary;
})();