morph.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. let resolveStep = () => {}
  2. let logger = () => {}
  3. export function morph(from, toHtml, options) {
  4. monkeyPatchDomSetAttributeToAllowAtSymbols()
  5. // We're defining these globals and methods inside this function (instead of outside)
  6. // because it's an async function and if run twice, they would overwrite
  7. // each other.
  8. let fromEl
  9. let toEl
  10. let key, lookahead, updating, updated, removing, removed, adding, added
  11. function assignOptions(options = {}) {
  12. let defaultGetKey = el => el.getAttribute('key')
  13. let noop = () => {}
  14. updating = options.updating || noop
  15. updated = options.updated || noop
  16. removing = options.removing || noop
  17. removed = options.removed || noop
  18. adding = options.adding || noop
  19. added = options.added || noop
  20. key = options.key || defaultGetKey
  21. lookahead = options.lookahead || false
  22. }
  23. function patch(from, to) {
  24. if (differentElementNamesTypesOrKeys(from, to)) {
  25. return swapElements(from, to)
  26. }
  27. let updateChildrenOnly = false
  28. if (shouldSkip(updating, from, to, () => updateChildrenOnly = true)) return
  29. // Initialize the server-side HTML element with Alpine...
  30. if (from.nodeType === 1 && window.Alpine) {
  31. window.Alpine.cloneNode(from, to)
  32. }
  33. if (textOrComment(to)) {
  34. patchNodeValue(from, to)
  35. updated(from, to)
  36. return
  37. }
  38. if (! updateChildrenOnly) {
  39. patchAttributes(from, to)
  40. }
  41. updated(from, to)
  42. patchChildren(from, to)
  43. }
  44. function differentElementNamesTypesOrKeys(from, to) {
  45. return from.nodeType != to.nodeType
  46. || from.nodeName != to.nodeName
  47. || getKey(from) != getKey(to)
  48. }
  49. function swapElements(from, to) {
  50. if (shouldSkip(removing, from)) return
  51. let toCloned = to.cloneNode(true)
  52. if (shouldSkip(adding, toCloned)) return
  53. from.replaceWith(toCloned)
  54. removed(from)
  55. added(toCloned)
  56. }
  57. function patchNodeValue(from, to) {
  58. let value = to.nodeValue
  59. if (from.nodeValue !== value) {
  60. // Change text node...
  61. from.nodeValue = value
  62. }
  63. }
  64. function patchAttributes(from, to) {
  65. if (from._x_transitioning) return
  66. if (from._x_isShown && ! to._x_isShown) {
  67. return
  68. }
  69. if (! from._x_isShown && to._x_isShown) {
  70. return
  71. }
  72. let domAttributes = Array.from(from.attributes)
  73. let toAttributes = Array.from(to.attributes)
  74. for (let i = domAttributes.length - 1; i >= 0; i--) {
  75. let name = domAttributes[i].name;
  76. if (! to.hasAttribute(name)) {
  77. // Remove attribute...
  78. from.removeAttribute(name)
  79. }
  80. }
  81. for (let i = toAttributes.length - 1; i >= 0; i--) {
  82. let name = toAttributes[i].name
  83. let value = toAttributes[i].value
  84. if (from.getAttribute(name) !== value) {
  85. from.setAttribute(name, value)
  86. }
  87. }
  88. }
  89. function patchChildren(from, to) {
  90. // If we hit a <template x-teleport="body">,
  91. // let's use the teleported nodes for this patch...
  92. if (from._x_teleport) from = from._x_teleport
  93. if (to._x_teleport) to = to._x_teleport
  94. let fromKeys = keyToMap(from.children)
  95. let fromKeyHoldovers = {}
  96. let currentTo = getFirstNode(to)
  97. let currentFrom = getFirstNode(from)
  98. while (currentTo) {
  99. // If the "from" element has a dynamically bound "id" (x-bind:id="..."),
  100. // Let's transfer it to the "to" element so that there isn't a key mismatch...
  101. seedingMatchingId(currentTo, currentFrom)
  102. let toKey = getKey(currentTo)
  103. let fromKey = getKey(currentFrom)
  104. // Add new elements...
  105. if (! currentFrom) {
  106. if (toKey && fromKeyHoldovers[toKey]) {
  107. // Add element (from key)...
  108. let holdover = fromKeyHoldovers[toKey]
  109. from.appendChild(holdover)
  110. currentFrom = holdover
  111. } else {
  112. if(! shouldSkip(adding, currentTo)) {
  113. // Add element...
  114. let clone = currentTo.cloneNode(true)
  115. from.appendChild(clone)
  116. added(clone)
  117. }
  118. currentTo = getNextSibling(to, currentTo)
  119. continue
  120. }
  121. }
  122. // Handle conditional markers (presumably added by backends like Livewire)...
  123. let isIf = node => node && node.nodeType === 8 && node.textContent === '[if BLOCK]><![endif]'
  124. let isEnd = node => node && node.nodeType === 8 && node.textContent === '[if ENDBLOCK]><![endif]'
  125. if (isIf(currentTo) && isIf(currentFrom)) {
  126. let nestedIfCount = 0
  127. let fromBlockStart = currentFrom
  128. while (currentFrom) {
  129. let next = getNextSibling(from, currentFrom)
  130. if (isIf(next)) {
  131. nestedIfCount++
  132. } else if (isEnd(next) && nestedIfCount > 0) {
  133. nestedIfCount--
  134. } else if (isEnd(next) && nestedIfCount === 0) {
  135. currentFrom = next
  136. break;
  137. }
  138. currentFrom = next
  139. }
  140. let fromBlockEnd = currentFrom
  141. nestedIfCount = 0
  142. let toBlockStart = currentTo
  143. while (currentTo) {
  144. let next = getNextSibling(to, currentTo)
  145. if (isIf(next)) {
  146. nestedIfCount++
  147. } else if (isEnd(next) && nestedIfCount > 0) {
  148. nestedIfCount--
  149. } else if (isEnd(next) && nestedIfCount === 0) {
  150. currentTo = next
  151. break;
  152. }
  153. currentTo = next
  154. }
  155. let toBlockEnd = currentTo
  156. let fromBlock = new Block(fromBlockStart, fromBlockEnd)
  157. let toBlock = new Block(toBlockStart, toBlockEnd)
  158. patchChildren(fromBlock, toBlock)
  159. continue
  160. }
  161. // Lookaheads should only apply to non-text-or-comment elements...
  162. if (currentFrom.nodeType === 1 && lookahead && ! currentFrom.isEqualNode(currentTo)) {
  163. let nextToElementSibling = getNextSibling(to, currentTo)
  164. let found = false
  165. while (! found && nextToElementSibling) {
  166. if (nextToElementSibling.nodeType === 1 && currentFrom.isEqualNode(nextToElementSibling)) {
  167. found = true; // This ";" needs to be here...
  168. currentFrom = addNodeBefore(from, currentTo, currentFrom)
  169. fromKey = getKey(currentFrom)
  170. }
  171. nextToElementSibling = getNextSibling(to, nextToElementSibling)
  172. }
  173. }
  174. if (toKey !== fromKey) {
  175. if (! toKey && fromKey) {
  176. // No "to" key...
  177. fromKeyHoldovers[fromKey] = currentFrom; // This ";" needs to be here...
  178. currentFrom = addNodeBefore(from, currentTo, currentFrom)
  179. fromKeyHoldovers[fromKey].remove()
  180. currentFrom = getNextSibling(from, currentFrom)
  181. currentTo = getNextSibling(to, currentTo)
  182. continue
  183. }
  184. if (toKey && ! fromKey) {
  185. if (fromKeys[toKey]) {
  186. // No "from" key...
  187. currentFrom.replaceWith(fromKeys[toKey])
  188. currentFrom = fromKeys[toKey]
  189. }
  190. }
  191. if (toKey && fromKey) {
  192. let fromKeyNode = fromKeys[toKey]
  193. if (fromKeyNode) {
  194. // Move "from" key...
  195. fromKeyHoldovers[fromKey] = currentFrom
  196. currentFrom.replaceWith(fromKeyNode)
  197. currentFrom = fromKeyNode
  198. } else {
  199. // Swap elements with keys...
  200. fromKeyHoldovers[fromKey] = currentFrom; // This ";" needs to be here...
  201. currentFrom = addNodeBefore(from, currentTo, currentFrom)
  202. fromKeyHoldovers[fromKey].remove()
  203. currentFrom = getNextSibling(from, currentFrom)
  204. currentTo = getNextSibling(to, currentTo)
  205. continue
  206. }
  207. }
  208. }
  209. // Get next from sibling before patching in case the node is replaced
  210. let currentFromNext = currentFrom && getNextSibling(from, currentFrom) //dom.next(from, fromChildren, currentFrom))
  211. // Patch elements
  212. patch(currentFrom, currentTo)
  213. currentTo = currentTo && getNextSibling(to, currentTo) // dom.next(from, toChildren, currentTo))
  214. currentFrom = currentFromNext
  215. }
  216. // Cleanup extra forms.
  217. let removals = []
  218. // We need to collect the "removals" first before actually
  219. // removing them so we don't mess with the order of things.
  220. while (currentFrom) {
  221. if (! shouldSkip(removing, currentFrom)) removals.push(currentFrom)
  222. // currentFrom = dom.next(fromChildren, currentFrom)
  223. currentFrom = getNextSibling(from, currentFrom)
  224. }
  225. // Now we can do the actual removals.
  226. while (removals.length) {
  227. let domForRemoval = removals.shift()
  228. domForRemoval.remove()
  229. removed(domForRemoval)
  230. }
  231. }
  232. function getKey(el) {
  233. return el && el.nodeType === 1 && key(el)
  234. }
  235. function keyToMap(els) {
  236. let map = {}
  237. for (let el of els) {
  238. let theKey = getKey(el)
  239. if (theKey) {
  240. map[theKey] = el
  241. }
  242. }
  243. return map
  244. }
  245. function addNodeBefore(parent, node, beforeMe) {
  246. if(! shouldSkip(adding, node)) {
  247. let clone = node.cloneNode(true)
  248. parent.insertBefore(clone, beforeMe)
  249. added(clone)
  250. return clone
  251. }
  252. return node
  253. }
  254. // Finally we morph the element
  255. assignOptions(options)
  256. fromEl = from
  257. toEl = typeof toHtml === 'string' ? createElement(toHtml) : toHtml
  258. if (window.Alpine && window.Alpine.closestDataStack && ! from._x_dataStack) {
  259. // Just in case a part of this template uses Alpine scope from somewhere
  260. // higher in the DOM tree, we'll find that state and replace it on the root
  261. // element so everything is synced up accurately.
  262. toEl._x_dataStack = window.Alpine.closestDataStack(from)
  263. // We will kick off a clone on the root element.
  264. toEl._x_dataStack && window.Alpine.cloneNode(from, toEl)
  265. }
  266. patch(from, toEl)
  267. // Release these for the garbage collector.
  268. fromEl = undefined
  269. toEl = undefined
  270. return from
  271. }
  272. // These are legacy holdovers that don't do anything anymore...
  273. morph.step = () => {}
  274. morph.log = () => {}
  275. function shouldSkip(hook, ...args) {
  276. let skip = false
  277. hook(...args, () => skip = true)
  278. return skip
  279. }
  280. let patched = false
  281. export function createElement(html) {
  282. const template = document.createElement('template')
  283. template.innerHTML = html
  284. return template.content.firstElementChild
  285. }
  286. export function textOrComment(el) {
  287. return el.nodeType === 3
  288. || el.nodeType === 8
  289. }
  290. // "Block"s are used when morphing with conditional markers.
  291. // They allow us to patch isolated portions of a list of
  292. // siblings in a DOM tree...
  293. class Block {
  294. constructor(start, end) {
  295. // We're assuming here that the start and end caps are comment blocks...
  296. this.startComment = start
  297. this.endComment = end
  298. }
  299. get children() {
  300. let children = [];
  301. let currentNode = this.startComment.nextSibling
  302. while (currentNode && currentNode !== this.endComment) {
  303. children.push(currentNode)
  304. currentNode = currentNode.nextSibling
  305. }
  306. return children
  307. }
  308. appendChild(child) {
  309. this.endComment.before(child)
  310. }
  311. get firstChild() {
  312. let first = this.startComment.nextSibling
  313. if (first === this.endComment) return
  314. return first
  315. }
  316. nextNode(reference) {
  317. let next = reference.nextSibling
  318. if (next === this.endComment) return
  319. return next
  320. }
  321. insertBefore(newNode, reference) {
  322. reference.before(newNode)
  323. return newNode
  324. }
  325. }
  326. function getFirstNode(parent) {
  327. return parent.firstChild
  328. }
  329. function getNextSibling(parent, reference) {
  330. let next
  331. if (parent instanceof Block) {
  332. next = parent.nextNode(reference)
  333. } else {
  334. next = reference.nextSibling
  335. }
  336. return next
  337. }
  338. function monkeyPatchDomSetAttributeToAllowAtSymbols() {
  339. if (patched) return
  340. patched = true
  341. // Because morphdom may add attributes to elements containing "@" symbols
  342. // like in the case of an Alpine `@click` directive, we have to patch
  343. // the standard Element.setAttribute method to allow this to work.
  344. let original = Element.prototype.setAttribute
  345. let hostDiv = document.createElement('div')
  346. Element.prototype.setAttribute = function newSetAttribute(name, value) {
  347. if (! name.includes('@')) {
  348. return original.call(this, name, value)
  349. }
  350. hostDiv.innerHTML = `<span ${name}="${value}"></span>`
  351. let attr = hostDiv.firstElementChild.getAttributeNode(name)
  352. hostDiv.firstElementChild.removeAttributeNode(attr)
  353. this.setAttributeNode(attr)
  354. }
  355. }
  356. function seedingMatchingId(to, from) {
  357. let fromId = from && from._x_bindings && from._x_bindings.id
  358. if (! fromId) return
  359. to.setAttribute('id', fromId)
  360. to.id = fromId
  361. }