@symfony/ux-live-component 2.26.1 → 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.
- package/dist/Component/index.d.ts +1 -1
- package/dist/live_controller.js +944 -944
- package/package.json +1 -1
package/dist/live_controller.js
CHANGED
|
@@ -195,680 +195,363 @@ const findParent = (currentComponent) => {
|
|
|
195
195
|
return null;
|
|
196
196
|
};
|
|
197
197
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
return;
|
|
339
|
+
return element.checked;
|
|
243
340
|
}
|
|
244
|
-
|
|
341
|
+
return inputValue(element);
|
|
245
342
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
261
|
-
return
|
|
349
|
+
if (element.dataset.value) {
|
|
350
|
+
return element.dataset.value;
|
|
262
351
|
}
|
|
263
|
-
|
|
264
|
-
return
|
|
352
|
+
if ('value' in element) {
|
|
353
|
+
return element.value;
|
|
265
354
|
}
|
|
266
|
-
|
|
267
|
-
return
|
|
355
|
+
if (element.hasAttribute('value')) {
|
|
356
|
+
return element.getAttribute('value');
|
|
268
357
|
}
|
|
358
|
+
return null;
|
|
269
359
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
289
|
-
|
|
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
|
-
|
|
292
|
-
|
|
391
|
+
value = value === undefined ? '' : value;
|
|
392
|
+
element.value = value;
|
|
393
|
+
}
|
|
394
|
+
function getAllModelDirectiveFromElements(element) {
|
|
395
|
+
if (!element.dataset.model) {
|
|
396
|
+
return [];
|
|
293
397
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
301
|
-
|
|
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
|
-
|
|
304
|
-
return
|
|
424
|
+
if (!throwOnMissing) {
|
|
425
|
+
return null;
|
|
305
426
|
}
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
310
|
-
return
|
|
433
|
+
if (!component.element.contains(element)) {
|
|
434
|
+
return false;
|
|
311
435
|
}
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
336
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
359
|
-
childList: true,
|
|
360
|
-
subtree: true,
|
|
361
|
-
attributes: true,
|
|
362
|
-
attributeOldValue: true,
|
|
363
|
-
});
|
|
364
|
-
this.isStarted = true;
|
|
470
|
+
return finalValues;
|
|
365
471
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
this.mutationObserver.disconnect();
|
|
369
|
-
this.isStarted = false;
|
|
370
|
-
}
|
|
472
|
+
if (index > -1) {
|
|
473
|
+
finalValues.splice(index, 1);
|
|
371
474
|
}
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
376
|
-
|
|
483
|
+
register(hookName, callback) {
|
|
484
|
+
const hooks = this.hooks.get(hookName) || [];
|
|
485
|
+
hooks.push(callback);
|
|
486
|
+
this.hooks.set(hookName, hooks);
|
|
377
487
|
}
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
382
|
-
this.
|
|
497
|
+
triggerHook(hookName, ...args) {
|
|
498
|
+
const hooks = this.hooks.get(hookName) || [];
|
|
499
|
+
hooks.forEach((callback) => callback(...args));
|
|
383
500
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
402
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1454
|
-
|
|
1327
|
+
//=============================================================================
|
|
1328
|
+
// This is what ends up becoming the Idiomorph global object
|
|
1329
|
+
//=============================================================================
|
|
1330
|
+
return {
|
|
1331
|
+
morph,
|
|
1332
|
+
defaults
|
|
1333
|
+
}
|
|
1334
|
+
})();
|
|
1455
1335
|
|
|
1456
|
-
|
|
1457
|
-
|
|
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
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
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
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
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
|
-
|
|
1529
|
-
|
|
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
|
-
|
|
1495
|
+
this.changedItems.set(itemName, { original: originalRecord.original, new: newValue });
|
|
1496
|
+
return;
|
|
1532
1497
|
}
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
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
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
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
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
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
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
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
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
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
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
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
|
-
|
|
1660
|
-
|
|
1719
|
+
if (changedElement.isEmpty()) {
|
|
1720
|
+
this.changedElements.delete(element);
|
|
1721
|
+
this.changedElementsCount--;
|
|
1661
1722
|
}
|
|
1662
1723
|
}
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
const
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
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
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
const
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
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
|
-
|
|
1687
|
-
|
|
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
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1764
|
+
if (!element.hasAttribute(attributeName)) {
|
|
1765
|
+
if (oldValue === null) {
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
elementChanges.removeAttribute(attributeName, mutation.oldValue);
|
|
1769
|
+
return;
|
|
1698
1770
|
}
|
|
1699
|
-
|
|
1700
|
-
|
|
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
|
-
|
|
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
|
}
|