@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
JavaScript
// 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