NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @namespace https://openuserjs.org/users/parlor.trickss // @exclude * // ==UserLibrary== // @name Mutation Summary // @description Mutation Summary is a JavaScript library that makes observing changes to the DOM fast, easy and safe. // @copyright 2011, Google Inc. // @license Apache-2.0 // @version 1.0 // @homepageURL https://github.com/rafaelw/mutation-summary // ==/UserScript== // ==/UserLibrary== // ==OpenUserJS== // @author parlor.trickss // ==/OpenUseJs== // 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 === Movement.ENTERED || changeType === Movement.EXITED; } var NodeChange = (function () { function NodeChange(node, childList, attributes, characterData, oldParentNode, added, attributeOldValues, characterDataOldValue) { if (childList === void 0) { childList = false; } if (attributes === void 0) { attributes = false; } if (characterData === void 0) { characterData = false; } if (oldParentNode === void 0) { oldParentNode = null; } if (added === void 0) { added = false; } if (attributeOldValues === void 0) { attributeOldValues = null; } if (characterDataOldValue === void 0) { 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) ? Movement.STAYED_IN : Movement.ENTERED; } return this.getWasReachable(node) ? Movement.EXITED : Movement.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 === Movement.STAYED_OUT) return; // Cache match results for sub-patterns. this.matchabilityChange(node); if (reachable === Movement.ENTERED) { this.entered.push(node); } else if (reachable === Movement.EXITED) { this.exited.push(node); this.ensureHasOldPreviousSiblingIfNeeded(node); } else if (reachable === Movement.STAYED_IN) { var movement = Movement.STAYED_IN; if (change && change.childList) { if (change.oldParentNode !== node.parentNode) { movement = Movement.REPARENTED; this.ensureHasOldPreviousSiblingIfNeeded(node); } else if (this.calcReordered && this.wasReordered(node)) { movement = Movement.REORDERED; } } this.stayedIn.set(node, movement); } if (reachable === Movement.STAYED_IN) return; // reachable === ENTERED || reachable === EXITED. 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 === Movement.ENTERED || matchable === Movement.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 === Movement.ENTERED) { summary.added.push(node); } else if (matchable === Movement.EXITED) { summary.removed.push(node); } else if (matchable === Movement.STAYED_IN && (summary.reparented || summary.reordered)) { var movement = this.stayedIn.get(node); if (summary.reparented && movement === Movement.REPARENTED) summary.reparented.push(node); else if (summary.reordered && movement === Movement.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 === Movement.EXITED || matchable === Movement.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 === Movement.STAYED_OUT || reachabilityChange === Movement.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 {}; // No attributes mutations occurred. 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 (Movement.STAYED_IN !== this.treeChanges.reachabilityChange(node) || Movement.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 []; // No characterData mutations occurred. var nodes = this.treeChanges.keys(); var result = []; for (var i = 0; i < nodes.length; i++) { var target = nodes[i]; if (Movement.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 Movement.STAYED_IN; default: return Movement.STAYED_OUT; } } // No element filter. Include all nodes. if (!this.selectors) return Movement.STAYED_IN; // Element filter. Exclude non-elements. if (node.nodeType !== Node.ELEMENT_NODE) return Movement.STAYED_OUT; var el = node; var matchChanges = this.selectors.map(function (selector) { return _this.computeMatchabilityChange(selector, el); }); var accum = Movement.STAYED_OUT; var i = 0; while (accum !== Movement.STAYED_IN && i < matchChanges.length) { switch (matchChanges[i]) { case Movement.STAYED_IN: accum = Movement.STAYED_IN; break; case Movement.ENTERED: if (accum === Movement.EXITED) accum = Movement.STAYED_IN; else accum = Movement.ENTERED; break; case Movement.EXITED: if (accum === Movement.ENTERED) accum = Movement.STAYED_IN; else accum = Movement.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) !== Movement.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 || query.characterData ? [] : 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) ? Movement.STAYED_IN : Movement.ENTERED; else return this.wasMatching(el, change, isMatching) ? Movement.EXITED : Movement.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; // already observing all. 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; // exposed for use in TreeMirror. MutationSummary.parseElementFilter = Selector.parseSelectors; // exposed for testing. MutationSummary.optionKeys = { 'callback': true, 'queries': true, 'rootNode': true, 'oldPreviousSibling': true, 'observeOwnChanges': true }; return MutationSummary; })();