MutationObserver.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. /*!
  2. * Shim for MutationObserver interface
  3. * Author: Graeme Yeates (github.com/megawac)
  4. * Repository: https://github.com/megawac/MutationObserver.js
  5. * License: WTFPL V2, 2004 (wtfpl.net).
  6. * Though credit and staring the repo will make me feel pretty, you can modify and redistribute as you please.
  7. * Attempts to follow spec (https://www.w3.org/TR/dom/#mutation-observers) as closely as possible for native javascript
  8. * See https://github.com/WebKit/webkit/blob/master/Source/WebCore/dom/MutationObserver.cpp for current webkit source c++ implementation
  9. */
  10. /**
  11. * prefix bugs:
  12. - https://bugs.webkit.org/show_bug.cgi?id=85161
  13. - https://bugzilla.mozilla.org/show_bug.cgi?id=749920
  14. * Don't use WebKitMutationObserver as Safari (6.0.5-6.1) use a buggy implementation
  15. */
  16. if (!window.MutationObserver) {
  17. window.MutationObserver = (function (undefined) {
  18. "use strict";
  19. /**
  20. * @param {function(Array.<MutationRecord>, MutationObserver)} listener
  21. * @constructor
  22. */
  23. function MutationObserver(listener) {
  24. /**
  25. * @type {Array.<Object>}
  26. * @private
  27. */
  28. this._watched = [];
  29. /** @private */
  30. this._listener = listener;
  31. }
  32. /**
  33. * Start a recursive timeout function to check all items being observed for mutations
  34. * @type {MutationObserver} observer
  35. * @private
  36. */
  37. function startMutationChecker(observer) {
  38. (function check() {
  39. var mutations = observer.takeRecords();
  40. if (mutations.length) { // fire away
  41. // calling the listener with context is not spec but currently consistent with FF and WebKit
  42. observer._listener(mutations, observer);
  43. }
  44. /** @private */
  45. observer._timeout = setTimeout(check, MutationObserver._period);
  46. })();
  47. }
  48. /**
  49. * Period to check for mutations (~32 times/sec)
  50. * @type {number}
  51. * @expose
  52. */
  53. MutationObserver._period = 30 /*ms+runtime*/;
  54. /**
  55. * Exposed API
  56. * @expose
  57. * @final
  58. */
  59. MutationObserver.prototype = {
  60. /**
  61. * see https://dom.spec.whatwg.org/#dom-mutationobserver-observe
  62. * not going to throw here but going to follow the current spec config sets
  63. * @param {Node|null} $target
  64. * @param {Object|null} config : MutationObserverInit configuration dictionary
  65. * @expose
  66. * @return undefined
  67. */
  68. observe: function ($target, config) {
  69. /**
  70. * Using slightly different names so closure can go ham
  71. * @type {!Object} : A custom mutation config
  72. */
  73. var settings = {
  74. attr: !!(config.attributes || config.attributeFilter || config.attributeOldValue),
  75. // some browsers enforce that subtree must be set with childList, attributes or characterData.
  76. // We don't care as spec doesn't specify this rule.
  77. kids: !!config.childList,
  78. descendents: !!config.subtree,
  79. charData: !!(config.characterData || config.characterDataOldValue)
  80. };
  81. var watched = this._watched;
  82. // remove already observed target element from pool
  83. for (var i = 0; i < watched.length; i++) {
  84. if (watched[i].tar === $target) watched.splice(i, 1);
  85. }
  86. if (config.attributeFilter) {
  87. /**
  88. * converts to a {key: true} dict for faster lookup
  89. * @type {Object.<String,Boolean>}
  90. */
  91. settings.afilter = reduce(config.attributeFilter, function (a, b) {
  92. a[b] = true;
  93. return a;
  94. }, {});
  95. }
  96. watched.push({
  97. tar: $target,
  98. fn: createMutationSearcher($target, settings)
  99. });
  100. // reconnect if not connected
  101. if (!this._timeout) {
  102. startMutationChecker(this);
  103. }
  104. },
  105. /**
  106. * Finds mutations since last check and empties the "record queue" i.e. mutations will only be found once
  107. * @expose
  108. * @return {Array.<MutationRecord>}
  109. */
  110. takeRecords: function () {
  111. var mutations = [];
  112. var watched = this._watched;
  113. for (var i = 0; i < watched.length; i++) {
  114. watched[i].fn(mutations);
  115. }
  116. return mutations;
  117. },
  118. /**
  119. * @expose
  120. * @return undefined
  121. */
  122. disconnect: function () {
  123. this._watched = []; // clear the stuff being observed
  124. clearTimeout(this._timeout); // ready for garbage collection
  125. /** @private */
  126. this._timeout = null;
  127. }
  128. };
  129. /**
  130. * Simple MutationRecord pseudoclass. No longer exposing as its not fully compliant
  131. * @param {Object} data
  132. * @return {Object} a MutationRecord
  133. */
  134. function MutationRecord(data) {
  135. var settings = { // technically these should be on proto so hasOwnProperty will return false for non explicitly props
  136. type: null,
  137. target: null,
  138. addedNodes: [],
  139. removedNodes: [],
  140. previousSibling: null,
  141. nextSibling: null,
  142. attributeName: null,
  143. attributeNamespace: null,
  144. oldValue: null
  145. };
  146. for (var prop in data) {
  147. if (has(settings, prop) && data[prop] !== undefined) settings[prop] = data[prop];
  148. }
  149. return settings;
  150. }
  151. /**
  152. * Creates a func to find all the mutations
  153. *
  154. * @param {Node} $target
  155. * @param {!Object} config : A custom mutation config
  156. */
  157. function createMutationSearcher($target, config) {
  158. /** type {Elestuct} */
  159. var $oldstate = clone($target, config); // create the cloned datastructure
  160. /**
  161. * consumes array of mutations we can push to
  162. *
  163. * @param {Array.<MutationRecord>} mutations
  164. */
  165. return function (mutations) {
  166. var olen = mutations.length, dirty;
  167. if (config.charData && $target.nodeType === 3 && $target.nodeValue !== $oldstate.charData) {
  168. mutations.push(new MutationRecord({
  169. type: "characterData",
  170. target: $target,
  171. oldValue: $oldstate.charData
  172. }));
  173. }
  174. // Alright we check base level changes in attributes... easy
  175. if (config.attr && $oldstate.attr) {
  176. findAttributeMutations(mutations, $target, $oldstate.attr, config.afilter);
  177. }
  178. // check childlist or subtree for mutations
  179. if (config.kids || config.descendents) {
  180. dirty = searchSubtree(mutations, $target, $oldstate, config);
  181. }
  182. // reclone data structure if theres changes
  183. if (dirty || mutations.length !== olen) {
  184. /** type {Elestuct} */
  185. $oldstate = clone($target, config);
  186. }
  187. };
  188. }
  189. /* attributes + attributeFilter helpers */
  190. // Check if the environment has the attribute bug (#4) which cause
  191. // element.attributes.style to always be null.
  192. var hasAttributeBug = document.createElement("i");
  193. hasAttributeBug.style.top = 0;
  194. hasAttributeBug = hasAttributeBug.attributes.style.value != "null";
  195. /**
  196. * Gets an attribute value in an environment without attribute bug
  197. *
  198. * @param {Node} el
  199. * @param {Attr} attr
  200. * @return {String} an attribute value
  201. */
  202. function getAttributeSimple(el, attr) {
  203. // There is a potential for a warning to occur here if the attribute is a
  204. // custom attribute in IE<9 with a custom .toString() method. This is
  205. // just a warning and doesn't affect execution (see #21)
  206. return attr.value;
  207. }
  208. /**
  209. * Gets an attribute value with special hack for style attribute (see #4)
  210. *
  211. * @param {Node} el
  212. * @param {Attr} attr
  213. * @return {String} an attribute value
  214. */
  215. function getAttributeWithStyleHack(el, attr) {
  216. // As with getAttributeSimple there is a potential warning for custom attribtues in IE7.
  217. return attr.name !== "style" ? attr.value : el.style.cssText;
  218. }
  219. var getAttributeValue = hasAttributeBug ? getAttributeSimple : getAttributeWithStyleHack;
  220. /**
  221. * fast helper to check to see if attributes object of an element has changed
  222. * doesnt handle the textnode case
  223. *
  224. * @param {Array.<MutationRecord>} mutations
  225. * @param {Node} $target
  226. * @param {Object.<string, string>} $oldstate : Custom attribute clone data structure from clone
  227. * @param {Object} filter
  228. */
  229. function findAttributeMutations(mutations, $target, $oldstate, filter) {
  230. var checked = {};
  231. var attributes = $target.attributes;
  232. var attr;
  233. var name;
  234. var i = attributes.length;
  235. while (i--) {
  236. attr = attributes[i];
  237. name = attr.name;
  238. if (!filter || has(filter, name)) {
  239. if (getAttributeValue($target, attr) !== $oldstate[name]) {
  240. // The pushing is redundant but gzips very nicely
  241. mutations.push(MutationRecord({
  242. type: "attributes",
  243. target: $target,
  244. attributeName: name,
  245. oldValue: $oldstate[name],
  246. attributeNamespace: attr.namespaceURI // in ie<8 it incorrectly will return undefined
  247. }));
  248. }
  249. checked[name] = true;
  250. }
  251. }
  252. for (name in $oldstate) {
  253. if (!(checked[name])) {
  254. mutations.push(MutationRecord({
  255. target: $target,
  256. type: "attributes",
  257. attributeName: name,
  258. oldValue: $oldstate[name]
  259. }));
  260. }
  261. }
  262. }
  263. /**
  264. * searchSubtree: array of mutations so far, element, element clone, bool
  265. * synchronous dfs comparision of two nodes
  266. * This function is applied to any observed element with childList or subtree specified
  267. * Sorry this is kind of confusing as shit, tried to comment it a bit...
  268. * codereview.stackexchange.com/questions/38351 discussion of an earlier version of this func
  269. *
  270. * @param {Array} mutations
  271. * @param {Node} $target
  272. * @param {!Object} $oldstate : A custom cloned node from clone()
  273. * @param {!Object} config : A custom mutation config
  274. */
  275. function searchSubtree(mutations, $target, $oldstate, config) {
  276. // Track if the tree is dirty and has to be recomputed (#14).
  277. var dirty;
  278. /*
  279. * Helper to identify node rearrangment and stuff...
  280. * There is no gaurentee that the same node will be identified for both added and removed nodes
  281. * if the positions have been shuffled.
  282. * conflicts array will be emptied by end of operation
  283. */
  284. function resolveConflicts(conflicts, node, $kids, $oldkids, numAddedNodes) {
  285. // the distance between the first conflicting node and the last
  286. var distance = conflicts.length - 1;
  287. // prevents same conflict being resolved twice consider when two nodes switch places.
  288. // only one should be given a mutation event (note -~ is used as a math.ceil shorthand)
  289. var counter = -~((distance - numAddedNodes) / 2);
  290. var $cur;
  291. var oldstruct;
  292. var conflict;
  293. while ((conflict = conflicts.pop())) {
  294. $cur = $kids[conflict.i];
  295. oldstruct = $oldkids[conflict.j];
  296. // attempt to determine if there was node rearrangement... won't gaurentee all matches
  297. // also handles case where added/removed nodes cause nodes to be identified as conflicts
  298. if (config.kids && counter && Math.abs(conflict.i - conflict.j) >= distance) {
  299. mutations.push(MutationRecord({
  300. type: "childList",
  301. target: node,
  302. addedNodes: [$cur],
  303. removedNodes: [$cur],
  304. // haha don't rely on this please
  305. nextSibling: $cur.nextSibling,
  306. previousSibling: $cur.previousSibling
  307. }));
  308. counter--; // found conflict
  309. }
  310. // Alright we found the resorted nodes now check for other types of mutations
  311. if (config.attr && oldstruct.attr) findAttributeMutations(mutations, $cur, oldstruct.attr, config.afilter);
  312. if (config.charData && $cur.nodeType === 3 && $cur.nodeValue !== oldstruct.charData) {
  313. mutations.push(MutationRecord({
  314. type: "characterData",
  315. target: $cur,
  316. oldValue: oldstruct.charData
  317. }));
  318. }
  319. // now look @ subtree
  320. if (config.descendents) findMutations($cur, oldstruct);
  321. }
  322. }
  323. /**
  324. * Main worker. Finds and adds mutations if there are any
  325. * @param {Node} node
  326. * @param {!Object} old : A cloned data structure using internal clone
  327. */
  328. function findMutations(node, old) {
  329. var $kids = node.childNodes;
  330. var $oldkids = old.kids;
  331. var klen = $kids.length;
  332. // $oldkids will be undefined for text and comment nodes
  333. var olen = $oldkids ? $oldkids.length : 0;
  334. // if (!olen && !klen) return; // both empty; clearly no changes
  335. // we delay the intialization of these for marginal performance in the expected case (actually quite signficant on large subtrees when these would be otherwise unused)
  336. // map of checked element of ids to prevent registering the same conflict twice
  337. var map;
  338. // array of potential conflicts (ie nodes that may have been re arranged)
  339. var conflicts;
  340. var id; // element id from getElementId helper
  341. var idx; // index of a moved or inserted element
  342. var oldstruct;
  343. // current and old nodes
  344. var $cur;
  345. var $old;
  346. // track the number of added nodes so we can resolve conflicts more accurately
  347. var numAddedNodes = 0;
  348. // iterate over both old and current child nodes at the same time
  349. var i = 0, j = 0;
  350. // while there is still anything left in $kids or $oldkids (same as i < $kids.length || j < $oldkids.length;)
  351. while (i < klen || j < olen) {
  352. // current and old nodes at the indexs
  353. $cur = $kids[i];
  354. oldstruct = $oldkids[j];
  355. $old = oldstruct && oldstruct.node;
  356. if ($cur === $old) { // expected case - optimized for this case
  357. // check attributes as specified by config
  358. if (config.attr && oldstruct.attr) /* oldstruct.attr instead of textnode check */findAttributeMutations(mutations, $cur, oldstruct.attr, config.afilter);
  359. // check character data if node is a comment or textNode and it's being observed
  360. if (config.charData && oldstruct.charData !== undefined && $cur.nodeValue !== oldstruct.charData) {
  361. mutations.push(MutationRecord({
  362. type: "characterData",
  363. target: $cur,
  364. oldValue: oldstruct.charData
  365. }));
  366. }
  367. // resolve conflicts; it will be undefined if there are no conflicts - otherwise an array
  368. if (conflicts) resolveConflicts(conflicts, node, $kids, $oldkids, numAddedNodes);
  369. // recurse on next level of children. Avoids the recursive call when there are no children left to iterate
  370. if (config.descendents && ($cur.childNodes.length || oldstruct.kids && oldstruct.kids.length)) findMutations($cur, oldstruct);
  371. i++;
  372. j++;
  373. } else { // (uncommon case) lookahead until they are the same again or the end of children
  374. dirty = true;
  375. if (!map) { // delayed initalization (big perf benefit)
  376. map = {};
  377. conflicts = [];
  378. }
  379. if ($cur) {
  380. // check id is in the location map otherwise do a indexOf search
  381. if (!(map[id = getElementId($cur)])) { // to prevent double checking
  382. // mark id as found
  383. map[id] = true;
  384. // custom indexOf using comparitor checking oldkids[i].node === $cur
  385. if ((idx = indexOfCustomNode($oldkids, $cur, j)) === -1) {
  386. if (config.kids) {
  387. mutations.push(MutationRecord({
  388. type: "childList",
  389. target: node,
  390. addedNodes: [$cur], // $cur is a new node
  391. nextSibling: $cur.nextSibling,
  392. previousSibling: $cur.previousSibling
  393. }));
  394. numAddedNodes++;
  395. }
  396. } else {
  397. conflicts.push({ // add conflict
  398. i: i,
  399. j: idx
  400. });
  401. }
  402. }
  403. i++;
  404. }
  405. if ($old &&
  406. // special case: the changes may have been resolved: i and j appear congurent so we can continue using the expected case
  407. $old !== $kids[i]
  408. ) {
  409. if (!(map[id = getElementId($old)])) {
  410. map[id] = true;
  411. if ((idx = indexOf($kids, $old, i)) === -1) {
  412. if (config.kids) {
  413. mutations.push(MutationRecord({
  414. type: "childList",
  415. target: old.node,
  416. removedNodes: [$old],
  417. nextSibling: $oldkids[j + 1], // praise no indexoutofbounds exception
  418. previousSibling: $oldkids[j - 1]
  419. }));
  420. numAddedNodes--;
  421. }
  422. } else {
  423. conflicts.push({
  424. i: idx,
  425. j: j
  426. });
  427. }
  428. }
  429. j++;
  430. }
  431. }// end uncommon case
  432. }// end loop
  433. // resolve any remaining conflicts
  434. if (conflicts) resolveConflicts(conflicts, node, $kids, $oldkids, numAddedNodes);
  435. }
  436. findMutations($target, $oldstate);
  437. return dirty;
  438. }
  439. /**
  440. * Utility
  441. * Cones a element into a custom data structure designed for comparision. https://gist.github.com/megawac/8201012
  442. *
  443. * @param {Node} $target
  444. * @param {!Object} config : A custom mutation config
  445. * @return {!Object} : Cloned data structure
  446. */
  447. function clone($target, config) {
  448. var recurse = true; // set true so childList we'll always check the first level
  449. return (function copy($target) {
  450. var elestruct = {
  451. /** @type {Node} */
  452. node: $target
  453. };
  454. // Store current character data of target text or comment node if the config requests
  455. // those properties to be observed.
  456. if (config.charData && ($target.nodeType === 3 || $target.nodeType === 8)) {
  457. elestruct.charData = $target.nodeValue;
  458. }
  459. // its either a element, comment, doc frag or document node
  460. else {
  461. // Add attr only if subtree is specified or top level and avoid if
  462. // attributes is a document object (#13).
  463. if (config.attr && recurse && $target.nodeType === 1) {
  464. /**
  465. * clone live attribute list to an object structure {name: val}
  466. * @type {Object.<string, string>}
  467. */
  468. elestruct.attr = reduce($target.attributes, function (memo, attr) {
  469. if (!config.afilter || config.afilter[attr.name]) {
  470. memo[attr.name] = getAttributeValue($target, attr);
  471. }
  472. return memo;
  473. }, {});
  474. }
  475. // whether we should iterate the children of $target node
  476. if (recurse && ((config.kids || config.charData) || (config.attr && config.descendents))) {
  477. /** @type {Array.<!Object>} : Array of custom clone */
  478. elestruct.kids = map($target.childNodes, copy);
  479. }
  480. recurse = config.descendents;
  481. }
  482. return elestruct;
  483. })($target);
  484. }
  485. /**
  486. * indexOf an element in a collection of custom nodes
  487. *
  488. * @param {NodeList} set
  489. * @param {!Object} $node : A custom cloned node
  490. * @param {number} idx : index to start the loop
  491. * @return {number}
  492. */
  493. function indexOfCustomNode(set, $node, idx) {
  494. return indexOf(set, $node, idx, JSCompiler_renameProperty("node"));
  495. }
  496. // using a non id (eg outerHTML or nodeValue) is extremely naive and will run into issues with nodes that may appear the same like <li></li>
  497. var counter = 1; // don't use 0 as id (falsy)
  498. /** @const */
  499. var expando = "mo_id";
  500. /**
  501. * Attempt to uniquely id an element for hashing. We could optimize this for legacy browsers but it hopefully wont be called enough to be a concern
  502. *
  503. * @param {Node} $ele
  504. * @return {(string|number)}
  505. */
  506. function getElementId($ele) {
  507. try {
  508. return $ele.id || ($ele[expando] = $ele[expando] || counter++);
  509. } catch (o_O) { // ie <8 will throw if you set an unknown property on a text node
  510. try {
  511. return $ele.nodeValue; // naive
  512. } catch (shitie) { // when text node is removed: https://gist.github.com/megawac/8355978 :(
  513. return counter++;
  514. }
  515. }
  516. }
  517. /**
  518. * **map** Apply a mapping function to each item of a set
  519. * @param {Array|NodeList} set
  520. * @param {Function} iterator
  521. */
  522. function map(set, iterator) {
  523. var results = [];
  524. for (var index = 0; index < set.length; index++) {
  525. results[index] = iterator(set[index], index, set);
  526. }
  527. return results;
  528. }
  529. /**
  530. * **Reduce** builds up a single result from a list of values
  531. * @param {Array|NodeList|NamedNodeMap} set
  532. * @param {Function} iterator
  533. * @param {*} [memo] Initial value of the memo.
  534. */
  535. function reduce(set, iterator, memo) {
  536. for (var index = 0; index < set.length; index++) {
  537. memo = iterator(memo, set[index], index, set);
  538. }
  539. return memo;
  540. }
  541. /**
  542. * **indexOf** find index of item in collection.
  543. * @param {Array|NodeList} set
  544. * @param {Object} item
  545. * @param {number} idx
  546. * @param {string} [prop] Property on set item to compare to item
  547. */
  548. function indexOf(set, item, idx, prop) {
  549. for (/*idx = ~~idx*/; idx < set.length; idx++) {// start idx is always given as this is internal
  550. if ((prop ? set[idx][prop] : set[idx]) === item) return idx;
  551. }
  552. return -1;
  553. }
  554. /**
  555. * @param {Object} obj
  556. * @param {(string|number)} prop
  557. * @return {boolean}
  558. */
  559. function has(obj, prop) {
  560. return obj[prop] !== undefined; // will be nicely inlined by gcc
  561. }
  562. // GCC hack see https://stackoverflow.com/a/23202438/1517919
  563. function JSCompiler_renameProperty(a) {
  564. return a;
  565. }
  566. return MutationObserver;
  567. })(void 0);
  568. }