UNPKG

@alpinejs/morph

Version:

Diff and patch a block of HTML on a page with an HTML template

420 lines (415 loc) 14.2 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // packages/morph/builds/module.js var module_exports = {}; __export(module_exports, { default: () => module_default, morph: () => src_default }); module.exports = __toCommonJS(module_exports); // packages/morph/src/morph.js function morph(from, toHtml, options) { monkeyPatchDomSetAttributeToAllowAtSymbols(); let context = createMorphContext(options); let toEl = typeof toHtml === "string" ? createElement(toHtml) : toHtml; if (window.Alpine && window.Alpine.closestDataStack && !from._x_dataStack) { toEl._x_dataStack = window.Alpine.closestDataStack(from); toEl._x_dataStack && window.Alpine.cloneNode(from, toEl); } context.patch(from, toEl); return from; } function morphBetween(startMarker, endMarker, toHtml, options = {}) { monkeyPatchDomSetAttributeToAllowAtSymbols(); let context = createMorphContext(options); let fromContainer = startMarker.parentNode; let fromBlock = new Block(startMarker, endMarker); let toContainer = typeof toHtml === "string" ? (() => { let container = document.createElement("div"); container.insertAdjacentHTML("beforeend", toHtml); return container; })() : toHtml; let toStartMarker = document.createComment("[morph-start]"); let toEndMarker = document.createComment("[morph-end]"); toContainer.insertBefore(toStartMarker, toContainer.firstChild); toContainer.appendChild(toEndMarker); let toBlock = new Block(toStartMarker, toEndMarker); if (window.Alpine && window.Alpine.closestDataStack) { toContainer._x_dataStack = window.Alpine.closestDataStack(fromContainer); toContainer._x_dataStack && window.Alpine.cloneNode(fromContainer, toContainer); } context.patchChildren(fromBlock, toBlock); } function createMorphContext(options = {}) { let defaultGetKey = (el) => el.getAttribute("key"); let noop = () => { }; let context = { key: options.key || defaultGetKey, lookahead: options.lookahead || false, updating: options.updating || noop, updated: options.updated || noop, removing: options.removing || noop, removed: options.removed || noop, adding: options.adding || noop, added: options.added || noop }; context.patch = function(from, to) { if (context.differentElementNamesTypesOrKeys(from, to)) { return context.swapElements(from, to); } let updateChildrenOnly = false; let skipChildren = false; let skipUntil = (predicate) => context.skipUntilCondition = predicate; if (shouldSkipChildren(context.updating, () => skipChildren = true, skipUntil, from, to, () => updateChildrenOnly = true)) return; if (from.nodeType === 1 && window.Alpine) { window.Alpine.cloneNode(from, to); if (from._x_teleport && to._x_teleport) { context.patch(from._x_teleport, to._x_teleport); } } if (textOrComment(to)) { context.patchNodeValue(from, to); context.updated(from, to); return; } if (!updateChildrenOnly) { context.patchAttributes(from, to); } context.updated(from, to); if (!skipChildren) { context.patchChildren(from, to); } }; context.differentElementNamesTypesOrKeys = function(from, to) { return from.nodeType != to.nodeType || from.nodeName != to.nodeName || context.getKey(from) != context.getKey(to); }; context.swapElements = function(from, to) { if (shouldSkip(context.removing, from)) return; let toCloned = to.cloneNode(true); if (shouldSkip(context.adding, toCloned)) return; from.replaceWith(toCloned); context.removed(from); context.added(toCloned); }; context.patchNodeValue = function(from, to) { let value = to.nodeValue; if (from.nodeValue !== value) { from.nodeValue = value; } }; context.patchAttributes = function(from, to) { if (from._x_transitioning) return; if (from._x_isShown && !to._x_isShown) { return; } if (!from._x_isShown && to._x_isShown) { return; } let domAttributes = Array.from(from.attributes); let toAttributes = Array.from(to.attributes); for (let i = domAttributes.length - 1; i >= 0; i--) { let name = domAttributes[i].name; if (!to.hasAttribute(name)) { if (name === "open" && from.nodeName === "DIALOG" && from.open) { from.close(); } else { from.removeAttribute(name); } } } for (let i = toAttributes.length - 1; i >= 0; i--) { let name = toAttributes[i].name; let value = toAttributes[i].value; if (from.getAttribute(name) !== value) { from.setAttribute(name, value); } } }; context.patchChildren = function(from, to) { let fromKeys = context.keyToMap(from.children); let fromKeyHoldovers = {}; let currentTo = getFirstNode(to); let currentFrom = getFirstNode(from); while (currentTo) { seedingMatchingId(currentTo, currentFrom); let toKey = context.getKey(currentTo); let fromKey = context.getKey(currentFrom); if (context.skipUntilCondition) { let fromDone = !currentFrom || context.skipUntilCondition(currentFrom); let toDone = !currentTo || context.skipUntilCondition(currentTo); if (fromDone && toDone) { context.skipUntilCondition = null; } else { if (!fromDone) currentFrom = currentFrom && getNextSibling(from, currentFrom); if (!toDone) currentTo = currentTo && getNextSibling(to, currentTo); continue; } } if (!currentFrom) { if (toKey && fromKeyHoldovers[toKey]) { let holdover = fromKeyHoldovers[toKey]; from.appendChild(holdover); currentFrom = holdover; fromKey = context.getKey(currentFrom); } else { if (!shouldSkip(context.adding, currentTo)) { let clone = currentTo.cloneNode(true); from.appendChild(clone); context.added(clone); } currentTo = getNextSibling(to, currentTo); continue; } } let isIf = (node) => node && node.nodeType === 8 && node.textContent === "[if BLOCK]><![endif]"; let isEnd = (node) => node && node.nodeType === 8 && node.textContent === "[if ENDBLOCK]><![endif]"; if (isIf(currentTo) && isIf(currentFrom)) { let nestedIfCount = 0; let fromBlockStart = currentFrom; while (currentFrom) { let next = getNextSibling(from, currentFrom); if (isIf(next)) { nestedIfCount++; } else if (isEnd(next) && nestedIfCount > 0) { nestedIfCount--; } else if (isEnd(next) && nestedIfCount === 0) { currentFrom = next; break; } currentFrom = next; } let fromBlockEnd = currentFrom; nestedIfCount = 0; let toBlockStart = currentTo; while (currentTo) { let next = getNextSibling(to, currentTo); if (isIf(next)) { nestedIfCount++; } else if (isEnd(next) && nestedIfCount > 0) { nestedIfCount--; } else if (isEnd(next) && nestedIfCount === 0) { currentTo = next; break; } currentTo = next; } let toBlockEnd = currentTo; let fromBlock = new Block(fromBlockStart, fromBlockEnd); let toBlock = new Block(toBlockStart, toBlockEnd); context.patchChildren(fromBlock, toBlock); continue; } if (currentFrom.nodeType === 1 && context.lookahead && !currentFrom.isEqualNode(currentTo)) { let nextToElementSibling = getNextSibling(to, currentTo); let found = false; while (!found && nextToElementSibling) { if (nextToElementSibling.nodeType === 1 && currentFrom.isEqualNode(nextToElementSibling)) { found = true; currentFrom = context.addNodeBefore(from, currentTo, currentFrom); fromKey = context.getKey(currentFrom); } nextToElementSibling = getNextSibling(to, nextToElementSibling); } } if (toKey !== fromKey) { if (!toKey && fromKey) { fromKeyHoldovers[fromKey] = currentFrom; currentFrom = context.addNodeBefore(from, currentTo, currentFrom); fromKeyHoldovers[fromKey].remove(); currentFrom = getNextSibling(from, currentFrom); currentTo = getNextSibling(to, currentTo); continue; } if (toKey && !fromKey) { if (fromKeys[toKey]) { currentFrom.replaceWith(fromKeys[toKey]); currentFrom = fromKeys[toKey]; fromKey = context.getKey(currentFrom); } } if (toKey && fromKey) { let fromKeyNode = fromKeys[toKey]; if (fromKeyNode) { fromKeyHoldovers[fromKey] = currentFrom; currentFrom.replaceWith(fromKeyNode); currentFrom = fromKeyNode; fromKey = context.getKey(currentFrom); } else { fromKeyHoldovers[fromKey] = currentFrom; currentFrom = context.addNodeBefore(from, currentTo, currentFrom); fromKeyHoldovers[fromKey].remove(); currentFrom = getNextSibling(from, currentFrom); currentTo = getNextSibling(to, currentTo); continue; } } } let currentFromNext = currentFrom && getNextSibling(from, currentFrom); context.patch(currentFrom, currentTo); currentTo = currentTo && getNextSibling(to, currentTo); currentFrom = currentFromNext; } let removals = []; while (currentFrom) { if (!shouldSkip(context.removing, currentFrom)) removals.push(currentFrom); currentFrom = getNextSibling(from, currentFrom); } while (removals.length) { let domForRemoval = removals.shift(); domForRemoval.remove(); context.removed(domForRemoval); } }; context.getKey = function(el) { return el && el.nodeType === 1 && context.key(el); }; context.keyToMap = function(els) { let map = {}; for (let el of els) { let theKey = context.getKey(el); if (theKey) { map[theKey] = el; } } return map; }; context.addNodeBefore = function(parent, node, beforeMe) { if (!shouldSkip(context.adding, node)) { let clone = node.cloneNode(true); parent.insertBefore(clone, beforeMe); context.added(clone); return clone; } return node; }; return context; } morph.step = () => { }; morph.log = () => { }; function shouldSkip(hook, ...args) { let skip = false; hook(...args, () => skip = true); return skip; } function shouldSkipChildren(hook, skipChildren, skipUntil, ...args) { let skip = false; hook(...args, () => skip = true, skipChildren, skipUntil); return skip; } var patched = false; function createElement(html) { const template = document.createElement("template"); template.innerHTML = html; return template.content.firstElementChild; } function textOrComment(el) { return el.nodeType === 3 || el.nodeType === 8; } var Block = class { constructor(start, end) { this.startComment = start; this.endComment = end; } get children() { let children = []; let currentNode = this.startComment.nextSibling; while (currentNode && currentNode !== this.endComment) { children.push(currentNode); currentNode = currentNode.nextSibling; } return children; } appendChild(child) { this.endComment.before(child); } get firstChild() { let first = this.startComment.nextSibling; if (first === this.endComment) return; return first; } nextNode(reference) { let next = reference.nextSibling; if (next === this.endComment) return; return next; } insertBefore(newNode, reference) { reference.before(newNode); return newNode; } }; function getFirstNode(parent) { return parent.firstChild; } function getNextSibling(parent, reference) { let next; if (parent instanceof Block) { next = parent.nextNode(reference); } else { next = reference.nextSibling; } return next; } function monkeyPatchDomSetAttributeToAllowAtSymbols() { if (patched) return; patched = true; let original = Element.prototype.setAttribute; let hostDiv = document.createElement("div"); Element.prototype.setAttribute = function newSetAttribute(name, value) { if (!name.includes("@")) { return original.call(this, name, value); } let escapedValue = value.replace(/&/g, "&amp;").replace(/"/g, "&quot;"); hostDiv.innerHTML = `<span ${name}="${escapedValue}"></span>`; let attr = hostDiv.firstElementChild.getAttributeNode(name); hostDiv.firstElementChild.removeAttributeNode(attr); this.setAttributeNode(attr); }; } function seedingMatchingId(to, from) { let fromId = from && from._x_bindings && from._x_bindings.id; if (!fromId) return; if (!to.setAttribute) return; to.setAttribute("id", fromId); to.id = fromId; } // packages/morph/src/index.js function src_default(Alpine) { Alpine.morph = morph; Alpine.morphBetween = morphBetween; } // packages/morph/builds/module.js var module_default = src_default; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { morph });