UNPKG

@symfony/ux-live-component

Version:

Live Component: bring server-side re-rendering & model binding to any element.

1,499 lines (1,483 loc) 101 kB
// src/live_controller.ts import { Controller } from "@hotwired/stimulus"; // src/Backend/BackendRequest.ts var BackendRequest_default = class { constructor(promise, actions, updateModels) { this.isResolved = false; this.promise = promise; this.promise.then((response) => { this.isResolved = true; return response; }); this.actions = actions; this.updatedModels = updateModels; } /** * Does this BackendRequest contain at least on action in targetedActions? */ containsOneOfActions(targetedActions) { return this.actions.filter((action) => targetedActions.includes(action)).length > 0; } /** * Does this BackendRequest includes updates for any of these models? */ areAnyModelsUpdated(targetedModels) { return this.updatedModels.filter((model) => targetedModels.includes(model)).length > 0; } }; // src/Backend/RequestBuilder.ts var RequestBuilder_default = class { constructor(url, method = "post") { this.url = url; this.method = method; } buildRequest(props, actions, updated, children, updatedPropsFromParent, files) { const splitUrl = this.url.split("?"); let [url] = splitUrl; const [, queryString] = splitUrl; const params = new URLSearchParams(queryString || ""); const fetchOptions = {}; fetchOptions.headers = { Accept: "application/vnd.live-component+html", "X-Requested-With": "XMLHttpRequest", "X-Live-Url": window.location.pathname + window.location.search }; const totalFiles = Object.entries(files).reduce((total, current) => total + current.length, 0); const hasFingerprints = Object.keys(children).length > 0; if (actions.length === 0 && totalFiles === 0 && this.method === "get" && this.willDataFitInUrl( JSON.stringify(props), JSON.stringify(updated), params, JSON.stringify(children), JSON.stringify(updatedPropsFromParent) )) { params.set("props", JSON.stringify(props)); params.set("updated", JSON.stringify(updated)); if (Object.keys(updatedPropsFromParent).length > 0) { params.set("propsFromParent", JSON.stringify(updatedPropsFromParent)); } if (hasFingerprints) { params.set("children", JSON.stringify(children)); } fetchOptions.method = "GET"; } else { fetchOptions.method = "POST"; const requestData = { props, updated }; if (Object.keys(updatedPropsFromParent).length > 0) { requestData.propsFromParent = updatedPropsFromParent; } if (hasFingerprints) { requestData.children = children; } if (actions.length > 0) { if (actions.length === 1) { requestData.args = actions[0].args; url += `/${encodeURIComponent(actions[0].name)}`; } else { url += "/_batch"; requestData.actions = actions; } } const formData = new FormData(); formData.append("data", JSON.stringify(requestData)); for (const [key, value] of Object.entries(files)) { const length = value.length; for (let i = 0; i < length; ++i) { formData.append(key, value[i]); } } fetchOptions.body = formData; } const paramsString = params.toString(); return { url: `${url}${paramsString.length > 0 ? `?${paramsString}` : ""}`, fetchOptions }; } willDataFitInUrl(propsJson, updatedJson, params, childrenJson, propsFromParentJson) { const urlEncodedJsonData = new URLSearchParams( propsJson + updatedJson + childrenJson + propsFromParentJson ).toString(); return (urlEncodedJsonData + params.toString()).length < 1500; } }; // src/Backend/Backend.ts var Backend_default = class { constructor(url, method = "post") { this.requestBuilder = new RequestBuilder_default(url, method); } makeRequest(props, actions, updated, children, updatedPropsFromParent, files) { const { url, fetchOptions } = this.requestBuilder.buildRequest( props, actions, updated, children, updatedPropsFromParent, files ); return new BackendRequest_default( fetch(url, fetchOptions), actions.map((backendAction) => backendAction.name), Object.keys(updated) ); } }; // src/Backend/BackendResponse.ts var BackendResponse_default = class { constructor(response) { this.response = response; } async getBody() { if (!this.body) { this.body = await this.response.text(); } return this.body; } getLiveUrl() { if (void 0 === this.liveUrl) { this.liveUrl = this.response.headers.get("X-Live-Url"); } return this.liveUrl; } }; // src/Util/getElementAsTagText.ts function getElementAsTagText(element) { return element.innerHTML ? element.outerHTML.slice(0, element.outerHTML.indexOf(element.innerHTML)) : element.outerHTML; } // src/ComponentRegistry.ts var componentMapByElement = /* @__PURE__ */ new WeakMap(); var componentMapByComponent = /* @__PURE__ */ new Map(); var registerComponent = (component) => { componentMapByElement.set(component.element, component); componentMapByComponent.set(component, component.name); }; var unregisterComponent = (component) => { componentMapByElement.delete(component.element); componentMapByComponent.delete(component); }; var getComponent = (element) => new Promise((resolve, reject) => { let count = 0; const maxCount = 10; const interval = setInterval(() => { const component = componentMapByElement.get(element); if (component) { clearInterval(interval); resolve(component); } count++; if (count > maxCount) { clearInterval(interval); reject(new Error(`Component not found for element ${getElementAsTagText(element)}`)); } }, 5); }); var findComponents = (currentComponent, onlyParents, onlyMatchName) => { const components = []; componentMapByComponent.forEach((componentName, component) => { if (onlyParents && (currentComponent === component || !component.element.contains(currentComponent.element))) { return; } if (onlyMatchName && componentName !== onlyMatchName) { return; } components.push(component); }); return components; }; var findChildren = (currentComponent) => { const children = []; componentMapByComponent.forEach((componentName, component) => { if (currentComponent === component) { return; } if (!currentComponent.element.contains(component.element)) { return; } let foundChildComponent = false; componentMapByComponent.forEach((childComponentName, childComponent) => { if (foundChildComponent) { return; } if (childComponent === component) { return; } if (childComponent.element.contains(component.element)) { foundChildComponent = true; } }); children.push(component); }); return children; }; var findParent = (currentComponent) => { let parentElement = currentComponent.element.parentElement; while (parentElement) { const component = componentMapByElement.get(parentElement); if (component) { return component; } parentElement = parentElement.parentElement; } return null; }; // src/Directive/directives_parser.ts function parseDirectives(content) { const directives = []; if (!content) { return directives; } let currentActionName = ""; let currentArgumentValue = ""; let currentArguments = []; let currentModifiers = []; let state = "action"; const getLastActionName = () => { if (currentActionName) { return currentActionName; } if (directives.length === 0) { throw new Error("Could not find any directives"); } return directives[directives.length - 1].action; }; const pushInstruction = () => { directives.push({ action: currentActionName, args: currentArguments, modifiers: currentModifiers, getString: () => { return content; } }); currentActionName = ""; currentArgumentValue = ""; currentArguments = []; currentModifiers = []; state = "action"; }; const pushArgument = () => { currentArguments.push(currentArgumentValue.trim()); currentArgumentValue = ""; }; const pushModifier = () => { if (currentArguments.length > 1) { throw new Error(`The modifier "${currentActionName}()" does not support multiple arguments.`); } currentModifiers.push({ name: currentActionName, value: currentArguments.length > 0 ? currentArguments[0] : null }); currentActionName = ""; currentArguments = []; state = "action"; }; for (let i = 0; i < content.length; i++) { const char = content[i]; switch (state) { case "action": if (char === "(") { state = "arguments"; break; } if (char === " ") { if (currentActionName) { pushInstruction(); } break; } if (char === "|") { pushModifier(); break; } currentActionName += char; break; case "arguments": if (char === ")") { pushArgument(); state = "after_arguments"; break; } if (char === ",") { pushArgument(); break; } currentArgumentValue += char; break; case "after_arguments": if (char === "|") { pushModifier(); break; } if (char !== " ") { throw new Error(`Missing space after ${getLastActionName()}()`); } pushInstruction(); break; } } switch (state) { case "action": case "after_arguments": if (currentActionName) { pushInstruction(); } break; default: throw new Error(`Did you forget to add a closing ")" after "${currentActionName}"?`); } return directives; } // src/string_utils.ts function combineSpacedArray(parts) { const finalParts = []; parts.forEach((part) => { finalParts.push(...trimAll(part).split(" ")); }); return finalParts; } function trimAll(str) { return str.replace(/[\s]+/g, " ").trim(); } function normalizeModelName(model) { return model.replace(/\[]$/, "").split("[").map((s) => s.replace("]", "")).join("."); } // src/dom_utils.ts function getValueFromElement(element, valueStore) { if (element instanceof HTMLInputElement) { if (element.type === "checkbox") { const modelNameData = getModelDirectiveFromElement(element, false); if (modelNameData !== null) { const modelValue = valueStore.get(modelNameData.action); if (Array.isArray(modelValue)) { return getMultipleCheckboxValue(element, modelValue); } if (Object(modelValue) === modelValue) { return getMultipleCheckboxValue(element, Object.values(modelValue)); } } if (element.hasAttribute("value")) { return element.checked ? element.getAttribute("value") : null; } return element.checked; } return inputValue(element); } if (element instanceof HTMLSelectElement) { if (element.multiple) { return Array.from(element.selectedOptions).map((el) => el.value); } return element.value; } if (element.dataset.value) { return element.dataset.value; } if ("value" in element) { return element.value; } if (element.hasAttribute("value")) { return element.getAttribute("value"); } return null; } function setValueOnElement(element, value) { if (element instanceof HTMLInputElement) { if (element.type === "file") { return; } if (element.type === "radio") { element.checked = element.value == value; return; } if (element.type === "checkbox") { if (Array.isArray(value)) { element.checked = value.some((val) => val == element.value); } else if (element.hasAttribute("value")) { element.checked = element.value == value; } else { element.checked = value; } return; } } if (element instanceof HTMLSelectElement) { const arrayWrappedValue = [].concat(value).map((value2) => { return `${value2}`; }); Array.from(element.options).forEach((option) => { option.selected = arrayWrappedValue.includes(option.value); }); return; } value = value === void 0 ? "" : value; element.value = value; } function getAllModelDirectiveFromElements(element) { if (!element.dataset.model) { return []; } const directives = parseDirectives(element.dataset.model); directives.forEach((directive) => { if (directive.args.length > 0) { throw new Error( `The data-model="${element.dataset.model}" format is invalid: it does not support passing arguments to the model.` ); } directive.action = normalizeModelName(directive.action); }); return directives; } function getModelDirectiveFromElement(element, throwOnMissing = true) { const dataModelDirectives = getAllModelDirectiveFromElements(element); if (dataModelDirectives.length > 0) { return dataModelDirectives[0]; } if (element.getAttribute("name")) { const formElement = element.closest("form"); if (formElement && "model" in formElement.dataset) { const directives = parseDirectives(formElement.dataset.model || "*"); const directive = directives[0]; if (directive.args.length > 0) { throw new Error( `The data-model="${formElement.dataset.model}" format is invalid: it does not support passing arguments to the model.` ); } directive.action = normalizeModelName(element.getAttribute("name")); return directive; } } if (!throwOnMissing) { return null; } throw new Error( `Cannot determine the model name for "${getElementAsTagText( element )}": the element must either have a "data-model" (or "name" attribute living inside a <form data-model="*">).` ); } function elementBelongsToThisComponent(element, component) { if (component.element === element) { return true; } if (!component.element.contains(element)) { return false; } const closestLiveComponent = element.closest('[data-controller~="live"]'); return closestLiveComponent === component.element; } function cloneHTMLElement(element) { const newElement = element.cloneNode(true); if (!(newElement instanceof HTMLElement)) { throw new Error("Could not clone element"); } return newElement; } function htmlToElement(html) { const template = document.createElement("template"); html = html.trim(); template.innerHTML = html; if (template.content.childElementCount > 1) { throw new Error( `Component HTML contains ${template.content.childElementCount} elements, but only 1 root element is allowed.` ); } const child = template.content.firstElementChild; if (!child) { throw new Error("Child not found"); } if (!(child instanceof HTMLElement)) { throw new Error(`Created element is not an HTMLElement: ${html.trim()}`); } return child; } var getMultipleCheckboxValue = (element, currentValues) => { const finalValues = [...currentValues]; const value = inputValue(element); const index = currentValues.indexOf(value); if (element.checked) { if (index === -1) { finalValues.push(value); } return finalValues; } if (index > -1) { finalValues.splice(index, 1); } return finalValues; }; var inputValue = (element) => element.dataset.value ? element.dataset.value : element.value; function isTextualInputElement(el) { return el instanceof HTMLInputElement && ["text", "email", "password", "search", "tel", "url"].includes(el.type); } function isTextareaElement(el) { return el instanceof HTMLTextAreaElement; } function isNumericalInputElement(element) { return element instanceof HTMLInputElement && ["number", "range"].includes(element.type); } // src/HookManager.ts var HookManager_default = class { constructor() { this.hooks = /* @__PURE__ */ new Map(); } register(hookName, callback) { const hooks = this.hooks.get(hookName) || []; hooks.push(callback); this.hooks.set(hookName, hooks); } unregister(hookName, callback) { const hooks = this.hooks.get(hookName) || []; const index = hooks.indexOf(callback); if (index === -1) { return; } hooks.splice(index, 1); this.hooks.set(hookName, hooks); } triggerHook(hookName, ...args) { const hooks = this.hooks.get(hookName) || []; hooks.forEach((callback) => callback(...args)); } }; // ../../../node_modules/.pnpm/idiomorph@0.3.0/node_modules/idiomorph/dist/idiomorph.esm.js var Idiomorph = function() { "use strict"; let EMPTY_SET = /* @__PURE__ */ new Set(); let defaults = { morphStyle: "outerHTML", callbacks: { beforeNodeAdded: noOp, afterNodeAdded: noOp, beforeNodeMorphed: noOp, afterNodeMorphed: noOp, beforeNodeRemoved: noOp, afterNodeRemoved: noOp, beforeAttributeUpdated: noOp }, head: { style: "merge", shouldPreserve: function(elt) { return elt.getAttribute("im-preserve") === "true"; }, shouldReAppend: function(elt) { return elt.getAttribute("im-re-append") === "true"; }, shouldRemove: noOp, afterHeadMorphed: noOp } }; function morph(oldNode, newContent, config = {}) { if (oldNode instanceof Document) { oldNode = oldNode.documentElement; } if (typeof newContent === "string") { newContent = parseContent(newContent); } let normalizedContent = normalizeContent(newContent); let ctx = createMorphContext(oldNode, normalizedContent, config); return morphNormalizedContent(oldNode, normalizedContent, ctx); } function morphNormalizedContent(oldNode, normalizedNewContent, ctx) { if (ctx.head.block) { let oldHead = oldNode.querySelector("head"); let newHead = normalizedNewContent.querySelector("head"); if (oldHead && newHead) { let promises = handleHeadElement(newHead, oldHead, ctx); Promise.all(promises).then(function() { morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, { head: { block: false, ignore: true } })); }); return; } } if (ctx.morphStyle === "innerHTML") { morphChildren(normalizedNewContent, oldNode, ctx); return oldNode.children; } else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) { let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx); let previousSibling = bestMatch?.previousSibling; let nextSibling = bestMatch?.nextSibling; let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx); if (bestMatch) { return insertSiblings(previousSibling, morphedNode, nextSibling); } else { return []; } } else { throw "Do not understand how to morph style " + ctx.morphStyle; } } function ignoreValueOfActiveElement(possibleActiveElement, ctx) { return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement; } function morphOldNodeTo(oldNode, newContent, ctx) { if (ctx.ignoreActive && oldNode === document.activeElement) { } else if (newContent == null) { if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode; oldNode.remove(); ctx.callbacks.afterNodeRemoved(oldNode); return null; } else if (!isSoftMatch(oldNode, newContent)) { if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode; if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode; oldNode.parentElement.replaceChild(newContent, oldNode); ctx.callbacks.afterNodeAdded(newContent); ctx.callbacks.afterNodeRemoved(oldNode); return newContent; } else { if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode; if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) { } else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") { handleHeadElement(newContent, oldNode, ctx); } else { syncNodeFrom(newContent, oldNode, ctx); if (!ignoreValueOfActiveElement(oldNode, ctx)) { morphChildren(newContent, oldNode, ctx); } } ctx.callbacks.afterNodeMorphed(oldNode, newContent); return oldNode; } } function morphChildren(newParent, oldParent, ctx) { let nextNewChild = newParent.firstChild; let insertionPoint = oldParent.firstChild; let newChild; while (nextNewChild) { newChild = nextNewChild; nextNewChild = newChild.nextSibling; if (insertionPoint == null) { if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; oldParent.appendChild(newChild); ctx.callbacks.afterNodeAdded(newChild); removeIdsFromConsideration(ctx, newChild); continue; } if (isIdSetMatch(newChild, insertionPoint, ctx)) { morphOldNodeTo(insertionPoint, newChild, ctx); insertionPoint = insertionPoint.nextSibling; removeIdsFromConsideration(ctx, newChild); continue; } let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx); if (idSetMatch) { insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx); morphOldNodeTo(idSetMatch, newChild, ctx); removeIdsFromConsideration(ctx, newChild); continue; } let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx); if (softMatch) { insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx); morphOldNodeTo(softMatch, newChild, ctx); removeIdsFromConsideration(ctx, newChild); continue; } if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; oldParent.insertBefore(newChild, insertionPoint); ctx.callbacks.afterNodeAdded(newChild); removeIdsFromConsideration(ctx, newChild); } while (insertionPoint !== null) { let tempNode = insertionPoint; insertionPoint = insertionPoint.nextSibling; removeNode(tempNode, ctx); } } function ignoreAttribute(attr, to, updateType, ctx) { if (attr === "value" && ctx.ignoreActiveValue && to === document.activeElement) { return true; } return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false; } function syncNodeFrom(from, to, ctx) { let type = from.nodeType; if (type === 1) { const fromAttributes = from.attributes; const toAttributes = to.attributes; for (const fromAttribute of fromAttributes) { if (ignoreAttribute(fromAttribute.name, to, "update", ctx)) { continue; } if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) { to.setAttribute(fromAttribute.name, fromAttribute.value); } } for (let i = toAttributes.length - 1; 0 <= i; i--) { const toAttribute = toAttributes[i]; if (ignoreAttribute(toAttribute.name, to, "remove", ctx)) { continue; } if (!from.hasAttribute(toAttribute.name)) { to.removeAttribute(toAttribute.name); } } } if (type === 8 || type === 3) { if (to.nodeValue !== from.nodeValue) { to.nodeValue = from.nodeValue; } } if (!ignoreValueOfActiveElement(to, ctx)) { syncInputValue(from, to, ctx); } } function syncBooleanAttribute(from, to, attributeName, ctx) { if (from[attributeName] !== to[attributeName]) { let ignoreUpdate = ignoreAttribute(attributeName, to, "update", ctx); if (!ignoreUpdate) { to[attributeName] = from[attributeName]; } if (from[attributeName]) { if (!ignoreUpdate) { to.setAttribute(attributeName, from[attributeName]); } } else { if (!ignoreAttribute(attributeName, to, "remove", ctx)) { to.removeAttribute(attributeName); } } } } function syncInputValue(from, to, ctx) { if (from instanceof HTMLInputElement && to instanceof HTMLInputElement && from.type !== "file") { let fromValue = from.value; let toValue = to.value; syncBooleanAttribute(from, to, "checked", ctx); syncBooleanAttribute(from, to, "disabled", ctx); if (!from.hasAttribute("value")) { if (!ignoreAttribute("value", to, "remove", ctx)) { to.value = ""; to.removeAttribute("value"); } } else if (fromValue !== toValue) { if (!ignoreAttribute("value", to, "update", ctx)) { to.setAttribute("value", fromValue); to.value = fromValue; } } } else if (from instanceof HTMLOptionElement) { syncBooleanAttribute(from, to, "selected", ctx); } else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) { let fromValue = from.value; let toValue = to.value; if (ignoreAttribute("value", to, "update", ctx)) { return; } if (fromValue !== toValue) { to.value = fromValue; } if (to.firstChild && to.firstChild.nodeValue !== fromValue) { to.firstChild.nodeValue = fromValue; } } } function handleHeadElement(newHeadTag, currentHead, ctx) { let added = []; let removed = []; let preserved = []; let nodesToAppend = []; let headMergeStyle = ctx.head.style; let srcToNewHeadNodes = /* @__PURE__ */ new Map(); for (const newHeadChild of newHeadTag.children) { srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild); } for (const currentHeadElt of currentHead.children) { let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML); let isReAppended = ctx.head.shouldReAppend(currentHeadElt); let isPreserved = ctx.head.shouldPreserve(currentHeadElt); if (inNewContent || isPreserved) { if (isReAppended) { removed.push(currentHeadElt); } else { srcToNewHeadNodes.delete(currentHeadElt.outerHTML); preserved.push(currentHeadElt); } } else { if (headMergeStyle === "append") { if (isReAppended) { removed.push(currentHeadElt); nodesToAppend.push(currentHeadElt); } } else { if (ctx.head.shouldRemove(currentHeadElt) !== false) { removed.push(currentHeadElt); } } } } nodesToAppend.push(...srcToNewHeadNodes.values()); log("to append: ", nodesToAppend); let promises = []; for (const newNode of nodesToAppend) { log("adding: ", newNode); let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild; log(newElt); if (ctx.callbacks.beforeNodeAdded(newElt) !== false) { if (newElt.href || newElt.src) { let resolve = null; let promise = new Promise(function(_resolve) { resolve = _resolve; }); newElt.addEventListener("load", function() { resolve(); }); promises.push(promise); } currentHead.appendChild(newElt); ctx.callbacks.afterNodeAdded(newElt); added.push(newElt); } } for (const removedElement of removed) { if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) { currentHead.removeChild(removedElement); ctx.callbacks.afterNodeRemoved(removedElement); } } ctx.head.afterHeadMorphed(currentHead, { added, kept: preserved, removed }); return promises; } function log() { } function noOp() { } function mergeDefaults(config) { let finalConfig = {}; Object.assign(finalConfig, defaults); Object.assign(finalConfig, config); finalConfig.callbacks = {}; Object.assign(finalConfig.callbacks, defaults.callbacks); Object.assign(finalConfig.callbacks, config.callbacks); finalConfig.head = {}; Object.assign(finalConfig.head, defaults.head); Object.assign(finalConfig.head, config.head); return finalConfig; } function createMorphContext(oldNode, newContent, config) { config = mergeDefaults(config); return { target: oldNode, newContent, config, morphStyle: config.morphStyle, ignoreActive: config.ignoreActive, ignoreActiveValue: config.ignoreActiveValue, idMap: createIdMap(oldNode, newContent), deadIds: /* @__PURE__ */ new Set(), callbacks: config.callbacks, head: config.head }; } function isIdSetMatch(node1, node2, ctx) { if (node1 == null || node2 == null) { return false; } if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) { if (node1.id !== "" && node1.id === node2.id) { return true; } else { return getIdIntersectionCount(ctx, node1, node2) > 0; } } return false; } function isSoftMatch(node1, node2) { if (node1 == null || node2 == null) { return false; } return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName; } function removeNodesBetween(startInclusive, endExclusive, ctx) { while (startInclusive !== endExclusive) { let tempNode = startInclusive; startInclusive = startInclusive.nextSibling; removeNode(tempNode, ctx); } removeIdsFromConsideration(ctx, endExclusive); return endExclusive.nextSibling; } function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) { let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent); let potentialMatch = null; if (newChildPotentialIdCount > 0) { let potentialMatch2 = insertionPoint; let otherMatchCount = 0; while (potentialMatch2 != null) { if (isIdSetMatch(newChild, potentialMatch2, ctx)) { return potentialMatch2; } otherMatchCount += getIdIntersectionCount(ctx, potentialMatch2, newContent); if (otherMatchCount > newChildPotentialIdCount) { return null; } potentialMatch2 = potentialMatch2.nextSibling; } } return potentialMatch; } function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) { let potentialSoftMatch = insertionPoint; let nextSibling = newChild.nextSibling; let siblingSoftMatchCount = 0; while (potentialSoftMatch != null) { if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) { return null; } if (isSoftMatch(newChild, potentialSoftMatch)) { return potentialSoftMatch; } if (isSoftMatch(nextSibling, potentialSoftMatch)) { siblingSoftMatchCount++; nextSibling = nextSibling.nextSibling; if (siblingSoftMatchCount >= 2) { return null; } } potentialSoftMatch = potentialSoftMatch.nextSibling; } return potentialSoftMatch; } function parseContent(newContent) { let parser = new DOMParser(); let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, ""); if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) { let content = parser.parseFromString(newContent, "text/html"); if (contentWithSvgsRemoved.match(/<\/html>/)) { content.generatedByIdiomorph = true; return content; } else { let htmlElement = content.firstChild; if (htmlElement) { htmlElement.generatedByIdiomorph = true; return htmlElement; } else { return null; } } } else { let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html"); let content = responseDoc.body.querySelector("template").content; content.generatedByIdiomorph = true; return content; } } function normalizeContent(newContent) { if (newContent == null) { const dummyParent = document.createElement("div"); return dummyParent; } else if (newContent.generatedByIdiomorph) { return newContent; } else if (newContent instanceof Node) { const dummyParent = document.createElement("div"); dummyParent.append(newContent); return dummyParent; } else { const dummyParent = document.createElement("div"); for (const elt of [...newContent]) { dummyParent.append(elt); } return dummyParent; } } function insertSiblings(previousSibling, morphedNode, nextSibling) { let stack = []; let added = []; while (previousSibling != null) { stack.push(previousSibling); previousSibling = previousSibling.previousSibling; } while (stack.length > 0) { let node = stack.pop(); added.push(node); morphedNode.parentElement.insertBefore(node, morphedNode); } added.push(morphedNode); while (nextSibling != null) { stack.push(nextSibling); added.push(nextSibling); nextSibling = nextSibling.nextSibling; } while (stack.length > 0) { morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling); } return added; } function findBestNodeMatch(newContent, oldNode, ctx) { let currentElement; currentElement = newContent.firstChild; let bestElement = currentElement; let score = 0; while (currentElement) { let newScore = scoreElement(currentElement, oldNode, ctx); if (newScore > score) { bestElement = currentElement; score = newScore; } currentElement = currentElement.nextSibling; } return bestElement; } function scoreElement(node1, node2, ctx) { if (isSoftMatch(node1, node2)) { return 0.5 + getIdIntersectionCount(ctx, node1, node2); } return 0; } function removeNode(tempNode, ctx) { removeIdsFromConsideration(ctx, tempNode); if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return; tempNode.remove(); ctx.callbacks.afterNodeRemoved(tempNode); } function isIdInConsideration(ctx, id) { return !ctx.deadIds.has(id); } function idIsWithinNode(ctx, id, targetNode) { let idSet = ctx.idMap.get(targetNode) || EMPTY_SET; return idSet.has(id); } function removeIdsFromConsideration(ctx, node) { let idSet = ctx.idMap.get(node) || EMPTY_SET; for (const id of idSet) { ctx.deadIds.add(id); } } function getIdIntersectionCount(ctx, node1, node2) { let sourceSet = ctx.idMap.get(node1) || EMPTY_SET; let matchCount = 0; for (const id of sourceSet) { if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) { ++matchCount; } } return matchCount; } function populateIdMapForNode(node, idMap) { let nodeParent = node.parentElement; let idElements = node.querySelectorAll("[id]"); for (const elt of idElements) { let current = elt; while (current !== nodeParent && current != null) { let idSet = idMap.get(current); if (idSet == null) { idSet = /* @__PURE__ */ new Set(); idMap.set(current, idSet); } idSet.add(elt.id); current = current.parentElement; } } } function createIdMap(oldContent, newContent) { let idMap = /* @__PURE__ */ new Map(); populateIdMapForNode(oldContent, idMap); populateIdMapForNode(newContent, idMap); return idMap; } return { morph, defaults }; }(); // src/normalize_attributes_for_comparison.ts function normalizeAttributesForComparison(element) { const isFileInput = element instanceof HTMLInputElement && element.type === "file"; if (!isFileInput) { if ("value" in element) { element.setAttribute("value", element.value); } else if (element.hasAttribute("value")) { element.setAttribute("value", ""); } } Array.from(element.children).forEach((child) => { normalizeAttributesForComparison(child); }); } // src/morphdom.ts var syncAttributes = (fromEl, toEl) => { for (let i = 0; i < fromEl.attributes.length; i++) { const attr = fromEl.attributes[i]; toEl.setAttribute(attr.name, attr.value); } }; function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements, getElementValue, externalMutationTracker) { const originalElementIdsToSwapAfter = []; const originalElementsToPreserve = /* @__PURE__ */ new Map(); const markElementAsNeedingPostMorphSwap = (id, replaceWithClone) => { const oldElement = originalElementsToPreserve.get(id); if (!(oldElement instanceof HTMLElement)) { throw new Error(`Original element with id ${id} not found`); } originalElementIdsToSwapAfter.push(id); if (!replaceWithClone) { return null; } const clonedOldElement = cloneHTMLElement(oldElement); oldElement.replaceWith(clonedOldElement); return clonedOldElement; }; rootToElement.querySelectorAll("[data-live-preserve]").forEach((newElement) => { const id = newElement.id; if (!id) { throw new Error("The data-live-preserve attribute requires an id attribute to be set on the element"); } const oldElement = rootFromElement.querySelector(`#${id}`); if (!(oldElement instanceof HTMLElement)) { throw new Error(`The element with id "${id}" was not found in the original HTML`); } newElement.removeAttribute("data-live-preserve"); originalElementsToPreserve.set(id, oldElement); syncAttributes(newElement, oldElement); }); Idiomorph.morph(rootFromElement, rootToElement, { callbacks: { beforeNodeMorphed: (fromEl, toEl) => { if (!(fromEl instanceof Element) || !(toEl instanceof Element)) { return true; } if (fromEl === rootFromElement) { return true; } if (fromEl.id && originalElementsToPreserve.has(fromEl.id)) { if (fromEl.id === toEl.id) { return false; } const clonedFromEl = markElementAsNeedingPostMorphSwap(fromEl.id, true); if (!clonedFromEl) { throw new Error("missing clone"); } Idiomorph.morph(clonedFromEl, toEl); return false; } if (fromEl instanceof HTMLElement && toEl instanceof HTMLElement) { if (typeof fromEl.__x !== "undefined") { if (!window.Alpine) { throw new Error( "Unable to access Alpine.js though the global window.Alpine variable. Please make sure Alpine.js is loaded before Symfony UX LiveComponent." ); } if (typeof window.Alpine.morph !== "function") { throw new Error( "Unable to access Alpine.js morph function. Please make sure the Alpine.js Morph plugin is installed and loaded, see https://alpinejs.dev/plugins/morph for more information." ); } window.Alpine.morph(fromEl.__x, toEl); } if (externalMutationTracker.wasElementAdded(fromEl)) { fromEl.insertAdjacentElement("afterend", toEl); return false; } if (modifiedFieldElements.includes(fromEl)) { setValueOnElement(toEl, getElementValue(fromEl)); } if (fromEl === document.activeElement && fromEl !== document.body && null !== getModelDirectiveFromElement(fromEl, false)) { setValueOnElement(toEl, getElementValue(fromEl)); } const elementChanges = externalMutationTracker.getChangedElement(fromEl); if (elementChanges) { elementChanges.applyToElement(toEl); } if (fromEl.nodeName.toUpperCase() !== "OPTION" && fromEl.isEqualNode(toEl)) { const normalizedFromEl = cloneHTMLElement(fromEl); normalizeAttributesForComparison(normalizedFromEl); const normalizedToEl = cloneHTMLElement(toEl); normalizeAttributesForComparison(normalizedToEl); if (normalizedFromEl.isEqualNode(normalizedToEl)) { return false; } } } if (fromEl.hasAttribute("data-skip-morph") || fromEl.id && fromEl.id !== toEl.id) { fromEl.innerHTML = toEl.innerHTML; return true; } if (fromEl.parentElement?.hasAttribute("data-skip-morph")) { return false; } return !fromEl.hasAttribute("data-live-ignore"); }, beforeNodeRemoved(node) { if (!(node instanceof HTMLElement)) { return true; } if (node.id && originalElementsToPreserve.has(node.id)) { markElementAsNeedingPostMorphSwap(node.id, false); return true; } if (externalMutationTracker.wasElementAdded(node)) { return false; } return !node.hasAttribute("data-live-ignore"); } } }); originalElementIdsToSwapAfter.forEach((id) => { const newElement = rootFromElement.querySelector(`#${id}`); const originalElement = originalElementsToPreserve.get(id); if (!(newElement instanceof HTMLElement) || !(originalElement instanceof HTMLElement)) { throw new Error("Missing elements."); } newElement.replaceWith(originalElement); }); } // src/Rendering/ChangingItemsTracker.ts var ChangingItemsTracker_default = class { constructor() { // e.g. a Map with key "color" & value { original: 'previousValue', new: 'newValue' }, this.changedItems = /* @__PURE__ */ new Map(); this.removedItems = /* @__PURE__ */ new Map(); } /** * A "null" previousValue means the item was NOT previously present. */ setItem(itemName, newValue, previousValue) { if (this.removedItems.has(itemName)) { const removedRecord = this.removedItems.get(itemName); this.removedItems.delete(itemName); if (removedRecord.original === newValue) { return; } } if (this.changedItems.has(itemName)) { const originalRecord = this.changedItems.get(itemName); if (originalRecord.original === newValue) { this.changedItems.delete(itemName); return; } this.changedItems.set(itemName, { original: originalRecord.original, new: newValue }); return; } this.changedItems.set(itemName, { original: previousValue, new: newValue }); } removeItem(itemName, currentValue) { let trueOriginalValue = currentValue; if (this.changedItems.has(itemName)) { const originalRecord = this.changedItems.get(itemName); trueOriginalValue = originalRecord.original; this.changedItems.delete(itemName); if (trueOriginalValue === null) { return; } } if (!this.removedItems.has(itemName)) { this.removedItems.set(itemName, { original: trueOriginalValue }); } } getChangedItems() { return Array.from(this.changedItems, ([name, { new: value }]) => ({ name, value })); } getRemovedItems() { return Array.from(this.removedItems.keys()); } isEmpty() { return this.changedItems.size === 0 && this.removedItems.size === 0; } }; // src/Rendering/ElementChanges.ts var ElementChanges = class { constructor() { this.addedClasses = /* @__PURE__ */ new Set(); this.removedClasses = /* @__PURE__ */ new Set(); this.styleChanges = new ChangingItemsTracker_default(); this.attributeChanges = new ChangingItemsTracker_default(); } addClass(className) { if (!this.removedClasses.delete(className)) { this.addedClasses.add(className); } } removeClass(className) { if (!this.addedClasses.delete(className)) { this.removedClasses.add(className); } } addStyle(styleName, newValue, originalValue) { this.styleChanges.setItem(styleName, newValue, originalValue); } removeStyle(styleName, originalValue) { this.styleChanges.removeItem(styleName, originalValue); } addAttribute(attributeName, newValue, originalValue) { this.attributeChanges.setItem(attributeName, newValue, originalValue); } removeAttribute(attributeName, originalValue) { this.attributeChanges.removeItem(attributeName, originalValue); } getAddedClasses() { return [...this.addedClasses]; } getRemovedClasses() { return [...this.removedClasses]; } getChangedStyles() { return this.styleChanges.getChangedItems(); } getRemovedStyles() { return this.styleChanges.getRemovedItems(); } getChangedAttributes() { return this.attributeChanges.getChangedItems(); } getRemovedAttributes() { return this.attributeChanges.getRemovedItems(); } applyToElement(element) { element.classList.add(...this.addedClasses); element.classList.remove(...this.removedClasses); this.styleChanges.getChangedItems().forEach((change) => { element.style.setProperty(change.name, change.value); return; }); this.styleChanges.getRemovedItems().forEach((styleName) => { element.style.removeProperty(styleName); }); this.attributeChanges.getChangedItems().forEach((change) => { element.setAttribute(change.name, change.value); }); this.attributeChanges.getRemovedItems().forEach((attributeName) => { element.removeAttribute(attributeName); }); } isEmpty() { return this.addedClasses.size === 0 && this.removedClasses.size === 0 && this.styleChanges.isEmpty() && this.attributeChanges.isEmpty(); } }; // src/Rendering/ExternalMutationTracker.ts var ExternalMutationTracker_default = class { constructor(element, shouldTrackChangeCallback) { this.changedElements = /* @__PURE__ */ new WeakMap(); /** For testing */ this.changedElementsCount = 0; this.addedElements = []; this.removedElements = []; this.isStarted = false; this.element = element; this.shouldTrackChangeCallback = shouldTrackChangeCallback; this.mutationObserver = new MutationObserver(this.onMutations.bind(this)); } start() { if (this.isStarted) { return; } this.mutationObserver.observe(this.element, { childList: true, subtree: true, attributes: true, attributeOldValue: true }); this.isStarted = true; } stop() { if (this.isStarted) { this.mutationObserver.disconnect(); this.isStarted = false; } } getChangedElement(element) { return this.changedElements.has(element) ? this.changedElements.get(element) : null; } getAddedElements() { return this.addedElements; } wasElementAdded(element) { return this.addedElements.includes(element); } /** * Forces any pending mutations to be handled immediately, then clears the queue. */ handlePendingChanges() { this.onMutations(this.mutationObserver.takeRecords()); } onMutations(mutations) { const handledAttributeMutations = /* @__PURE__ */ new WeakMap(); for (const mutation of mutations) { const element = mutation.target; if (!this.shouldTrackChangeCallback(element)) { continue; } if (this.isElementAddedByTranslation(element)) { continue; } let isChangeInAddedElement = false; for (const addedElement of this.addedElements) { if (addedElement.contains(element)) { isChangeInAddedElement = true; break; } } if (isChangeInAddedElement) { continue; } switch (mutation.type) { case "childList": this.handleChildListMutation(mutation); break; case "attributes": if (!handledAttributeMutations.has(element)) { handledAttributeMutations.set(element, []); } if (!handledAttributeMutations.get(element).includes(mutation.attributeName)) { this.handleAttributeMutation(mutation); handledAttributeMutations.set(element, [ ...handledAttributeMutations.get(element), mutation.attributeName ]); } break; } } } handleChildListMutation(mutation) { mutation.addedNodes.forEach((node) => { if (!(node instanceof Element)) { return; } if (this.removedElements.includes(node)) { this.removedElements.splice(this.removedElements.indexOf(node), 1); return; } if (this.isElementAddedByTranslation(node)) { return; } this.addedElements.push(node); }); mutation.removedNodes.forEach((node) => { if (!(node instanceof Element)) { return; } if (this.addedElements.includes(node)) { this.addedElements.splice(this.addedElements.indexOf(node), 1); return; } this.removedElements.push(node); }); } handleAttributeMutation(mutation) { const element = mutation.target; if (!this.changedElements.has(element)) { this.changedElements.set(element, new ElementChanges()); this.changedElementsCount++; } const changedElement = this.changedElements.get(element); switch (mutation.attributeName) { case "class": this.handleClassAttributeMutation(mutation, changedElement); break; case "style": this.handleStyleAttributeMutation(mutation, changedElement); break; default: this.handleGenericAttributeMutation(mutation, changedElement); } if (changedElement.isEmpty()) { this.changedElements.delete(element); this.changedElementsCount--; } } handleClassAttributeMutation(mutation, elementChanges) { const element = mutation.target; const previousValue = mutati