MutationObserver.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. /**
  2. * @license
  3. * Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
  4. * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
  5. * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
  6. * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
  7. * Code distributed by Google as part of the polymer project is also
  8. * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
  9. */
  10. (function(global) {
  11. var registrationsTable = new WeakMap();
  12. var setImmediate;
  13. // As much as we would like to use the native implementation, IE
  14. // (all versions) suffers a rather annoying bug where it will drop or defer
  15. // callbacks when heavy DOM operations are being performed concurrently.
  16. //
  17. // For a thorough discussion on this, see:
  18. // http://codeforhire.com/2013/09/21/setimmediate-and-messagechannel-broken-on-internet-explorer-10/
  19. if (/Trident|Edge/.test(navigator.userAgent)) {
  20. // Sadly, this bug also affects postMessage and MessageQueues.
  21. //
  22. // We would like to use the onreadystatechange hack for IE <= 10, but it is
  23. // dangerous in the polyfilled environment due to requiring that the
  24. // observed script element be in the document.
  25. setImmediate = setTimeout;
  26. // If some other browser ever implements it, let's prefer their native
  27. // implementation:
  28. } else if (window.setImmediate) {
  29. setImmediate = window.setImmediate;
  30. // Otherwise, we fall back to postMessage as a means of emulating the next
  31. // task semantics of setImmediate.
  32. } else {
  33. var setImmediateQueue = [];
  34. var sentinel = String(Math.random());
  35. window.addEventListener('message', function(e) {
  36. if (e.data === sentinel) {
  37. var queue = setImmediateQueue;
  38. setImmediateQueue = [];
  39. queue.forEach(function(func) {
  40. func();
  41. });
  42. }
  43. });
  44. setImmediate = function(func) {
  45. setImmediateQueue.push(func);
  46. window.postMessage(sentinel, '*');
  47. };
  48. }
  49. // This is used to ensure that we never schedule 2 callas to setImmediate
  50. var isScheduled = false;
  51. // Keep track of observers that needs to be notified next time.
  52. var scheduledObservers = [];
  53. /**
  54. * Schedules |dispatchCallback| to be called in the future.
  55. * @param {MutationObserver} observer
  56. */
  57. function scheduleCallback(observer) {
  58. scheduledObservers.push(observer);
  59. if (!isScheduled) {
  60. isScheduled = true;
  61. setImmediate(dispatchCallbacks);
  62. }
  63. }
  64. function wrapIfNeeded(node) {
  65. return window.ShadowDOMPolyfill &&
  66. window.ShadowDOMPolyfill.wrapIfNeeded(node) ||
  67. node;
  68. }
  69. function dispatchCallbacks() {
  70. // http://dom.spec.whatwg.org/#mutation-observers
  71. isScheduled = false; // Used to allow a new setImmediate call above.
  72. var observers = scheduledObservers;
  73. scheduledObservers = [];
  74. // Sort observers based on their creation UID (incremental).
  75. observers.sort(function(o1, o2) {
  76. return o1.uid_ - o2.uid_;
  77. });
  78. var anyNonEmpty = false;
  79. observers.forEach(function(observer) {
  80. // 2.1, 2.2
  81. var queue = observer.takeRecords();
  82. // 2.3. Remove all transient registered observers whose observer is mo.
  83. removeTransientObserversFor(observer);
  84. // 2.4
  85. if (queue.length) {
  86. observer.callback_(queue, observer);
  87. anyNonEmpty = true;
  88. }
  89. });
  90. // 3.
  91. if (anyNonEmpty)
  92. dispatchCallbacks();
  93. }
  94. function removeTransientObserversFor(observer) {
  95. observer.nodes_.forEach(function(node) {
  96. var registrations = registrationsTable.get(node);
  97. if (!registrations)
  98. return;
  99. registrations.forEach(function(registration) {
  100. if (registration.observer === observer)
  101. registration.removeTransientObservers();
  102. });
  103. });
  104. }
  105. /**
  106. * This function is used for the "For each registered observer observer (with
  107. * observer's options as options) in target's list of registered observers,
  108. * run these substeps:" and the "For each ancestor ancestor of target, and for
  109. * each registered observer observer (with options options) in ancestor's list
  110. * of registered observers, run these substeps:" part of the algorithms. The
  111. * |options.subtree| is checked to ensure that the callback is called
  112. * correctly.
  113. *
  114. * @param {Node} target
  115. * @param {function(MutationObserverInit):MutationRecord} callback
  116. */
  117. function forEachAncestorAndObserverEnqueueRecord(target, callback) {
  118. for (var node = target; node; node = node.parentNode) {
  119. var registrations = registrationsTable.get(node);
  120. if (registrations) {
  121. for (var j = 0; j < registrations.length; j++) {
  122. var registration = registrations[j];
  123. var options = registration.options;
  124. // Only target ignores subtree.
  125. if (node !== target && !options.subtree)
  126. continue;
  127. var record = callback(options);
  128. if (record)
  129. registration.enqueue(record);
  130. }
  131. }
  132. }
  133. }
  134. var uidCounter = 0;
  135. /**
  136. * The class that maps to the DOM MutationObserver interface.
  137. * @param {Function} callback.
  138. * @constructor
  139. */
  140. function JsMutationObserver(callback) {
  141. this.callback_ = callback;
  142. this.nodes_ = [];
  143. this.records_ = [];
  144. this.uid_ = ++uidCounter;
  145. }
  146. JsMutationObserver.prototype = {
  147. observe: function(target, options) {
  148. target = wrapIfNeeded(target);
  149. // 1.1
  150. if (!options.childList && !options.attributes && !options.characterData ||
  151. // 1.2
  152. options.attributeOldValue && !options.attributes ||
  153. // 1.3
  154. options.attributeFilter && options.attributeFilter.length &&
  155. !options.attributes ||
  156. // 1.4
  157. options.characterDataOldValue && !options.characterData) {
  158. throw new SyntaxError();
  159. }
  160. var registrations = registrationsTable.get(target);
  161. if (!registrations)
  162. registrationsTable.set(target, registrations = []);
  163. // 2
  164. // If target's list of registered observers already includes a registered
  165. // observer associated with the context object, replace that registered
  166. // observer's options with options.
  167. var registration;
  168. for (var i = 0; i < registrations.length; i++) {
  169. if (registrations[i].observer === this) {
  170. registration = registrations[i];
  171. registration.removeListeners();
  172. registration.options = options;
  173. break;
  174. }
  175. }
  176. // 3.
  177. // Otherwise, add a new registered observer to target's list of registered
  178. // observers with the context object as the observer and options as the
  179. // options, and add target to context object's list of nodes on which it
  180. // is registered.
  181. if (!registration) {
  182. registration = new Registration(this, target, options);
  183. registrations.push(registration);
  184. this.nodes_.push(target);
  185. }
  186. registration.addListeners();
  187. },
  188. disconnect: function() {
  189. this.nodes_.forEach(function(node) {
  190. var registrations = registrationsTable.get(node);
  191. for (var i = 0; i < registrations.length; i++) {
  192. var registration = registrations[i];
  193. if (registration.observer === this) {
  194. registration.removeListeners();
  195. registrations.splice(i, 1);
  196. // Each node can only have one registered observer associated with
  197. // this observer.
  198. break;
  199. }
  200. }
  201. }, this);
  202. this.records_ = [];
  203. },
  204. takeRecords: function() {
  205. var copyOfRecords = this.records_;
  206. this.records_ = [];
  207. return copyOfRecords;
  208. }
  209. };
  210. /**
  211. * @param {string} type
  212. * @param {Node} target
  213. * @constructor
  214. */
  215. function MutationRecord(type, target) {
  216. this.type = type;
  217. this.target = target;
  218. this.addedNodes = [];
  219. this.removedNodes = [];
  220. this.previousSibling = null;
  221. this.nextSibling = null;
  222. this.attributeName = null;
  223. this.attributeNamespace = null;
  224. this.oldValue = null;
  225. }
  226. function copyMutationRecord(original) {
  227. var record = new MutationRecord(original.type, original.target);
  228. record.addedNodes = original.addedNodes.slice();
  229. record.removedNodes = original.removedNodes.slice();
  230. record.previousSibling = original.previousSibling;
  231. record.nextSibling = original.nextSibling;
  232. record.attributeName = original.attributeName;
  233. record.attributeNamespace = original.attributeNamespace;
  234. record.oldValue = original.oldValue;
  235. return record;
  236. };
  237. // We keep track of the two (possibly one) records used in a single mutation.
  238. var currentRecord, recordWithOldValue;
  239. /**
  240. * Creates a record without |oldValue| and caches it as |currentRecord| for
  241. * later use.
  242. * @param {string} oldValue
  243. * @return {MutationRecord}
  244. */
  245. function getRecord(type, target) {
  246. return currentRecord = new MutationRecord(type, target);
  247. }
  248. /**
  249. * Gets or creates a record with |oldValue| based in the |currentRecord|
  250. * @param {string} oldValue
  251. * @return {MutationRecord}
  252. */
  253. function getRecordWithOldValue(oldValue) {
  254. if (recordWithOldValue)
  255. return recordWithOldValue;
  256. recordWithOldValue = copyMutationRecord(currentRecord);
  257. recordWithOldValue.oldValue = oldValue;
  258. return recordWithOldValue;
  259. }
  260. function clearRecords() {
  261. currentRecord = recordWithOldValue = undefined;
  262. }
  263. /**
  264. * @param {MutationRecord} record
  265. * @return {boolean} Whether the record represents a record from the current
  266. * mutation event.
  267. */
  268. function recordRepresentsCurrentMutation(record) {
  269. return record === recordWithOldValue || record === currentRecord;
  270. }
  271. /**
  272. * Selects which record, if any, to replace the last record in the queue.
  273. * This returns |null| if no record should be replaced.
  274. *
  275. * @param {MutationRecord} lastRecord
  276. * @param {MutationRecord} newRecord
  277. * @param {MutationRecord}
  278. */
  279. function selectRecord(lastRecord, newRecord) {
  280. if (lastRecord === newRecord)
  281. return lastRecord;
  282. // Check if the record we are adding represents the same record. If
  283. // so, we keep the one with the oldValue in it.
  284. if (recordWithOldValue && recordRepresentsCurrentMutation(lastRecord))
  285. return recordWithOldValue;
  286. return null;
  287. }
  288. /**
  289. * Class used to represent a registered observer.
  290. * @param {MutationObserver} observer
  291. * @param {Node} target
  292. * @param {MutationObserverInit} options
  293. * @constructor
  294. */
  295. function Registration(observer, target, options) {
  296. this.observer = observer;
  297. this.target = target;
  298. this.options = options;
  299. this.transientObservedNodes = [];
  300. }
  301. Registration.prototype = {
  302. enqueue: function(record) {
  303. var records = this.observer.records_;
  304. var length = records.length;
  305. // There are cases where we replace the last record with the new record.
  306. // For example if the record represents the same mutation we need to use
  307. // the one with the oldValue. If we get same record (this can happen as we
  308. // walk up the tree) we ignore the new record.
  309. if (records.length > 0) {
  310. var lastRecord = records[length - 1];
  311. var recordToReplaceLast = selectRecord(lastRecord, record);
  312. if (recordToReplaceLast) {
  313. records[length - 1] = recordToReplaceLast;
  314. return;
  315. }
  316. } else {
  317. scheduleCallback(this.observer);
  318. }
  319. records[length] = record;
  320. },
  321. addListeners: function() {
  322. this.addListeners_(this.target);
  323. },
  324. addListeners_: function(node) {
  325. var options = this.options;
  326. if (options.attributes)
  327. node.addEventListener('DOMAttrModified', this, true);
  328. if (options.characterData)
  329. node.addEventListener('DOMCharacterDataModified', this, true);
  330. if (options.childList)
  331. node.addEventListener('DOMNodeInserted', this, true);
  332. if (options.childList || options.subtree)
  333. node.addEventListener('DOMNodeRemoved', this, true);
  334. },
  335. removeListeners: function() {
  336. this.removeListeners_(this.target);
  337. },
  338. removeListeners_: function(node) {
  339. var options = this.options;
  340. if (options.attributes)
  341. node.removeEventListener('DOMAttrModified', this, true);
  342. if (options.characterData)
  343. node.removeEventListener('DOMCharacterDataModified', this, true);
  344. if (options.childList)
  345. node.removeEventListener('DOMNodeInserted', this, true);
  346. if (options.childList || options.subtree)
  347. node.removeEventListener('DOMNodeRemoved', this, true);
  348. },
  349. /**
  350. * Adds a transient observer on node. The transient observer gets removed
  351. * next time we deliver the change records.
  352. * @param {Node} node
  353. */
  354. addTransientObserver: function(node) {
  355. // Don't add transient observers on the target itself. We already have all
  356. // the required listeners set up on the target.
  357. if (node === this.target)
  358. return;
  359. this.addListeners_(node);
  360. this.transientObservedNodes.push(node);
  361. var registrations = registrationsTable.get(node);
  362. if (!registrations)
  363. registrationsTable.set(node, registrations = []);
  364. // We know that registrations does not contain this because we already
  365. // checked if node === this.target.
  366. registrations.push(this);
  367. },
  368. removeTransientObservers: function() {
  369. var transientObservedNodes = this.transientObservedNodes;
  370. this.transientObservedNodes = [];
  371. transientObservedNodes.forEach(function(node) {
  372. // Transient observers are never added to the target.
  373. this.removeListeners_(node);
  374. var registrations = registrationsTable.get(node);
  375. for (var i = 0; i < registrations.length; i++) {
  376. if (registrations[i] === this) {
  377. registrations.splice(i, 1);
  378. // Each node can only have one registered observer associated with
  379. // this observer.
  380. break;
  381. }
  382. }
  383. }, this);
  384. },
  385. handleEvent: function(e) {
  386. // Stop propagation since we are managing the propagation manually.
  387. // This means that other mutation events on the page will not work
  388. // correctly but that is by design.
  389. e.stopImmediatePropagation();
  390. switch (e.type) {
  391. case 'DOMAttrModified':
  392. // http://dom.spec.whatwg.org/#concept-mo-queue-attributes
  393. var name = e.attrName;
  394. var namespace = e.relatedNode.namespaceURI;
  395. var target = e.target;
  396. // 1.
  397. var record = new getRecord('attributes', target);
  398. record.attributeName = name;
  399. record.attributeNamespace = namespace;
  400. // 2.
  401. var oldValue =
  402. e.attrChange === MutationEvent.ADDITION ? null : e.prevValue;
  403. forEachAncestorAndObserverEnqueueRecord(target, function(options) {
  404. // 3.1, 4.2
  405. if (!options.attributes)
  406. return;
  407. // 3.2, 4.3
  408. if (options.attributeFilter && options.attributeFilter.length &&
  409. options.attributeFilter.indexOf(name) === -1 &&
  410. options.attributeFilter.indexOf(namespace) === -1) {
  411. return;
  412. }
  413. // 3.3, 4.4
  414. if (options.attributeOldValue)
  415. return getRecordWithOldValue(oldValue);
  416. // 3.4, 4.5
  417. return record;
  418. });
  419. break;
  420. case 'DOMCharacterDataModified':
  421. // http://dom.spec.whatwg.org/#concept-mo-queue-characterdata
  422. var target = e.target;
  423. // 1.
  424. var record = getRecord('characterData', target);
  425. // 2.
  426. var oldValue = e.prevValue;
  427. forEachAncestorAndObserverEnqueueRecord(target, function(options) {
  428. // 3.1, 4.2
  429. if (!options.characterData)
  430. return;
  431. // 3.2, 4.3
  432. if (options.characterDataOldValue)
  433. return getRecordWithOldValue(oldValue);
  434. // 3.3, 4.4
  435. return record;
  436. });
  437. break;
  438. case 'DOMNodeRemoved':
  439. this.addTransientObserver(e.target);
  440. // Fall through.
  441. case 'DOMNodeInserted':
  442. // http://dom.spec.whatwg.org/#concept-mo-queue-childlist
  443. var changedNode = e.target;
  444. var addedNodes, removedNodes;
  445. if (e.type === 'DOMNodeInserted') {
  446. addedNodes = [changedNode];
  447. removedNodes = [];
  448. } else {
  449. addedNodes = [];
  450. removedNodes = [changedNode];
  451. }
  452. var previousSibling = changedNode.previousSibling;
  453. var nextSibling = changedNode.nextSibling;
  454. // 1.
  455. var record = getRecord('childList', e.target.parentNode);
  456. record.addedNodes = addedNodes;
  457. record.removedNodes = removedNodes;
  458. record.previousSibling = previousSibling;
  459. record.nextSibling = nextSibling;
  460. forEachAncestorAndObserverEnqueueRecord(e.relatedNode, function(options) {
  461. // 2.1, 3.2
  462. if (!options.childList)
  463. return;
  464. // 2.2, 3.3
  465. return record;
  466. });
  467. }
  468. clearRecords();
  469. }
  470. };
  471. global.JsMutationObserver = JsMutationObserver;
  472. if (!global.MutationObserver)
  473. global.MutationObserver = JsMutationObserver;
  474. })(this);