@symfony/ux-live-component 2.23.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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +14 -0
  3. package/dist/Backend/Backend.d.ts +31 -0
  4. package/dist/Backend/BackendRequest.d.ts +9 -0
  5. package/dist/Backend/BackendResponse.d.ts +6 -0
  6. package/dist/Backend/RequestBuilder.d.ts +17 -0
  7. package/dist/Component/ElementDriver.d.ts +31 -0
  8. package/dist/Component/UnsyncedInputsTracker.d.ts +28 -0
  9. package/dist/Component/ValueStore.d.ts +17 -0
  10. package/dist/Component/index.d.ts +74 -0
  11. package/dist/Component/plugins/ChildComponentPlugin.d.ts +11 -0
  12. package/dist/Component/plugins/LazyPlugin.d.ts +7 -0
  13. package/dist/Component/plugins/LoadingPlugin.d.ts +23 -0
  14. package/dist/Component/plugins/PageUnloadingPlugin.d.ts +6 -0
  15. package/dist/Component/plugins/PluginInterface.d.ts +4 -0
  16. package/dist/Component/plugins/PollingPlugin.d.ts +10 -0
  17. package/dist/Component/plugins/QueryStringPlugin.d.ts +13 -0
  18. package/dist/Component/plugins/SetValueOntoModelFieldsPlugin.d.ts +6 -0
  19. package/dist/Component/plugins/ValidatedFieldsPlugin.d.ts +6 -0
  20. package/dist/ComponentRegistry.d.ts +8 -0
  21. package/dist/Directive/directives_parser.d.ts +11 -0
  22. package/dist/Directive/get_model_binding.d.ts +9 -0
  23. package/dist/HookManager.d.ts +7 -0
  24. package/dist/PollingDirector.d.ts +16 -0
  25. package/dist/Rendering/ChangingItemsTracker.d.ts +12 -0
  26. package/dist/Rendering/ElementChanges.d.ts +26 -0
  27. package/dist/Rendering/ExternalMutationTracker.d.ts +26 -0
  28. package/dist/Util/getElementAsTagText.d.ts +1 -0
  29. package/dist/data_manipulation_utils.d.ts +2 -0
  30. package/dist/dom_utils.d.ts +10 -0
  31. package/dist/live.min.css +1 -0
  32. package/dist/live_controller.d.ts +112 -0
  33. package/dist/live_controller.js +3190 -0
  34. package/dist/morphdom.d.ts +2 -0
  35. package/dist/normalize_attributes_for_comparison.d.ts +1 -0
  36. package/dist/string_utils.d.ts +3 -0
  37. package/dist/url_utils.d.ts +11 -0
  38. package/package.json +60 -0
