diff --git a/biome.json b/biome.json index 36d42169ba6..a174663a75f 100644 --- a/biome.json +++ b/biome.json @@ -1,21 +1,19 @@ { "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "files": { - "include": [ - "*.json", - "*.md", - "bin/*.js", - "test/*.js", - "src/*/*.json", - "src/*/*/md", - "src/*/assets/src/**", - "src/*/assets/test/**", - "src/*/src/Bridge/*.json", - "src/*/src/Bridge/*.md", - "src/*/src/Bridge/*/assets/src/**", - "src/*/src/Bridge/*/assets/test/**" - ], - "ignore": ["**/composer.json", "**/vendor", "**/package.json", "**/node_modules", "**/var"] + "includes": [ + "**/*.json", + "**/*.md", + "**/bin/**/*.js", + "**/src/**/assets/src/**", + "**/src/**/assets/test/**", + "!**/composer.json", + "!**/vendor", + "!**/package.json", + "!**/node_modules", + "!**/var", + "!**/dist" + ] }, "linter": { "rules": { @@ -28,7 +26,16 @@ "noForEach": "off" }, "style": { - "noParameterAssign": "off" + "noParameterAssign": "off", + "useAsConstAssertion": "error", + "useDefaultParameterLast": "error", + "useEnumInitializers": "error", + "useSelfClosingElements": "error", + "useSingleVarDeclarator": "error", + "noUnusedTemplateLiteral": "error", + "useNumberNamespace": "error", + "noInferrableTypes": "error", + "noUselessElse": "error" }, "performance": { "noDelete": "off" @@ -49,7 +56,7 @@ }, "overrides": [ { - "include": ["*.svelte"], + "includes": ["**/*.svelte"], "linter": { "rules": { "style": { @@ -57,6 +64,12 @@ } } } + }, + { + "includes": ["src/Map/**/*map_controller.ts"], + "formatter": { + "lineWidth": 160 + } } ] } diff --git a/package.json b/package.json index a01058a2c74..f08a413159b 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@babel/preset-env": "^7.25.3", "@babel/preset-react": "^7.24.7", "@babel/preset-typescript": "^7.24.7", - "@biomejs/biome": "^1.8.3", + "@biomejs/biome": "^2.0.4", "@rollup/plugin-commonjs": "^28.0.0", "@rollup/plugin-node-resolve": "^15.3.0", "@rollup/plugin-typescript": "^11.1.6", diff --git a/src/Cropperjs/assets/src/controller.ts b/src/Cropperjs/assets/src/controller.ts index 10f6deb6822..70087b0dd8e 100644 --- a/src/Cropperjs/assets/src/controller.ts +++ b/src/Cropperjs/assets/src/controller.ts @@ -9,6 +9,7 @@ import { Controller } from '@hotwired/stimulus'; import Cropper from 'cropperjs'; + import CropEvent = Cropper.CropEvent; export default class CropperController extends Controller { diff --git a/src/Dropzone/assets/src/style.css b/src/Dropzone/assets/src/style.css index 4cd21ac6b8e..d942b59cb60 100644 --- a/src/Dropzone/assets/src/style.css +++ b/src/Dropzone/assets/src/style.css @@ -60,7 +60,7 @@ } .dropzone-preview-button::before { - content: '×'; + content: "×"; padding: 3px 7px; cursor: pointer; } diff --git a/src/LiveComponent/assets/dist/Component/index.d.ts b/src/LiveComponent/assets/dist/Component/index.d.ts index 84b3336c289..65a18a486db 100644 --- a/src/LiveComponent/assets/dist/Component/index.d.ts +++ b/src/LiveComponent/assets/dist/Component/index.d.ts @@ -2,8 +2,8 @@ import type { BackendInterface } from '../Backend/Backend'; import type BackendRequest from '../Backend/BackendRequest'; import BackendResponse from '../Backend/BackendResponse'; import type { ElementDriver } from './ElementDriver'; -import ValueStore from './ValueStore'; import type { PluginInterface } from './plugins/PluginInterface'; +import ValueStore from './ValueStore'; type MaybePromise = T | Promise; export type ComponentHooks = { connect: (component: Component) => MaybePromise; diff --git a/src/LiveComponent/assets/dist/live_controller.js b/src/LiveComponent/assets/dist/live_controller.js index 8591ab0f0ab..4902665e1de 100644 --- a/src/LiveComponent/assets/dist/live_controller.js +++ b/src/LiveComponent/assets/dist/live_controller.js @@ -195,680 +195,363 @@ const findParent = (currentComponent) => { return null; }; -class HookManager { - constructor() { - this.hooks = new Map(); - } - register(hookName, callback) { - const hooks = this.hooks.get(hookName) || []; - hooks.push(callback); - this.hooks.set(hookName, hooks); +function parseDirectives(content) { + const directives = []; + if (!content) { + return directives; } - unregister(hookName, callback) { - const hooks = this.hooks.get(hookName) || []; - const index = hooks.indexOf(callback); - if (index === -1) { - return; + 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; } - hooks.splice(index, 1); - this.hooks.set(hookName, hooks); } - triggerHook(hookName, ...args) { - const hooks = this.hooks.get(hookName) || []; - hooks.forEach((callback) => callback(...args)); + 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; } -class ChangingItemsTracker { - constructor() { - this.changedItems = new Map(); - this.removedItems = new Map(); - } - setItem(itemName, newValue, previousValue) { - if (this.removedItems.has(itemName)) { - const removedRecord = this.removedItems.get(itemName); - this.removedItems.delete(itemName); - if (removedRecord.original === newValue) { - return; +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('.')); +} + +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 (this.changedItems.has(itemName)) { - const originalRecord = this.changedItems.get(itemName); - if (originalRecord.original === newValue) { - this.changedItems.delete(itemName); - return; + if (element.hasAttribute('value')) { + return element.checked ? element.getAttribute('value') : null; } - this.changedItems.set(itemName, { original: originalRecord.original, new: newValue }); - return; + return element.checked; } - this.changedItems.set(itemName, { original: previousValue, new: newValue }); + return inputValue(element); } - 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 }); + if (element instanceof HTMLSelectElement) { + if (element.multiple) { + return Array.from(element.selectedOptions).map((el) => el.value); } + return element.value; } - getChangedItems() { - return Array.from(this.changedItems, ([name, { new: value }]) => ({ name, value })); + if (element.dataset.value) { + return element.dataset.value; } - getRemovedItems() { - return Array.from(this.removedItems.keys()); + if ('value' in element) { + return element.value; } - isEmpty() { - return this.changedItems.size === 0 && this.removedItems.size === 0; + if (element.hasAttribute('value')) { + return element.getAttribute('value'); } + return null; } - -class ElementChanges { - constructor() { - this.addedClasses = new Set(); - this.removedClasses = new Set(); - this.styleChanges = new ChangingItemsTracker(); - this.attributeChanges = new ChangingItemsTracker(); - } - addClass(className) { - if (!this.removedClasses.delete(className)) { - this.addedClasses.add(className); +function setValueOnElement(element, value) { + if (element instanceof HTMLInputElement) { + if (element.type === 'file') { + return; } - } - removeClass(className) { - if (!this.addedClasses.delete(className)) { - this.removedClasses.add(className); + 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; } } - addStyle(styleName, newValue, originalValue) { - this.styleChanges.setItem(styleName, newValue, originalValue); + if (element instanceof HTMLSelectElement) { + const arrayWrappedValue = [].concat(value).map((value) => { + return `${value}`; + }); + Array.from(element.options).forEach((option) => { + option.selected = arrayWrappedValue.includes(option.value); + }); + return; } - removeStyle(styleName, originalValue) { - this.styleChanges.removeItem(styleName, originalValue); + value = value === undefined ? '' : value; + element.value = value; +} +function getAllModelDirectiveFromElements(element) { + if (!element.dataset.model) { + return []; } - addAttribute(attributeName, newValue, originalValue) { - this.attributeChanges.setItem(attributeName, newValue, originalValue); - } - removeAttribute(attributeName, originalValue) { - this.attributeChanges.removeItem(attributeName, originalValue); + 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]; } - getAddedClasses() { - return [...this.addedClasses]; + 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; + } } - getRemovedClasses() { - return [...this.removedClasses]; + if (!throwOnMissing) { + return null; } - getChangedStyles() { - return this.styleChanges.getChangedItems(); + 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
).`); +} +function elementBelongsToThisComponent(element, component) { + if (component.element === element) { + return true; } - getRemovedStyles() { - return this.styleChanges.getRemovedItems(); + if (!component.element.contains(element)) { + return false; } - getChangedAttributes() { - return this.attributeChanges.getChangedItems(); + 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'); } - getRemovedAttributes() { - return this.attributeChanges.getRemovedItems(); + 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.`); } - 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); - }); + const child = template.content.firstElementChild; + if (!child) { + throw new Error('Child not found'); } - isEmpty() { - return (this.addedClasses.size === 0 && - this.removedClasses.size === 0 && - this.styleChanges.isEmpty() && - this.attributeChanges.isEmpty()); + if (!(child instanceof HTMLElement)) { + throw new Error(`Created element is not an HTMLElement: ${html.trim()}`); } + return child; } - -class ExternalMutationTracker { - constructor(element, shouldTrackChangeCallback) { - this.changedElements = new WeakMap(); - 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; +const getMultipleCheckboxValue = (element, currentValues) => { + const finalValues = [...currentValues]; + const value = inputValue(element); + const index = currentValues.indexOf(value); + if (element.checked) { + if (index === -1) { + finalValues.push(value); } - this.mutationObserver.observe(this.element, { - childList: true, - subtree: true, - attributes: true, - attributeOldValue: true, - }); - this.isStarted = true; + return finalValues; } - stop() { - if (this.isStarted) { - this.mutationObserver.disconnect(); - this.isStarted = false; - } + if (index > -1) { + finalValues.splice(index, 1); } - getChangedElement(element) { - return this.changedElements.has(element) ? this.changedElements.get(element) : null; + return finalValues; +}; +const inputValue = (element) => element.dataset.value ? element.dataset.value : element.value; + +class HookManager { + constructor() { + this.hooks = new Map(); } - getAddedElements() { - return this.addedElements; + register(hookName, callback) { + const hooks = this.hooks.get(hookName) || []; + hooks.push(callback); + this.hooks.set(hookName, hooks); } - wasElementAdded(element) { - return this.addedElements.includes(element); + 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); } - handlePendingChanges() { - this.onMutations(this.mutationObserver.takeRecords()); + triggerHook(hookName, ...args) { + const hooks = this.hooks.get(hookName) || []; + hooks.forEach((callback) => callback(...args)); } - onMutations(mutations) { - const handledAttributeMutations = 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; - } +} + +// base IIFE to define idiomorph +var Idiomorph = (function () { + + //============================================================================= + // AND NOW IT BEGINS... + //============================================================================= + let EMPTY_SET = new Set(); + + // default configuration values, updatable by users now + 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, } - if (isChangeInAddedElement) { - continue; + }; + + //============================================================================= + // Core Morphing Algorithm - morph, morphNormalizedContent, morphOldNodeTo, morphChildren + //============================================================================= + function morph(oldNode, newContent, config = {}) { + + if (oldNode instanceof Document) { + oldNode = oldNode.documentElement; } - 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; + + if (typeof newContent === 'string') { + newContent = parseContent(newContent); } - } - } - 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 = mutation.oldValue || ''; - const previousValues = previousValue.match(/(\S+)/gu) || []; - const newValues = [].slice.call(element.classList); - const addedValues = newValues.filter((value) => !previousValues.includes(value)); - const removedValues = previousValues.filter((value) => !newValues.includes(value)); - addedValues.forEach((value) => { - elementChanges.addClass(value); - }); - removedValues.forEach((value) => { - elementChanges.removeClass(value); - }); - } - handleStyleAttributeMutation(mutation, elementChanges) { - const element = mutation.target; - const previousValue = mutation.oldValue || ''; - const previousStyles = this.extractStyles(previousValue); - const newValue = element.getAttribute('style') || ''; - const newStyles = this.extractStyles(newValue); - const addedOrChangedStyles = Object.keys(newStyles).filter((key) => previousStyles[key] === undefined || previousStyles[key] !== newStyles[key]); - const removedStyles = Object.keys(previousStyles).filter((key) => !newStyles[key]); - addedOrChangedStyles.forEach((style) => { - elementChanges.addStyle(style, newStyles[style], previousStyles[style] === undefined ? null : previousStyles[style]); - }); - removedStyles.forEach((style) => { - elementChanges.removeStyle(style, previousStyles[style]); - }); - } - handleGenericAttributeMutation(mutation, elementChanges) { - const attributeName = mutation.attributeName; - const element = mutation.target; - let oldValue = mutation.oldValue; - let newValue = element.getAttribute(attributeName); - if (oldValue === attributeName) { - oldValue = ''; - } - if (newValue === attributeName) { - newValue = ''; - } - if (!element.hasAttribute(attributeName)) { - if (oldValue === null) { - return; - } - elementChanges.removeAttribute(attributeName, mutation.oldValue); - return; - } - if (newValue === oldValue) { - return; - } - elementChanges.addAttribute(attributeName, element.getAttribute(attributeName), mutation.oldValue); - } - extractStyles(styles) { - const styleObject = {}; - styles.split(';').forEach((style) => { - const parts = style.split(':'); - if (parts.length === 1) { - return; - } - const property = parts[0].trim(); - styleObject[property] = parts.slice(1).join(':').trim(); - }); - return styleObject; - } - isElementAddedByTranslation(element) { - return element.tagName === 'FONT' && element.getAttribute('style') === 'vertical-align: inherit;'; - } -} - -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; -} - -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('.')); -} - -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((value) => { - return `${value}`; - }); - Array.from(element.options).forEach((option) => { - option.selected = arrayWrappedValue.includes(option.value); - }); - return; - } - value = value === undefined ? '' : 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 ).`); -} -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; -} -const 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; -}; -const inputValue = (element) => element.dataset.value ? element.dataset.value : element.value; - -// base IIFE to define idiomorph -var Idiomorph = (function () { - - //============================================================================= - // AND NOW IT BEGINS... - //============================================================================= - let EMPTY_SET = new Set(); - - // default configuration values, updatable by users now - 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, - } - }; - - //============================================================================= - // Core Morphing Algorithm - morph, morphNormalizedContent, morphOldNodeTo, morphChildren - //============================================================================= - 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); + + let normalizedContent = normalizeContent(newContent); + + let ctx = createMorphContext(oldNode, normalizedContent, config); + + return morphNormalizedContent(oldNode, normalizedContent, ctx); } function morphNormalizedContent(oldNode, normalizedNewContent, ctx) { @@ -1442,352 +1125,669 @@ var Idiomorph = (function () { return null; } } - - // advanced to the next old content child - potentialSoftMatch = potentialSoftMatch.nextSibling; + + // advanced to the next old content child + potentialSoftMatch = potentialSoftMatch.nextSibling; + } + + return potentialSoftMatch; + } + + function parseContent(newContent) { + let parser = new DOMParser(); + + // remove svgs to avoid false-positive matches on head, etc. + let contentWithSvgsRemoved = newContent.replace(/]*>|>)([\s\S]*?)<\/svg>/gim, ''); + + // if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping + if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) { + let content = parser.parseFromString(newContent, "text/html"); + // if it is a full HTML document, return the document itself as the parent container + if (contentWithSvgsRemoved.match(/<\/html>/)) { + content.generatedByIdiomorph = true; + return content; + } else { + // otherwise return the html element as the parent container + let htmlElement = content.firstChild; + if (htmlElement) { + htmlElement.generatedByIdiomorph = true; + return htmlElement; + } else { + return null; + } + } + } else { + // if it is partial HTML, wrap it in a template tag to provide a parent element and also to help + // deal with touchy tags like tr, tbody, etc. + let responseDoc = parser.parseFromString("", "text/html"); + let content = responseDoc.body.querySelector('template').content; + content.generatedByIdiomorph = true; + return content + } + } + + function normalizeContent(newContent) { + if (newContent == null) { + // noinspection UnnecessaryLocalVariableJS + const dummyParent = document.createElement('div'); + return dummyParent; + } else if (newContent.generatedByIdiomorph) { + // the template tag created by idiomorph parsing can serve as a dummy parent + return newContent; + } else if (newContent instanceof Node) { + // a single node is added as a child to a dummy parent + const dummyParent = document.createElement('div'); + dummyParent.append(newContent); + return dummyParent; + } else { + // all nodes in the array or HTMLElement collection are consolidated under + // a single dummy parent element + 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); // push added preceding siblings on in order and insert + morphedNode.parentElement.insertBefore(node, morphedNode); + } + added.push(morphedNode); + while (nextSibling != null) { + stack.push(nextSibling); + added.push(nextSibling); // here we are going in order, so push on as we scan, rather than add + 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 .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); + } + + //============================================================================= + // ID Set Functions + //============================================================================= + + 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) { + // a potential match is an id in the source and potentialIdsSet, but + // that has not already been merged into the DOM + if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) { + ++matchCount; + } + } + return matchCount; + } + + /** + * A bottom up algorithm that finds all elements with ids inside of the node + * argument and populates id sets for those nodes and all their parents, generating + * a set of ids contained within all nodes for the entire hierarchy in the DOM + * + * @param node {Element} + * @param {Map>} idMap + */ + function populateIdMapForNode(node, idMap) { + let nodeParent = node.parentElement; + // find all elements with an id property + let idElements = node.querySelectorAll('[id]'); + for (const elt of idElements) { + let current = elt; + // walk up the parent hierarchy of that element, adding the id + // of element to the parent's id set + while (current !== nodeParent && current != null) { + let idSet = idMap.get(current); + // if the id set doesn't exist, create it and insert it in the map + if (idSet == null) { + idSet = new Set(); + idMap.set(current, idSet); + } + idSet.add(elt.id); + current = current.parentElement; + } } + } - return potentialSoftMatch; + /** + * This function computes a map of nodes to all ids contained within that node (inclusive of the + * node). This map can be used to ask if two nodes have intersecting sets of ids, which allows + * for a looser definition of "matching" than tradition id matching, and allows child nodes + * to contribute to a parent nodes matching. + * + * @param {Element} oldContent the old content that will be morphed + * @param {Element} newContent the new content to morph to + * @returns {Map>} a map of nodes to id sets for the + */ + function createIdMap(oldContent, newContent) { + let idMap = new Map(); + populateIdMapForNode(oldContent, idMap); + populateIdMapForNode(newContent, idMap); + return idMap; } - function parseContent(newContent) { - let parser = new DOMParser(); + //============================================================================= + // This is what ends up becoming the Idiomorph global object + //============================================================================= + return { + morph, + defaults + } + })(); - // remove svgs to avoid false-positive matches on head, etc. - let contentWithSvgsRemoved = newContent.replace(/]*>|>)([\s\S]*?)<\/svg>/gim, ''); +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); + }); +} - // if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping - if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) { - let content = parser.parseFromString(newContent, "text/html"); - // if it is a full HTML document, return the document itself as the parent container - if (contentWithSvgsRemoved.match(/<\/html>/)) { - content.generatedByIdiomorph = true; - return content; - } else { - // otherwise return the html element as the parent container - let htmlElement = content.firstChild; - if (htmlElement) { - htmlElement.generatedByIdiomorph = true; - return htmlElement; - } else { - return null; +const 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 = 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; } - } else { - // if it is partial HTML, wrap it in a template tag to provide a parent element and also to help - // deal with touchy tags like tr, tbody, etc. - let responseDoc = parser.parseFromString("", "text/html"); - let content = responseDoc.body.querySelector('template').content; - content.generatedByIdiomorph = true; - return content - } - } - - function normalizeContent(newContent) { - if (newContent == null) { - // noinspection UnnecessaryLocalVariableJS - const dummyParent = document.createElement('div'); - return dummyParent; - } else if (newContent.generatedByIdiomorph) { - // the template tag created by idiomorph parsing can serve as a dummy parent - return newContent; - } else if (newContent instanceof Node) { - // a single node is added as a child to a dummy parent - const dummyParent = document.createElement('div'); - dummyParent.append(newContent); - return dummyParent; - } else { - // all nodes in the array or HTMLElement collection are consolidated under - // a single dummy parent element - const dummyParent = document.createElement('div'); - for (const elt of [...newContent]) { - dummyParent.append(elt); + 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; + } + } } - return dummyParent; - } + 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); + }); +} - 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); // push added preceding siblings on in order and insert - morphedNode.parentElement.insertBefore(node, morphedNode); - } - added.push(morphedNode); - while (nextSibling != null) { - stack.push(nextSibling); - added.push(nextSibling); // here we are going in order, so push on as we scan, rather than add - nextSibling = nextSibling.nextSibling; +class ChangingItemsTracker { + constructor() { + this.changedItems = new Map(); + this.removedItems = new Map(); + } + setItem(itemName, newValue, previousValue) { + if (this.removedItems.has(itemName)) { + const removedRecord = this.removedItems.get(itemName); + this.removedItems.delete(itemName); + if (removedRecord.original === newValue) { + return; } - while (stack.length > 0) { - morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling); + } + if (this.changedItems.has(itemName)) { + const originalRecord = this.changedItems.get(itemName); + if (originalRecord.original === newValue) { + this.changedItems.delete(itemName); + return; } - return added; + this.changedItems.set(itemName, { original: originalRecord.original, new: newValue }); + return; } - - 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; + 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; } - return bestElement; } - - function scoreElement(node1, node2, ctx) { - if (isSoftMatch(node1, node2)) { - return .5 + getIdIntersectionCount(ctx, node1, node2); - } - return 0; + 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; + } +} - function removeNode(tempNode, ctx) { - removeIdsFromConsideration(ctx, tempNode); - if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return; - - tempNode.remove(); - ctx.callbacks.afterNodeRemoved(tempNode); +class ElementChanges { + constructor() { + this.addedClasses = new Set(); + this.removedClasses = new Set(); + this.styleChanges = new ChangingItemsTracker(); + this.attributeChanges = new ChangingItemsTracker(); + } + addClass(className) { + if (!this.removedClasses.delete(className)) { + this.addedClasses.add(className); } - - //============================================================================= - // ID Set Functions - //============================================================================= - - function isIdInConsideration(ctx, id) { - return !ctx.deadIds.has(id); + } + 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()); + } +} - function idIsWithinNode(ctx, id, targetNode) { - let idSet = ctx.idMap.get(targetNode) || EMPTY_SET; - return idSet.has(id); +class ExternalMutationTracker { + constructor(element, shouldTrackChangeCallback) { + this.changedElements = new WeakMap(); + 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; } - - function removeIdsFromConsideration(ctx, node) { - let idSet = ctx.idMap.get(node) || EMPTY_SET; - for (const id of idSet) { - ctx.deadIds.add(id); - } + 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; } - - function getIdIntersectionCount(ctx, node1, node2) { - let sourceSet = ctx.idMap.get(node1) || EMPTY_SET; - let matchCount = 0; - for (const id of sourceSet) { - // a potential match is an id in the source and potentialIdsSet, but - // that has not already been merged into the DOM - if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) { - ++matchCount; - } + } + getChangedElement(element) { + return this.changedElements.has(element) ? this.changedElements.get(element) : null; + } + getAddedElements() { + return this.addedElements; + } + wasElementAdded(element) { + return this.addedElements.includes(element); + } + handlePendingChanges() { + this.onMutations(this.mutationObserver.takeRecords()); + } + onMutations(mutations) { + const handledAttributeMutations = new WeakMap(); + for (const mutation of mutations) { + const element = mutation.target; + if (!this.shouldTrackChangeCallback(element)) { + continue; } - return matchCount; - } - - /** - * A bottom up algorithm that finds all elements with ids inside of the node - * argument and populates id sets for those nodes and all their parents, generating - * a set of ids contained within all nodes for the entire hierarchy in the DOM - * - * @param node {Element} - * @param {Map>} idMap - */ - function populateIdMapForNode(node, idMap) { - let nodeParent = node.parentElement; - // find all elements with an id property - let idElements = node.querySelectorAll('[id]'); - for (const elt of idElements) { - let current = elt; - // walk up the parent hierarchy of that element, adding the id - // of element to the parent's id set - while (current !== nodeParent && current != null) { - let idSet = idMap.get(current); - // if the id set doesn't exist, create it and insert it in the map - if (idSet == null) { - idSet = new Set(); - idMap.set(current, idSet); - } - idSet.add(elt.id); - current = current.parentElement; + 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; + } } - - /** - * This function computes a map of nodes to all ids contained within that node (inclusive of the - * node). This map can be used to ask if two nodes have intersecting sets of ids, which allows - * for a looser definition of "matching" than tradition id matching, and allows child nodes - * to contribute to a parent nodes matching. - * - * @param {Element} oldContent the old content that will be morphed - * @param {Element} newContent the new content to morph to - * @returns {Map>} a map of nodes to id sets for the - */ - function createIdMap(oldContent, newContent) { - let idMap = new Map(); - populateIdMapForNode(oldContent, idMap); - populateIdMapForNode(newContent, idMap); - return idMap; - } - - //============================================================================= - // This is what ends up becoming the Idiomorph global object - //============================================================================= - return { - morph, - defaults + } + 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++; } - })(); - -function normalizeAttributesForComparison(element) { - const isFileInput = element instanceof HTMLInputElement && element.type === 'file'; - if (!isFileInput) { - if ('value' in element) { - element.setAttribute('value', element.value); + 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); } - else if (element.hasAttribute('value')) { - element.setAttribute('value', ''); + if (changedElement.isEmpty()) { + this.changedElements.delete(element); + this.changedElementsCount--; } } - Array.from(element.children).forEach((child) => { - normalizeAttributesForComparison(child); - }); -} - -const syncAttributes = (fromEl, toEl) => { - for (let i = 0; i < fromEl.attributes.length; i++) { - const attr = fromEl.attributes[i]; - toEl.setAttribute(attr.name, attr.value); + handleClassAttributeMutation(mutation, elementChanges) { + const element = mutation.target; + const previousValue = mutation.oldValue || ''; + const previousValues = previousValue.match(/(\S+)/gu) || []; + const newValues = [].slice.call(element.classList); + const addedValues = newValues.filter((value) => !previousValues.includes(value)); + const removedValues = previousValues.filter((value) => !newValues.includes(value)); + addedValues.forEach((value) => { + elementChanges.addClass(value); + }); + removedValues.forEach((value) => { + elementChanges.removeClass(value); + }); } -}; -function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements, getElementValue, externalMutationTracker) { - const originalElementIdsToSwapAfter = []; - const originalElementsToPreserve = 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; + handleStyleAttributeMutation(mutation, elementChanges) { + const element = mutation.target; + const previousValue = mutation.oldValue || ''; + const previousStyles = this.extractStyles(previousValue); + const newValue = element.getAttribute('style') || ''; + const newStyles = this.extractStyles(newValue); + const addedOrChangedStyles = Object.keys(newStyles).filter((key) => previousStyles[key] === undefined || previousStyles[key] !== newStyles[key]); + const removedStyles = Object.keys(previousStyles).filter((key) => !newStyles[key]); + addedOrChangedStyles.forEach((style) => { + elementChanges.addStyle(style, newStyles[style], previousStyles[style] === undefined ? null : previousStyles[style]); + }); + removedStyles.forEach((style) => { + elementChanges.removeStyle(style, previousStyles[style]); + }); + } + handleGenericAttributeMutation(mutation, elementChanges) { + const attributeName = mutation.attributeName; + const element = mutation.target; + let oldValue = mutation.oldValue; + let newValue = element.getAttribute(attributeName); + if (oldValue === attributeName) { + oldValue = ''; } - 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'); + if (newValue === attributeName) { + newValue = ''; } - const oldElement = rootFromElement.querySelector(`#${id}`); - if (!(oldElement instanceof HTMLElement)) { - throw new Error(`The element with id "${id}" was not found in the original HTML`); + if (!element.hasAttribute(attributeName)) { + if (oldValue === null) { + return; + } + elementChanges.removeAttribute(attributeName, mutation.oldValue); + return; } - 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.'); + if (newValue === oldValue) { + return; } - newElement.replaceWith(originalElement); - }); + elementChanges.addAttribute(attributeName, element.getAttribute(attributeName), mutation.oldValue); + } + extractStyles(styles) { + const styleObject = {}; + styles.split(';').forEach((style) => { + const parts = style.split(':'); + if (parts.length === 1) { + return; + } + const property = parts[0].trim(); + styleObject[property] = parts.slice(1).join(':').trim(); + }); + return styleObject; + } + isElementAddedByTranslation(element) { + return element.tagName === 'FONT' && element.getAttribute('style') === 'vertical-align: inherit;'; + } } class UnsyncedInputsTracker { diff --git a/src/LiveComponent/assets/src/Component/index.ts b/src/LiveComponent/assets/src/Component/index.ts index 7db1f564a7b..e42739f6149 100644 --- a/src/LiveComponent/assets/src/Component/index.ts +++ b/src/LiveComponent/assets/src/Component/index.ts @@ -2,15 +2,15 @@ import type { BackendAction, BackendInterface } from '../Backend/Backend'; import type BackendRequest from '../Backend/BackendRequest'; import BackendResponse from '../Backend/BackendResponse'; import { findComponents, registerComponent, unregisterComponent } from '../ComponentRegistry'; -import HookManager from '../HookManager'; -import ExternalMutationTracker from '../Rendering/ExternalMutationTracker'; import { elementBelongsToThisComponent, getValueFromElement, htmlToElement } from '../dom_utils'; +import HookManager from '../HookManager'; import { executeMorphdom } from '../morphdom'; +import ExternalMutationTracker from '../Rendering/ExternalMutationTracker'; import { normalizeModelName } from '../string_utils'; import type { ElementDriver } from './ElementDriver'; +import type { PluginInterface } from './plugins/PluginInterface'; import UnsyncedInputsTracker from './UnsyncedInputsTracker'; import ValueStore from './ValueStore'; -import type { PluginInterface } from './plugins/PluginInterface'; declare const Turbo: any; diff --git a/src/LiveComponent/assets/src/Component/plugins/ValidatedFieldsPlugin.ts b/src/LiveComponent/assets/src/Component/plugins/ValidatedFieldsPlugin.ts index 5225b5f3c05..dbc67681211 100644 --- a/src/LiveComponent/assets/src/Component/plugins/ValidatedFieldsPlugin.ts +++ b/src/LiveComponent/assets/src/Component/plugins/ValidatedFieldsPlugin.ts @@ -1,5 +1,5 @@ -import type ValueStore from '../ValueStore'; import type Component from '../index'; +import type ValueStore from '../ValueStore'; import type { PluginInterface } from './PluginInterface'; export default class implements PluginInterface { diff --git a/src/LiveComponent/assets/src/dom_utils.ts b/src/LiveComponent/assets/src/dom_utils.ts index 745e40c58a1..9fb1390c639 100644 --- a/src/LiveComponent/assets/src/dom_utils.ts +++ b/src/LiveComponent/assets/src/dom_utils.ts @@ -1,8 +1,8 @@ import type Component from './Component'; import type ValueStore from './Component/ValueStore'; import { type Directive, parseDirectives } from './Directive/directives_parser'; -import getElementAsTagText from './Util/getElementAsTagText'; import { normalizeModelName } from './string_utils'; +import getElementAsTagText from './Util/getElementAsTagText'; /** * Return the "value" of any given element. diff --git a/src/LiveComponent/assets/src/live_controller.ts b/src/LiveComponent/assets/src/live_controller.ts index a9ea7f115ee..34380a8fa73 100644 --- a/src/LiveComponent/assets/src/live_controller.ts +++ b/src/LiveComponent/assets/src/live_controller.ts @@ -13,8 +13,8 @@ import SetValueOntoModelFieldsPlugin from './Component/plugins/SetValueOntoModel import ValidatedFieldsPlugin from './Component/plugins/ValidatedFieldsPlugin'; import { type DirectiveModifier, parseDirectives } from './Directive/directives_parser'; import getModelBinding from './Directive/get_model_binding'; -import getElementAsTagText from './Util/getElementAsTagText'; import { elementBelongsToThisComponent, getModelDirectiveFromElement, getValueFromElement } from './dom_utils'; +import getElementAsTagText from './Util/getElementAsTagText'; export { Component }; export { getComponent } from './ComponentRegistry'; diff --git a/src/LiveComponent/assets/src/morphdom.ts b/src/LiveComponent/assets/src/morphdom.ts index af0eb630a9a..fda89c363af 100644 --- a/src/LiveComponent/assets/src/morphdom.ts +++ b/src/LiveComponent/assets/src/morphdom.ts @@ -1,8 +1,8 @@ // @ts-ignore import { Idiomorph } from 'idiomorph/dist/idiomorph.esm.js'; -import type ExternalMutationTracker from './Rendering/ExternalMutationTracker'; import { cloneHTMLElement, getModelDirectiveFromElement, setValueOnElement } from './dom_utils'; import { normalizeAttributesForComparison } from './normalize_attributes_for_comparison'; +import type ExternalMutationTracker from './Rendering/ExternalMutationTracker'; const syncAttributes = (fromEl: Element, toEl: Element): void => { for (let i = 0; i < fromEl.attributes.length; i++) { diff --git a/src/LiveComponent/assets/test/Rendering/ExternalMutationTracker.test.ts b/src/LiveComponent/assets/test/Rendering/ExternalMutationTracker.test.ts index f92f910fa05..f3a64e77b32 100644 --- a/src/LiveComponent/assets/test/Rendering/ExternalMutationTracker.test.ts +++ b/src/LiveComponent/assets/test/Rendering/ExternalMutationTracker.test.ts @@ -1,5 +1,5 @@ -import ExternalMutationTracker from '../../src/Rendering/ExternalMutationTracker'; import { htmlToElement } from '../../src/dom_utils'; +import ExternalMutationTracker from '../../src/Rendering/ExternalMutationTracker'; const mountElement = (html: string): HTMLElement => { const element = htmlToElement(html); diff --git a/src/LiveComponent/assets/test/Util/getElementAsTagText.test.ts b/src/LiveComponent/assets/test/Util/getElementAsTagText.test.ts index b311ea7cc58..4bd12b3a907 100644 --- a/src/LiveComponent/assets/test/Util/getElementAsTagText.test.ts +++ b/src/LiveComponent/assets/test/Util/getElementAsTagText.test.ts @@ -1,5 +1,5 @@ -import getElementAsTagText from '../../src/Util/getElementAsTagText'; import { htmlToElement } from '../../src/dom_utils'; +import getElementAsTagText from '../../src/Util/getElementAsTagText'; describe('getElementAsTagText', () => { it('returns self-closing tag correctly', () => { diff --git a/src/Map/assets/dist/abstract_map_controller.d.ts b/src/Map/assets/dist/abstract_map_controller.d.ts index 67d5c70c9f0..fe4e9038179 100644 --- a/src/Map/assets/dist/abstract_map_controller.d.ts +++ b/src/Map/assets/dist/abstract_map_controller.d.ts @@ -131,29 +131,29 @@ export default abstract class; }): Marker; protected abstract doRemoveMarker(marker: Marker): void; - protected abstract doCreatePolygon({ definition, }: { + protected abstract doCreatePolygon({ definition }: { definition: PolygonDefinition; }): Polygon; protected abstract doRemovePolygon(polygon: Polygon): void; - protected abstract doCreatePolyline({ definition, }: { + protected abstract doCreatePolyline({ definition }: { definition: PolylineDefinition; }): Polyline; protected abstract doRemovePolyline(polyline: Polyline): void; - protected abstract doCreateCircle({ definition, }: { + protected abstract doCreateCircle({ definition }: { definition: CircleDefinition; }): Circle; protected abstract doRemoveCircle(circle: Circle): void; - protected abstract doCreateRectangle({ definition, }: { + protected abstract doCreateRectangle({ definition }: { definition: RectangleDefinition; }): Rectangle; protected abstract doRemoveRectangle(rectangle: Rectangle): void; @@ -161,7 +161,7 @@ export default abstract class; element: Marker | Polygon | Polyline | Circle | Rectangle; }): InfoWindow; - protected abstract doCreateIcon({ definition, element, }: { + protected abstract doCreateIcon({ definition, element }: { definition: Icon; element: Marker; }): void; diff --git a/src/Map/assets/src/abstract_map_controller.ts b/src/Map/assets/src/abstract_map_controller.ts index e81e267171a..007dbe2cfca 100644 --- a/src/Map/assets/src/abstract_map_controller.ts +++ b/src/Map/assets/src/abstract_map_controller.ts @@ -136,10 +136,7 @@ export type InfoWindowDefinition = { extra: Record; }; -export type InfoWindowWithoutPositionDefinition = Omit< - InfoWindowDefinition, - 'position' ->; +export type InfoWindowWithoutPositionDefinition = Omit, 'position'>; export default abstract class< MapOptions, @@ -199,21 +196,11 @@ export default abstract class< protected infoWindows: Array = []; private isConnected = false; - private createMarker: ({ - definition, - }: { definition: MarkerDefinition }) => Marker; - private createPolygon: ({ - definition, - }: { definition: PolygonDefinition }) => Polygon; - private createPolyline: ({ - definition, - }: { definition: PolylineDefinition }) => Polyline; - private createCircle: ({ - definition, - }: { definition: CircleDefinition }) => Circle; - private createRectangle: ({ - definition, - }: { definition: RectangleDefinition }) => Rectangle; + private createMarker: ({ definition }: { definition: MarkerDefinition }) => Marker; + private createPolygon: ({ definition }: { definition: PolygonDefinition }) => Polygon; + private createPolyline: ({ definition }: { definition: PolylineDefinition }) => Polyline; + private createCircle: ({ definition }: { definition: CircleDefinition }) => Circle; + private createRectangle: ({ definition }: { definition: RectangleDefinition }) => Rectangle; protected abstract dispatchEvent(name: string, payload: Record): void; @@ -226,11 +213,7 @@ export default abstract class< this.createPolygon = this.createDrawingFactory('polygon', this.polygons, this.doCreatePolygon.bind(this)); this.createPolyline = this.createDrawingFactory('polyline', this.polylines, this.doCreatePolyline.bind(this)); this.createCircle = this.createDrawingFactory('circle', this.circles, this.doCreateCircle.bind(this)); - this.createRectangle = this.createDrawingFactory( - 'rectangle', - this.rectangles, - this.doCreateRectangle.bind(this) - ); + this.createRectangle = this.createDrawingFactory('rectangle', this.rectangles, this.doCreateRectangle.bind(this)); this.map = this.doCreateMap({ center: this.hasCenterValue ? this.centerValue : null, @@ -331,53 +314,27 @@ export default abstract class< //endregion //region Abstract factory methods to be implemented by the concrete classes, they are specific to the map provider - protected abstract doCreateMap({ - center, - zoom, - options, - }: { - center: Point | null; - zoom: number | null; - options: MapOptions; - }): Map; + protected abstract doCreateMap({ center, zoom, options }: { center: Point | null; zoom: number | null; options: MapOptions }): Map; protected abstract doFitBoundsToMarkers(): void; - protected abstract doCreateMarker({ - definition, - }: { definition: MarkerDefinition }): Marker; + protected abstract doCreateMarker({ definition }: { definition: MarkerDefinition }): Marker; protected abstract doRemoveMarker(marker: Marker): void; - protected abstract doCreatePolygon({ - definition, - }: { - definition: PolygonDefinition; - }): Polygon; + protected abstract doCreatePolygon({ definition }: { definition: PolygonDefinition }): Polygon; protected abstract doRemovePolygon(polygon: Polygon): void; - protected abstract doCreatePolyline({ - definition, - }: { - definition: PolylineDefinition; - }): Polyline; + protected abstract doCreatePolyline({ definition }: { definition: PolylineDefinition }): Polyline; protected abstract doRemovePolyline(polyline: Polyline): void; - protected abstract doCreateCircle({ - definition, - }: { - definition: CircleDefinition; - }): Circle; + protected abstract doCreateCircle({ definition }: { definition: CircleDefinition }): Circle; protected abstract doRemoveCircle(circle: Circle): void; - protected abstract doCreateRectangle({ - definition, - }: { - definition: RectangleDefinition; - }): Rectangle; + protected abstract doCreateRectangle({ definition }: { definition: RectangleDefinition }): Rectangle; protected abstract doRemoveRectangle(rectangle: Rectangle): void; @@ -388,42 +345,16 @@ export default abstract class< definition: InfoWindowWithoutPositionDefinition; element: Marker | Polygon | Polyline | Circle | Rectangle; }): InfoWindow; - protected abstract doCreateIcon({ - definition, - element, - }: { - definition: Icon; - element: Marker; - }): void; + protected abstract doCreateIcon({ definition, element }: { definition: Icon; element: Marker }): void; //endregion //region Private APIs - private createDrawingFactory( - type: 'marker', - draws: typeof this.markers, - factory: typeof this.doCreateMarker - ): typeof this.doCreateMarker; - private createDrawingFactory( - type: 'polygon', - draws: typeof this.polygons, - factory: typeof this.doCreatePolygon - ): typeof this.doCreatePolygon; - private createDrawingFactory( - type: 'polyline', - draws: typeof this.polylines, - factory: typeof this.doCreatePolyline - ): typeof this.doCreatePolyline; - private createDrawingFactory( - type: 'circle', - draws: typeof this.circles, - factory: typeof this.doCreateCircle - ): typeof this.doCreateCircle; - private createDrawingFactory( - type: 'rectangle', - draws: typeof this.rectangles, - factory: typeof this.doCreateRectangle - ): typeof this.doCreateRectangle; + private createDrawingFactory(type: 'marker', draws: typeof this.markers, factory: typeof this.doCreateMarker): typeof this.doCreateMarker; + private createDrawingFactory(type: 'polygon', draws: typeof this.polygons, factory: typeof this.doCreatePolygon): typeof this.doCreatePolygon; + private createDrawingFactory(type: 'polyline', draws: typeof this.polylines, factory: typeof this.doCreatePolyline): typeof this.doCreatePolyline; + private createDrawingFactory(type: 'circle', draws: typeof this.circles, factory: typeof this.doCreateCircle): typeof this.doCreateCircle; + private createDrawingFactory(type: 'rectangle', draws: typeof this.rectangles, factory: typeof this.doCreateRectangle): typeof this.doCreateRectangle; private createDrawingFactory< Factory extends | typeof this.doCreateMarker @@ -432,11 +363,7 @@ export default abstract class< | typeof this.doCreateCircle | typeof this.doCreateRectangle, Draw extends ReturnType, - >( - type: 'marker' | 'polygon' | 'polyline' | 'circle' | 'rectangle', - draws: globalThis.Map, Draw>, - factory: Factory - ): Factory { + >(type: 'marker' | 'polygon' | 'polyline' | 'circle' | 'rectangle', draws: globalThis.Map, Draw>, factory: Factory): Factory { const eventBefore = `${type}:before-create`; const eventAfter = `${type}:after-create`; diff --git a/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts b/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts index 4895c2bf881..2bb32519e15 100644 --- a/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts +++ b/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts @@ -1,6 +1,6 @@ import type { LoaderOptions } from '@googlemaps/js-api-loader'; -import AbstractMapController from '@symfony/ux-map'; import type { CircleDefinition, Icon, InfoWindowWithoutPositionDefinition, MarkerDefinition, Point, PolygonDefinition, PolylineDefinition, RectangleDefinition } from '@symfony/ux-map'; +import AbstractMapController from '@symfony/ux-map'; type MapOptions = Pick; export default class extends AbstractMapController { providerOptionsValue: Pick; @@ -10,7 +10,7 @@ export default class extends AbstractMapController): void; - protected doCreateMap({ center, zoom, options, }: { + protected doCreateMap({ center, zoom, options }: { center: Point | null; zoom: number | null; options: MapOptions; @@ -27,7 +27,7 @@ export default class extends AbstractMapController; }): google.maps.Polyline; protected doRemovePolyline(polyline: google.maps.Polyline): void; - protected doCreateCircle({ definition, }: { + protected doCreateCircle({ definition }: { definition: CircleDefinition; }): google.maps.Circle; protected doRemoveCircle(circle: google.maps.Circle): void; @@ -41,7 +41,7 @@ export default class extends AbstractMapController { - declare providerOptionsValue: Pick< - LoaderOptions, - 'apiKey' | 'id' | 'language' | 'region' | 'nonce' | 'retries' | 'url' | 'version' | 'libraries' - >; + declare providerOptionsValue: Pick; declare map: google.maps.Map; @@ -141,15 +138,7 @@ export default class extends AbstractMapController< }); } - protected doCreateMap({ - center, - zoom, - options, - }: { - center: Point | null; - zoom: number | null; - options: MapOptions; - }): google.maps.Map { + protected doCreateMap({ center, zoom, options }: { center: Point | null; zoom: number | null; options: MapOptions }): google.maps.Map { // We assume the following control options are enabled if their options are set options.zoomControl = typeof options.zoomControlOptions !== 'undefined'; options.mapTypeControl = typeof options.mapTypeControlOptions !== 'undefined'; @@ -249,11 +238,7 @@ export default class extends AbstractMapController< polyline.setMap(null); } - protected doCreateCircle({ - definition, - }: { - definition: CircleDefinition; - }): google.maps.Circle { + protected doCreateCircle({ definition }: { definition: CircleDefinition }): google.maps.Circle { const { '@id': _id, center, radius, title, infoWindow, rawOptions = {} } = definition; const circle = new _google.maps.Circle({ @@ -387,13 +372,7 @@ export default class extends AbstractMapController< return content; } - protected doCreateIcon({ - definition, - element, - }: { - definition: Icon; - element: google.maps.marker.AdvancedMarkerElement; - }): void { + protected doCreateIcon({ definition, element }: { definition: Icon; element: google.maps.marker.AdvancedMarkerElement }): void { const { type, width, height } = definition; if (type === IconTypes.Svg) { diff --git a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts index 120df29fa3d..40eb259f702 100644 --- a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts +++ b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts @@ -1,8 +1,8 @@ -import AbstractMapController from '@symfony/ux-map'; import type { CircleDefinition, Icon, InfoWindowWithoutPositionDefinition, MarkerDefinition, Point, PolygonDefinition, PolylineDefinition, RectangleDefinition } from '@symfony/ux-map'; +import AbstractMapController from '@symfony/ux-map'; import 'leaflet/dist/leaflet.min.css'; -import * as L from 'leaflet'; import type { CircleOptions, ControlPosition, MapOptions as LeafletMapOptions, MarkerOptions, PolylineOptions as PolygonOptions, PolylineOptions, PopupOptions, PolylineOptions as RectangleOptions } from 'leaflet'; +import * as L from 'leaflet'; type MapOptions = Pick & { attributionControlOptions?: { position: ControlPosition; @@ -27,7 +27,7 @@ export default class extends AbstractMapController): void; - protected doCreateMap({ center, zoom, options, }: { + protected doCreateMap({ center, zoom, options }: { center: Point | null; zoom: number | null; options: MapOptions; @@ -36,11 +36,11 @@ export default class extends AbstractMapController; }): L.Marker; protected doRemoveMarker(marker: L.Marker): void; - protected doCreatePolygon({ definition, }: { + protected doCreatePolygon({ definition }: { definition: PolygonDefinition; }): L.Polygon; protected doRemovePolygon(polygon: L.Polygon): void; - protected doCreatePolyline({ definition, }: { + protected doCreatePolyline({ definition }: { definition: PolylineDefinition; }): L.Polyline; protected doRemovePolyline(polyline: L.Polyline): void; @@ -48,7 +48,7 @@ export default class extends AbstractMapController; }): L.Circle; protected doRemoveCircle(circle: L.Circle): void; - protected doCreateRectangle({ definition, }: { + protected doCreateRectangle({ definition }: { definition: RectangleDefinition; }): L.Rectangle; protected doRemoveRectangle(rectangle: L.Rectangle): void; @@ -56,7 +56,7 @@ export default class extends AbstractMapController; element: L.Marker | L.Polygon | L.Polyline | L.Circle | L.Rectangle; }): L.Popup; - protected doCreateIcon({ definition, element, }: { + protected doCreateIcon({ definition, element }: { definition: Icon; element: L.Marker; }): void; diff --git a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js index eda5299743b..0dbbb24df91 100644 --- a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js +++ b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js @@ -161,7 +161,7 @@ class map_controller extends default_1 { }, }); } - doCreateMap({ center, zoom, options, }) { + doCreateMap({ center, zoom, options }) { const map = L.map(this.element, { ...options, center: center === null ? undefined : center, @@ -197,7 +197,7 @@ class map_controller extends default_1 { doRemoveMarker(marker) { marker.remove(); } - doCreatePolygon({ definition, }) { + doCreatePolygon({ definition }) { const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition; const polygon = L.polygon(points, { ...rawOptions }).addTo(this.map); if (title) { @@ -211,7 +211,7 @@ class map_controller extends default_1 { doRemovePolygon(polygon) { polygon.remove(); } - doCreatePolyline({ definition, }) { + doCreatePolyline({ definition }) { const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition; const polyline = L.polyline(points, { ...rawOptions }).addTo(this.map); if (title) { @@ -239,7 +239,7 @@ class map_controller extends default_1 { doRemoveCircle(circle) { circle.remove(); } - doCreateRectangle({ definition, }) { + doCreateRectangle({ definition }) { const { '@id': _id, southWest, northEast, title, infoWindow, rawOptions = {} } = definition; const rectangle = L.rectangle([ [southWest.lat, southWest.lng], @@ -268,7 +268,7 @@ class map_controller extends default_1 { } return popup; } - doCreateIcon({ definition, element, }) { + doCreateIcon({ definition, element }) { const { type, width, height } = definition; let icon; if (type === IconTypes.Svg) { diff --git a/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts b/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts index 277cde10c6a..45133521756 100644 --- a/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts +++ b/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts @@ -1,4 +1,3 @@ -import AbstractMapController, { IconTypes } from '@symfony/ux-map'; import type { CircleDefinition, Icon, @@ -9,8 +8,8 @@ import type { PolylineDefinition, RectangleDefinition, } from '@symfony/ux-map'; +import AbstractMapController, { IconTypes } from '@symfony/ux-map'; import 'leaflet/dist/leaflet.min.css'; -import * as L from 'leaflet'; import type { CircleOptions, ControlPosition, @@ -22,6 +21,7 @@ import type { PopupOptions, PolylineOptions as RectangleOptions, } from 'leaflet'; +import * as L from 'leaflet'; type MapOptions = Pick & { attributionControlOptions?: { position: ControlPosition; prefix: string | false }; @@ -87,11 +87,7 @@ export default class extends AbstractMapController< }); } - protected doCreateMap({ - center, - zoom, - options, - }: { center: Point | null; zoom: number | null; options: MapOptions }): L.Map { + protected doCreateMap({ center, zoom, options }: { center: Point | null; zoom: number | null; options: MapOptions }): L.Map { const map = L.map(this.element, { ...options, center: center === null ? undefined : center, @@ -121,9 +117,7 @@ export default class extends AbstractMapController< protected doCreateMarker({ definition }: { definition: MarkerDefinition }): L.Marker { const { '@id': _id, position, title, infoWindow, icon, extra, rawOptions = {}, ...otherOptions } = definition; - const marker = L.marker(position, { title: title || undefined, ...otherOptions, ...rawOptions }).addTo( - this.map - ); + const marker = L.marker(position, { title: title || undefined, ...otherOptions, ...rawOptions }).addTo(this.map); if (infoWindow) { this.createInfoWindow({ definition: infoWindow, element: marker }); @@ -140,9 +134,7 @@ export default class extends AbstractMapController< marker.remove(); } - protected doCreatePolygon({ - definition, - }: { definition: PolygonDefinition }): L.Polygon { + protected doCreatePolygon({ definition }: { definition: PolygonDefinition }): L.Polygon { const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition; const polygon = L.polygon(points, { ...rawOptions }).addTo(this.map); @@ -162,9 +154,7 @@ export default class extends AbstractMapController< polygon.remove(); } - protected doCreatePolyline({ - definition, - }: { definition: PolylineDefinition }): L.Polyline { + protected doCreatePolyline({ definition }: { definition: PolylineDefinition }): L.Polyline { const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition; const polyline = L.polyline(points, { ...rawOptions }).addTo(this.map); @@ -204,9 +194,7 @@ export default class extends AbstractMapController< circle.remove(); } - protected doCreateRectangle({ - definition, - }: { definition: RectangleDefinition }): L.Rectangle { + protected doCreateRectangle({ definition }: { definition: RectangleDefinition }): L.Rectangle { const { '@id': _id, southWest, northEast, title, infoWindow, rawOptions = {} } = definition; const rectangle = L.rectangle( @@ -255,13 +243,7 @@ export default class extends AbstractMapController< return popup; } - protected doCreateIcon({ - definition, - element, - }: { - definition: Icon; - element: L.Marker; - }): void { + protected doCreateIcon({ definition, element }: { definition: Icon; element: L.Marker }): void { const { type, width, height } = definition; let icon: L.DivIcon | L.Icon; diff --git a/src/React/assets/test/register_controller.test.tsx b/src/React/assets/test/register_controller.test.tsx index dcb3808219b..66e04ee1971 100644 --- a/src/React/assets/test/register_controller.test.tsx +++ b/src/React/assets/test/register_controller.test.tsx @@ -11,6 +11,7 @@ import { registerReactControllerComponents } from '../src/register_controller'; // @ts-ignore import MyJsxComponent from './fixtures/MyJsxComponent'; import MyTsxComponent from './fixtures/MyTsxComponent'; + import RequireContext = __WebpackModuleApi.RequireContext; const createFakeFixturesContext = (): RequireContext => { diff --git a/src/StimulusBundle/assets/src/loader.ts b/src/StimulusBundle/assets/src/loader.ts index 178214a6aad..2145a239b44 100644 --- a/src/StimulusBundle/assets/src/loader.ts +++ b/src/StimulusBundle/assets/src/loader.ts @@ -15,9 +15,9 @@ import { Application, type ControllerConstructor } from '@hotwired/stimulus'; import { type EagerControllersCollection, - type LazyControllersCollection, eagerControllers, isApplicationDebug, + type LazyControllersCollection, lazyControllers, } from './controllers.js'; diff --git a/src/Svelte/assets/test/register_controller.test.ts b/src/Svelte/assets/test/register_controller.test.ts index 35eb6e333a7..ca56d41640e 100644 --- a/src/Svelte/assets/test/register_controller.test.ts +++ b/src/Svelte/assets/test/register_controller.test.ts @@ -9,6 +9,7 @@ import { registerSvelteControllerComponents } from '../src/register_controller'; import MyComponent from './fixtures/MyComponent.svelte'; + import RequireContext = __WebpackModuleApi.RequireContext; const createFakeFixturesContext = (): RequireContext => { diff --git a/src/Translator/assets/test/translator.test.ts b/src/Translator/assets/test/translator.test.ts index 2ca0ee28399..77cf2c780c9 100644 --- a/src/Translator/assets/test/translator.test.ts +++ b/src/Translator/assets/test/translator.test.ts @@ -1,7 +1,7 @@ import { + getLocale, type Message, type NoParametersType, - getLocale, setLocale, setLocaleFallbacks, throwWhenNotFound, diff --git a/src/Vue/assets/test/register_controller.test.ts b/src/Vue/assets/test/register_controller.test.ts index 290ba5b8223..50b7b4e0c1a 100644 --- a/src/Vue/assets/test/register_controller.test.ts +++ b/src/Vue/assets/test/register_controller.test.ts @@ -8,8 +8,9 @@ */ import { registerVueControllerComponents } from '../src/register_controller'; -import Goodbye from './fixtures-lazy/Goodbye.vue'; import Hello from './fixtures/Hello.vue'; +import Goodbye from './fixtures-lazy/Goodbye.vue'; + import RequireContext = __WebpackModuleApi.RequireContext; const createFakeFixturesContext = (lazyDir: boolean): RequireContext => { diff --git a/yarn.lock b/yarn.lock index 3fe81d3cf16..200ffc18b69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1504,18 +1504,18 @@ __metadata: languageName: node linkType: hard -"@biomejs/biome@npm:^1.8.3": - version: 1.8.3 - resolution: "@biomejs/biome@npm:1.8.3" - dependencies: - "@biomejs/cli-darwin-arm64": "npm:1.8.3" - "@biomejs/cli-darwin-x64": "npm:1.8.3" - "@biomejs/cli-linux-arm64": "npm:1.8.3" - "@biomejs/cli-linux-arm64-musl": "npm:1.8.3" - "@biomejs/cli-linux-x64": "npm:1.8.3" - "@biomejs/cli-linux-x64-musl": "npm:1.8.3" - "@biomejs/cli-win32-arm64": "npm:1.8.3" - "@biomejs/cli-win32-x64": "npm:1.8.3" +"@biomejs/biome@npm:^2.0.4": + version: 2.0.4 + resolution: "@biomejs/biome@npm:2.0.4" + dependencies: + "@biomejs/cli-darwin-arm64": "npm:2.0.4" + "@biomejs/cli-darwin-x64": "npm:2.0.4" + "@biomejs/cli-linux-arm64": "npm:2.0.4" + "@biomejs/cli-linux-arm64-musl": "npm:2.0.4" + "@biomejs/cli-linux-x64": "npm:2.0.4" + "@biomejs/cli-linux-x64-musl": "npm:2.0.4" + "@biomejs/cli-win32-arm64": "npm:2.0.4" + "@biomejs/cli-win32-x64": "npm:2.0.4" dependenciesMeta: "@biomejs/cli-darwin-arm64": optional: true @@ -1535,62 +1535,62 @@ __metadata: optional: true bin: biome: bin/biome - checksum: 10c0/95fe99ce82cd8242f1be51cbf3ac26043b253f5a369d3dc24df09bdb32ec04dba679b1d4fa8b9d602b1bf2c30ecd80af14aa8f5c92d6e0cd6214a99a1099a65b + checksum: 10c0/8ca201a4ac2dc008fc4ecdf96f6bb6004c8b09e7c3121af56a050be67694367ad5e2902762dde9282dc5954930676e786f29d15f12433e269bd404306996f93c languageName: node linkType: hard -"@biomejs/cli-darwin-arm64@npm:1.8.3": - version: 1.8.3 - resolution: "@biomejs/cli-darwin-arm64@npm:1.8.3" +"@biomejs/cli-darwin-arm64@npm:2.0.4": + version: 2.0.4 + resolution: "@biomejs/cli-darwin-arm64@npm:2.0.4" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@biomejs/cli-darwin-x64@npm:1.8.3": - version: 1.8.3 - resolution: "@biomejs/cli-darwin-x64@npm:1.8.3" +"@biomejs/cli-darwin-x64@npm:2.0.4": + version: 2.0.4 + resolution: "@biomejs/cli-darwin-x64@npm:2.0.4" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@biomejs/cli-linux-arm64-musl@npm:1.8.3": - version: 1.8.3 - resolution: "@biomejs/cli-linux-arm64-musl@npm:1.8.3" +"@biomejs/cli-linux-arm64-musl@npm:2.0.4": + version: 2.0.4 + resolution: "@biomejs/cli-linux-arm64-musl@npm:2.0.4" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@biomejs/cli-linux-arm64@npm:1.8.3": - version: 1.8.3 - resolution: "@biomejs/cli-linux-arm64@npm:1.8.3" +"@biomejs/cli-linux-arm64@npm:2.0.4": + version: 2.0.4 + resolution: "@biomejs/cli-linux-arm64@npm:2.0.4" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@biomejs/cli-linux-x64-musl@npm:1.8.3": - version: 1.8.3 - resolution: "@biomejs/cli-linux-x64-musl@npm:1.8.3" +"@biomejs/cli-linux-x64-musl@npm:2.0.4": + version: 2.0.4 + resolution: "@biomejs/cli-linux-x64-musl@npm:2.0.4" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@biomejs/cli-linux-x64@npm:1.8.3": - version: 1.8.3 - resolution: "@biomejs/cli-linux-x64@npm:1.8.3" +"@biomejs/cli-linux-x64@npm:2.0.4": + version: 2.0.4 + resolution: "@biomejs/cli-linux-x64@npm:2.0.4" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@biomejs/cli-win32-arm64@npm:1.8.3": - version: 1.8.3 - resolution: "@biomejs/cli-win32-arm64@npm:1.8.3" +"@biomejs/cli-win32-arm64@npm:2.0.4": + version: 2.0.4 + resolution: "@biomejs/cli-win32-arm64@npm:2.0.4" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@biomejs/cli-win32-x64@npm:1.8.3": - version: 1.8.3 - resolution: "@biomejs/cli-win32-x64@npm:1.8.3" +"@biomejs/cli-win32-x64@npm:2.0.4": + version: 2.0.4 + resolution: "@biomejs/cli-win32-x64@npm:2.0.4" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -8797,7 +8797,7 @@ __metadata: "@babel/preset-env": "npm:^7.25.3" "@babel/preset-react": "npm:^7.24.7" "@babel/preset-typescript": "npm:^7.24.7" - "@biomejs/biome": "npm:^1.8.3" + "@biomejs/biome": "npm:^2.0.4" "@rollup/plugin-commonjs": "npm:^28.0.0" "@rollup/plugin-node-resolve": "npm:^15.3.0" "@rollup/plugin-typescript": "npm:^11.1.6"