@symfony/ux-live-component 2.26.0 → 2.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -195,680 +195,363 @@ const findParent = (currentComponent) => {
195
195
  return null;
196
196
  };
197
197
 
198
- class HookManager {
199
- constructor() {
200
- this.hooks = new Map();
201
- }
202
- register(hookName, callback) {
203
- const hooks = this.hooks.get(hookName) || [];
204
- hooks.push(callback);
205
- this.hooks.set(hookName, hooks);
198
+ function parseDirectives(content) {
199
+ const directives = [];
200
+ if (!content) {
201
+ return directives;
206
202
  }
207
- unregister(hookName, callback) {
208
- const hooks = this.hooks.get(hookName) || [];
209
- const index = hooks.indexOf(callback);
210
- if (index === -1) {
211
- return;
203
+ let currentActionName = '';
204
+ let currentArgumentValue = '';
205
+ let currentArguments = [];
206
+ let currentModifiers = [];
207
+ let state = 'action';
208
+ const getLastActionName = () => {
209
+ if (currentActionName) {
210
+ return currentActionName;
211
+ }
212
+ if (directives.length === 0) {
213
+ throw new Error('Could not find any directives');
214
+ }
215
+ return directives[directives.length - 1].action;
216
+ };
217
+ const pushInstruction = () => {
218
+ directives.push({
219
+ action: currentActionName,
220
+ args: currentArguments,
221
+ modifiers: currentModifiers,
222
+ getString: () => {
223
+ return content;
224
+ },
225
+ });
226
+ currentActionName = '';
227
+ currentArgumentValue = '';
228
+ currentArguments = [];
229
+ currentModifiers = [];
230
+ state = 'action';
231
+ };
232
+ const pushArgument = () => {
233
+ currentArguments.push(currentArgumentValue.trim());
234
+ currentArgumentValue = '';
235
+ };
236
+ const pushModifier = () => {
237
+ if (currentArguments.length > 1) {
238
+ throw new Error(`The modifier "${currentActionName}()" does not support multiple arguments.`);
239
+ }
240
+ currentModifiers.push({
241
+ name: currentActionName,
242
+ value: currentArguments.length > 0 ? currentArguments[0] : null,
243
+ });
244
+ currentActionName = '';
245
+ currentArguments = [];
246
+ state = 'action';
247
+ };
248
+ for (let i = 0; i < content.length; i++) {
249
+ const char = content[i];
250
+ switch (state) {
251
+ case 'action':
252
+ if (char === '(') {
253
+ state = 'arguments';
254
+ break;
255
+ }
256
+ if (char === ' ') {
257
+ if (currentActionName) {
258
+ pushInstruction();
259
+ }
260
+ break;
261
+ }
262
+ if (char === '|') {
263
+ pushModifier();
264
+ break;
265
+ }
266
+ currentActionName += char;
267
+ break;
268
+ case 'arguments':
269
+ if (char === ')') {
270
+ pushArgument();
271
+ state = 'after_arguments';
272
+ break;
273
+ }
274
+ if (char === ',') {
275
+ pushArgument();
276
+ break;
277
+ }
278
+ currentArgumentValue += char;
279
+ break;
280
+ case 'after_arguments':
281
+ if (char === '|') {
282
+ pushModifier();
283
+ break;
284
+ }
285
+ if (char !== ' ') {
286
+ throw new Error(`Missing space after ${getLastActionName()}()`);
287
+ }
288
+ pushInstruction();
289
+ break;
212
290
  }
213
- hooks.splice(index, 1);
214
- this.hooks.set(hookName, hooks);
215
291
  }
216
- triggerHook(hookName, ...args) {
217
- const hooks = this.hooks.get(hookName) || [];
218
- hooks.forEach((callback) => callback(...args));
292
+ switch (state) {
293
+ case 'action':
294
+ case 'after_arguments':
295
+ if (currentActionName) {
296
+ pushInstruction();
297
+ }
298
+ break;
299
+ default:
300
+ throw new Error(`Did you forget to add a closing ")" after "${currentActionName}"?`);
219
301
  }
302
+ return directives;
220
303
  }
221
304
 
222
- class ChangingItemsTracker {
223
- constructor() {
224
- this.changedItems = new Map();
225
- this.removedItems = new Map();
226
- }
227
- setItem(itemName, newValue, previousValue) {
228
- if (this.removedItems.has(itemName)) {
229
- const removedRecord = this.removedItems.get(itemName);
230
- this.removedItems.delete(itemName);
231
- if (removedRecord.original === newValue) {
232
- return;
305
+ function combineSpacedArray(parts) {
306
+ const finalParts = [];
307
+ parts.forEach((part) => {
308
+ finalParts.push(...trimAll(part).split(' '));
309
+ });
310
+ return finalParts;
311
+ }
312
+ function trimAll(str) {
313
+ return str.replace(/[\s]+/g, ' ').trim();
314
+ }
315
+ function normalizeModelName(model) {
316
+ return (model
317
+ .replace(/\[]$/, '')
318
+ .split('[')
319
+ .map((s) => s.replace(']', ''))
320
+ .join('.'));
321
+ }
322
+
323
+ function getValueFromElement(element, valueStore) {
324
+ if (element instanceof HTMLInputElement) {
325
+ if (element.type === 'checkbox') {
326
+ const modelNameData = getModelDirectiveFromElement(element, false);
327
+ if (modelNameData !== null) {
328
+ const modelValue = valueStore.get(modelNameData.action);
329
+ if (Array.isArray(modelValue)) {
330
+ return getMultipleCheckboxValue(element, modelValue);
331
+ }
332
+ if (Object(modelValue) === modelValue) {
333
+ return getMultipleCheckboxValue(element, Object.values(modelValue));
334
+ }
233
335
  }
234
- }
235
- if (this.changedItems.has(itemName)) {
236
- const originalRecord = this.changedItems.get(itemName);
237
- if (originalRecord.original === newValue) {
238
- this.changedItems.delete(itemName);
239
- return;
336
+ if (element.hasAttribute('value')) {
337
+ return element.checked ? element.getAttribute('value') : null;
240
338
  }
241
- this.changedItems.set(itemName, { original: originalRecord.original, new: newValue });
242
- return;
339
+ return element.checked;
243
340
  }
244
- this.changedItems.set(itemName, { original: previousValue, new: newValue });
341
+ return inputValue(element);
245
342
  }
246
- removeItem(itemName, currentValue) {
247
- let trueOriginalValue = currentValue;
248
- if (this.changedItems.has(itemName)) {
249
- const originalRecord = this.changedItems.get(itemName);
250
- trueOriginalValue = originalRecord.original;
251
- this.changedItems.delete(itemName);
252
- if (trueOriginalValue === null) {
253
- return;
254
- }
255
- }
256
- if (!this.removedItems.has(itemName)) {
257
- this.removedItems.set(itemName, { original: trueOriginalValue });
343
+ if (element instanceof HTMLSelectElement) {
344
+ if (element.multiple) {
345
+ return Array.from(element.selectedOptions).map((el) => el.value);
258
346
  }
347
+ return element.value;
259
348
  }
260
- getChangedItems() {
261
- return Array.from(this.changedItems, ([name, { new: value }]) => ({ name, value }));
349
+ if (element.dataset.value) {
350
+ return element.dataset.value;
262
351
  }
263
- getRemovedItems() {
264
- return Array.from(this.removedItems.keys());
352
+ if ('value' in element) {
353
+ return element.value;
265
354
  }
266
- isEmpty() {
267
- return this.changedItems.size === 0 && this.removedItems.size === 0;
355
+ if (element.hasAttribute('value')) {
356
+ return element.getAttribute('value');
268
357
  }
358
+ return null;
269
359
  }
270
-
271
- class ElementChanges {
272
- constructor() {
273
- this.addedClasses = new Set();
274
- this.removedClasses = new Set();
275
- this.styleChanges = new ChangingItemsTracker();
276
- this.attributeChanges = new ChangingItemsTracker();
277
- }
278
- addClass(className) {
279
- if (!this.removedClasses.delete(className)) {
280
- this.addedClasses.add(className);
360
+ function setValueOnElement(element, value) {
361
+ if (element instanceof HTMLInputElement) {
362
+ if (element.type === 'file') {
363
+ return;
281
364
  }
282
- }
283
- removeClass(className) {
284
- if (!this.addedClasses.delete(className)) {
285
- this.removedClasses.add(className);
365
+ if (element.type === 'radio') {
366
+ element.checked = element.value == value;
367
+ return;
368
+ }
369
+ if (element.type === 'checkbox') {
370
+ if (Array.isArray(value)) {
371
+ element.checked = value.some((val) => val == element.value);
372
+ }
373
+ else if (element.hasAttribute('value')) {
374
+ element.checked = element.value == value;
375
+ }
376
+ else {
377
+ element.checked = value;
378
+ }
379
+ return;
286
380
  }
287
381
  }
288
- addStyle(styleName, newValue, originalValue) {
289
- this.styleChanges.setItem(styleName, newValue, originalValue);
382
+ if (element instanceof HTMLSelectElement) {
383
+ const arrayWrappedValue = [].concat(value).map((value) => {
384
+ return `${value}`;
385
+ });
386
+ Array.from(element.options).forEach((option) => {
387
+ option.selected = arrayWrappedValue.includes(option.value);
388
+ });
389
+ return;
290
390
  }
291
- removeStyle(styleName, originalValue) {
292
- this.styleChanges.removeItem(styleName, originalValue);
391
+ value = value === undefined ? '' : value;
392
+ element.value = value;
393
+ }
394
+ function getAllModelDirectiveFromElements(element) {
395
+ if (!element.dataset.model) {
396
+ return [];
293
397
  }
294
- addAttribute(attributeName, newValue, originalValue) {
295
- this.attributeChanges.setItem(attributeName, newValue, originalValue);
296
- }
297
- removeAttribute(attributeName, originalValue) {
298
- this.attributeChanges.removeItem(attributeName, originalValue);
398
+ const directives = parseDirectives(element.dataset.model);
399
+ directives.forEach((directive) => {
400
+ if (directive.args.length > 0) {
401
+ throw new Error(`The data-model="${element.dataset.model}" format is invalid: it does not support passing arguments to the model.`);
402
+ }
403
+ directive.action = normalizeModelName(directive.action);
404
+ });
405
+ return directives;
406
+ }
407
+ function getModelDirectiveFromElement(element, throwOnMissing = true) {
408
+ const dataModelDirectives = getAllModelDirectiveFromElements(element);
409
+ if (dataModelDirectives.length > 0) {
410
+ return dataModelDirectives[0];
299
411
  }
300
- getAddedClasses() {
301
- return [...this.addedClasses];
412
+ if (element.getAttribute('name')) {
413
+ const formElement = element.closest('form');
414
+ if (formElement && 'model' in formElement.dataset) {
415
+ const directives = parseDirectives(formElement.dataset.model || '*');
416
+ const directive = directives[0];
417
+ if (directive.args.length > 0) {
418
+ throw new Error(`The data-model="${formElement.dataset.model}" format is invalid: it does not support passing arguments to the model.`);
419
+ }
420
+ directive.action = normalizeModelName(element.getAttribute('name'));
421
+ return directive;
422
+ }
302
423
  }
303
- getRemovedClasses() {
304
- return [...this.removedClasses];
424
+ if (!throwOnMissing) {
425
+ return null;
305
426
  }
306
- getChangedStyles() {
307
- return this.styleChanges.getChangedItems();
427
+ throw new Error(`Cannot determine the model name for "${getElementAsTagText(element)}": the element must either have a "data-model" (or "name" attribute living inside a <form data-model="*">).`);
428
+ }
429
+ function elementBelongsToThisComponent(element, component) {
430
+ if (component.element === element) {
431
+ return true;
308
432
  }
309
- getRemovedStyles() {
310
- return this.styleChanges.getRemovedItems();
433
+ if (!component.element.contains(element)) {
434
+ return false;
311
435
  }
312
- getChangedAttributes() {
313
- return this.attributeChanges.getChangedItems();
436
+ const closestLiveComponent = element.closest('[data-controller~="live"]');
437
+ return closestLiveComponent === component.element;
438
+ }
439
+ function cloneHTMLElement(element) {
440
+ const newElement = element.cloneNode(true);
441
+ if (!(newElement instanceof HTMLElement)) {
442
+ throw new Error('Could not clone element');
314
443
  }
315
- getRemovedAttributes() {
316
- return this.attributeChanges.getRemovedItems();
444
+ return newElement;
445
+ }
446
+ function htmlToElement(html) {
447
+ const template = document.createElement('template');
448
+ html = html.trim();
449
+ template.innerHTML = html;
450
+ if (template.content.childElementCount > 1) {
451
+ throw new Error(`Component HTML contains ${template.content.childElementCount} elements, but only 1 root element is allowed.`);
317
452
  }
318
- applyToElement(element) {
319
- element.classList.add(...this.addedClasses);
320
- element.classList.remove(...this.removedClasses);
321
- this.styleChanges.getChangedItems().forEach((change) => {
322
- element.style.setProperty(change.name, change.value);
323
- return;
324
- });
325
- this.styleChanges.getRemovedItems().forEach((styleName) => {
326
- element.style.removeProperty(styleName);
327
- });
328
- this.attributeChanges.getChangedItems().forEach((change) => {
329
- element.setAttribute(change.name, change.value);
330
- });
331
- this.attributeChanges.getRemovedItems().forEach((attributeName) => {
332
- element.removeAttribute(attributeName);
333
- });
453
+ const child = template.content.firstElementChild;
454
+ if (!child) {
455
+ throw new Error('Child not found');
334
456
  }
335
- isEmpty() {
336
- return (this.addedClasses.size === 0 &&
337
- this.removedClasses.size === 0 &&
338
- this.styleChanges.isEmpty() &&
339
- this.attributeChanges.isEmpty());
457
+ if (!(child instanceof HTMLElement)) {
458
+ throw new Error(`Created element is not an HTMLElement: ${html.trim()}`);
340
459
  }
460
+ return child;
341
461
  }
342
-
343
- class ExternalMutationTracker {
344
- constructor(element, shouldTrackChangeCallback) {
345
- this.changedElements = new WeakMap();
346
- this.changedElementsCount = 0;
347
- this.addedElements = [];
348
- this.removedElements = [];
349
- this.isStarted = false;
350
- this.element = element;
351
- this.shouldTrackChangeCallback = shouldTrackChangeCallback;
352
- this.mutationObserver = new MutationObserver(this.onMutations.bind(this));
353
- }
354
- start() {
355
- if (this.isStarted) {
356
- return;
462
+ const getMultipleCheckboxValue = (element, currentValues) => {
463
+ const finalValues = [...currentValues];
464
+ const value = inputValue(element);
465
+ const index = currentValues.indexOf(value);
466
+ if (element.checked) {
467
+ if (index === -1) {
468
+ finalValues.push(value);
357
469
  }
358
- this.mutationObserver.observe(this.element, {
359
- childList: true,
360
- subtree: true,
361
- attributes: true,
362
- attributeOldValue: true,
363
- });
364
- this.isStarted = true;
470
+ return finalValues;
365
471
  }
366
- stop() {
367
- if (this.isStarted) {
368
- this.mutationObserver.disconnect();
369
- this.isStarted = false;
370
- }
472
+ if (index > -1) {
473
+ finalValues.splice(index, 1);
371
474
  }
372
- getChangedElement(element) {
373
- return this.changedElements.has(element) ? this.changedElements.get(element) : null;
475
+ return finalValues;
476
+ };
477
+ const inputValue = (element) => element.dataset.value ? element.dataset.value : element.value;
478
+
479
+ class HookManager {
480
+ constructor() {
481
+ this.hooks = new Map();
374
482
  }
375
- getAddedElements() {
376
- return this.addedElements;
483
+ register(hookName, callback) {
484
+ const hooks = this.hooks.get(hookName) || [];
485
+ hooks.push(callback);
486
+ this.hooks.set(hookName, hooks);
377
487
  }
378
- wasElementAdded(element) {
379
- return this.addedElements.includes(element);
488
+ unregister(hookName, callback) {
489
+ const hooks = this.hooks.get(hookName) || [];
490
+ const index = hooks.indexOf(callback);
491
+ if (index === -1) {
492
+ return;
493
+ }
494
+ hooks.splice(index, 1);
495
+ this.hooks.set(hookName, hooks);
380
496
  }
381
- handlePendingChanges() {
382
- this.onMutations(this.mutationObserver.takeRecords());
497
+ triggerHook(hookName, ...args) {
498
+ const hooks = this.hooks.get(hookName) || [];
499
+ hooks.forEach((callback) => callback(...args));
383
500
  }
384
- onMutations(mutations) {
385
- const handledAttributeMutations = new WeakMap();
386
- for (const mutation of mutations) {
387
- const element = mutation.target;
388
- if (!this.shouldTrackChangeCallback(element)) {
389
- continue;
390
- }
391
- if (this.isElementAddedByTranslation(element)) {
392
- continue;
393
- }
394
- let isChangeInAddedElement = false;
395
- for (const addedElement of this.addedElements) {
396
- if (addedElement.contains(element)) {
397
- isChangeInAddedElement = true;
398
- break;
399
- }
501
+ }
502
+
503
+ // base IIFE to define idiomorph
504
+ var Idiomorph = (function () {
505
+
506
+ //=============================================================================
507
+ // AND NOW IT BEGINS...
508
+ //=============================================================================
509
+ let EMPTY_SET = new Set();
510
+
511
+ // default configuration values, updatable by users now
512
+ let defaults = {
513
+ morphStyle: "outerHTML",
514
+ callbacks : {
515
+ beforeNodeAdded: noOp,
516
+ afterNodeAdded: noOp,
517
+ beforeNodeMorphed: noOp,
518
+ afterNodeMorphed: noOp,
519
+ beforeNodeRemoved: noOp,
520
+ afterNodeRemoved: noOp,
521
+ beforeAttributeUpdated: noOp,
522
+
523
+ },
524
+ head: {
525
+ style: 'merge',
526
+ shouldPreserve: function (elt) {
527
+ return elt.getAttribute("im-preserve") === "true";
528
+ },
529
+ shouldReAppend: function (elt) {
530
+ return elt.getAttribute("im-re-append") === "true";
531
+ },
532
+ shouldRemove: noOp,
533
+ afterHeadMorphed: noOp,
400
534
  }
401
- if (isChangeInAddedElement) {
402
- continue;
535
+ };
536
+
537
+ //=============================================================================
538
+ // Core Morphing Algorithm - morph, morphNormalizedContent, morphOldNodeTo, morphChildren
539
+ //=============================================================================
540
+ function morph(oldNode, newContent, config = {}) {
541
+
542
+ if (oldNode instanceof Document) {
543
+ oldNode = oldNode.documentElement;
403
544
  }
404
- switch (mutation.type) {
405
- case 'childList':
406
- this.handleChildListMutation(mutation);
407
- break;
408
- case 'attributes':
409
- if (!handledAttributeMutations.has(element)) {
410
- handledAttributeMutations.set(element, []);
411
- }
412
- if (!handledAttributeMutations.get(element).includes(mutation.attributeName)) {
413
- this.handleAttributeMutation(mutation);
414
- handledAttributeMutations.set(element, [
415
- ...handledAttributeMutations.get(element),
416
- mutation.attributeName,
417
- ]);
418
- }
419
- break;
545
+
546
+ if (typeof newContent === 'string') {
547
+ newContent = parseContent(newContent);
420
548
  }
421
- }
422
- }
423
- handleChildListMutation(mutation) {
424
- mutation.addedNodes.forEach((node) => {
425
- if (!(node instanceof Element)) {
426
- return;
427
- }
428
- if (this.removedElements.includes(node)) {
429
- this.removedElements.splice(this.removedElements.indexOf(node), 1);
430
- return;
431
- }
432
- if (this.isElementAddedByTranslation(node)) {
433
- return;
434
- }
435
- this.addedElements.push(node);
436
- });
437
- mutation.removedNodes.forEach((node) => {
438
- if (!(node instanceof Element)) {
439
- return;
440
- }
441
- if (this.addedElements.includes(node)) {
442
- this.addedElements.splice(this.addedElements.indexOf(node), 1);
443
- return;
444
- }
445
- this.removedElements.push(node);
446
- });
447
- }
448
- handleAttributeMutation(mutation) {
449
- const element = mutation.target;
450
- if (!this.changedElements.has(element)) {
451
- this.changedElements.set(element, new ElementChanges());
452
- this.changedElementsCount++;
453
- }
454
- const changedElement = this.changedElements.get(element);
455
- switch (mutation.attributeName) {
456
- case 'class':
457
- this.handleClassAttributeMutation(mutation, changedElement);
458
- break;
459
- case 'style':
460
- this.handleStyleAttributeMutation(mutation, changedElement);
461
- break;
462
- default:
463
- this.handleGenericAttributeMutation(mutation, changedElement);
464
- }
465
- if (changedElement.isEmpty()) {
466
- this.changedElements.delete(element);
467
- this.changedElementsCount--;
468
- }
469
- }
470
- handleClassAttributeMutation(mutation, elementChanges) {
471
- const element = mutation.target;
472
- const previousValue = mutation.oldValue || '';
473
- const previousValues = previousValue.match(/(\S+)/gu) || [];
474
- const newValues = [].slice.call(element.classList);
475
- const addedValues = newValues.filter((value) => !previousValues.includes(value));
476
- const removedValues = previousValues.filter((value) => !newValues.includes(value));
477
- addedValues.forEach((value) => {
478
- elementChanges.addClass(value);
479
- });
480
- removedValues.forEach((value) => {
481
- elementChanges.removeClass(value);
482
- });
483
- }
484
- handleStyleAttributeMutation(mutation, elementChanges) {
485
- const element = mutation.target;
486
- const previousValue = mutation.oldValue || '';
487
- const previousStyles = this.extractStyles(previousValue);
488
- const newValue = element.getAttribute('style') || '';
489
- const newStyles = this.extractStyles(newValue);
490
- const addedOrChangedStyles = Object.keys(newStyles).filter((key) => previousStyles[key] === undefined || previousStyles[key] !== newStyles[key]);
491
- const removedStyles = Object.keys(previousStyles).filter((key) => !newStyles[key]);
492
- addedOrChangedStyles.forEach((style) => {
493
- elementChanges.addStyle(style, newStyles[style], previousStyles[style] === undefined ? null : previousStyles[style]);
494
- });
495
- removedStyles.forEach((style) => {
496
- elementChanges.removeStyle(style, previousStyles[style]);
497
- });
498
- }
499
- handleGenericAttributeMutation(mutation, elementChanges) {
500
- const attributeName = mutation.attributeName;
501
- const element = mutation.target;
502
- let oldValue = mutation.oldValue;
503
- let newValue = element.getAttribute(attributeName);
504
- if (oldValue === attributeName) {
505
- oldValue = '';
506
- }
507
- if (newValue === attributeName) {
508
- newValue = '';
509
- }
510
- if (!element.hasAttribute(attributeName)) {
511
- if (oldValue === null) {
512
- return;
513
- }
514
- elementChanges.removeAttribute(attributeName, mutation.oldValue);
515
- return;
516
- }
517
- if (newValue === oldValue) {
518
- return;
519
- }
520
- elementChanges.addAttribute(attributeName, element.getAttribute(attributeName), mutation.oldValue);
521
- }
522
- extractStyles(styles) {
523
- const styleObject = {};
524
- styles.split(';').forEach((style) => {
525
- const parts = style.split(':');
526
- if (parts.length === 1) {
527
- return;
528
- }
529
- const property = parts[0].trim();
530
- styleObject[property] = parts.slice(1).join(':').trim();
531
- });
532
- return styleObject;
533
- }
534
- isElementAddedByTranslation(element) {
535
- return element.tagName === 'FONT' && element.getAttribute('style') === 'vertical-align: inherit;';
536
- }
537
- }
538
-
539
- function parseDirectives(content) {
540
- const directives = [];
541
- if (!content) {
542
- return directives;
543
- }
544
- let currentActionName = '';
545
- let currentArgumentValue = '';
546
- let currentArguments = [];
547
- let currentModifiers = [];
548
- let state = 'action';
549
- const getLastActionName = () => {
550
- if (currentActionName) {
551
- return currentActionName;
552
- }
553
- if (directives.length === 0) {
554
- throw new Error('Could not find any directives');
555
- }
556
- return directives[directives.length - 1].action;
557
- };
558
- const pushInstruction = () => {
559
- directives.push({
560
- action: currentActionName,
561
- args: currentArguments,
562
- modifiers: currentModifiers,
563
- getString: () => {
564
- return content;
565
- },
566
- });
567
- currentActionName = '';
568
- currentArgumentValue = '';
569
- currentArguments = [];
570
- currentModifiers = [];
571
- state = 'action';
572
- };
573
- const pushArgument = () => {
574
- currentArguments.push(currentArgumentValue.trim());
575
- currentArgumentValue = '';
576
- };
577
- const pushModifier = () => {
578
- if (currentArguments.length > 1) {
579
- throw new Error(`The modifier "${currentActionName}()" does not support multiple arguments.`);
580
- }
581
- currentModifiers.push({
582
- name: currentActionName,
583
- value: currentArguments.length > 0 ? currentArguments[0] : null,
584
- });
585
- currentActionName = '';
586
- currentArguments = [];
587
- state = 'action';
588
- };
589
- for (let i = 0; i < content.length; i++) {
590
- const char = content[i];
591
- switch (state) {
592
- case 'action':
593
- if (char === '(') {
594
- state = 'arguments';
595
- break;
596
- }
597
- if (char === ' ') {
598
- if (currentActionName) {
599
- pushInstruction();
600
- }
601
- break;
602
- }
603
- if (char === '|') {
604
- pushModifier();
605
- break;
606
- }
607
- currentActionName += char;
608
- break;
609
- case 'arguments':
610
- if (char === ')') {
611
- pushArgument();
612
- state = 'after_arguments';
613
- break;
614
- }
615
- if (char === ',') {
616
- pushArgument();
617
- break;
618
- }
619
- currentArgumentValue += char;
620
- break;
621
- case 'after_arguments':
622
- if (char === '|') {
623
- pushModifier();
624
- break;
625
- }
626
- if (char !== ' ') {
627
- throw new Error(`Missing space after ${getLastActionName()}()`);
628
- }
629
- pushInstruction();
630
- break;
631
- }
632
- }
633
- switch (state) {
634
- case 'action':
635
- case 'after_arguments':
636
- if (currentActionName) {
637
- pushInstruction();
638
- }
639
- break;
640
- default:
641
- throw new Error(`Did you forget to add a closing ")" after "${currentActionName}"?`);
642
- }
643
- return directives;
644
- }
645
-
646
- function combineSpacedArray(parts) {
647
- const finalParts = [];
648
- parts.forEach((part) => {
649
- finalParts.push(...trimAll(part).split(' '));
650
- });
651
- return finalParts;
652
- }
653
- function trimAll(str) {
654
- return str.replace(/[\s]+/g, ' ').trim();
655
- }
656
- function normalizeModelName(model) {
657
- return (model
658
- .replace(/\[]$/, '')
659
- .split('[')
660
- .map((s) => s.replace(']', ''))
661
- .join('.'));
662
- }
663
-
664
- function getValueFromElement(element, valueStore) {
665
- if (element instanceof HTMLInputElement) {
666
- if (element.type === 'checkbox') {
667
- const modelNameData = getModelDirectiveFromElement(element, false);
668
- if (modelNameData !== null) {
669
- const modelValue = valueStore.get(modelNameData.action);
670
- if (Array.isArray(modelValue)) {
671
- return getMultipleCheckboxValue(element, modelValue);
672
- }
673
- if (Object(modelValue) === modelValue) {
674
- return getMultipleCheckboxValue(element, Object.values(modelValue));
675
- }
676
- }
677
- if (element.hasAttribute('value')) {
678
- return element.checked ? element.getAttribute('value') : null;
679
- }
680
- return element.checked;
681
- }
682
- return inputValue(element);
683
- }
684
- if (element instanceof HTMLSelectElement) {
685
- if (element.multiple) {
686
- return Array.from(element.selectedOptions).map((el) => el.value);
687
- }
688
- return element.value;
689
- }
690
- if (element.dataset.value) {
691
- return element.dataset.value;
692
- }
693
- if ('value' in element) {
694
- return element.value;
695
- }
696
- if (element.hasAttribute('value')) {
697
- return element.getAttribute('value');
698
- }
699
- return null;
700
- }
701
- function setValueOnElement(element, value) {
702
- if (element instanceof HTMLInputElement) {
703
- if (element.type === 'file') {
704
- return;
705
- }
706
- if (element.type === 'radio') {
707
- element.checked = element.value == value;
708
- return;
709
- }
710
- if (element.type === 'checkbox') {
711
- if (Array.isArray(value)) {
712
- element.checked = value.some((val) => val == element.value);
713
- }
714
- else if (element.hasAttribute('value')) {
715
- element.checked = element.value == value;
716
- }
717
- else {
718
- element.checked = value;
719
- }
720
- return;
721
- }
722
- }
723
- if (element instanceof HTMLSelectElement) {
724
- const arrayWrappedValue = [].concat(value).map((value) => {
725
- return `${value}`;
726
- });
727
- Array.from(element.options).forEach((option) => {
728
- option.selected = arrayWrappedValue.includes(option.value);
729
- });
730
- return;
731
- }
732
- value = value === undefined ? '' : value;
733
- element.value = value;
734
- }
735
- function getAllModelDirectiveFromElements(element) {
736
- if (!element.dataset.model) {
737
- return [];
738
- }
739
- const directives = parseDirectives(element.dataset.model);
740
- directives.forEach((directive) => {
741
- if (directive.args.length > 0) {
742
- throw new Error(`The data-model="${element.dataset.model}" format is invalid: it does not support passing arguments to the model.`);
743
- }
744
- directive.action = normalizeModelName(directive.action);
745
- });
746
- return directives;
747
- }
748
- function getModelDirectiveFromElement(element, throwOnMissing = true) {
749
- const dataModelDirectives = getAllModelDirectiveFromElements(element);
750
- if (dataModelDirectives.length > 0) {
751
- return dataModelDirectives[0];
752
- }
753
- if (element.getAttribute('name')) {
754
- const formElement = element.closest('form');
755
- if (formElement && 'model' in formElement.dataset) {
756
- const directives = parseDirectives(formElement.dataset.model || '*');
757
- const directive = directives[0];
758
- if (directive.args.length > 0) {
759
- throw new Error(`The data-model="${formElement.dataset.model}" format is invalid: it does not support passing arguments to the model.`);
760
- }
761
- directive.action = normalizeModelName(element.getAttribute('name'));
762
- return directive;
763
- }
764
- }
765
- if (!throwOnMissing) {
766
- return null;
767
- }
768
- throw new Error(`Cannot determine the model name for "${getElementAsTagText(element)}": the element must either have a "data-model" (or "name" attribute living inside a <form data-model="*">).`);
769
- }
770
- function elementBelongsToThisComponent(element, component) {
771
- if (component.element === element) {
772
- return true;
773
- }
774
- if (!component.element.contains(element)) {
775
- return false;
776
- }
777
- const closestLiveComponent = element.closest('[data-controller~="live"]');
778
- return closestLiveComponent === component.element;
779
- }
780
- function cloneHTMLElement(element) {
781
- const newElement = element.cloneNode(true);
782
- if (!(newElement instanceof HTMLElement)) {
783
- throw new Error('Could not clone element');
784
- }
785
- return newElement;
786
- }
787
- function htmlToElement(html) {
788
- const template = document.createElement('template');
789
- html = html.trim();
790
- template.innerHTML = html;
791
- if (template.content.childElementCount > 1) {
792
- throw new Error(`Component HTML contains ${template.content.childElementCount} elements, but only 1 root element is allowed.`);
793
- }
794
- const child = template.content.firstElementChild;
795
- if (!child) {
796
- throw new Error('Child not found');
797
- }
798
- if (!(child instanceof HTMLElement)) {
799
- throw new Error(`Created element is not an HTMLElement: ${html.trim()}`);
800
- }
801
- return child;
802
- }
803
- const getMultipleCheckboxValue = (element, currentValues) => {
804
- const finalValues = [...currentValues];
805
- const value = inputValue(element);
806
- const index = currentValues.indexOf(value);
807
- if (element.checked) {
808
- if (index === -1) {
809
- finalValues.push(value);
810
- }
811
- return finalValues;
812
- }
813
- if (index > -1) {
814
- finalValues.splice(index, 1);
815
- }
816
- return finalValues;
817
- };
818
- const inputValue = (element) => element.dataset.value ? element.dataset.value : element.value;
819
-
820
- // base IIFE to define idiomorph
821
- var Idiomorph = (function () {
822
-
823
- //=============================================================================
824
- // AND NOW IT BEGINS...
825
- //=============================================================================
826
- let EMPTY_SET = new Set();
827
-
828
- // default configuration values, updatable by users now
829
- let defaults = {
830
- morphStyle: "outerHTML",
831
- callbacks : {
832
- beforeNodeAdded: noOp,
833
- afterNodeAdded: noOp,
834
- beforeNodeMorphed: noOp,
835
- afterNodeMorphed: noOp,
836
- beforeNodeRemoved: noOp,
837
- afterNodeRemoved: noOp,
838
- beforeAttributeUpdated: noOp,
839
-
840
- },
841
- head: {
842
- style: 'merge',
843
- shouldPreserve: function (elt) {
844
- return elt.getAttribute("im-preserve") === "true";
845
- },
846
- shouldReAppend: function (elt) {
847
- return elt.getAttribute("im-re-append") === "true";
848
- },
849
- shouldRemove: noOp,
850
- afterHeadMorphed: noOp,
851
- }
852
- };
853
-
854
- //=============================================================================
855
- // Core Morphing Algorithm - morph, morphNormalizedContent, morphOldNodeTo, morphChildren
856
- //=============================================================================
857
- function morph(oldNode, newContent, config = {}) {
858
-
859
- if (oldNode instanceof Document) {
860
- oldNode = oldNode.documentElement;
861
- }
862
-
863
- if (typeof newContent === 'string') {
864
- newContent = parseContent(newContent);
865
- }
866
-
867
- let normalizedContent = normalizeContent(newContent);
868
-
869
- let ctx = createMorphContext(oldNode, normalizedContent, config);
870
-
871
- return morphNormalizedContent(oldNode, normalizedContent, ctx);
549
+
550
+ let normalizedContent = normalizeContent(newContent);
551
+
552
+ let ctx = createMorphContext(oldNode, normalizedContent, config);
553
+
554
+ return morphNormalizedContent(oldNode, normalizedContent, ctx);
872
555
  }
873
556
 
874
557
  function morphNormalizedContent(oldNode, normalizedNewContent, ctx) {
@@ -1442,352 +1125,669 @@ var Idiomorph = (function () {
1442
1125
  return null;
1443
1126
  }
1444
1127
  }
1445
-
1446
- // advanced to the next old content child
1447
- potentialSoftMatch = potentialSoftMatch.nextSibling;
1128
+
1129
+ // advanced to the next old content child
1130
+ potentialSoftMatch = potentialSoftMatch.nextSibling;
1131
+ }
1132
+
1133
+ return potentialSoftMatch;
1134
+ }
1135
+
1136
+ function parseContent(newContent) {
1137
+ let parser = new DOMParser();
1138
+
1139
+ // remove svgs to avoid false-positive matches on head, etc.
1140
+ let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
1141
+
1142
+ // if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping
1143
+ if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
1144
+ let content = parser.parseFromString(newContent, "text/html");
1145
+ // if it is a full HTML document, return the document itself as the parent container
1146
+ if (contentWithSvgsRemoved.match(/<\/html>/)) {
1147
+ content.generatedByIdiomorph = true;
1148
+ return content;
1149
+ } else {
1150
+ // otherwise return the html element as the parent container
1151
+ let htmlElement = content.firstChild;
1152
+ if (htmlElement) {
1153
+ htmlElement.generatedByIdiomorph = true;
1154
+ return htmlElement;
1155
+ } else {
1156
+ return null;
1157
+ }
1158
+ }
1159
+ } else {
1160
+ // if it is partial HTML, wrap it in a template tag to provide a parent element and also to help
1161
+ // deal with touchy tags like tr, tbody, etc.
1162
+ let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
1163
+ let content = responseDoc.body.querySelector('template').content;
1164
+ content.generatedByIdiomorph = true;
1165
+ return content
1166
+ }
1167
+ }
1168
+
1169
+ function normalizeContent(newContent) {
1170
+ if (newContent == null) {
1171
+ // noinspection UnnecessaryLocalVariableJS
1172
+ const dummyParent = document.createElement('div');
1173
+ return dummyParent;
1174
+ } else if (newContent.generatedByIdiomorph) {
1175
+ // the template tag created by idiomorph parsing can serve as a dummy parent
1176
+ return newContent;
1177
+ } else if (newContent instanceof Node) {
1178
+ // a single node is added as a child to a dummy parent
1179
+ const dummyParent = document.createElement('div');
1180
+ dummyParent.append(newContent);
1181
+ return dummyParent;
1182
+ } else {
1183
+ // all nodes in the array or HTMLElement collection are consolidated under
1184
+ // a single dummy parent element
1185
+ const dummyParent = document.createElement('div');
1186
+ for (const elt of [...newContent]) {
1187
+ dummyParent.append(elt);
1188
+ }
1189
+ return dummyParent;
1190
+ }
1191
+ }
1192
+
1193
+ function insertSiblings(previousSibling, morphedNode, nextSibling) {
1194
+ let stack = [];
1195
+ let added = [];
1196
+ while (previousSibling != null) {
1197
+ stack.push(previousSibling);
1198
+ previousSibling = previousSibling.previousSibling;
1199
+ }
1200
+ while (stack.length > 0) {
1201
+ let node = stack.pop();
1202
+ added.push(node); // push added preceding siblings on in order and insert
1203
+ morphedNode.parentElement.insertBefore(node, morphedNode);
1204
+ }
1205
+ added.push(morphedNode);
1206
+ while (nextSibling != null) {
1207
+ stack.push(nextSibling);
1208
+ added.push(nextSibling); // here we are going in order, so push on as we scan, rather than add
1209
+ nextSibling = nextSibling.nextSibling;
1210
+ }
1211
+ while (stack.length > 0) {
1212
+ morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
1213
+ }
1214
+ return added;
1215
+ }
1216
+
1217
+ function findBestNodeMatch(newContent, oldNode, ctx) {
1218
+ let currentElement;
1219
+ currentElement = newContent.firstChild;
1220
+ let bestElement = currentElement;
1221
+ let score = 0;
1222
+ while (currentElement) {
1223
+ let newScore = scoreElement(currentElement, oldNode, ctx);
1224
+ if (newScore > score) {
1225
+ bestElement = currentElement;
1226
+ score = newScore;
1227
+ }
1228
+ currentElement = currentElement.nextSibling;
1229
+ }
1230
+ return bestElement;
1231
+ }
1232
+
1233
+ function scoreElement(node1, node2, ctx) {
1234
+ if (isSoftMatch(node1, node2)) {
1235
+ return .5 + getIdIntersectionCount(ctx, node1, node2);
1236
+ }
1237
+ return 0;
1238
+ }
1239
+
1240
+ function removeNode(tempNode, ctx) {
1241
+ removeIdsFromConsideration(ctx, tempNode);
1242
+ if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return;
1243
+
1244
+ tempNode.remove();
1245
+ ctx.callbacks.afterNodeRemoved(tempNode);
1246
+ }
1247
+
1248
+ //=============================================================================
1249
+ // ID Set Functions
1250
+ //=============================================================================
1251
+
1252
+ function isIdInConsideration(ctx, id) {
1253
+ return !ctx.deadIds.has(id);
1254
+ }
1255
+
1256
+ function idIsWithinNode(ctx, id, targetNode) {
1257
+ let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
1258
+ return idSet.has(id);
1259
+ }
1260
+
1261
+ function removeIdsFromConsideration(ctx, node) {
1262
+ let idSet = ctx.idMap.get(node) || EMPTY_SET;
1263
+ for (const id of idSet) {
1264
+ ctx.deadIds.add(id);
1265
+ }
1266
+ }
1267
+
1268
+ function getIdIntersectionCount(ctx, node1, node2) {
1269
+ let sourceSet = ctx.idMap.get(node1) || EMPTY_SET;
1270
+ let matchCount = 0;
1271
+ for (const id of sourceSet) {
1272
+ // a potential match is an id in the source and potentialIdsSet, but
1273
+ // that has not already been merged into the DOM
1274
+ if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) {
1275
+ ++matchCount;
1276
+ }
1277
+ }
1278
+ return matchCount;
1279
+ }
1280
+
1281
+ /**
1282
+ * A bottom up algorithm that finds all elements with ids inside of the node
1283
+ * argument and populates id sets for those nodes and all their parents, generating
1284
+ * a set of ids contained within all nodes for the entire hierarchy in the DOM
1285
+ *
1286
+ * @param node {Element}
1287
+ * @param {Map<Node, Set<String>>} idMap
1288
+ */
1289
+ function populateIdMapForNode(node, idMap) {
1290
+ let nodeParent = node.parentElement;
1291
+ // find all elements with an id property
1292
+ let idElements = node.querySelectorAll('[id]');
1293
+ for (const elt of idElements) {
1294
+ let current = elt;
1295
+ // walk up the parent hierarchy of that element, adding the id
1296
+ // of element to the parent's id set
1297
+ while (current !== nodeParent && current != null) {
1298
+ let idSet = idMap.get(current);
1299
+ // if the id set doesn't exist, create it and insert it in the map
1300
+ if (idSet == null) {
1301
+ idSet = new Set();
1302
+ idMap.set(current, idSet);
1303
+ }
1304
+ idSet.add(elt.id);
1305
+ current = current.parentElement;
1306
+ }
1448
1307
  }
1308
+ }
1449
1309
 
1450
- return potentialSoftMatch;
1310
+ /**
1311
+ * This function computes a map of nodes to all ids contained within that node (inclusive of the
1312
+ * node). This map can be used to ask if two nodes have intersecting sets of ids, which allows
1313
+ * for a looser definition of "matching" than tradition id matching, and allows child nodes
1314
+ * to contribute to a parent nodes matching.
1315
+ *
1316
+ * @param {Element} oldContent the old content that will be morphed
1317
+ * @param {Element} newContent the new content to morph to
1318
+ * @returns {Map<Node, Set<String>>} a map of nodes to id sets for the
1319
+ */
1320
+ function createIdMap(oldContent, newContent) {
1321
+ let idMap = new Map();
1322
+ populateIdMapForNode(oldContent, idMap);
1323
+ populateIdMapForNode(newContent, idMap);
1324
+ return idMap;
1451
1325
  }
1452
1326
 
1453
- function parseContent(newContent) {
1454
- let parser = new DOMParser();
1327
+ //=============================================================================
1328
+ // This is what ends up becoming the Idiomorph global object
1329
+ //=============================================================================
1330
+ return {
1331
+ morph,
1332
+ defaults
1333
+ }
1334
+ })();
1455
1335
 
1456
- // remove svgs to avoid false-positive matches on head, etc.
1457
- let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
1336
+ function normalizeAttributesForComparison(element) {
1337
+ const isFileInput = element instanceof HTMLInputElement && element.type === 'file';
1338
+ if (!isFileInput) {
1339
+ if ('value' in element) {
1340
+ element.setAttribute('value', element.value);
1341
+ }
1342
+ else if (element.hasAttribute('value')) {
1343
+ element.setAttribute('value', '');
1344
+ }
1345
+ }
1346
+ Array.from(element.children).forEach((child) => {
1347
+ normalizeAttributesForComparison(child);
1348
+ });
1349
+ }
1458
1350
 
1459
- // if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping
1460
- if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
1461
- let content = parser.parseFromString(newContent, "text/html");
1462
- // if it is a full HTML document, return the document itself as the parent container
1463
- if (contentWithSvgsRemoved.match(/<\/html>/)) {
1464
- content.generatedByIdiomorph = true;
1465
- return content;
1466
- } else {
1467
- // otherwise return the html element as the parent container
1468
- let htmlElement = content.firstChild;
1469
- if (htmlElement) {
1470
- htmlElement.generatedByIdiomorph = true;
1471
- return htmlElement;
1472
- } else {
1473
- return null;
1351
+ const syncAttributes = (fromEl, toEl) => {
1352
+ for (let i = 0; i < fromEl.attributes.length; i++) {
1353
+ const attr = fromEl.attributes[i];
1354
+ toEl.setAttribute(attr.name, attr.value);
1355
+ }
1356
+ };
1357
+ function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements, getElementValue, externalMutationTracker) {
1358
+ const originalElementIdsToSwapAfter = [];
1359
+ const originalElementsToPreserve = new Map();
1360
+ const markElementAsNeedingPostMorphSwap = (id, replaceWithClone) => {
1361
+ const oldElement = originalElementsToPreserve.get(id);
1362
+ if (!(oldElement instanceof HTMLElement)) {
1363
+ throw new Error(`Original element with id ${id} not found`);
1364
+ }
1365
+ originalElementIdsToSwapAfter.push(id);
1366
+ if (!replaceWithClone) {
1367
+ return null;
1368
+ }
1369
+ const clonedOldElement = cloneHTMLElement(oldElement);
1370
+ oldElement.replaceWith(clonedOldElement);
1371
+ return clonedOldElement;
1372
+ };
1373
+ rootToElement.querySelectorAll('[data-live-preserve]').forEach((newElement) => {
1374
+ const id = newElement.id;
1375
+ if (!id) {
1376
+ throw new Error('The data-live-preserve attribute requires an id attribute to be set on the element');
1377
+ }
1378
+ const oldElement = rootFromElement.querySelector(`#${id}`);
1379
+ if (!(oldElement instanceof HTMLElement)) {
1380
+ throw new Error(`The element with id "${id}" was not found in the original HTML`);
1381
+ }
1382
+ newElement.removeAttribute('data-live-preserve');
1383
+ originalElementsToPreserve.set(id, oldElement);
1384
+ syncAttributes(newElement, oldElement);
1385
+ });
1386
+ Idiomorph.morph(rootFromElement, rootToElement, {
1387
+ callbacks: {
1388
+ beforeNodeMorphed: (fromEl, toEl) => {
1389
+ if (!(fromEl instanceof Element) || !(toEl instanceof Element)) {
1390
+ return true;
1391
+ }
1392
+ if (fromEl === rootFromElement) {
1393
+ return true;
1394
+ }
1395
+ if (fromEl.id && originalElementsToPreserve.has(fromEl.id)) {
1396
+ if (fromEl.id === toEl.id) {
1397
+ return false;
1398
+ }
1399
+ const clonedFromEl = markElementAsNeedingPostMorphSwap(fromEl.id, true);
1400
+ if (!clonedFromEl) {
1401
+ throw new Error('missing clone');
1474
1402
  }
1403
+ Idiomorph.morph(clonedFromEl, toEl);
1404
+ return false;
1475
1405
  }
1476
- } else {
1477
- // if it is partial HTML, wrap it in a template tag to provide a parent element and also to help
1478
- // deal with touchy tags like tr, tbody, etc.
1479
- let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
1480
- let content = responseDoc.body.querySelector('template').content;
1481
- content.generatedByIdiomorph = true;
1482
- return content
1483
- }
1484
- }
1485
-
1486
- function normalizeContent(newContent) {
1487
- if (newContent == null) {
1488
- // noinspection UnnecessaryLocalVariableJS
1489
- const dummyParent = document.createElement('div');
1490
- return dummyParent;
1491
- } else if (newContent.generatedByIdiomorph) {
1492
- // the template tag created by idiomorph parsing can serve as a dummy parent
1493
- return newContent;
1494
- } else if (newContent instanceof Node) {
1495
- // a single node is added as a child to a dummy parent
1496
- const dummyParent = document.createElement('div');
1497
- dummyParent.append(newContent);
1498
- return dummyParent;
1499
- } else {
1500
- // all nodes in the array or HTMLElement collection are consolidated under
1501
- // a single dummy parent element
1502
- const dummyParent = document.createElement('div');
1503
- for (const elt of [...newContent]) {
1504
- dummyParent.append(elt);
1406
+ if (fromEl instanceof HTMLElement && toEl instanceof HTMLElement) {
1407
+ if (typeof fromEl.__x !== 'undefined') {
1408
+ if (!window.Alpine) {
1409
+ 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.');
1410
+ }
1411
+ if (typeof window.Alpine.morph !== 'function') {
1412
+ 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.');
1413
+ }
1414
+ window.Alpine.morph(fromEl.__x, toEl);
1415
+ }
1416
+ if (externalMutationTracker.wasElementAdded(fromEl)) {
1417
+ fromEl.insertAdjacentElement('afterend', toEl);
1418
+ return false;
1419
+ }
1420
+ if (modifiedFieldElements.includes(fromEl)) {
1421
+ setValueOnElement(toEl, getElementValue(fromEl));
1422
+ }
1423
+ if (fromEl === document.activeElement &&
1424
+ fromEl !== document.body &&
1425
+ null !== getModelDirectiveFromElement(fromEl, false)) {
1426
+ setValueOnElement(toEl, getElementValue(fromEl));
1427
+ }
1428
+ const elementChanges = externalMutationTracker.getChangedElement(fromEl);
1429
+ if (elementChanges) {
1430
+ elementChanges.applyToElement(toEl);
1431
+ }
1432
+ if (fromEl.nodeName.toUpperCase() !== 'OPTION' && fromEl.isEqualNode(toEl)) {
1433
+ const normalizedFromEl = cloneHTMLElement(fromEl);
1434
+ normalizeAttributesForComparison(normalizedFromEl);
1435
+ const normalizedToEl = cloneHTMLElement(toEl);
1436
+ normalizeAttributesForComparison(normalizedToEl);
1437
+ if (normalizedFromEl.isEqualNode(normalizedToEl)) {
1438
+ return false;
1439
+ }
1440
+ }
1505
1441
  }
1506
- return dummyParent;
1507
- }
1442
+ if (fromEl.hasAttribute('data-skip-morph') || (fromEl.id && fromEl.id !== toEl.id)) {
1443
+ fromEl.innerHTML = toEl.innerHTML;
1444
+ return true;
1445
+ }
1446
+ if (fromEl.parentElement?.hasAttribute('data-skip-morph')) {
1447
+ return false;
1448
+ }
1449
+ return !fromEl.hasAttribute('data-live-ignore');
1450
+ },
1451
+ beforeNodeRemoved(node) {
1452
+ if (!(node instanceof HTMLElement)) {
1453
+ return true;
1454
+ }
1455
+ if (node.id && originalElementsToPreserve.has(node.id)) {
1456
+ markElementAsNeedingPostMorphSwap(node.id, false);
1457
+ return true;
1458
+ }
1459
+ if (externalMutationTracker.wasElementAdded(node)) {
1460
+ return false;
1461
+ }
1462
+ return !node.hasAttribute('data-live-ignore');
1463
+ },
1464
+ },
1465
+ });
1466
+ originalElementIdsToSwapAfter.forEach((id) => {
1467
+ const newElement = rootFromElement.querySelector(`#${id}`);
1468
+ const originalElement = originalElementsToPreserve.get(id);
1469
+ if (!(newElement instanceof HTMLElement) || !(originalElement instanceof HTMLElement)) {
1470
+ throw new Error('Missing elements.');
1508
1471
  }
1472
+ newElement.replaceWith(originalElement);
1473
+ });
1474
+ }
1509
1475
 
1510
- function insertSiblings(previousSibling, morphedNode, nextSibling) {
1511
- let stack = [];
1512
- let added = [];
1513
- while (previousSibling != null) {
1514
- stack.push(previousSibling);
1515
- previousSibling = previousSibling.previousSibling;
1516
- }
1517
- while (stack.length > 0) {
1518
- let node = stack.pop();
1519
- added.push(node); // push added preceding siblings on in order and insert
1520
- morphedNode.parentElement.insertBefore(node, morphedNode);
1521
- }
1522
- added.push(morphedNode);
1523
- while (nextSibling != null) {
1524
- stack.push(nextSibling);
1525
- added.push(nextSibling); // here we are going in order, so push on as we scan, rather than add
1526
- nextSibling = nextSibling.nextSibling;
1476
+ class ChangingItemsTracker {
1477
+ constructor() {
1478
+ this.changedItems = new Map();
1479
+ this.removedItems = new Map();
1480
+ }
1481
+ setItem(itemName, newValue, previousValue) {
1482
+ if (this.removedItems.has(itemName)) {
1483
+ const removedRecord = this.removedItems.get(itemName);
1484
+ this.removedItems.delete(itemName);
1485
+ if (removedRecord.original === newValue) {
1486
+ return;
1527
1487
  }
1528
- while (stack.length > 0) {
1529
- morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
1488
+ }
1489
+ if (this.changedItems.has(itemName)) {
1490
+ const originalRecord = this.changedItems.get(itemName);
1491
+ if (originalRecord.original === newValue) {
1492
+ this.changedItems.delete(itemName);
1493
+ return;
1530
1494
  }
1531
- return added;
1495
+ this.changedItems.set(itemName, { original: originalRecord.original, new: newValue });
1496
+ return;
1532
1497
  }
1533
-
1534
- function findBestNodeMatch(newContent, oldNode, ctx) {
1535
- let currentElement;
1536
- currentElement = newContent.firstChild;
1537
- let bestElement = currentElement;
1538
- let score = 0;
1539
- while (currentElement) {
1540
- let newScore = scoreElement(currentElement, oldNode, ctx);
1541
- if (newScore > score) {
1542
- bestElement = currentElement;
1543
- score = newScore;
1544
- }
1545
- currentElement = currentElement.nextSibling;
1498
+ this.changedItems.set(itemName, { original: previousValue, new: newValue });
1499
+ }
1500
+ removeItem(itemName, currentValue) {
1501
+ let trueOriginalValue = currentValue;
1502
+ if (this.changedItems.has(itemName)) {
1503
+ const originalRecord = this.changedItems.get(itemName);
1504
+ trueOriginalValue = originalRecord.original;
1505
+ this.changedItems.delete(itemName);
1506
+ if (trueOriginalValue === null) {
1507
+ return;
1546
1508
  }
1547
- return bestElement;
1548
1509
  }
1549
-
1550
- function scoreElement(node1, node2, ctx) {
1551
- if (isSoftMatch(node1, node2)) {
1552
- return .5 + getIdIntersectionCount(ctx, node1, node2);
1553
- }
1554
- return 0;
1510
+ if (!this.removedItems.has(itemName)) {
1511
+ this.removedItems.set(itemName, { original: trueOriginalValue });
1555
1512
  }
1513
+ }
1514
+ getChangedItems() {
1515
+ return Array.from(this.changedItems, ([name, { new: value }]) => ({ name, value }));
1516
+ }
1517
+ getRemovedItems() {
1518
+ return Array.from(this.removedItems.keys());
1519
+ }
1520
+ isEmpty() {
1521
+ return this.changedItems.size === 0 && this.removedItems.size === 0;
1522
+ }
1523
+ }
1556
1524
 
1557
- function removeNode(tempNode, ctx) {
1558
- removeIdsFromConsideration(ctx, tempNode);
1559
- if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return;
1560
-
1561
- tempNode.remove();
1562
- ctx.callbacks.afterNodeRemoved(tempNode);
1525
+ class ElementChanges {
1526
+ constructor() {
1527
+ this.addedClasses = new Set();
1528
+ this.removedClasses = new Set();
1529
+ this.styleChanges = new ChangingItemsTracker();
1530
+ this.attributeChanges = new ChangingItemsTracker();
1531
+ }
1532
+ addClass(className) {
1533
+ if (!this.removedClasses.delete(className)) {
1534
+ this.addedClasses.add(className);
1563
1535
  }
1564
-
1565
- //=============================================================================
1566
- // ID Set Functions
1567
- //=============================================================================
1568
-
1569
- function isIdInConsideration(ctx, id) {
1570
- return !ctx.deadIds.has(id);
1536
+ }
1537
+ removeClass(className) {
1538
+ if (!this.addedClasses.delete(className)) {
1539
+ this.removedClasses.add(className);
1571
1540
  }
1541
+ }
1542
+ addStyle(styleName, newValue, originalValue) {
1543
+ this.styleChanges.setItem(styleName, newValue, originalValue);
1544
+ }
1545
+ removeStyle(styleName, originalValue) {
1546
+ this.styleChanges.removeItem(styleName, originalValue);
1547
+ }
1548
+ addAttribute(attributeName, newValue, originalValue) {
1549
+ this.attributeChanges.setItem(attributeName, newValue, originalValue);
1550
+ }
1551
+ removeAttribute(attributeName, originalValue) {
1552
+ this.attributeChanges.removeItem(attributeName, originalValue);
1553
+ }
1554
+ getAddedClasses() {
1555
+ return [...this.addedClasses];
1556
+ }
1557
+ getRemovedClasses() {
1558
+ return [...this.removedClasses];
1559
+ }
1560
+ getChangedStyles() {
1561
+ return this.styleChanges.getChangedItems();
1562
+ }
1563
+ getRemovedStyles() {
1564
+ return this.styleChanges.getRemovedItems();
1565
+ }
1566
+ getChangedAttributes() {
1567
+ return this.attributeChanges.getChangedItems();
1568
+ }
1569
+ getRemovedAttributes() {
1570
+ return this.attributeChanges.getRemovedItems();
1571
+ }
1572
+ applyToElement(element) {
1573
+ element.classList.add(...this.addedClasses);
1574
+ element.classList.remove(...this.removedClasses);
1575
+ this.styleChanges.getChangedItems().forEach((change) => {
1576
+ element.style.setProperty(change.name, change.value);
1577
+ return;
1578
+ });
1579
+ this.styleChanges.getRemovedItems().forEach((styleName) => {
1580
+ element.style.removeProperty(styleName);
1581
+ });
1582
+ this.attributeChanges.getChangedItems().forEach((change) => {
1583
+ element.setAttribute(change.name, change.value);
1584
+ });
1585
+ this.attributeChanges.getRemovedItems().forEach((attributeName) => {
1586
+ element.removeAttribute(attributeName);
1587
+ });
1588
+ }
1589
+ isEmpty() {
1590
+ return (this.addedClasses.size === 0 &&
1591
+ this.removedClasses.size === 0 &&
1592
+ this.styleChanges.isEmpty() &&
1593
+ this.attributeChanges.isEmpty());
1594
+ }
1595
+ }
1572
1596
 
1573
- function idIsWithinNode(ctx, id, targetNode) {
1574
- let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
1575
- return idSet.has(id);
1597
+ class ExternalMutationTracker {
1598
+ constructor(element, shouldTrackChangeCallback) {
1599
+ this.changedElements = new WeakMap();
1600
+ this.changedElementsCount = 0;
1601
+ this.addedElements = [];
1602
+ this.removedElements = [];
1603
+ this.isStarted = false;
1604
+ this.element = element;
1605
+ this.shouldTrackChangeCallback = shouldTrackChangeCallback;
1606
+ this.mutationObserver = new MutationObserver(this.onMutations.bind(this));
1607
+ }
1608
+ start() {
1609
+ if (this.isStarted) {
1610
+ return;
1576
1611
  }
1577
-
1578
- function removeIdsFromConsideration(ctx, node) {
1579
- let idSet = ctx.idMap.get(node) || EMPTY_SET;
1580
- for (const id of idSet) {
1581
- ctx.deadIds.add(id);
1582
- }
1612
+ this.mutationObserver.observe(this.element, {
1613
+ childList: true,
1614
+ subtree: true,
1615
+ attributes: true,
1616
+ attributeOldValue: true,
1617
+ });
1618
+ this.isStarted = true;
1619
+ }
1620
+ stop() {
1621
+ if (this.isStarted) {
1622
+ this.mutationObserver.disconnect();
1623
+ this.isStarted = false;
1583
1624
  }
1584
-
1585
- function getIdIntersectionCount(ctx, node1, node2) {
1586
- let sourceSet = ctx.idMap.get(node1) || EMPTY_SET;
1587
- let matchCount = 0;
1588
- for (const id of sourceSet) {
1589
- // a potential match is an id in the source and potentialIdsSet, but
1590
- // that has not already been merged into the DOM
1591
- if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) {
1592
- ++matchCount;
1593
- }
1625
+ }
1626
+ getChangedElement(element) {
1627
+ return this.changedElements.has(element) ? this.changedElements.get(element) : null;
1628
+ }
1629
+ getAddedElements() {
1630
+ return this.addedElements;
1631
+ }
1632
+ wasElementAdded(element) {
1633
+ return this.addedElements.includes(element);
1634
+ }
1635
+ handlePendingChanges() {
1636
+ this.onMutations(this.mutationObserver.takeRecords());
1637
+ }
1638
+ onMutations(mutations) {
1639
+ const handledAttributeMutations = new WeakMap();
1640
+ for (const mutation of mutations) {
1641
+ const element = mutation.target;
1642
+ if (!this.shouldTrackChangeCallback(element)) {
1643
+ continue;
1594
1644
  }
1595
- return matchCount;
1596
- }
1597
-
1598
- /**
1599
- * A bottom up algorithm that finds all elements with ids inside of the node
1600
- * argument and populates id sets for those nodes and all their parents, generating
1601
- * a set of ids contained within all nodes for the entire hierarchy in the DOM
1602
- *
1603
- * @param node {Element}
1604
- * @param {Map<Node, Set<String>>} idMap
1605
- */
1606
- function populateIdMapForNode(node, idMap) {
1607
- let nodeParent = node.parentElement;
1608
- // find all elements with an id property
1609
- let idElements = node.querySelectorAll('[id]');
1610
- for (const elt of idElements) {
1611
- let current = elt;
1612
- // walk up the parent hierarchy of that element, adding the id
1613
- // of element to the parent's id set
1614
- while (current !== nodeParent && current != null) {
1615
- let idSet = idMap.get(current);
1616
- // if the id set doesn't exist, create it and insert it in the map
1617
- if (idSet == null) {
1618
- idSet = new Set();
1619
- idMap.set(current, idSet);
1620
- }
1621
- idSet.add(elt.id);
1622
- current = current.parentElement;
1645
+ if (this.isElementAddedByTranslation(element)) {
1646
+ continue;
1647
+ }
1648
+ let isChangeInAddedElement = false;
1649
+ for (const addedElement of this.addedElements) {
1650
+ if (addedElement.contains(element)) {
1651
+ isChangeInAddedElement = true;
1652
+ break;
1623
1653
  }
1624
1654
  }
1655
+ if (isChangeInAddedElement) {
1656
+ continue;
1657
+ }
1658
+ switch (mutation.type) {
1659
+ case 'childList':
1660
+ this.handleChildListMutation(mutation);
1661
+ break;
1662
+ case 'attributes':
1663
+ if (!handledAttributeMutations.has(element)) {
1664
+ handledAttributeMutations.set(element, []);
1665
+ }
1666
+ if (!handledAttributeMutations.get(element).includes(mutation.attributeName)) {
1667
+ this.handleAttributeMutation(mutation);
1668
+ handledAttributeMutations.set(element, [
1669
+ ...handledAttributeMutations.get(element),
1670
+ mutation.attributeName,
1671
+ ]);
1672
+ }
1673
+ break;
1674
+ }
1625
1675
  }
1626
-
1627
- /**
1628
- * This function computes a map of nodes to all ids contained within that node (inclusive of the
1629
- * node). This map can be used to ask if two nodes have intersecting sets of ids, which allows
1630
- * for a looser definition of "matching" than tradition id matching, and allows child nodes
1631
- * to contribute to a parent nodes matching.
1632
- *
1633
- * @param {Element} oldContent the old content that will be morphed
1634
- * @param {Element} newContent the new content to morph to
1635
- * @returns {Map<Node, Set<String>>} a map of nodes to id sets for the
1636
- */
1637
- function createIdMap(oldContent, newContent) {
1638
- let idMap = new Map();
1639
- populateIdMapForNode(oldContent, idMap);
1640
- populateIdMapForNode(newContent, idMap);
1641
- return idMap;
1642
- }
1643
-
1644
- //=============================================================================
1645
- // This is what ends up becoming the Idiomorph global object
1646
- //=============================================================================
1647
- return {
1648
- morph,
1649
- defaults
1676
+ }
1677
+ handleChildListMutation(mutation) {
1678
+ mutation.addedNodes.forEach((node) => {
1679
+ if (!(node instanceof Element)) {
1680
+ return;
1681
+ }
1682
+ if (this.removedElements.includes(node)) {
1683
+ this.removedElements.splice(this.removedElements.indexOf(node), 1);
1684
+ return;
1685
+ }
1686
+ if (this.isElementAddedByTranslation(node)) {
1687
+ return;
1688
+ }
1689
+ this.addedElements.push(node);
1690
+ });
1691
+ mutation.removedNodes.forEach((node) => {
1692
+ if (!(node instanceof Element)) {
1693
+ return;
1694
+ }
1695
+ if (this.addedElements.includes(node)) {
1696
+ this.addedElements.splice(this.addedElements.indexOf(node), 1);
1697
+ return;
1698
+ }
1699
+ this.removedElements.push(node);
1700
+ });
1701
+ }
1702
+ handleAttributeMutation(mutation) {
1703
+ const element = mutation.target;
1704
+ if (!this.changedElements.has(element)) {
1705
+ this.changedElements.set(element, new ElementChanges());
1706
+ this.changedElementsCount++;
1650
1707
  }
1651
- })();
1652
-
1653
- function normalizeAttributesForComparison(element) {
1654
- const isFileInput = element instanceof HTMLInputElement && element.type === 'file';
1655
- if (!isFileInput) {
1656
- if ('value' in element) {
1657
- element.setAttribute('value', element.value);
1708
+ const changedElement = this.changedElements.get(element);
1709
+ switch (mutation.attributeName) {
1710
+ case 'class':
1711
+ this.handleClassAttributeMutation(mutation, changedElement);
1712
+ break;
1713
+ case 'style':
1714
+ this.handleStyleAttributeMutation(mutation, changedElement);
1715
+ break;
1716
+ default:
1717
+ this.handleGenericAttributeMutation(mutation, changedElement);
1658
1718
  }
1659
- else if (element.hasAttribute('value')) {
1660
- element.setAttribute('value', '');
1719
+ if (changedElement.isEmpty()) {
1720
+ this.changedElements.delete(element);
1721
+ this.changedElementsCount--;
1661
1722
  }
1662
1723
  }
1663
- Array.from(element.children).forEach((child) => {
1664
- normalizeAttributesForComparison(child);
1665
- });
1666
- }
1667
-
1668
- const syncAttributes = (fromEl, toEl) => {
1669
- for (let i = 0; i < fromEl.attributes.length; i++) {
1670
- const attr = fromEl.attributes[i];
1671
- toEl.setAttribute(attr.name, attr.value);
1724
+ handleClassAttributeMutation(mutation, elementChanges) {
1725
+ const element = mutation.target;
1726
+ const previousValue = mutation.oldValue || '';
1727
+ const previousValues = previousValue.match(/(\S+)/gu) || [];
1728
+ const newValues = [].slice.call(element.classList);
1729
+ const addedValues = newValues.filter((value) => !previousValues.includes(value));
1730
+ const removedValues = previousValues.filter((value) => !newValues.includes(value));
1731
+ addedValues.forEach((value) => {
1732
+ elementChanges.addClass(value);
1733
+ });
1734
+ removedValues.forEach((value) => {
1735
+ elementChanges.removeClass(value);
1736
+ });
1672
1737
  }
1673
- };
1674
- function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements, getElementValue, externalMutationTracker) {
1675
- const originalElementIdsToSwapAfter = [];
1676
- const originalElementsToPreserve = new Map();
1677
- const markElementAsNeedingPostMorphSwap = (id, replaceWithClone) => {
1678
- const oldElement = originalElementsToPreserve.get(id);
1679
- if (!(oldElement instanceof HTMLElement)) {
1680
- throw new Error(`Original element with id ${id} not found`);
1681
- }
1682
- originalElementIdsToSwapAfter.push(id);
1683
- if (!replaceWithClone) {
1684
- return null;
1738
+ handleStyleAttributeMutation(mutation, elementChanges) {
1739
+ const element = mutation.target;
1740
+ const previousValue = mutation.oldValue || '';
1741
+ const previousStyles = this.extractStyles(previousValue);
1742
+ const newValue = element.getAttribute('style') || '';
1743
+ const newStyles = this.extractStyles(newValue);
1744
+ const addedOrChangedStyles = Object.keys(newStyles).filter((key) => previousStyles[key] === undefined || previousStyles[key] !== newStyles[key]);
1745
+ const removedStyles = Object.keys(previousStyles).filter((key) => !newStyles[key]);
1746
+ addedOrChangedStyles.forEach((style) => {
1747
+ elementChanges.addStyle(style, newStyles[style], previousStyles[style] === undefined ? null : previousStyles[style]);
1748
+ });
1749
+ removedStyles.forEach((style) => {
1750
+ elementChanges.removeStyle(style, previousStyles[style]);
1751
+ });
1752
+ }
1753
+ handleGenericAttributeMutation(mutation, elementChanges) {
1754
+ const attributeName = mutation.attributeName;
1755
+ const element = mutation.target;
1756
+ let oldValue = mutation.oldValue;
1757
+ let newValue = element.getAttribute(attributeName);
1758
+ if (oldValue === attributeName) {
1759
+ oldValue = '';
1685
1760
  }
1686
- const clonedOldElement = cloneHTMLElement(oldElement);
1687
- oldElement.replaceWith(clonedOldElement);
1688
- return clonedOldElement;
1689
- };
1690
- rootToElement.querySelectorAll('[data-live-preserve]').forEach((newElement) => {
1691
- const id = newElement.id;
1692
- if (!id) {
1693
- throw new Error('The data-live-preserve attribute requires an id attribute to be set on the element');
1761
+ if (newValue === attributeName) {
1762
+ newValue = '';
1694
1763
  }
1695
- const oldElement = rootFromElement.querySelector(`#${id}`);
1696
- if (!(oldElement instanceof HTMLElement)) {
1697
- throw new Error(`The element with id "${id}" was not found in the original HTML`);
1764
+ if (!element.hasAttribute(attributeName)) {
1765
+ if (oldValue === null) {
1766
+ return;
1767
+ }
1768
+ elementChanges.removeAttribute(attributeName, mutation.oldValue);
1769
+ return;
1698
1770
  }
1699
- newElement.removeAttribute('data-live-preserve');
1700
- originalElementsToPreserve.set(id, oldElement);
1701
- syncAttributes(newElement, oldElement);
1702
- });
1703
- Idiomorph.morph(rootFromElement, rootToElement, {
1704
- callbacks: {
1705
- beforeNodeMorphed: (fromEl, toEl) => {
1706
- if (!(fromEl instanceof Element) || !(toEl instanceof Element)) {
1707
- return true;
1708
- }
1709
- if (fromEl === rootFromElement) {
1710
- return true;
1711
- }
1712
- if (fromEl.id && originalElementsToPreserve.has(fromEl.id)) {
1713
- if (fromEl.id === toEl.id) {
1714
- return false;
1715
- }
1716
- const clonedFromEl = markElementAsNeedingPostMorphSwap(fromEl.id, true);
1717
- if (!clonedFromEl) {
1718
- throw new Error('missing clone');
1719
- }
1720
- Idiomorph.morph(clonedFromEl, toEl);
1721
- return false;
1722
- }
1723
- if (fromEl instanceof HTMLElement && toEl instanceof HTMLElement) {
1724
- if (typeof fromEl.__x !== 'undefined') {
1725
- if (!window.Alpine) {
1726
- 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.');
1727
- }
1728
- if (typeof window.Alpine.morph !== 'function') {
1729
- 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.');
1730
- }
1731
- window.Alpine.morph(fromEl.__x, toEl);
1732
- }
1733
- if (externalMutationTracker.wasElementAdded(fromEl)) {
1734
- fromEl.insertAdjacentElement('afterend', toEl);
1735
- return false;
1736
- }
1737
- if (modifiedFieldElements.includes(fromEl)) {
1738
- setValueOnElement(toEl, getElementValue(fromEl));
1739
- }
1740
- if (fromEl === document.activeElement &&
1741
- fromEl !== document.body &&
1742
- null !== getModelDirectiveFromElement(fromEl, false)) {
1743
- setValueOnElement(toEl, getElementValue(fromEl));
1744
- }
1745
- const elementChanges = externalMutationTracker.getChangedElement(fromEl);
1746
- if (elementChanges) {
1747
- elementChanges.applyToElement(toEl);
1748
- }
1749
- if (fromEl.nodeName.toUpperCase() !== 'OPTION' && fromEl.isEqualNode(toEl)) {
1750
- const normalizedFromEl = cloneHTMLElement(fromEl);
1751
- normalizeAttributesForComparison(normalizedFromEl);
1752
- const normalizedToEl = cloneHTMLElement(toEl);
1753
- normalizeAttributesForComparison(normalizedToEl);
1754
- if (normalizedFromEl.isEqualNode(normalizedToEl)) {
1755
- return false;
1756
- }
1757
- }
1758
- }
1759
- if (fromEl.hasAttribute('data-skip-morph') || (fromEl.id && fromEl.id !== toEl.id)) {
1760
- fromEl.innerHTML = toEl.innerHTML;
1761
- return true;
1762
- }
1763
- if (fromEl.parentElement?.hasAttribute('data-skip-morph')) {
1764
- return false;
1765
- }
1766
- return !fromEl.hasAttribute('data-live-ignore');
1767
- },
1768
- beforeNodeRemoved(node) {
1769
- if (!(node instanceof HTMLElement)) {
1770
- return true;
1771
- }
1772
- if (node.id && originalElementsToPreserve.has(node.id)) {
1773
- markElementAsNeedingPostMorphSwap(node.id, false);
1774
- return true;
1775
- }
1776
- if (externalMutationTracker.wasElementAdded(node)) {
1777
- return false;
1778
- }
1779
- return !node.hasAttribute('data-live-ignore');
1780
- },
1781
- },
1782
- });
1783
- originalElementIdsToSwapAfter.forEach((id) => {
1784
- const newElement = rootFromElement.querySelector(`#${id}`);
1785
- const originalElement = originalElementsToPreserve.get(id);
1786
- if (!(newElement instanceof HTMLElement) || !(originalElement instanceof HTMLElement)) {
1787
- throw new Error('Missing elements.');
1771
+ if (newValue === oldValue) {
1772
+ return;
1788
1773
  }
1789
- newElement.replaceWith(originalElement);
1790
- });
1774
+ elementChanges.addAttribute(attributeName, element.getAttribute(attributeName), mutation.oldValue);
1775
+ }
1776
+ extractStyles(styles) {
1777
+ const styleObject = {};
1778
+ styles.split(';').forEach((style) => {
1779
+ const parts = style.split(':');
1780
+ if (parts.length === 1) {
1781
+ return;
1782
+ }
1783
+ const property = parts[0].trim();
1784
+ styleObject[property] = parts.slice(1).join(':').trim();
1785
+ });
1786
+ return styleObject;
1787
+ }
1788
+ isElementAddedByTranslation(element) {
1789
+ return element.tagName === 'FONT' && element.getAttribute('style') === 'vertical-align: inherit;';
1790
+ }
1791
1791
  }
1792
1792
 
1793
1793
  class UnsyncedInputsTracker {
@@ -2798,7 +2798,7 @@ function fromQueryString(search) {
2798
2798
  const entries = search.split('&').map((i) => i.split('='));
2799
2799
  const data = {};
2800
2800
  entries.forEach(([key, value]) => {
2801
- value = decodeURIComponent(value.replace(/\+/g, '%20'));
2801
+ value = decodeURIComponent(String(value || '').replace(/\+/g, '%20'));
2802
2802
  if (!key.includes('[')) {
2803
2803
  data[key] = value;
2804
2804
  }