@@ -0,0 +1,3190 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ class BackendRequest {
4
+ constructor(promise, actions, updateModels) {
5
+ this.isResolved = false;
6
+ this.promise = promise;
7
+ this.promise.then((response) => {
8
+ this.isResolved = true;
9
+ return response;
10
+ });
11
+ this.actions = actions;
12
+ this.updatedModels = updateModels;
13
+ }
14
+ containsOneOfActions(targetedActions) {
15
+ return this.actions.filter((action) => targetedActions.includes(action)).length > 0;
16
+ }
17
+ areAnyModelsUpdated(targetedModels) {
18
+ return this.updatedModels.filter((model) => targetedModels.includes(model)).length > 0;
19
+ }
20
+ }
21
+
22
+ class RequestBuilder {
23
+ constructor(url, method = 'post') {
24
+ this.url = url;
25
+ this.method = method;
26
+ }
27
+ buildRequest(props, actions, updated, children, updatedPropsFromParent, files) {
28
+ const splitUrl = this.url.split('?');
29
+ let [url] = splitUrl;
30
+ const [, queryString] = splitUrl;
31
+ const params = new URLSearchParams(queryString || '');
32
+ const fetchOptions = {};
33
+ fetchOptions.headers = {
34
+ Accept: 'application/vnd.live-component+html',
35
+ 'X-Requested-With': 'XMLHttpRequest',
36
+ };
37
+ const totalFiles = Object.entries(files).reduce((total, current) => total + current.length, 0);
38
+ const hasFingerprints = Object.keys(children).length > 0;
39
+ if (actions.length === 0 &&
40
+ totalFiles === 0 &&
41
+ this.method === 'get' &&
42
+ this.willDataFitInUrl(JSON.stringify(props), JSON.stringify(updated), params, JSON.stringify(children), JSON.stringify(updatedPropsFromParent))) {
43
+ params.set('props', JSON.stringify(props));
44
+ params.set('updated', JSON.stringify(updated));
45
+ if (Object.keys(updatedPropsFromParent).length > 0) {
46
+ params.set('propsFromParent', JSON.stringify(updatedPropsFromParent));
47
+ }
48
+ if (hasFingerprints) {
49
+ params.set('children', JSON.stringify(children));
50
+ }
51
+ fetchOptions.method = 'GET';
52
+ }
53
+ else {
54
+ fetchOptions.method = 'POST';
55
+ const requestData = { props, updated };
56
+ if (Object.keys(updatedPropsFromParent).length > 0) {
57
+ requestData.propsFromParent = updatedPropsFromParent;
58
+ }
59
+ if (hasFingerprints) {
60
+ requestData.children = children;
61
+ }
62
+ if (actions.length > 0) {
63
+ if (actions.length === 1) {
64
+ requestData.args = actions[0].args;
65
+ url += `/${encodeURIComponent(actions[0].name)}`;
66
+ }
67
+ else {
68
+ url += '/_batch';
69
+ requestData.actions = actions;
70
+ }
71
+ }
72
+ const formData = new FormData();
73
+ formData.append('data', JSON.stringify(requestData));
74
+ for (const [key, value] of Object.entries(files)) {
75
+ const length = value.length;
76
+ for (let i = 0; i < length; ++i) {
77
+ formData.append(key, value[i]);
78
+ }
79
+ }
80
+ fetchOptions.body = formData;
81
+ }
82
+ const paramsString = params.toString();
83
+ return {
84
+ url: `${url}${paramsString.length > 0 ? `?${paramsString}` : ''}`,
85
+ fetchOptions,
86
+ };
87
+ }
88
+ willDataFitInUrl(propsJson, updatedJson, params, childrenJson, propsFromParentJson) {
89
+ const urlEncodedJsonData = new URLSearchParams(propsJson + updatedJson + childrenJson + propsFromParentJson).toString();
90
+ return (urlEncodedJsonData + params.toString()).length < 1500;
91
+ }
92
+ }
93
+
94
+ class Backend {
95
+ constructor(url, method = 'post') {
96
+ this.requestBuilder = new RequestBuilder(url, method);
97
+ }
98
+ makeRequest(props, actions, updated, children, updatedPropsFromParent, files) {
99
+ const { url, fetchOptions } = this.requestBuilder.buildRequest(props, actions, updated, children, updatedPropsFromParent, files);
100
+ return new BackendRequest(fetch(url, fetchOptions), actions.map((backendAction) => backendAction.name), Object.keys(updated));
101
+ }
102
+ }
103
+
104
+ class BackendResponse {
105
+ constructor(response) {
106
+ this.response = response;
107
+ }
108
+ async getBody() {
109
+ if (!this.body) {
110
+ this.body = await this.response.text();
111
+ }
112
+ return this.body;
113
+ }
114
+ }
115
+
116
+ function getElementAsTagText(element) {
117
+ return element.innerHTML
118
+ ? element.outerHTML.slice(0, element.outerHTML.indexOf(element.innerHTML))
119
+ : element.outerHTML;
120
+ }
121
+
122
+ let componentMapByElement = new WeakMap();
123
+ let componentMapByComponent = new Map();
124
+ const registerComponent = (component) => {
125
+ componentMapByElement.set(component.element, component);
126
+ componentMapByComponent.set(component, component.name);
127
+ };
128
+ const unregisterComponent = (component) => {
129
+ componentMapByElement.delete(component.element);
130
+ componentMapByComponent.delete(component);
131
+ };
132
+ const getComponent = (element) => new Promise((resolve, reject) => {
133
+ let count = 0;
134
+ const maxCount = 10;
135
+ const interval = setInterval(() => {
136
+ const component = componentMapByElement.get(element);
137
+ if (component) {
138
+ clearInterval(interval);
139
+ resolve(component);
140
+ }
141
+ count++;
142
+ if (count > maxCount) {
143
+ clearInterval(interval);
144
+ reject(new Error(`Component not found for element ${getElementAsTagText(element)}`));
145
+ }
146
+ }, 5);
147
+ });
148
+ const findComponents = (currentComponent, onlyParents, onlyMatchName) => {
149
+ const components = [];
150
+ componentMapByComponent.forEach((componentName, component) => {
151
+ if (onlyParents && (currentComponent === component || !component.element.contains(currentComponent.element))) {
152
+ return;
153
+ }
154
+ if (onlyMatchName && componentName !== onlyMatchName) {
155
+ return;
156
+ }
157
+ components.push(component);
158
+ });
159
+ return components;
160
+ };
161
+ const findChildren = (currentComponent) => {
162
+ const children = [];
163
+ componentMapByComponent.forEach((componentName, component) => {
164
+ if (currentComponent === component) {
165
+ return;
166
+ }
167
+ if (!currentComponent.element.contains(component.element)) {
168
+ return;
169
+ }
170
+ let foundChildComponent = false;
171
+ componentMapByComponent.forEach((childComponentName, childComponent) => {
172
+ if (foundChildComponent) {
173
+ return;
174
+ }
175
+ if (childComponent === component) {
176
+ return;
177
+ }
178
+ if (childComponent.element.contains(component.element)) {
179
+ foundChildComponent = true;
180
+ }
181
+ });
182
+ children.push(component);
183
+ });
184
+ return children;
185
+ };
186
+ const findParent = (currentComponent) => {
187
+ let parentElement = currentComponent.element.parentElement;
188
+ while (parentElement) {
189
+ const component = componentMapByElement.get(parentElement);
190
+ if (component) {
191
+ return component;
192
+ }
193
+ parentElement = parentElement.parentElement;
194
+ }
195
+ return null;
196
+ };
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);
206
+ }
207
+ unregister(hookName, callback) {
208
+ const hooks = this.hooks.get(hookName) || [];
209
+ const index = hooks.indexOf(callback);
210
+ if (index === -1) {
211
+ return;
212
+ }
213
+ hooks.splice(index, 1);
214
+ this.hooks.set(hookName, hooks);
215
+ }
216
+ triggerHook(hookName, ...args) {
217
+ const hooks = this.hooks.get(hookName) || [];
218
+ hooks.forEach((callback) => callback(...args));
219
+ }
220
+ }
221
+
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;
233
+ }
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;
240
+ }
241
+ this.changedItems.set(itemName, { original: originalRecord.original, new: newValue });
242
+ return;
243
+ }
244
+ this.changedItems.set(itemName, { original: previousValue, new: newValue });
245
+ }
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 });
258
+ }
259
+ }
260
+ getChangedItems() {
261
+ return Array.from(this.changedItems, ([name, { new: value }]) => ({ name, value }));
262
+ }
263
+ getRemovedItems() {
264
+ return Array.from(this.removedItems.keys());
265
+ }
266
+ isEmpty() {
267
+ return this.changedItems.size === 0 && this.removedItems.size === 0;
268
+ }
269
+ }
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);
281
+ }
282
+ }
283
+ removeClass(className) {
284
+ if (!this.addedClasses.delete(className)) {
285
+ this.removedClasses.add(className);
286
+ }
287
+ }
288
+ addStyle(styleName, newValue, originalValue) {
289
+ this.styleChanges.setItem(styleName, newValue, originalValue);
290
+ }
291
+ removeStyle(styleName, originalValue) {
292
+ this.styleChanges.removeItem(styleName, originalValue);
293
+ }
294
+ addAttribute(attributeName, newValue, originalValue) {
295
+ this.attributeChanges.setItem(attributeName, newValue, originalValue);
296
+ }
297
+ removeAttribute(attributeName, originalValue) {
298
+ this.attributeChanges.removeItem(attributeName, originalValue);
299
+ }
300
+ getAddedClasses() {
301
+ return [...this.addedClasses];
302
+ }
303
+ getRemovedClasses() {
304
+ return [...this.removedClasses];
305
+ }
306
+ getChangedStyles() {
307
+ return this.styleChanges.getChangedItems();
308
+ }
309
+ getRemovedStyles() {
310
+ return this.styleChanges.getRemovedItems();
311
+ }
312
+ getChangedAttributes() {
313
+ return this.attributeChanges.getChangedItems();
314
+ }
315
+ getRemovedAttributes() {
316
+ return this.attributeChanges.getRemovedItems();
317
+ }
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
+ });
334
+ }
335
+ isEmpty() {
336
+ return (this.addedClasses.size === 0 &&
337
+ this.removedClasses.size === 0 &&
338
+ this.styleChanges.isEmpty() &&
339
+ this.attributeChanges.isEmpty());
340
+ }
341
+ }
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;
357
+ }
358
+ this.mutationObserver.observe(this.element, {
359
+ childList: true,
360
+ subtree: true,
361
+ attributes: true,
362
+ attributeOldValue: true,
363
+ });
364
+ this.isStarted = true;
365
+ }
366
+ stop() {
367
+ if (this.isStarted) {
368
+ this.mutationObserver.disconnect();
369
+ this.isStarted = false;
370
+ }
371
+ }
372
+ getChangedElement(element) {
373
+ return this.changedElements.has(element) ? this.changedElements.get(element) : null;
374
+ }
375
+ getAddedElements() {
376
+ return this.addedElements;
377
+ }
378
+ wasElementAdded(element) {
379
+ return this.addedElements.includes(element);
380
+ }
381
+ handlePendingChanges() {
382
+ this.onMutations(this.mutationObserver.takeRecords());
383
+ }
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
+ }
400
+ }
401
+ if (isChangeInAddedElement) {
402
+ continue;
403
+ }
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;
420
+ }
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);
872
+ }
873
+
874
+ function morphNormalizedContent(oldNode, normalizedNewContent, ctx) {
875
+ if (ctx.head.block) {
876
+ let oldHead = oldNode.querySelector('head');
877
+ let newHead = normalizedNewContent.querySelector('head');
878
+ if (oldHead && newHead) {
879
+ let promises = handleHeadElement(newHead, oldHead, ctx);
880
+ // when head promises resolve, call morph again, ignoring the head tag
881
+ Promise.all(promises).then(function () {
882
+ morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, {
883
+ head: {
884
+ block: false,
885
+ ignore: true
886
+ }
887
+ }));
888
+ });
889
+ return;
890
+ }
891
+ }
892
+
893
+ if (ctx.morphStyle === "innerHTML") {
894
+
895
+ // innerHTML, so we are only updating the children
896
+ morphChildren(normalizedNewContent, oldNode, ctx);
897
+ return oldNode.children;
898
+
899
+ } else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) {
900
+ // otherwise find the best element match in the new content, morph that, and merge its siblings
901
+ // into either side of the best match
902
+ let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
903
+
904
+ // stash the siblings that will need to be inserted on either side of the best match
905
+ let previousSibling = bestMatch?.previousSibling;
906
+ let nextSibling = bestMatch?.nextSibling;
907
+
908
+ // morph it
909
+ let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
910
+
911
+ if (bestMatch) {
912
+ // if there was a best match, merge the siblings in too and return the
913
+ // whole bunch
914
+ return insertSiblings(previousSibling, morphedNode, nextSibling);
915
+ } else {
916
+ // otherwise nothing was added to the DOM
917
+ return []
918
+ }
919
+ } else {
920
+ throw "Do not understand how to morph style " + ctx.morphStyle;
921
+ }
922
+ }
923
+
924
+
925
+ /**
926
+ * @param possibleActiveElement
927
+ * @param ctx
928
+ * @returns {boolean}
929
+ */
930
+ function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
931
+ return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement;
932
+ }
933
+
934
+ /**
935
+ * @param oldNode root node to merge content into
936
+ * @param newContent new content to merge
937
+ * @param ctx the merge context
938
+ * @returns {Element} the element that ended up in the DOM
939
+ */
940
+ function morphOldNodeTo(oldNode, newContent, ctx) {
941
+ if (ctx.ignoreActive && oldNode === document.activeElement) ; else if (newContent == null) {
942
+ if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
943
+
944
+ oldNode.remove();
945
+ ctx.callbacks.afterNodeRemoved(oldNode);
946
+ return null;
947
+ } else if (!isSoftMatch(oldNode, newContent)) {
948
+ if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
949
+ if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode;
950
+
951
+ oldNode.parentElement.replaceChild(newContent, oldNode);
952
+ ctx.callbacks.afterNodeAdded(newContent);
953
+ ctx.callbacks.afterNodeRemoved(oldNode);
954
+ return newContent;
955
+ } else {
956
+ if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode;
957
+
958
+ if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
959
+ handleHeadElement(newContent, oldNode, ctx);
960
+ } else {
961
+ syncNodeFrom(newContent, oldNode, ctx);
962
+ if (!ignoreValueOfActiveElement(oldNode, ctx)) {
963
+ morphChildren(newContent, oldNode, ctx);
964
+ }
965
+ }
966
+ ctx.callbacks.afterNodeMorphed(oldNode, newContent);
967
+ return oldNode;
968
+ }
969
+ }
970
+
971
+ /**
972
+ * This is the core algorithm for matching up children. The idea is to use id sets to try to match up
973
+ * nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but
974
+ * by using id sets, we are able to better match up with content deeper in the DOM.
975
+ *
976
+ * Basic algorithm is, for each node in the new content:
977
+ *
978
+ * - if we have reached the end of the old parent, append the new content
979
+ * - if the new content has an id set match with the current insertion point, morph
980
+ * - search for an id set match
981
+ * - if id set match found, morph
982
+ * - otherwise search for a "soft" match
983
+ * - if a soft match is found, morph
984
+ * - otherwise, prepend the new node before the current insertion point
985
+ *
986
+ * The two search algorithms terminate if competing node matches appear to outweigh what can be achieved
987
+ * with the current node. See findIdSetMatch() and findSoftMatch() for details.
988
+ *
989
+ * @param {Element} newParent the parent element of the new content
990
+ * @param {Element } oldParent the old content that we are merging the new content into
991
+ * @param ctx the merge context
992
+ */
993
+ function morphChildren(newParent, oldParent, ctx) {
994
+
995
+ let nextNewChild = newParent.firstChild;
996
+ let insertionPoint = oldParent.firstChild;
997
+ let newChild;
998
+
999
+ // run through all the new content
1000
+ while (nextNewChild) {
1001
+
1002
+ newChild = nextNewChild;
1003
+ nextNewChild = newChild.nextSibling;
1004
+
1005
+ // if we are at the end of the exiting parent's children, just append
1006
+ if (insertionPoint == null) {
1007
+ if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
1008
+
1009
+ oldParent.appendChild(newChild);
1010
+ ctx.callbacks.afterNodeAdded(newChild);
1011
+ removeIdsFromConsideration(ctx, newChild);
1012
+ continue;
1013
+ }
1014
+
1015
+ // if the current node has an id set match then morph
1016
+ if (isIdSetMatch(newChild, insertionPoint, ctx)) {
1017
+ morphOldNodeTo(insertionPoint, newChild, ctx);
1018
+ insertionPoint = insertionPoint.nextSibling;
1019
+ removeIdsFromConsideration(ctx, newChild);
1020
+ continue;
1021
+ }
1022
+
1023
+ // otherwise search forward in the existing old children for an id set match
1024
+ let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx);
1025
+
1026
+ // if we found a potential match, remove the nodes until that point and morph
1027
+ if (idSetMatch) {
1028
+ insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx);
1029
+ morphOldNodeTo(idSetMatch, newChild, ctx);
1030
+ removeIdsFromConsideration(ctx, newChild);
1031
+ continue;
1032
+ }
1033
+
1034
+ // no id set match found, so scan forward for a soft match for the current node
1035
+ let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx);
1036
+
1037
+ // if we found a soft match for the current node, morph
1038
+ if (softMatch) {
1039
+ insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx);
1040
+ morphOldNodeTo(softMatch, newChild, ctx);
1041
+ removeIdsFromConsideration(ctx, newChild);
1042
+ continue;
1043
+ }
1044
+
1045
+ // abandon all hope of morphing, just insert the new child before the insertion point
1046
+ // and move on
1047
+ if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
1048
+
1049
+ oldParent.insertBefore(newChild, insertionPoint);
1050
+ ctx.callbacks.afterNodeAdded(newChild);
1051
+ removeIdsFromConsideration(ctx, newChild);
1052
+ }
1053
+
1054
+ // remove any remaining old nodes that didn't match up with new content
1055
+ while (insertionPoint !== null) {
1056
+
1057
+ let tempNode = insertionPoint;
1058
+ insertionPoint = insertionPoint.nextSibling;
1059
+ removeNode(tempNode, ctx);
1060
+ }
1061
+ }
1062
+
1063
+ //=============================================================================
1064
+ // Attribute Syncing Code
1065
+ //=============================================================================
1066
+
1067
+ /**
1068
+ * @param attr {String} the attribute to be mutated
1069
+ * @param to {Element} the element that is going to be updated
1070
+ * @param updateType {("update"|"remove")}
1071
+ * @param ctx the merge context
1072
+ * @returns {boolean} true if the attribute should be ignored, false otherwise
1073
+ */
1074
+ function ignoreAttribute(attr, to, updateType, ctx) {
1075
+ if(attr === 'value' && ctx.ignoreActiveValue && to === document.activeElement){
1076
+ return true;
1077
+ }
1078
+ return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false;
1079
+ }
1080
+
1081
+ /**
1082
+ * syncs a given node with another node, copying over all attributes and
1083
+ * inner element state from the 'from' node to the 'to' node
1084
+ *
1085
+ * @param {Element} from the element to copy attributes & state from
1086
+ * @param {Element} to the element to copy attributes & state to
1087
+ * @param ctx the merge context
1088
+ */
1089
+ function syncNodeFrom(from, to, ctx) {
1090
+ let type = from.nodeType;
1091
+
1092
+ // if is an element type, sync the attributes from the
1093
+ // new node into the new node
1094
+ if (type === 1 /* element type */) {
1095
+ const fromAttributes = from.attributes;
1096
+ const toAttributes = to.attributes;
1097
+ for (const fromAttribute of fromAttributes) {
1098
+ if (ignoreAttribute(fromAttribute.name, to, 'update', ctx)) {
1099
+ continue;
1100
+ }
1101
+ if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
1102
+ to.setAttribute(fromAttribute.name, fromAttribute.value);
1103
+ }
1104
+ }
1105
+ // iterate backwards to avoid skipping over items when a delete occurs
1106
+ for (let i = toAttributes.length - 1; 0 <= i; i--) {
1107
+ const toAttribute = toAttributes[i];
1108
+ if (ignoreAttribute(toAttribute.name, to, 'remove', ctx)) {
1109
+ continue;
1110
+ }
1111
+ if (!from.hasAttribute(toAttribute.name)) {
1112
+ to.removeAttribute(toAttribute.name);
1113
+ }
1114
+ }
1115
+ }
1116
+
1117
+ // sync text nodes
1118
+ if (type === 8 /* comment */ || type === 3 /* text */) {
1119
+ if (to.nodeValue !== from.nodeValue) {
1120
+ to.nodeValue = from.nodeValue;
1121
+ }
1122
+ }
1123
+
1124
+ if (!ignoreValueOfActiveElement(to, ctx)) {
1125
+ // sync input values
1126
+ syncInputValue(from, to, ctx);
1127
+ }
1128
+ }
1129
+
1130
+ /**
1131
+ * @param from {Element} element to sync the value from
1132
+ * @param to {Element} element to sync the value to
1133
+ * @param attributeName {String} the attribute name
1134
+ * @param ctx the merge context
1135
+ */
1136
+ function syncBooleanAttribute(from, to, attributeName, ctx) {
1137
+ if (from[attributeName] !== to[attributeName]) {
1138
+ let ignoreUpdate = ignoreAttribute(attributeName, to, 'update', ctx);
1139
+ if (!ignoreUpdate) {
1140
+ to[attributeName] = from[attributeName];
1141
+ }
1142
+ if (from[attributeName]) {
1143
+ if (!ignoreUpdate) {
1144
+ to.setAttribute(attributeName, from[attributeName]);
1145
+ }
1146
+ } else {
1147
+ if (!ignoreAttribute(attributeName, to, 'remove', ctx)) {
1148
+ to.removeAttribute(attributeName);
1149
+ }
1150
+ }
1151
+ }
1152
+ }
1153
+
1154
+ /**
1155
+ * NB: many bothans died to bring us information:
1156
+ *
1157
+ * https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js
1158
+ * https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113
1159
+ *
1160
+ * @param from {Element} the element to sync the input value from
1161
+ * @param to {Element} the element to sync the input value to
1162
+ * @param ctx the merge context
1163
+ */
1164
+ function syncInputValue(from, to, ctx) {
1165
+ if (from instanceof HTMLInputElement &&
1166
+ to instanceof HTMLInputElement &&
1167
+ from.type !== 'file') {
1168
+
1169
+ let fromValue = from.value;
1170
+ let toValue = to.value;
1171
+
1172
+ // sync boolean attributes
1173
+ syncBooleanAttribute(from, to, 'checked', ctx);
1174
+ syncBooleanAttribute(from, to, 'disabled', ctx);
1175
+
1176
+ if (!from.hasAttribute('value')) {
1177
+ if (!ignoreAttribute('value', to, 'remove', ctx)) {
1178
+ to.value = '';
1179
+ to.removeAttribute('value');
1180
+ }
1181
+ } else if (fromValue !== toValue) {
1182
+ if (!ignoreAttribute('value', to, 'update', ctx)) {
1183
+ to.setAttribute('value', fromValue);
1184
+ to.value = fromValue;
1185
+ }
1186
+ }
1187
+ } else if (from instanceof HTMLOptionElement) {
1188
+ syncBooleanAttribute(from, to, 'selected', ctx);
1189
+ } else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
1190
+ let fromValue = from.value;
1191
+ let toValue = to.value;
1192
+ if (ignoreAttribute('value', to, 'update', ctx)) {
1193
+ return;
1194
+ }
1195
+ if (fromValue !== toValue) {
1196
+ to.value = fromValue;
1197
+ }
1198
+ if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
1199
+ to.firstChild.nodeValue = fromValue;
1200
+ }
1201
+ }
1202
+ }
1203
+
1204
+ //=============================================================================
1205
+ // the HEAD tag can be handled specially, either w/ a 'merge' or 'append' style
1206
+ //=============================================================================
1207
+ function handleHeadElement(newHeadTag, currentHead, ctx) {
1208
+
1209
+ let added = [];
1210
+ let removed = [];
1211
+ let preserved = [];
1212
+ let nodesToAppend = [];
1213
+
1214
+ let headMergeStyle = ctx.head.style;
1215
+
1216
+ // put all new head elements into a Map, by their outerHTML
1217
+ let srcToNewHeadNodes = new Map();
1218
+ for (const newHeadChild of newHeadTag.children) {
1219
+ srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
1220
+ }
1221
+
1222
+ // for each elt in the current head
1223
+ for (const currentHeadElt of currentHead.children) {
1224
+
1225
+ // If the current head element is in the map
1226
+ let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
1227
+ let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
1228
+ let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
1229
+ if (inNewContent || isPreserved) {
1230
+ if (isReAppended) {
1231
+ // remove the current version and let the new version replace it and re-execute
1232
+ removed.push(currentHeadElt);
1233
+ } else {
1234
+ // this element already exists and should not be re-appended, so remove it from
1235
+ // the new content map, preserving it in the DOM
1236
+ srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
1237
+ preserved.push(currentHeadElt);
1238
+ }
1239
+ } else {
1240
+ if (headMergeStyle === "append") {
1241
+ // we are appending and this existing element is not new content
1242
+ // so if and only if it is marked for re-append do we do anything
1243
+ if (isReAppended) {
1244
+ removed.push(currentHeadElt);
1245
+ nodesToAppend.push(currentHeadElt);
1246
+ }
1247
+ } else {
1248
+ // if this is a merge, we remove this content since it is not in the new head
1249
+ if (ctx.head.shouldRemove(currentHeadElt) !== false) {
1250
+ removed.push(currentHeadElt);
1251
+ }
1252
+ }
1253
+ }
1254
+ }
1255
+
1256
+ // Push the remaining new head elements in the Map into the
1257
+ // nodes to append to the head tag
1258
+ nodesToAppend.push(...srcToNewHeadNodes.values());
1259
+
1260
+ let promises = [];
1261
+ for (const newNode of nodesToAppend) {
1262
+ let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
1263
+ if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
1264
+ if (newElt.href || newElt.src) {
1265
+ let resolve = null;
1266
+ let promise = new Promise(function (_resolve) {
1267
+ resolve = _resolve;
1268
+ });
1269
+ newElt.addEventListener('load', function () {
1270
+ resolve();
1271
+ });
1272
+ promises.push(promise);
1273
+ }
1274
+ currentHead.appendChild(newElt);
1275
+ ctx.callbacks.afterNodeAdded(newElt);
1276
+ added.push(newElt);
1277
+ }
1278
+ }
1279
+
1280
+ // remove all removed elements, after we have appended the new elements to avoid
1281
+ // additional network requests for things like style sheets
1282
+ for (const removedElement of removed) {
1283
+ if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
1284
+ currentHead.removeChild(removedElement);
1285
+ ctx.callbacks.afterNodeRemoved(removedElement);
1286
+ }
1287
+ }
1288
+
1289
+ ctx.head.afterHeadMorphed(currentHead, {added: added, kept: preserved, removed: removed});
1290
+ return promises;
1291
+ }
1292
+
1293
+ function noOp() {
1294
+ }
1295
+
1296
+ /*
1297
+ Deep merges the config object and the Idiomoroph.defaults object to
1298
+ produce a final configuration object
1299
+ */
1300
+ function mergeDefaults(config) {
1301
+ let finalConfig = {};
1302
+ // copy top level stuff into final config
1303
+ Object.assign(finalConfig, defaults);
1304
+ Object.assign(finalConfig, config);
1305
+
1306
+ // copy callbacks into final config (do this to deep merge the callbacks)
1307
+ finalConfig.callbacks = {};
1308
+ Object.assign(finalConfig.callbacks, defaults.callbacks);
1309
+ Object.assign(finalConfig.callbacks, config.callbacks);
1310
+
1311
+ // copy head config into final config (do this to deep merge the head)
1312
+ finalConfig.head = {};
1313
+ Object.assign(finalConfig.head, defaults.head);
1314
+ Object.assign(finalConfig.head, config.head);
1315
+ return finalConfig;
1316
+ }
1317
+
1318
+ function createMorphContext(oldNode, newContent, config) {
1319
+ config = mergeDefaults(config);
1320
+ return {
1321
+ target: oldNode,
1322
+ newContent: newContent,
1323
+ config: config,
1324
+ morphStyle: config.morphStyle,
1325
+ ignoreActive: config.ignoreActive,
1326
+ ignoreActiveValue: config.ignoreActiveValue,
1327
+ idMap: createIdMap(oldNode, newContent),
1328
+ deadIds: new Set(),
1329
+ callbacks: config.callbacks,
1330
+ head: config.head
1331
+ }
1332
+ }
1333
+
1334
+ function isIdSetMatch(node1, node2, ctx) {
1335
+ if (node1 == null || node2 == null) {
1336
+ return false;
1337
+ }
1338
+ if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) {
1339
+ if (node1.id !== "" && node1.id === node2.id) {
1340
+ return true;
1341
+ } else {
1342
+ return getIdIntersectionCount(ctx, node1, node2) > 0;
1343
+ }
1344
+ }
1345
+ return false;
1346
+ }
1347
+
1348
+ function isSoftMatch(node1, node2) {
1349
+ if (node1 == null || node2 == null) {
1350
+ return false;
1351
+ }
1352
+ return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName
1353
+ }
1354
+
1355
+ function removeNodesBetween(startInclusive, endExclusive, ctx) {
1356
+ while (startInclusive !== endExclusive) {
1357
+ let tempNode = startInclusive;
1358
+ startInclusive = startInclusive.nextSibling;
1359
+ removeNode(tempNode, ctx);
1360
+ }
1361
+ removeIdsFromConsideration(ctx, endExclusive);
1362
+ return endExclusive.nextSibling;
1363
+ }
1364
+
1365
+ //=============================================================================
1366
+ // Scans forward from the insertionPoint in the old parent looking for a potential id match
1367
+ // for the newChild. We stop if we find a potential id match for the new child OR
1368
+ // if the number of potential id matches we are discarding is greater than the
1369
+ // potential id matches for the new child
1370
+ //=============================================================================
1371
+ function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
1372
+
1373
+ // max id matches we are willing to discard in our search
1374
+ let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent);
1375
+
1376
+ let potentialMatch = null;
1377
+
1378
+ // only search forward if there is a possibility of an id match
1379
+ if (newChildPotentialIdCount > 0) {
1380
+ let potentialMatch = insertionPoint;
1381
+ // if there is a possibility of an id match, scan forward
1382
+ // keep track of the potential id match count we are discarding (the
1383
+ // newChildPotentialIdCount must be greater than this to make it likely
1384
+ // worth it)
1385
+ let otherMatchCount = 0;
1386
+ while (potentialMatch != null) {
1387
+
1388
+ // If we have an id match, return the current potential match
1389
+ if (isIdSetMatch(newChild, potentialMatch, ctx)) {
1390
+ return potentialMatch;
1391
+ }
1392
+
1393
+ // computer the other potential matches of this new content
1394
+ otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
1395
+ if (otherMatchCount > newChildPotentialIdCount) {
1396
+ // if we have more potential id matches in _other_ content, we
1397
+ // do not have a good candidate for an id match, so return null
1398
+ return null;
1399
+ }
1400
+
1401
+ // advanced to the next old content child
1402
+ potentialMatch = potentialMatch.nextSibling;
1403
+ }
1404
+ }
1405
+ return potentialMatch;
1406
+ }
1407
+
1408
+ //=============================================================================
1409
+ // Scans forward from the insertionPoint in the old parent looking for a potential soft match
1410
+ // for the newChild. We stop if we find a potential soft match for the new child OR
1411
+ // if we find a potential id match in the old parents children OR if we find two
1412
+ // potential soft matches for the next two pieces of new content
1413
+ //=============================================================================
1414
+ function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
1415
+
1416
+ let potentialSoftMatch = insertionPoint;
1417
+ let nextSibling = newChild.nextSibling;
1418
+ let siblingSoftMatchCount = 0;
1419
+
1420
+ while (potentialSoftMatch != null) {
1421
+
1422
+ if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) {
1423
+ // the current potential soft match has a potential id set match with the remaining new
1424
+ // content so bail out of looking
1425
+ return null;
1426
+ }
1427
+
1428
+ // if we have a soft match with the current node, return it
1429
+ if (isSoftMatch(newChild, potentialSoftMatch)) {
1430
+ return potentialSoftMatch;
1431
+ }
1432
+
1433
+ if (isSoftMatch(nextSibling, potentialSoftMatch)) {
1434
+ // the next new node has a soft match with this node, so
1435
+ // increment the count of future soft matches
1436
+ siblingSoftMatchCount++;
1437
+ nextSibling = nextSibling.nextSibling;
1438
+
1439
+ // If there are two future soft matches, bail to allow the siblings to soft match
1440
+ // so that we don't consume future soft matches for the sake of the current node
1441
+ if (siblingSoftMatchCount >= 2) {
1442
+ return null;
1443
+ }
1444
+ }
1445
+
1446
+ // advanced to the next old content child
1447
+ potentialSoftMatch = potentialSoftMatch.nextSibling;
1448
+ }
1449
+
1450
+ return potentialSoftMatch;
1451
+ }
1452
+
1453
+ function parseContent(newContent) {
1454
+ let parser = new DOMParser();
1455
+
1456
+ // remove svgs to avoid false-positive matches on head, etc.
1457
+ let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
1458
+
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;
1474
+ }
1475
+ }
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);
1505
+ }
1506
+ return dummyParent;
1507
+ }
1508
+ }
1509
+
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;
1527
+ }
1528
+ while (stack.length > 0) {
1529
+ morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
1530
+ }
1531
+ return added;
1532
+ }
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;
1546
+ }
1547
+ return bestElement;
1548
+ }
1549
+
1550
+ function scoreElement(node1, node2, ctx) {
1551
+ if (isSoftMatch(node1, node2)) {
1552
+ return .5 + getIdIntersectionCount(ctx, node1, node2);
1553
+ }
1554
+ return 0;
1555
+ }
1556
+
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);
1563
+ }
1564
+
1565
+ //=============================================================================
1566
+ // ID Set Functions
1567
+ //=============================================================================
1568
+
1569
+ function isIdInConsideration(ctx, id) {
1570
+ return !ctx.deadIds.has(id);
1571
+ }
1572
+
1573
+ function idIsWithinNode(ctx, id, targetNode) {
1574
+ let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
1575
+ return idSet.has(id);
1576
+ }
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
+ }
1583
+ }
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
+ }
1594
+ }
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;
1623
+ }
1624
+ }
1625
+ }
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
1650
+ }
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);
1658
+ }
1659
+ else if (element.hasAttribute('value')) {
1660
+ element.setAttribute('value', '');
1661
+ }
1662
+ }
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);
1672
+ }
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;
1685
+ }
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');
1694
+ }
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`);
1698
+ }
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.');
1788
+ }
1789
+ newElement.replaceWith(originalElement);
1790
+ });
1791
+ }
1792
+
1793
+ class UnsyncedInputsTracker {
1794
+ constructor(component, modelElementResolver) {
1795
+ this.elementEventListeners = [
1796
+ { event: 'input', callback: (event) => this.handleInputEvent(event) },
1797
+ ];
1798
+ this.component = component;
1799
+ this.modelElementResolver = modelElementResolver;
1800
+ this.unsyncedInputs = new UnsyncedInputContainer();
1801
+ }
1802
+ activate() {
1803
+ this.elementEventListeners.forEach(({ event, callback }) => {
1804
+ this.component.element.addEventListener(event, callback);
1805
+ });
1806
+ }
1807
+ deactivate() {
1808
+ this.elementEventListeners.forEach(({ event, callback }) => {
1809
+ this.component.element.removeEventListener(event, callback);
1810
+ });
1811
+ }
1812
+ markModelAsSynced(modelName) {
1813
+ this.unsyncedInputs.markModelAsSynced(modelName);
1814
+ }
1815
+ handleInputEvent(event) {
1816
+ const target = event.target;
1817
+ if (!target) {
1818
+ return;
1819
+ }
1820
+ this.updateModelFromElement(target);
1821
+ }
1822
+ updateModelFromElement(element) {
1823
+ if (!elementBelongsToThisComponent(element, this.component)) {
1824
+ return;
1825
+ }
1826
+ if (!(element instanceof HTMLElement)) {
1827
+ throw new Error('Could not update model for non HTMLElement');
1828
+ }
1829
+ const modelName = this.modelElementResolver.getModelName(element);
1830
+ this.unsyncedInputs.add(element, modelName);
1831
+ }
1832
+ getUnsyncedInputs() {
1833
+ return this.unsyncedInputs.allUnsyncedInputs();
1834
+ }
1835
+ getUnsyncedModels() {
1836
+ return Array.from(this.unsyncedInputs.getUnsyncedModelNames());
1837
+ }
1838
+ resetUnsyncedFields() {
1839
+ this.unsyncedInputs.resetUnsyncedFields();
1840
+ }
1841
+ }
1842
+ class UnsyncedInputContainer {
1843
+ constructor() {
1844
+ this.unsyncedNonModelFields = [];
1845
+ this.unsyncedModelNames = [];
1846
+ this.unsyncedModelFields = new Map();
1847
+ }
1848
+ add(element, modelName = null) {
1849
+ if (modelName) {
1850
+ this.unsyncedModelFields.set(modelName, element);
1851
+ if (!this.unsyncedModelNames.includes(modelName)) {
1852
+ this.unsyncedModelNames.push(modelName);
1853
+ }
1854
+ return;
1855
+ }
1856
+ this.unsyncedNonModelFields.push(element);
1857
+ }
1858
+ resetUnsyncedFields() {
1859
+ this.unsyncedModelFields.forEach((value, key) => {
1860
+ if (!this.unsyncedModelNames.includes(key)) {
1861
+ this.unsyncedModelFields.delete(key);
1862
+ }
1863
+ });
1864
+ }
1865
+ allUnsyncedInputs() {
1866
+ return [...this.unsyncedNonModelFields, ...this.unsyncedModelFields.values()];
1867
+ }
1868
+ markModelAsSynced(modelName) {
1869
+ const index = this.unsyncedModelNames.indexOf(modelName);
1870
+ if (index !== -1) {
1871
+ this.unsyncedModelNames.splice(index, 1);
1872
+ }
1873
+ }
1874
+ getUnsyncedModelNames() {
1875
+ return this.unsyncedModelNames;
1876
+ }
1877
+ }
1878
+
1879
+ function getDeepData(data, propertyPath) {
1880
+ const { currentLevelData, finalKey } = parseDeepData(data, propertyPath);
1881
+ if (currentLevelData === undefined) {
1882
+ return undefined;
1883
+ }
1884
+ return currentLevelData[finalKey];
1885
+ }
1886
+ const parseDeepData = (data, propertyPath) => {
1887
+ const finalData = JSON.parse(JSON.stringify(data));
1888
+ let currentLevelData = finalData;
1889
+ const parts = propertyPath.split('.');
1890
+ for (let i = 0; i < parts.length - 1; i++) {
1891
+ currentLevelData = currentLevelData[parts[i]];
1892
+ }
1893
+ const finalKey = parts[parts.length - 1];
1894
+ return {
1895
+ currentLevelData,
1896
+ finalData,
1897
+ finalKey,
1898
+ parts,
1899
+ };
1900
+ };
1901
+
1902
+ class ValueStore {
1903
+ constructor(props) {
1904
+ this.props = {};
1905
+ this.dirtyProps = {};
1906
+ this.pendingProps = {};
1907
+ this.updatedPropsFromParent = {};
1908
+ this.props = props;
1909
+ }
1910
+ get(name) {
1911
+ const normalizedName = normalizeModelName(name);
1912
+ if (this.dirtyProps[normalizedName] !== undefined) {
1913
+ return this.dirtyProps[normalizedName];
1914
+ }
1915
+ if (this.pendingProps[normalizedName] !== undefined) {
1916
+ return this.pendingProps[normalizedName];
1917
+ }
1918
+ if (this.props[normalizedName] !== undefined) {
1919
+ return this.props[normalizedName];
1920
+ }
1921
+ return getDeepData(this.props, normalizedName);
1922
+ }
1923
+ has(name) {
1924
+ return this.get(name) !== undefined;
1925
+ }
1926
+ set(name, value) {
1927
+ const normalizedName = normalizeModelName(name);
1928
+ if (this.get(normalizedName) === value) {
1929
+ return false;
1930
+ }
1931
+ this.dirtyProps[normalizedName] = value;
1932
+ return true;
1933
+ }
1934
+ getOriginalProps() {
1935
+ return { ...this.props };
1936
+ }
1937
+ getDirtyProps() {
1938
+ return { ...this.dirtyProps };
1939
+ }
1940
+ getUpdatedPropsFromParent() {
1941
+ return { ...this.updatedPropsFromParent };
1942
+ }
1943
+ flushDirtyPropsToPending() {
1944
+ this.pendingProps = { ...this.dirtyProps };
1945
+ this.dirtyProps = {};
1946
+ }
1947
+ reinitializeAllProps(props) {
1948
+ this.props = props;
1949
+ this.updatedPropsFromParent = {};
1950
+ this.pendingProps = {};
1951
+ }
1952
+ pushPendingPropsBackToDirty() {
1953
+ this.dirtyProps = { ...this.pendingProps, ...this.dirtyProps };
1954
+ this.pendingProps = {};
1955
+ }
1956
+ storeNewPropsFromParent(props) {
1957
+ let changed = false;
1958
+ for (const [key, value] of Object.entries(props)) {
1959
+ const currentValue = this.get(key);
1960
+ if (currentValue !== value) {
1961
+ changed = true;
1962
+ }
1963
+ }
1964
+ if (changed) {
1965
+ this.updatedPropsFromParent = props;
1966
+ }
1967
+ return changed;
1968
+ }
1969
+ }
1970
+
1971
+ class Component {
1972
+ constructor(element, name, props, listeners, id, backend, elementDriver) {
1973
+ this.fingerprint = '';
1974
+ this.defaultDebounce = 150;
1975
+ this.backendRequest = null;
1976
+ this.pendingActions = [];
1977
+ this.pendingFiles = {};
1978
+ this.isRequestPending = false;
1979
+ this.requestDebounceTimeout = null;
1980
+ this.element = element;
1981
+ this.name = name;
1982
+ this.backend = backend;
1983
+ this.elementDriver = elementDriver;
1984
+ this.id = id;
1985
+ this.listeners = new Map();
1986
+ listeners.forEach((listener) => {
1987
+ if (!this.listeners.has(listener.event)) {
1988
+ this.listeners.set(listener.event, []);
1989
+ }
1990
+ this.listeners.get(listener.event)?.push(listener.action);
1991
+ });
1992
+ this.valueStore = new ValueStore(props);
1993
+ this.unsyncedInputsTracker = new UnsyncedInputsTracker(this, elementDriver);
1994
+ this.hooks = new HookManager();
1995
+ this.resetPromise();
1996
+ this.externalMutationTracker = new ExternalMutationTracker(this.element, (element) => elementBelongsToThisComponent(element, this));
1997
+ this.externalMutationTracker.start();
1998
+ }
1999
+ addPlugin(plugin) {
2000
+ plugin.attachToComponent(this);
2001
+ }
2002
+ connect() {
2003
+ registerComponent(this);
2004
+ this.hooks.triggerHook('connect', this);
2005
+ this.unsyncedInputsTracker.activate();
2006
+ this.externalMutationTracker.start();
2007
+ }
2008
+ disconnect() {
2009
+ unregisterComponent(this);
2010
+ this.hooks.triggerHook('disconnect', this);
2011
+ this.clearRequestDebounceTimeout();
2012
+ this.unsyncedInputsTracker.deactivate();
2013
+ this.externalMutationTracker.stop();
2014
+ }
2015
+ on(hookName, callback) {
2016
+ this.hooks.register(hookName, callback);
2017
+ }
2018
+ off(hookName, callback) {
2019
+ this.hooks.unregister(hookName, callback);
2020
+ }
2021
+ set(model, value, reRender = false, debounce = false) {
2022
+ const promise = this.nextRequestPromise;
2023
+ const modelName = normalizeModelName(model);
2024
+ if (!this.valueStore.has(modelName)) {
2025
+ throw new Error(`Invalid model name "${model}".`);
2026
+ }
2027
+ const isChanged = this.valueStore.set(modelName, value);
2028
+ this.hooks.triggerHook('model:set', model, value, this);
2029
+ this.unsyncedInputsTracker.markModelAsSynced(modelName);
2030
+ if (reRender && isChanged) {
2031
+ this.debouncedStartRequest(debounce);
2032
+ }
2033
+ return promise;
2034
+ }
2035
+ getData(model) {
2036
+ const modelName = normalizeModelName(model);
2037
+ if (!this.valueStore.has(modelName)) {
2038
+ throw new Error(`Invalid model "${model}".`);
2039
+ }
2040
+ return this.valueStore.get(modelName);
2041
+ }
2042
+ action(name, args = {}, debounce = false) {
2043
+ const promise = this.nextRequestPromise;
2044
+ this.pendingActions.push({
2045
+ name,
2046
+ args,
2047
+ });
2048
+ this.debouncedStartRequest(debounce);
2049
+ return promise;
2050
+ }
2051
+ files(key, input) {
2052
+ this.pendingFiles[key] = input;
2053
+ }
2054
+ render() {
2055
+ const promise = this.nextRequestPromise;
2056
+ this.tryStartingRequest();
2057
+ return promise;
2058
+ }
2059
+ getUnsyncedModels() {
2060
+ return this.unsyncedInputsTracker.getUnsyncedModels();
2061
+ }
2062
+ emit(name, data, onlyMatchingComponentsNamed = null) {
2063
+ this.performEmit(name, data, false, onlyMatchingComponentsNamed);
2064
+ }
2065
+ emitUp(name, data, onlyMatchingComponentsNamed = null) {
2066
+ this.performEmit(name, data, true, onlyMatchingComponentsNamed);
2067
+ }
2068
+ emitSelf(name, data) {
2069
+ this.doEmit(name, data);
2070
+ }
2071
+ performEmit(name, data, emitUp, matchingName) {
2072
+ const components = findComponents(this, emitUp, matchingName);
2073
+ components.forEach((component) => {
2074
+ component.doEmit(name, data);
2075
+ });
2076
+ }
2077
+ doEmit(name, data) {
2078
+ if (!this.listeners.has(name)) {
2079
+ return;
2080
+ }
2081
+ const actions = this.listeners.get(name) || [];
2082
+ actions.forEach((action) => {
2083
+ this.action(action, data, 1);
2084
+ });
2085
+ }
2086
+ isTurboEnabled() {
2087
+ return typeof Turbo !== 'undefined' && !this.element.closest('[data-turbo="false"]');
2088
+ }
2089
+ tryStartingRequest() {
2090
+ if (!this.backendRequest) {
2091
+ this.performRequest();
2092
+ return;
2093
+ }
2094
+ this.isRequestPending = true;
2095
+ }
2096
+ performRequest() {
2097
+ const thisPromiseResolve = this.nextRequestPromiseResolve;
2098
+ this.resetPromise();
2099
+ this.unsyncedInputsTracker.resetUnsyncedFields();
2100
+ const filesToSend = {};
2101
+ for (const [key, value] of Object.entries(this.pendingFiles)) {
2102
+ if (value.files) {
2103
+ filesToSend[key] = value.files;
2104
+ }
2105
+ }
2106
+ const requestConfig = {
2107
+ props: this.valueStore.getOriginalProps(),
2108
+ actions: this.pendingActions,
2109
+ updated: this.valueStore.getDirtyProps(),
2110
+ children: {},
2111
+ updatedPropsFromParent: this.valueStore.getUpdatedPropsFromParent(),
2112
+ files: filesToSend,
2113
+ };
2114
+ this.hooks.triggerHook('request:started', requestConfig);
2115
+ this.backendRequest = this.backend.makeRequest(requestConfig.props, requestConfig.actions, requestConfig.updated, requestConfig.children, requestConfig.updatedPropsFromParent, requestConfig.files);
2116
+ this.hooks.triggerHook('loading.state:started', this.element, this.backendRequest);
2117
+ this.pendingActions = [];
2118
+ this.valueStore.flushDirtyPropsToPending();
2119
+ this.isRequestPending = false;
2120
+ this.backendRequest.promise.then(async (response) => {
2121
+ const backendResponse = new BackendResponse(response);
2122
+ const html = await backendResponse.getBody();
2123
+ for (const input of Object.values(this.pendingFiles)) {
2124
+ input.value = '';
2125
+ }
2126
+ const headers = backendResponse.response.headers;
2127
+ if (!headers.get('Content-Type')?.includes('application/vnd.live-component+html') &&
2128
+ !headers.get('X-Live-Redirect')) {
2129
+ const controls = { displayError: true };
2130
+ this.valueStore.pushPendingPropsBackToDirty();
2131
+ this.hooks.triggerHook('response:error', backendResponse, controls);
2132
+ if (controls.displayError) {
2133
+ this.renderError(html);
2134
+ }
2135
+ this.backendRequest = null;
2136
+ thisPromiseResolve(backendResponse);
2137
+ return response;
2138
+ }
2139
+ this.processRerender(html, backendResponse);
2140
+ this.backendRequest = null;
2141
+ thisPromiseResolve(backendResponse);
2142
+ if (this.isRequestPending) {
2143
+ this.isRequestPending = false;
2144
+ this.performRequest();
2145
+ }
2146
+ return response;
2147
+ });
2148
+ }
2149
+ processRerender(html, backendResponse) {
2150
+ const controls = { shouldRender: true };
2151
+ this.hooks.triggerHook('render:started', html, backendResponse, controls);
2152
+ if (!controls.shouldRender) {
2153
+ return;
2154
+ }
2155
+ if (backendResponse.response.headers.get('Location')) {
2156
+ if (this.isTurboEnabled()) {
2157
+ Turbo.visit(backendResponse.response.headers.get('Location'));
2158
+ }
2159
+ else {
2160
+ window.location.href = backendResponse.response.headers.get('Location') || '';
2161
+ }
2162
+ return;
2163
+ }
2164
+ this.hooks.triggerHook('loading.state:finished', this.element);
2165
+ const modifiedModelValues = {};
2166
+ Object.keys(this.valueStore.getDirtyProps()).forEach((modelName) => {
2167
+ modifiedModelValues[modelName] = this.valueStore.get(modelName);
2168
+ });
2169
+ let newElement;
2170
+ try {
2171
+ newElement = htmlToElement(html);
2172
+ if (!newElement.matches('[data-controller~=live]')) {
2173
+ throw new Error('A live component template must contain a single root controller element.');
2174
+ }
2175
+ }
2176
+ catch (error) {
2177
+ console.error(`There was a problem with the '${this.name}' component HTML returned:`, {
2178
+ id: this.id,
2179
+ });
2180
+ throw error;
2181
+ }
2182
+ this.externalMutationTracker.handlePendingChanges();
2183
+ this.externalMutationTracker.stop();
2184
+ executeMorphdom(this.element, newElement, this.unsyncedInputsTracker.getUnsyncedInputs(), (element) => getValueFromElement(element, this.valueStore), this.externalMutationTracker);
2185
+ this.externalMutationTracker.start();
2186
+ const newProps = this.elementDriver.getComponentProps();
2187
+ this.valueStore.reinitializeAllProps(newProps);
2188
+ const eventsToEmit = this.elementDriver.getEventsToEmit();
2189
+ const browserEventsToDispatch = this.elementDriver.getBrowserEventsToDispatch();
2190
+ Object.keys(modifiedModelValues).forEach((modelName) => {
2191
+ this.valueStore.set(modelName, modifiedModelValues[modelName]);
2192
+ });
2193
+ eventsToEmit.forEach(({ event, data, target, componentName }) => {
2194
+ if (target === 'up') {
2195
+ this.emitUp(event, data, componentName);
2196
+ return;
2197
+ }
2198
+ if (target === 'self') {
2199
+ this.emitSelf(event, data);
2200
+ return;
2201
+ }
2202
+ this.emit(event, data, componentName);
2203
+ });
2204
+ browserEventsToDispatch.forEach(({ event, payload }) => {
2205
+ this.element.dispatchEvent(new CustomEvent(event, {
2206
+ detail: payload,
2207
+ bubbles: true,
2208
+ }));
2209
+ });
2210
+ this.hooks.triggerHook('render:finished', this);
2211
+ }
2212
+ calculateDebounce(debounce) {
2213
+ if (debounce === true) {
2214
+ return this.defaultDebounce;
2215
+ }
2216
+ if (debounce === false) {
2217
+ return 0;
2218
+ }
2219
+ return debounce;
2220
+ }
2221
+ clearRequestDebounceTimeout() {
2222
+ if (this.requestDebounceTimeout) {
2223
+ clearTimeout(this.requestDebounceTimeout);
2224
+ this.requestDebounceTimeout = null;
2225
+ }
2226
+ }
2227
+ debouncedStartRequest(debounce) {
2228
+ this.clearRequestDebounceTimeout();
2229
+ this.requestDebounceTimeout = window.setTimeout(() => {
2230
+ this.render();
2231
+ }, this.calculateDebounce(debounce));
2232
+ }
2233
+ renderError(html) {
2234
+ let modal = document.getElementById('live-component-error');
2235
+ if (modal) {
2236
+ modal.innerHTML = '';
2237
+ }
2238
+ else {
2239
+ modal = document.createElement('div');
2240
+ modal.id = 'live-component-error';
2241
+ modal.style.padding = '50px';
2242
+ modal.style.backgroundColor = 'rgba(0, 0, 0, .5)';
2243
+ modal.style.zIndex = '100000';
2244
+ modal.style.position = 'fixed';
2245
+ modal.style.top = '0px';
2246
+ modal.style.bottom = '0px';
2247
+ modal.style.left = '0px';
2248
+ modal.style.right = '0px';
2249
+ modal.style.display = 'flex';
2250
+ modal.style.flexDirection = 'column';
2251
+ }
2252
+ const iframe = document.createElement('iframe');
2253
+ iframe.style.borderRadius = '5px';
2254
+ iframe.style.flexGrow = '1';
2255
+ modal.appendChild(iframe);
2256
+ document.body.prepend(modal);
2257
+ document.body.style.overflow = 'hidden';
2258
+ if (iframe.contentWindow) {
2259
+ iframe.contentWindow.document.open();
2260
+ iframe.contentWindow.document.write(html);
2261
+ iframe.contentWindow.document.close();
2262
+ }
2263
+ const closeModal = (modal) => {
2264
+ if (modal) {
2265
+ modal.outerHTML = '';
2266
+ }
2267
+ document.body.style.overflow = 'visible';
2268
+ };
2269
+ modal.addEventListener('click', () => closeModal(modal));
2270
+ modal.setAttribute('tabindex', '0');
2271
+ modal.addEventListener('keydown', (e) => {
2272
+ if (e.key === 'Escape') {
2273
+ closeModal(modal);
2274
+ }
2275
+ });
2276
+ modal.focus();
2277
+ }
2278
+ resetPromise() {
2279
+ this.nextRequestPromise = new Promise((resolve) => {
2280
+ this.nextRequestPromiseResolve = resolve;
2281
+ });
2282
+ }
2283
+ _updateFromParentProps(props) {
2284
+ const isChanged = this.valueStore.storeNewPropsFromParent(props);
2285
+ if (isChanged) {
2286
+ this.render();
2287
+ }
2288
+ }
2289
+ }
2290
+ function proxifyComponent(component) {
2291
+ return new Proxy(component, {
2292
+ get(component, prop) {
2293
+ if (prop in component || typeof prop !== 'string') {
2294
+ if (typeof component[prop] === 'function') {
2295
+ const callable = component[prop];
2296
+ return (...args) => {
2297
+ return callable.apply(component, args);
2298
+ };
2299
+ }
2300
+ return Reflect.get(component, prop);
2301
+ }
2302
+ if (component.valueStore.has(prop)) {
2303
+ return component.getData(prop);
2304
+ }
2305
+ return (args) => {
2306
+ return component.action.apply(component, [prop, args]);
2307
+ };
2308
+ },
2309
+ set(target, property, value) {
2310
+ if (property in target) {
2311
+ target[property] = value;
2312
+ return true;
2313
+ }
2314
+ target.set(property, value);
2315
+ return true;
2316
+ },
2317
+ });
2318
+ }
2319
+
2320
+ class StimulusElementDriver {
2321
+ constructor(controller) {
2322
+ this.controller = controller;
2323
+ }
2324
+ getModelName(element) {
2325
+ const modelDirective = getModelDirectiveFromElement(element, false);
2326
+ if (!modelDirective) {
2327
+ return null;
2328
+ }
2329
+ return modelDirective.action;
2330
+ }
2331
+ getComponentProps() {
2332
+ return this.controller.propsValue;
2333
+ }
2334
+ getEventsToEmit() {
2335
+ return this.controller.eventsToEmitValue;
2336
+ }
2337
+ getBrowserEventsToDispatch() {
2338
+ return this.controller.eventsToDispatchValue;
2339
+ }
2340
+ }
2341
+
2342
+ function getModelBinding (modelDirective) {
2343
+ let shouldRender = true;
2344
+ let targetEventName = null;
2345
+ let debounce = false;
2346
+ modelDirective.modifiers.forEach((modifier) => {
2347
+ switch (modifier.name) {
2348
+ case 'on':
2349
+ if (!modifier.value) {
2350
+ throw new Error(`The "on" modifier in ${modelDirective.getString()} requires a value - e.g. on(change).`);
2351
+ }
2352
+ if (!['input', 'change'].includes(modifier.value)) {
2353
+ throw new Error(`The "on" modifier in ${modelDirective.getString()} only accepts the arguments "input" or "change".`);
2354
+ }
2355
+ targetEventName = modifier.value;
2356
+ break;
2357
+ case 'norender':
2358
+ shouldRender = false;
2359
+ break;
2360
+ case 'debounce':
2361
+ debounce = modifier.value ? Number.parseInt(modifier.value) : true;
2362
+ break;
2363
+ default:
2364
+ throw new Error(`Unknown modifier "${modifier.name}" in data-model="${modelDirective.getString()}".`);
2365
+ }
2366
+ });
2367
+ const [modelName, innerModelName] = modelDirective.action.split(':');
2368
+ return {
2369
+ modelName,
2370
+ innerModelName: innerModelName || null,
2371
+ shouldRender,
2372
+ debounce,
2373
+ targetEventName,
2374
+ };
2375
+ }
2376
+
2377
+ class ChildComponentPlugin {
2378
+ constructor(component) {
2379
+ this.parentModelBindings = [];
2380
+ this.component = component;
2381
+ const modelDirectives = getAllModelDirectiveFromElements(this.component.element);
2382
+ this.parentModelBindings = modelDirectives.map(getModelBinding);
2383
+ }
2384
+ attachToComponent(component) {
2385
+ component.on('request:started', (requestData) => {
2386
+ requestData.children = this.getChildrenFingerprints();
2387
+ });
2388
+ component.on('model:set', (model, value) => {
2389
+ this.notifyParentModelChange(model, value);
2390
+ });
2391
+ }
2392
+ getChildrenFingerprints() {
2393
+ const fingerprints = {};
2394
+ this.getChildren().forEach((child) => {
2395
+ if (!child.id) {
2396
+ throw new Error('missing id');
2397
+ }
2398
+ fingerprints[child.id] = {
2399
+ fingerprint: child.fingerprint,
2400
+ tag: child.element.tagName.toLowerCase(),
2401
+ };
2402
+ });
2403
+ return fingerprints;
2404
+ }
2405
+ notifyParentModelChange(modelName, value) {
2406
+ const parentComponent = findParent(this.component);
2407
+ if (!parentComponent) {
2408
+ return;
2409
+ }
2410
+ this.parentModelBindings.forEach((modelBinding) => {
2411
+ const childModelName = modelBinding.innerModelName || 'value';
2412
+ if (childModelName !== modelName) {
2413
+ return;
2414
+ }
2415
+ parentComponent.set(modelBinding.modelName, value, modelBinding.shouldRender, modelBinding.debounce);
2416
+ });
2417
+ }
2418
+ getChildren() {
2419
+ return findChildren(this.component);
2420
+ }
2421
+ }
2422
+
2423
+ class LazyPlugin {
2424
+ constructor() {
2425
+ this.intersectionObserver = null;
2426
+ }
2427
+ attachToComponent(component) {
2428
+ if ('lazy' !== component.element.attributes.getNamedItem('loading')?.value) {
2429
+ return;
2430
+ }
2431
+ component.on('connect', () => {
2432
+ this.getObserver().observe(component.element);
2433
+ });
2434
+ component.on('disconnect', () => {
2435
+ this.intersectionObserver?.unobserve(component.element);
2436
+ });
2437
+ }
2438
+ getObserver() {
2439
+ if (!this.intersectionObserver) {
2440
+ this.intersectionObserver = new IntersectionObserver((entries, observer) => {
2441
+ entries.forEach((entry) => {
2442
+ if (entry.isIntersecting) {
2443
+ entry.target.dispatchEvent(new CustomEvent('live:appear'));
2444
+ observer.unobserve(entry.target);
2445
+ }
2446
+ });
2447
+ });
2448
+ }
2449
+ return this.intersectionObserver;
2450
+ }
2451
+ }
2452
+
2453
+ class LoadingPlugin {
2454
+ attachToComponent(component) {
2455
+ component.on('loading.state:started', (element, request) => {
2456
+ this.startLoading(component, element, request);
2457
+ });
2458
+ component.on('loading.state:finished', (element) => {
2459
+ this.finishLoading(component, element);
2460
+ });
2461
+ this.finishLoading(component, component.element);
2462
+ }
2463
+ startLoading(component, targetElement, backendRequest) {
2464
+ this.handleLoadingToggle(component, true, targetElement, backendRequest);
2465
+ }
2466
+ finishLoading(component, targetElement) {
2467
+ this.handleLoadingToggle(component, false, targetElement, null);
2468
+ }
2469
+ handleLoadingToggle(component, isLoading, targetElement, backendRequest) {
2470
+ if (isLoading) {
2471
+ this.addAttributes(targetElement, ['busy']);
2472
+ }
2473
+ else {
2474
+ this.removeAttributes(targetElement, ['busy']);
2475
+ }
2476
+ this.getLoadingDirectives(component, targetElement).forEach(({ element, directives }) => {
2477
+ if (isLoading) {
2478
+ this.addAttributes(element, ['data-live-is-loading']);
2479
+ }
2480
+ else {
2481
+ this.removeAttributes(element, ['data-live-is-loading']);
2482
+ }
2483
+ directives.forEach((directive) => {
2484
+ this.handleLoadingDirective(element, isLoading, directive, backendRequest);
2485
+ });
2486
+ });
2487
+ }
2488
+ handleLoadingDirective(element, isLoading, directive, backendRequest) {
2489
+ const finalAction = parseLoadingAction(directive.action, isLoading);
2490
+ const targetedActions = [];
2491
+ const targetedModels = [];
2492
+ let delay = 0;
2493
+ const validModifiers = new Map();
2494
+ validModifiers.set('delay', (modifier) => {
2495
+ if (!isLoading) {
2496
+ return;
2497
+ }
2498
+ delay = modifier.value ? Number.parseInt(modifier.value) : 200;
2499
+ });
2500
+ validModifiers.set('action', (modifier) => {
2501
+ if (!modifier.value) {
2502
+ throw new Error(`The "action" in data-loading must have an action name - e.g. action(foo). It's missing for "${directive.getString()}"`);
2503
+ }
2504
+ targetedActions.push(modifier.value);
2505
+ });
2506
+ validModifiers.set('model', (modifier) => {
2507
+ if (!modifier.value) {
2508
+ throw new Error(`The "model" in data-loading must have an action name - e.g. model(foo). It's missing for "${directive.getString()}"`);
2509
+ }
2510
+ targetedModels.push(modifier.value);
2511
+ });
2512
+ directive.modifiers.forEach((modifier) => {
2513
+ if (validModifiers.has(modifier.name)) {
2514
+ const callable = validModifiers.get(modifier.name) ?? (() => { });
2515
+ callable(modifier);
2516
+ return;
2517
+ }
2518
+ throw new Error(`Unknown modifier "${modifier.name}" used in data-loading="${directive.getString()}". Available modifiers are: ${Array.from(validModifiers.keys()).join(', ')}.`);
2519
+ });
2520
+ if (isLoading &&
2521
+ targetedActions.length > 0 &&
2522
+ backendRequest &&
2523
+ !backendRequest.containsOneOfActions(targetedActions)) {
2524
+ return;
2525
+ }
2526
+ if (isLoading &&
2527
+ targetedModels.length > 0 &&
2528
+ backendRequest &&
2529
+ !backendRequest.areAnyModelsUpdated(targetedModels)) {
2530
+ return;
2531
+ }
2532
+ let loadingDirective;
2533
+ switch (finalAction) {
2534
+ case 'show':
2535
+ loadingDirective = () => this.showElement(element);
2536
+ break;
2537
+ case 'hide':
2538
+ loadingDirective = () => this.hideElement(element);
2539
+ break;
2540
+ case 'addClass':
2541
+ loadingDirective = () => this.addClass(element, directive.args);
2542
+ break;
2543
+ case 'removeClass':
2544
+ loadingDirective = () => this.removeClass(element, directive.args);
2545
+ break;
2546
+ case 'addAttribute':
2547
+ loadingDirective = () => this.addAttributes(element, directive.args);
2548
+ break;
2549
+ case 'removeAttribute':
2550
+ loadingDirective = () => this.removeAttributes(element, directive.args);
2551
+ break;
2552
+ default:
2553
+ throw new Error(`Unknown data-loading action "${finalAction}"`);
2554
+ }
2555
+ if (delay) {
2556
+ window.setTimeout(() => {
2557
+ if (backendRequest && !backendRequest.isResolved) {
2558
+ loadingDirective();
2559
+ }
2560
+ }, delay);
2561
+ return;
2562
+ }
2563
+ loadingDirective();
2564
+ }
2565
+ getLoadingDirectives(component, element) {
2566
+ const loadingDirectives = [];
2567
+ let matchingElements = [...Array.from(element.querySelectorAll('[data-loading]'))];
2568
+ matchingElements = matchingElements.filter((elt) => elementBelongsToThisComponent(elt, component));
2569
+ if (element.hasAttribute('data-loading')) {
2570
+ matchingElements = [element, ...matchingElements];
2571
+ }
2572
+ matchingElements.forEach((element) => {
2573
+ if (!(element instanceof HTMLElement) && !(element instanceof SVGElement)) {
2574
+ throw new Error('Invalid Element Type');
2575
+ }
2576
+ const directives = parseDirectives(element.dataset.loading || 'show');
2577
+ loadingDirectives.push({
2578
+ element,
2579
+ directives,
2580
+ });
2581
+ });
2582
+ return loadingDirectives;
2583
+ }
2584
+ showElement(element) {
2585
+ element.style.display = 'revert';
2586
+ }
2587
+ hideElement(element) {
2588
+ element.style.display = 'none';
2589
+ }
2590
+ addClass(element, classes) {
2591
+ element.classList.add(...combineSpacedArray(classes));
2592
+ }
2593
+ removeClass(element, classes) {
2594
+ element.classList.remove(...combineSpacedArray(classes));
2595
+ if (element.classList.length === 0) {
2596
+ element.removeAttribute('class');
2597
+ }
2598
+ }
2599
+ addAttributes(element, attributes) {
2600
+ attributes.forEach((attribute) => {
2601
+ element.setAttribute(attribute, '');
2602
+ });
2603
+ }
2604
+ removeAttributes(element, attributes) {
2605
+ attributes.forEach((attribute) => {
2606
+ element.removeAttribute(attribute);
2607
+ });
2608
+ }
2609
+ }
2610
+ const parseLoadingAction = (action, isLoading) => {
2611
+ switch (action) {
2612
+ case 'show':
2613
+ return isLoading ? 'show' : 'hide';
2614
+ case 'hide':
2615
+ return isLoading ? 'hide' : 'show';
2616
+ case 'addClass':
2617
+ return isLoading ? 'addClass' : 'removeClass';
2618
+ case 'removeClass':
2619
+ return isLoading ? 'removeClass' : 'addClass';
2620
+ case 'addAttribute':
2621
+ return isLoading ? 'addAttribute' : 'removeAttribute';
2622
+ case 'removeAttribute':
2623
+ return isLoading ? 'removeAttribute' : 'addAttribute';
2624
+ }
2625
+ throw new Error(`Unknown data-loading action "${action}"`);
2626
+ };
2627
+
2628
+ class PageUnloadingPlugin {
2629
+ constructor() {
2630
+ this.isConnected = false;
2631
+ }
2632
+ attachToComponent(component) {
2633
+ component.on('render:started', (html, response, controls) => {
2634
+ if (!this.isConnected) {
2635
+ controls.shouldRender = false;
2636
+ }
2637
+ });
2638
+ component.on('connect', () => {
2639
+ this.isConnected = true;
2640
+ });
2641
+ component.on('disconnect', () => {
2642
+ this.isConnected = false;
2643
+ });
2644
+ }
2645
+ }
2646
+
2647
+ class PollingDirector {
2648
+ constructor(component) {
2649
+ this.isPollingActive = true;
2650
+ this.pollingIntervals = [];
2651
+ this.component = component;
2652
+ }
2653
+ addPoll(actionName, duration) {
2654
+ this.polls.push({ actionName, duration });
2655
+ if (this.isPollingActive) {
2656
+ this.initiatePoll(actionName, duration);
2657
+ }
2658
+ }
2659
+ startAllPolling() {
2660
+ if (this.isPollingActive) {
2661
+ return;
2662
+ }
2663
+ this.isPollingActive = true;
2664
+ this.polls.forEach(({ actionName, duration }) => {
2665
+ this.initiatePoll(actionName, duration);
2666
+ });
2667
+ }
2668
+ stopAllPolling() {
2669
+ this.isPollingActive = false;
2670
+ this.pollingIntervals.forEach((interval) => {
2671
+ clearInterval(interval);
2672
+ });
2673
+ }
2674
+ clearPolling() {
2675
+ this.stopAllPolling();
2676
+ this.polls = [];
2677
+ this.startAllPolling();
2678
+ }
2679
+ initiatePoll(actionName, duration) {
2680
+ let callback;
2681
+ if (actionName === '$render') {
2682
+ callback = () => {
2683
+ this.component.render();
2684
+ };
2685
+ }
2686
+ else {
2687
+ callback = () => {
2688
+ this.component.action(actionName, {}, 0);
2689
+ };
2690
+ }
2691
+ const timer = window.setInterval(() => {
2692
+ callback();
2693
+ }, duration);
2694
+ this.pollingIntervals.push(timer);
2695
+ }
2696
+ }
2697
+
2698
+ class PollingPlugin {
2699
+ attachToComponent(component) {
2700
+ this.element = component.element;
2701
+ this.pollingDirector = new PollingDirector(component);
2702
+ this.initializePolling();
2703
+ component.on('connect', () => {
2704
+ this.pollingDirector.startAllPolling();
2705
+ });
2706
+ component.on('disconnect', () => {
2707
+ this.pollingDirector.stopAllPolling();
2708
+ });
2709
+ component.on('render:finished', () => {
2710
+ this.initializePolling();
2711
+ });
2712
+ }
2713
+ addPoll(actionName, duration) {
2714
+ this.pollingDirector.addPoll(actionName, duration);
2715
+ }
2716
+ clearPolling() {
2717
+ this.pollingDirector.clearPolling();
2718
+ }
2719
+ initializePolling() {
2720
+ this.clearPolling();
2721
+ if (this.element.dataset.poll === undefined) {
2722
+ return;
2723
+ }
2724
+ const rawPollConfig = this.element.dataset.poll;
2725
+ const directives = parseDirectives(rawPollConfig || '$render');
2726
+ directives.forEach((directive) => {
2727
+ let duration = 2000;
2728
+ directive.modifiers.forEach((modifier) => {
2729
+ switch (modifier.name) {
2730
+ case 'delay':
2731
+ if (modifier.value) {
2732
+ duration = Number.parseInt(modifier.value);
2733
+ }
2734
+ break;
2735
+ default:
2736
+ console.warn(`Unknown modifier "${modifier.name}" in data-poll "${rawPollConfig}".`);
2737
+ }
2738
+ });
2739
+ this.addPoll(directive.action, duration);
2740
+ });
2741
+ }
2742
+ }
2743
+
2744
+ function isValueEmpty(value) {
2745
+ if (null === value || value === '' || undefined === value || (Array.isArray(value) && value.length === 0)) {
2746
+ return true;
2747
+ }
2748
+ if (typeof value !== 'object') {
2749
+ return false;
2750
+ }
2751
+ for (const key of Object.keys(value)) {
2752
+ if (!isValueEmpty(value[key])) {
2753
+ return false;
2754
+ }
2755
+ }
2756
+ return true;
2757
+ }
2758
+ function toQueryString(data) {
2759
+ const buildQueryStringEntries = (data, entries = {}, baseKey = '') => {
2760
+ Object.entries(data).forEach(([iKey, iValue]) => {
2761
+ const key = baseKey === '' ? iKey : `${baseKey}[${iKey}]`;
2762
+ if ('' === baseKey && isValueEmpty(iValue)) {
2763
+ entries[key] = '';
2764
+ }
2765
+ else if (null !== iValue) {
2766
+ if (typeof iValue === 'object') {
2767
+ entries = { ...entries, ...buildQueryStringEntries(iValue, entries, key) };
2768
+ }
2769
+ else {
2770
+ entries[key] = encodeURIComponent(iValue)
2771
+ .replace(/%20/g, '+')
2772
+ .replace(/%2C/g, ',');
2773
+ }
2774
+ }
2775
+ });
2776
+ return entries;
2777
+ };
2778
+ const entries = buildQueryStringEntries(data);
2779
+ return Object.entries(entries)
2780
+ .map(([key, value]) => `${key}=${value}`)
2781
+ .join('&');
2782
+ }
2783
+ function fromQueryString(search) {
2784
+ search = search.replace('?', '');
2785
+ if (search === '')
2786
+ return {};
2787
+ const insertDotNotatedValueIntoData = (key, value, data) => {
2788
+ const [first, second, ...rest] = key.split('.');
2789
+ if (!second) {
2790
+ data[key] = value;
2791
+ return value;
2792
+ }
2793
+ if (data[first] === undefined) {
2794
+ data[first] = Number.isNaN(Number.parseInt(second)) ? {} : [];
2795
+ }
2796
+ insertDotNotatedValueIntoData([second, ...rest].join('.'), value, data[first]);
2797
+ };
2798
+ const entries = search.split('&').map((i) => i.split('='));
2799
+ const data = {};
2800
+ entries.forEach(([key, value]) => {
2801
+ value = decodeURIComponent(value.replace(/\+/g, '%20'));
2802
+ if (!key.includes('[')) {
2803
+ data[key] = value;
2804
+ }
2805
+ else {
2806
+ if ('' === value)
2807
+ return;
2808
+ const dotNotatedKey = key.replace(/\[/g, '.').replace(/]/g, '');
2809
+ insertDotNotatedValueIntoData(dotNotatedKey, value, data);
2810
+ }
2811
+ });
2812
+ return data;
2813
+ }
2814
+ class UrlUtils extends URL {
2815
+ has(key) {
2816
+ const data = this.getData();
2817
+ return Object.keys(data).includes(key);
2818
+ }
2819
+ set(key, value) {
2820
+ const data = this.getData();
2821
+ data[key] = value;
2822
+ this.setData(data);
2823
+ }
2824
+ get(key) {
2825
+ return this.getData()[key];
2826
+ }
2827
+ remove(key) {
2828
+ const data = this.getData();
2829
+ delete data[key];
2830
+ this.setData(data);
2831
+ }
2832
+ getData() {
2833
+ if (!this.search) {
2834
+ return {};
2835
+ }
2836
+ return fromQueryString(this.search);
2837
+ }
2838
+ setData(data) {
2839
+ this.search = toQueryString(data);
2840
+ }
2841
+ }
2842
+ class HistoryStrategy {
2843
+ static replace(url) {
2844
+ history.replaceState(history.state, '', url);
2845
+ }
2846
+ }
2847
+
2848
+ class QueryStringPlugin {
2849
+ constructor(mapping) {
2850
+ this.mapping = mapping;
2851
+ }
2852
+ attachToComponent(component) {
2853
+ component.on('render:finished', (component) => {
2854
+ const urlUtils = new UrlUtils(window.location.href);
2855
+ const currentUrl = urlUtils.toString();
2856
+ Object.entries(this.mapping).forEach(([prop, mapping]) => {
2857
+ const value = component.valueStore.get(prop);
2858
+ urlUtils.set(mapping.name, value);
2859
+ });
2860
+ if (currentUrl !== urlUtils.toString()) {
2861
+ HistoryStrategy.replace(urlUtils);
2862
+ }
2863
+ });
2864
+ }
2865
+ }
2866
+
2867
+ class SetValueOntoModelFieldsPlugin {
2868
+ attachToComponent(component) {
2869
+ this.synchronizeValueOfModelFields(component);
2870
+ component.on('render:finished', () => {
2871
+ this.synchronizeValueOfModelFields(component);
2872
+ });
2873
+ }
2874
+ synchronizeValueOfModelFields(component) {
2875
+ component.element.querySelectorAll('[data-model]').forEach((element) => {
2876
+ if (!(element instanceof HTMLElement)) {
2877
+ throw new Error('Invalid element using data-model.');
2878
+ }
2879
+ if (element instanceof HTMLFormElement) {
2880
+ return;
2881
+ }
2882
+ if (!elementBelongsToThisComponent(element, component)) {
2883
+ return;
2884
+ }
2885
+ const modelDirective = getModelDirectiveFromElement(element);
2886
+ if (!modelDirective) {
2887
+ return;
2888
+ }
2889
+ const modelName = modelDirective.action;
2890
+ if (component.getUnsyncedModels().includes(modelName)) {
2891
+ return;
2892
+ }
2893
+ if (component.valueStore.has(modelName)) {
2894
+ setValueOnElement(element, component.valueStore.get(modelName));
2895
+ }
2896
+ if (element instanceof HTMLSelectElement && !element.multiple) {
2897
+ component.valueStore.set(modelName, getValueFromElement(element, component.valueStore));
2898
+ }
2899
+ });
2900
+ }
2901
+ }
2902
+
2903
+ class ValidatedFieldsPlugin {
2904
+ attachToComponent(component) {
2905
+ component.on('model:set', (modelName) => {
2906
+ this.handleModelSet(modelName, component.valueStore);
2907
+ });
2908
+ }
2909
+ handleModelSet(modelName, valueStore) {
2910
+ if (valueStore.has('validatedFields')) {
2911
+ const validatedFields = [...valueStore.get('validatedFields')];
2912
+ if (!validatedFields.includes(modelName)) {
2913
+ validatedFields.push(modelName);
2914
+ }
2915
+ valueStore.set('validatedFields', validatedFields);
2916
+ }
2917
+ }
2918
+ }
2919
+
2920
+ class LiveControllerDefault extends Controller {
2921
+ constructor() {
2922
+ super(...arguments);
2923
+ this.pendingActionTriggerModelElement = null;
2924
+ this.elementEventListeners = [
2925
+ { event: 'input', callback: (event) => this.handleInputEvent(event) },
2926
+ { event: 'change', callback: (event) => this.handleChangeEvent(event) },
2927
+ ];
2928
+ this.pendingFiles = {};
2929
+ }
2930
+ initialize() {
2931
+ this.mutationObserver = new MutationObserver(this.onMutations.bind(this));
2932
+ this.createComponent();
2933
+ }
2934
+ connect() {
2935
+ this.connectComponent();
2936
+ this.mutationObserver.observe(this.element, {
2937
+ attributes: true,
2938
+ });
2939
+ }
2940
+ disconnect() {
2941
+ this.disconnectComponent();
2942
+ this.mutationObserver.disconnect();
2943
+ }
2944
+ update(event) {
2945
+ if (event.type === 'input' || event.type === 'change') {
2946
+ throw new Error(`Since LiveComponents 2.3, you no longer need data-action="live#update" on form elements. Found on element: ${getElementAsTagText(event.currentTarget)}`);
2947
+ }
2948
+ this.updateModelFromElementEvent(event.currentTarget, null);
2949
+ }
2950
+ action(event) {
2951
+ const params = event.params;
2952
+ if (!params.action) {
2953
+ throw new Error(`No action name provided on element: ${getElementAsTagText(event.currentTarget)}. Did you forget to add the "data-live-action-param" attribute?`);
2954
+ }
2955
+ const rawAction = params.action;
2956
+ const actionArgs = { ...params };
2957
+ delete actionArgs.action;
2958
+ const directives = parseDirectives(rawAction);
2959
+ let debounce = false;
2960
+ directives.forEach((directive) => {
2961
+ let pendingFiles = {};
2962
+ const validModifiers = new Map();
2963
+ validModifiers.set('stop', () => {
2964
+ event.stopPropagation();
2965
+ });
2966
+ validModifiers.set('self', () => {
2967
+ if (event.target !== event.currentTarget) {
2968
+ return;
2969
+ }
2970
+ });
2971
+ validModifiers.set('debounce', (modifier) => {
2972
+ debounce = modifier.value ? Number.parseInt(modifier.value) : true;
2973
+ });
2974
+ validModifiers.set('files', (modifier) => {
2975
+ if (!modifier.value) {
2976
+ pendingFiles = this.pendingFiles;
2977
+ }
2978
+ else if (this.pendingFiles[modifier.value]) {
2979
+ pendingFiles[modifier.value] = this.pendingFiles[modifier.value];
2980
+ }
2981
+ });
2982
+ directive.modifiers.forEach((modifier) => {
2983
+ if (validModifiers.has(modifier.name)) {
2984
+ const callable = validModifiers.get(modifier.name) ?? (() => { });
2985
+ callable(modifier);
2986
+ return;
2987
+ }
2988
+ console.warn(`Unknown modifier ${modifier.name} in action "${rawAction}". Available modifiers are: ${Array.from(validModifiers.keys()).join(', ')}.`);
2989
+ });
2990
+ for (const [key, input] of Object.entries(pendingFiles)) {
2991
+ if (input.files) {
2992
+ this.component.files(key, input);
2993
+ }
2994
+ delete this.pendingFiles[key];
2995
+ }
2996
+ this.component.action(directive.action, actionArgs, debounce);
2997
+ if (getModelDirectiveFromElement(event.currentTarget, false)) {
2998
+ this.pendingActionTriggerModelElement = event.currentTarget;
2999
+ }
3000
+ });
3001
+ }
3002
+ $render() {
3003
+ return this.component.render();
3004
+ }
3005
+ emit(event) {
3006
+ this.getEmitDirectives(event).forEach(({ name, data, nameMatch }) => {
3007
+ this.component.emit(name, data, nameMatch);
3008
+ });
3009
+ }
3010
+ emitUp(event) {
3011
+ this.getEmitDirectives(event).forEach(({ name, data, nameMatch }) => {
3012
+ this.component.emitUp(name, data, nameMatch);
3013
+ });
3014
+ }
3015
+ emitSelf(event) {
3016
+ this.getEmitDirectives(event).forEach(({ name, data }) => {
3017
+ this.component.emitSelf(name, data);
3018
+ });
3019
+ }
3020
+ $updateModel(model, value, shouldRender = true, debounce = true) {
3021
+ return this.component.set(model, value, shouldRender, debounce);
3022
+ }
3023
+ propsUpdatedFromParentValueChanged() {
3024
+ this.component._updateFromParentProps(this.propsUpdatedFromParentValue);
3025
+ }
3026
+ fingerprintValueChanged() {
3027
+ this.component.fingerprint = this.fingerprintValue;
3028
+ }
3029
+ getEmitDirectives(event) {
3030
+ const params = event.params;
3031
+ if (!params.event) {
3032
+ throw new Error(`No event name provided on element: ${getElementAsTagText(event.currentTarget)}. Did you forget to add the "data-live-event-param" attribute?`);
3033
+ }
3034
+ const eventInfo = params.event;
3035
+ const eventArgs = { ...params };
3036
+ delete eventArgs.event;
3037
+ const directives = parseDirectives(eventInfo);
3038
+ const emits = [];
3039
+ directives.forEach((directive) => {
3040
+ let nameMatch = null;
3041
+ directive.modifiers.forEach((modifier) => {
3042
+ switch (modifier.name) {
3043
+ case 'name':
3044
+ nameMatch = modifier.value;
3045
+ break;
3046
+ default:
3047
+ throw new Error(`Unknown modifier ${modifier.name} in event "${eventInfo}".`);
3048
+ }
3049
+ });
3050
+ emits.push({
3051
+ name: directive.action,
3052
+ data: eventArgs,
3053
+ nameMatch,
3054
+ });
3055
+ });
3056
+ return emits;
3057
+ }
3058
+ createComponent() {
3059
+ const id = this.element.id || null;
3060
+ this.component = new Component(this.element, this.nameValue, this.propsValue, this.listenersValue, id, LiveControllerDefault.backendFactory(this), new StimulusElementDriver(this));
3061
+ this.proxiedComponent = proxifyComponent(this.component);
3062
+ Object.defineProperty(this.element, '__component', {
3063
+ value: this.proxiedComponent,
3064
+ writable: true,
3065
+ });
3066
+ if (this.hasDebounceValue) {
3067
+ this.component.defaultDebounce = this.debounceValue;
3068
+ }
3069
+ const plugins = [
3070
+ new LoadingPlugin(),
3071
+ new LazyPlugin(),
3072
+ new ValidatedFieldsPlugin(),
3073
+ new PageUnloadingPlugin(),
3074
+ new PollingPlugin(),
3075
+ new SetValueOntoModelFieldsPlugin(),
3076
+ new QueryStringPlugin(this.queryMappingValue),
3077
+ new ChildComponentPlugin(this.component),
3078
+ ];
3079
+ plugins.forEach((plugin) => {
3080
+ this.component.addPlugin(plugin);
3081
+ });
3082
+ }
3083
+ connectComponent() {
3084
+ this.component.connect();
3085
+ this.mutationObserver.observe(this.element, {
3086
+ attributes: true,
3087
+ });
3088
+ this.elementEventListeners.forEach(({ event, callback }) => {
3089
+ this.component.element.addEventListener(event, callback);
3090
+ });
3091
+ this.dispatchEvent('connect');
3092
+ }
3093
+ disconnectComponent() {
3094
+ this.component.disconnect();
3095
+ this.elementEventListeners.forEach(({ event, callback }) => {
3096
+ this.component.element.removeEventListener(event, callback);
3097
+ });
3098
+ this.dispatchEvent('disconnect');
3099
+ }
3100
+ handleInputEvent(event) {
3101
+ const target = event.target;
3102
+ if (!target) {
3103
+ return;
3104
+ }
3105
+ this.updateModelFromElementEvent(target, 'input');
3106
+ }
3107
+ handleChangeEvent(event) {
3108
+ const target = event.target;
3109
+ if (!target) {
3110
+ return;
3111
+ }
3112
+ this.updateModelFromElementEvent(target, 'change');
3113
+ }
3114
+ updateModelFromElementEvent(element, eventName) {
3115
+ if (!elementBelongsToThisComponent(element, this.component)) {
3116
+ return;
3117
+ }
3118
+ if (!(element instanceof HTMLElement)) {
3119
+ throw new Error('Could not update model for non HTMLElement');
3120
+ }
3121
+ if (element instanceof HTMLInputElement && element.type === 'file') {
3122
+ const key = element.name;
3123
+ if (element.files?.length) {
3124
+ this.pendingFiles[key] = element;
3125
+ }
3126
+ else if (this.pendingFiles[key]) {
3127
+ delete this.pendingFiles[key];
3128
+ }
3129
+ }
3130
+ const modelDirective = getModelDirectiveFromElement(element, false);
3131
+ if (!modelDirective) {
3132
+ return;
3133
+ }
3134
+ const modelBinding = getModelBinding(modelDirective);
3135
+ if (!modelBinding.targetEventName) {
3136
+ modelBinding.targetEventName = 'input';
3137
+ }
3138
+ if (this.pendingActionTriggerModelElement === element) {
3139
+ modelBinding.shouldRender = false;
3140
+ }
3141
+ if (eventName === 'change' && modelBinding.targetEventName === 'input') {
3142
+ modelBinding.targetEventName = 'change';
3143
+ }
3144
+ if (eventName && modelBinding.targetEventName !== eventName) {
3145
+ return;
3146
+ }
3147
+ if (false === modelBinding.debounce) {
3148
+ if (modelBinding.targetEventName === 'input') {
3149
+ modelBinding.debounce = true;
3150
+ }
3151
+ else {
3152
+ modelBinding.debounce = 0;
3153
+ }
3154
+ }
3155
+ const finalValue = getValueFromElement(element, this.component.valueStore);
3156
+ this.component.set(modelBinding.modelName, finalValue, modelBinding.shouldRender, modelBinding.debounce);
3157
+ }
3158
+ dispatchEvent(name, detail = {}, canBubble = true, cancelable = false) {
3159
+ detail.controller = this;
3160
+ detail.component = this.proxiedComponent;
3161
+ this.dispatch(name, { detail, prefix: 'live', cancelable, bubbles: canBubble });
3162
+ }
3163
+ onMutations(mutations) {
3164
+ mutations.forEach((mutation) => {
3165
+ if (mutation.type === 'attributes' &&
3166
+ mutation.attributeName === 'id' &&
3167
+ this.element.id !== this.component.id) {
3168
+ this.disconnectComponent();
3169
+ this.createComponent();
3170
+ this.connectComponent();
3171
+ }
3172
+ });
3173
+ }
3174
+ }
3175
+ LiveControllerDefault.values = {
3176
+ name: String,
3177
+ url: String,
3178
+ props: { type: Object, default: {} },
3179
+ propsUpdatedFromParent: { type: Object, default: {} },
3180
+ listeners: { type: Array, default: [] },
3181
+ eventsToEmit: { type: Array, default: [] },
3182
+ eventsToDispatch: { type: Array, default: [] },
3183
+ debounce: { type: Number, default: 150 },
3184
+ fingerprint: { type: String, default: '' },
3185
+ requestMethod: { type: String, default: 'post' },
3186
+ queryMapping: { type: Object, default: {} },
3187
+ };
3188
+ LiveControllerDefault.backendFactory = (controller) => new Backend(controller.urlValue, controller.requestMethodValue);
3189
+
3190
+ export { Component, LiveControllerDefault as default, getComponent };