morphdom.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.morphdom = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
  2. var specialElHandlers = {
  3. /**
  4. * Needed for IE. Apparently IE doesn't think
  5. * that "selected" is an attribute when reading
  6. * over the attributes using selectEl.attributes
  7. */
  8. OPTION: function(fromEl, toEl) {
  9. if ((fromEl.selected = toEl.selected)) {
  10. fromEl.setAttribute('selected', '');
  11. } else {
  12. fromEl.removeAttribute('selected', '');
  13. }
  14. },
  15. /**
  16. * The "value" attribute is special for the <input> element
  17. * since it sets the initial value. Changing the "value"
  18. * attribute without changing the "value" property will have
  19. * no effect since it is only used to the set the initial value.
  20. * Similar for the "checked" attribute.
  21. */
  22. /*INPUT: function(fromEl, toEl) {
  23. fromEl.checked = toEl.checked;
  24. fromEl.value = toEl.value;
  25. if (!toEl.hasAttribute('checked')) {
  26. fromEl.removeAttribute('checked');
  27. }
  28. if (!toEl.hasAttribute('value')) {
  29. fromEl.removeAttribute('value');
  30. }
  31. }*/
  32. };
  33. function noop() {}
  34. /**
  35. * Loop over all of the attributes on the target node and make sure the
  36. * original DOM node has the same attributes. If an attribute
  37. * found on the original node is not on the new node then remove it from
  38. * the original node
  39. * @param {HTMLElement} fromNode
  40. * @param {HTMLElement} toNode
  41. */
  42. function morphAttrs(fromNode, toNode) {
  43. var attrs = toNode.attributes;
  44. var i;
  45. var attr;
  46. var attrName;
  47. var attrValue;
  48. var foundAttrs = {};
  49. for (i=attrs.length-1; i>=0; i--) {
  50. attr = attrs[i];
  51. if (attr.specified !== false) {
  52. attrName = attr.name;
  53. attrValue = attr.value;
  54. foundAttrs[attrName] = true;
  55. if (fromNode.getAttribute(attrName) !== attrValue) {
  56. fromNode.setAttribute(attrName, attrValue);
  57. }
  58. }
  59. }
  60. // Delete any extra attributes found on the original DOM element that weren't
  61. // found on the target element.
  62. attrs = fromNode.attributes;
  63. for (i=attrs.length-1; i>=0; i--) {
  64. attr = attrs[i];
  65. if (attr.specified !== false) {
  66. attrName = attr.name;
  67. if (!foundAttrs.hasOwnProperty(attrName)) {
  68. fromNode.removeAttribute(attrName);
  69. }
  70. }
  71. }
  72. }
  73. /**
  74. * Copies the children of one DOM element to another DOM element
  75. */
  76. function moveChildren(from, to) {
  77. var curChild = from.firstChild;
  78. while(curChild) {
  79. var nextChild = curChild.nextSibling;
  80. to.appendChild(curChild);
  81. curChild = nextChild;
  82. }
  83. return to;
  84. }
  85. function morphdom(fromNode, toNode, options) {
  86. if (!options) {
  87. options = {};
  88. }
  89. if (typeof toNode === 'string') {
  90. var newBodyEl = document.createElement('body');
  91. newBodyEl.innerHTML = toNode;
  92. toNode = newBodyEl.childNodes[0];
  93. }
  94. var savedEls = {}; // Used to save off DOM elements with IDs
  95. var unmatchedEls = {};
  96. var onNodeDiscarded = options.onNodeDiscarded || noop;
  97. var onBeforeMorphEl = options.onBeforeMorphEl || noop;
  98. var onBeforeMorphElChildren = options.onBeforeMorphElChildren || noop;
  99. function removeNodeHelper(node, nestedInSavedEl) {
  100. var id = node.id;
  101. // If the node has an ID then save it off since we will want
  102. // to reuse it in case the target DOM tree has a DOM element
  103. // with the same ID
  104. if (id) {
  105. savedEls[id] = node;
  106. } else if (!nestedInSavedEl) {
  107. // If we are not nested in a saved element then we know that this node has been
  108. // completely discarded and will not exist in the final DOM.
  109. onNodeDiscarded(node);
  110. }
  111. if (node.nodeType === 1) {
  112. var curChild = node.firstChild;
  113. while(curChild) {
  114. removeNodeHelper(curChild, nestedInSavedEl || id);
  115. curChild = curChild.nextSibling;
  116. }
  117. }
  118. }
  119. function walkDiscardedChildNodes(node) {
  120. if (node.nodeType === 1) {
  121. var curChild = node.firstChild;
  122. while(curChild) {
  123. if (!curChild.id) {
  124. // We only want to handle nodes that don't have an ID to avoid double
  125. // walking the same saved element.
  126. onNodeDiscarded(curChild);
  127. // Walk recursively
  128. walkDiscardedChildNodes(curChild);
  129. }
  130. curChild = curChild.nextSibling;
  131. }
  132. }
  133. }
  134. function removeNode(node, parentNode, alreadyVisited) {
  135. parentNode.removeChild(node);
  136. if (alreadyVisited) {
  137. if (!node.id) {
  138. onNodeDiscarded(node);
  139. walkDiscardedChildNodes(node);
  140. }
  141. } else {
  142. removeNodeHelper(node);
  143. }
  144. }
  145. function morphEl(fromNode, toNode, alreadyVisited) {
  146. if (toNode.id) {
  147. // If an element with an ID is being morphed then it is will be in the final
  148. // DOM so clear it out of the saved elements collection
  149. delete savedEls[toNode.id];
  150. }
  151. if (onBeforeMorphEl(fromNode, toNode) === false) {
  152. return;
  153. }
  154. morphAttrs(fromNode, toNode);
  155. if (onBeforeMorphElChildren(fromNode, toNode) === false) {
  156. return;
  157. }
  158. var curToNodeChild = toNode.firstChild;
  159. var curFromNodeChild = fromNode.firstChild;
  160. var curToNodeId;
  161. var fromNextSibling;
  162. var toNextSibling;
  163. var savedEl;
  164. var unmatchedEl;
  165. outer: while(curToNodeChild) {
  166. toNextSibling = curToNodeChild.nextSibling;
  167. curToNodeId = curToNodeChild.id;
  168. while(curFromNodeChild) {
  169. var curFromNodeId = curFromNodeChild.id;
  170. fromNextSibling = curFromNodeChild.nextSibling;
  171. if (!alreadyVisited) {
  172. if (curFromNodeId && (unmatchedEl = unmatchedEls[curFromNodeId])) {
  173. unmatchedEl.parentNode.replaceChild(curFromNodeChild, unmatchedEl);
  174. morphEl(curFromNodeChild, unmatchedEl, alreadyVisited);
  175. curFromNodeChild = fromNextSibling;
  176. continue;
  177. }
  178. }
  179. var curFromNodeType = curFromNodeChild.nodeType;
  180. if (curFromNodeType === curToNodeChild.nodeType) {
  181. var isCompatible = false;
  182. if (curFromNodeType === 1) { // Both nodes being compared are Element nodes
  183. if (curFromNodeChild.tagName === curToNodeChild.tagName) {
  184. // We have compatible DOM elements
  185. if (curFromNodeId || curToNodeId) {
  186. // If either DOM element has an ID then we handle
  187. // those differently since we want to match up
  188. // by ID
  189. if (curToNodeId === curFromNodeId) {
  190. isCompatible = true;
  191. }
  192. } else {
  193. isCompatible = true;
  194. }
  195. }
  196. if (isCompatible) {
  197. // We found compatible DOM elements so add a
  198. // task to morph the compatible DOM elements
  199. morphEl(curFromNodeChild, curToNodeChild, alreadyVisited);
  200. }
  201. } else if (curFromNodeType === 3) { // Both nodes being compared are Text nodes
  202. isCompatible = true;
  203. curFromNodeChild.nodeValue = curToNodeChild.nodeValue;
  204. }
  205. if (isCompatible) {
  206. curToNodeChild = toNextSibling;
  207. curFromNodeChild = fromNextSibling;
  208. continue outer;
  209. }
  210. }
  211. // No compatible match so remove the old node from the DOM
  212. removeNode(curFromNodeChild, fromNode, alreadyVisited);
  213. curFromNodeChild = fromNextSibling;
  214. }
  215. if (curToNodeId) {
  216. if ((savedEl = savedEls[curToNodeId])) {
  217. morphEl(savedEl, curToNodeChild, true);
  218. curToNodeChild = savedEl; // We want to append the saved element instead
  219. } else {
  220. // The current DOM element in the target tree has an ID
  221. // but we did not find a match in any of the corresponding
  222. // siblings. We just put the target element in the old DOM tree
  223. // but if we later find an element in the old DOM tree that has
  224. // a matching ID then we will replace the target element
  225. // with the corresponding old element and morph the old element
  226. unmatchedEls[curToNodeId] = curToNodeChild;
  227. }
  228. }
  229. // If we got this far then we did not find a candidate match for our "to node"
  230. // and we exhausted all of the children "from" nodes. Therefore, we will just
  231. // append the current "to node" to the end
  232. fromNode.appendChild(curToNodeChild);
  233. curToNodeChild = toNextSibling;
  234. curFromNodeChild = fromNextSibling;
  235. }
  236. // We have processed all of the "to nodes". If curFromNodeChild is non-null then
  237. // we still have some from nodes left over that need to be removed
  238. while(curFromNodeChild) {
  239. fromNextSibling = curFromNodeChild.nextSibling;
  240. removeNode(curFromNodeChild, fromNode, alreadyVisited);
  241. curFromNodeChild = fromNextSibling;
  242. }
  243. var specialElHandler = specialElHandlers[fromNode.tagName];
  244. if (specialElHandler) {
  245. specialElHandler(fromNode, toNode);
  246. }
  247. }
  248. var morphedNode = fromNode;
  249. var morphedNodeType = morphedNode.nodeType;
  250. var toNodeType = toNode.nodeType;
  251. // Handle the case where we are given two DOM nodes that are not
  252. // compatible (e.g. <div> --> <span> or <div> --> TEXT)
  253. if (morphedNodeType === 1) {
  254. if (toNodeType === 1) {
  255. if (morphedNode.tagName !== toNode.tagName) {
  256. onNodeDiscarded(fromNode);
  257. morphedNode = moveChildren(morphedNode, document.createElement(toNode.tagName));
  258. }
  259. } else {
  260. // Going from an element node to a text node
  261. return toNode;
  262. }
  263. } else if (morphedNodeType === 3) { // Text node
  264. if (toNodeType === 3) {
  265. morphedNode.nodeValue = toNode.nodeValue;
  266. return morphedNode;
  267. } else {
  268. onNodeDiscarded(fromNode);
  269. // Text node to something else
  270. return toNode;
  271. }
  272. }
  273. morphEl(morphedNode, toNode, false);
  274. // Fire the "onNodeDiscarded" event for any saved elements
  275. // that never found a new home in the morphed DOM
  276. for (var savedElId in savedEls) {
  277. if (savedEls.hasOwnProperty(savedElId)) {
  278. var savedEl = savedEls[savedElId];
  279. onNodeDiscarded(savedEl);
  280. walkDiscardedChildNodes(savedEl);
  281. }
  282. }
  283. if (morphedNode !== fromNode && fromNode.parentNode) {
  284. fromNode.parentNode.replaceChild(morphedNode, fromNode);
  285. }
  286. return morphedNode;
  287. }
  288. module.exports = morphdom;
  289. },{}]},{},[1])(1)
  290. });