phoenix_live_view 1.2.0-rc.2 → 1.2.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 (27) hide show
  1. package/README.md +5 -5
  2. package/assets/js/phoenix_live_view/README.md +3 -0
  3. package/assets/js/phoenix_live_view/{aria.js → aria.ts} +18 -10
  4. package/assets/js/phoenix_live_view/{browser.js → browser.ts} +12 -8
  5. package/assets/js/phoenix_live_view/{dom.js → dom.ts} +107 -34
  6. package/assets/js/phoenix_live_view/{dom_patch.js → dom_patch.ts} +187 -124
  7. package/assets/js/phoenix_live_view/{dom_post_morph_restorer.js → dom_post_morph_restorer.ts} +17 -2
  8. package/assets/js/phoenix_live_view/{element_ref.js → element_ref.ts} +17 -11
  9. package/assets/js/phoenix_live_view/entry_uploader.js +4 -4
  10. package/assets/js/phoenix_live_view/{hooks.js → hooks.ts} +108 -91
  11. package/assets/js/phoenix_live_view/index.ts +14 -301
  12. package/assets/js/phoenix_live_view/js.js +2 -1
  13. package/assets/js/phoenix_live_view/js_commands.ts +12 -9
  14. package/assets/js/phoenix_live_view/{live_socket.js → live_socket.ts} +582 -114
  15. package/assets/js/phoenix_live_view/live_uploader.js +1 -1
  16. package/assets/js/phoenix_live_view/rendered.js +3 -0
  17. package/assets/js/phoenix_live_view/{utils.js → utils.ts} +35 -6
  18. package/assets/js/phoenix_live_view/{view.js → view.ts} +221 -110
  19. package/assets/js/phoenix_live_view/view_hook.ts +92 -32
  20. package/package.json +5 -2
  21. package/priv/static/phoenix_live_view.cjs.js +577 -314
  22. package/priv/static/phoenix_live_view.cjs.js.map +4 -4
  23. package/priv/static/phoenix_live_view.esm.js +577 -314
  24. package/priv/static/phoenix_live_view.esm.js.map +4 -4
  25. package/priv/static/phoenix_live_view.js +584 -314
  26. package/priv/static/phoenix_live_view.min.js +7 -7
  27. /package/assets/js/phoenix_live_view/{constants.js → constants.ts} +0 -0
@@ -20,61 +20,107 @@ import {
20
20
  PHX_RUNTIME_HOOK,
21
21
  } from "./constants";
22
22
 
23
- import { detectDuplicateIds, detectInvalidStreamInserts, isCid } from "./utils";
23
+ import { detectDuplicateIds, detectInvalidStreamInserts } from "./utils";
24
24
  import ElementRef from "./element_ref";
25
25
  import DOM from "./dom";
26
26
  import DOMPostMorphRestorer from "./dom_post_morph_restorer";
27
27
  import morphdom from "morphdom";
28
+ import View from "./view";
29
+ import LiveSocket from "./live_socket";
30
+
31
+ type Stream = Set<any>;
32
+ type MorphdomOptions = Parameters<typeof morphdom>[2] & {
33
+ // morphdom's types are outdated
34
+ onBeforeElUpdated: (fromEl: Element, toEl: Element) => boolean | Element;
35
+ };
36
+ type BeforeUpdatedCallback = (fromEl: Element, toEl: Element) => void;
37
+ type AfterAddedCallback = (el: Node) => void;
38
+ type AfterUpdatedCallback = (el: Element) => void;
39
+ type AfterPhxChildAddedCallback = (el: Element) => void;
40
+ type AfterDiscardedCallback = (el: Node) => void;
41
+ type AfterTransitionsDiscardedCallback = (els: Element[]) => void;
28
42
 
29
43
  export default class DOMPatch {
30
- constructor(view, container, id, html, streams, targetCID, opts = {}) {
44
+ private view: View;
45
+ private liveSocket: LiveSocket;
46
+ private container: Element;
47
+ private rootID: string;
48
+ private html: string | Node;
49
+ private streams: Stream;
50
+ private streamInserts: Record<string, any>;
51
+ private streamComponentRestore: Record<string, any>;
52
+ private targetCID: number | null;
53
+ private pendingRemoves: any[];
54
+ private phxRemove: string;
55
+ private targetContainer: Element;
56
+ private beforeUpdatedCallbacks: BeforeUpdatedCallback[];
57
+ private afterAddedCallbacks: AfterAddedCallback[];
58
+ private afterUpdatedCallbacks: AfterUpdatedCallback[];
59
+ private afterPhxChildAddedCallbacks: AfterPhxChildAddedCallback[];
60
+ private afterDiscardedCallbacks: AfterDiscardedCallback[];
61
+ private afterTransitionsDiscardedCallbacks: AfterTransitionsDiscardedCallback[];
62
+ private withChildren: boolean;
63
+ private undoRef: number | null;
64
+
65
+ constructor(
66
+ view: View,
67
+ container: Element,
68
+ html: string | Node,
69
+ streams: Set<Stream>,
70
+ targetCID: number | null,
71
+ opts: { withChildren?: boolean; undoRef?: number } = {},
72
+ ) {
31
73
  this.view = view;
32
74
  this.liveSocket = view.liveSocket;
33
75
  this.container = container;
34
- this.id = id;
35
76
  this.rootID = view.root.id;
36
77
  this.html = html;
37
78
  this.streams = streams;
38
79
  this.streamInserts = {};
39
80
  this.streamComponentRestore = {};
40
81
  this.targetCID = targetCID;
41
- this.cidPatch = isCid(this.targetCID);
42
82
  this.pendingRemoves = [];
43
83
  this.phxRemove = this.liveSocket.binding("remove");
44
- this.targetContainer = this.isCIDPatch()
45
- ? this.targetCIDContainer(html)
84
+ // If we patch a component, we always pass a string
85
+ this.targetContainer = targetCID
86
+ ? DOM.getComponent(this.view.id, targetCID)
46
87
  : container;
47
- this.callbacks = {
48
- beforeadded: [],
49
- beforeupdated: [],
50
- beforephxChildAdded: [],
51
- afteradded: [],
52
- afterupdated: [],
53
- afterdiscarded: [],
54
- afterphxChildAdded: [],
55
- aftertransitionsDiscarded: [],
56
- };
88
+ this.beforeUpdatedCallbacks = [];
89
+ this.afterAddedCallbacks = [];
90
+ this.afterUpdatedCallbacks = [];
91
+ this.afterPhxChildAddedCallbacks = [];
92
+ this.afterDiscardedCallbacks = [];
93
+ this.afterTransitionsDiscardedCallbacks = [];
57
94
  // unlock patches pass undoRef and must morph the locked element itself, not
58
95
  // only its children. The first client ref is 0, so this must check for the
59
96
  // option's presence rather than truthiness.
60
97
  this.withChildren =
61
98
  opts.withChildren || opts.undoRef !== undefined || false;
62
- this.undoRef = opts.undoRef;
99
+ this.undoRef = opts.undoRef ?? null;
100
+ }
101
+
102
+ beforeUpdated(callback: BeforeUpdatedCallback) {
103
+ this.beforeUpdatedCallbacks.push(callback);
63
104
  }
64
105
 
65
- before(kind, callback) {
66
- this.callbacks[`before${kind}`].push(callback);
106
+ afterAdded(callback: AfterAddedCallback) {
107
+ this.afterAddedCallbacks.push(callback);
67
108
  }
68
- after(kind, callback) {
69
- this.callbacks[`after${kind}`].push(callback);
109
+
110
+ afterUpdated(callback: AfterUpdatedCallback) {
111
+ this.afterUpdatedCallbacks.push(callback);
112
+ }
113
+
114
+ afterPhxChildAdded(callback: AfterPhxChildAddedCallback) {
115
+ this.afterPhxChildAddedCallbacks.push(callback);
70
116
  }
71
117
 
72
- trackBefore(kind, ...args) {
73
- this.callbacks[`before${kind}`].forEach((callback) => callback(...args));
118
+ afterDiscarded(callback: AfterDiscardedCallback) {
119
+ this.afterDiscardedCallbacks.push(callback);
74
120
  }
75
121
 
76
- trackAfter(kind, ...args) {
77
- this.callbacks[`after${kind}`].forEach((callback) => callback(...args));
122
+ afterTransitionsDiscarded(callback: AfterTransitionsDiscardedCallback) {
123
+ this.afterTransitionsDiscardedCallbacks.push(callback);
78
124
  }
79
125
 
80
126
  markPrunableContentForRemoval() {
@@ -92,11 +138,7 @@ export default class DOMPatch {
92
138
  const { view, liveSocket, html, container } = this;
93
139
  let targetContainer = this.targetContainer;
94
140
 
95
- if (this.isCIDPatch() && !this.targetContainer) {
96
- return;
97
- }
98
-
99
- if (this.isCIDPatch()) {
141
+ if (this.targetCID) {
100
142
  // https://github.com/phoenixframework/phoenix_live_view/pull/3942
101
143
  // we need to ensure that no parent is locked
102
144
  const closestLock = targetContainer.closest(`[${PHX_REF_LOCK}]`);
@@ -126,23 +168,23 @@ export default class DOMPatch {
126
168
  const phxViewportTop = liveSocket.binding(PHX_VIEWPORT_TOP);
127
169
  const phxViewportBottom = liveSocket.binding(PHX_VIEWPORT_BOTTOM);
128
170
  const phxTriggerExternal = liveSocket.binding(PHX_TRIGGER_ACTION);
129
- const added = [];
130
- const updates = [];
131
- const appendPrependUpdates = [];
171
+ const added: Array<Node> = [];
172
+ const updates: Array<Element> = [];
173
+ const appendPrependUpdates: Array<DOMPostMorphRestorer> = [];
132
174
 
133
175
  // as the portal target itself could be at the end of the DOM,
134
176
  // it may not be present while morphing previous parts;
135
177
  // therefore we apply all teleports after the morphing is done+
136
- let portalCallbacks = [];
178
+ let portalCallbacks: Array<() => void> = [];
137
179
 
138
- let externalFormTriggered = null;
180
+ let externalFormTriggered: Element | null = null;
139
181
 
140
182
  const morph = (
141
183
  targetContainer,
142
184
  source,
143
185
  withChildren = this.withChildren,
144
186
  ) => {
145
- const morphCallbacks = {
187
+ const morphCallbacks: MorphdomOptions = {
146
188
  // normally, we are running with childrenOnly, as the patch HTML for a LV
147
189
  // does not include the LV attrs (data-phx-session, etc.)
148
190
  // when we are patching a live component, we do want to patch the root element as well;
@@ -150,6 +192,7 @@ export default class DOMPatch {
150
192
  childrenOnly:
151
193
  targetContainer.getAttribute(PHX_COMPONENT) === null && !withChildren,
152
194
  getNodeKey: (node) => {
195
+ if (!(node instanceof Element)) return null;
153
196
  if (DOM.isPhxDestroyed(node)) {
154
197
  return null;
155
198
  }
@@ -162,19 +205,17 @@ export default class DOMPatch {
162
205
  // If ID was touched by JavaScript hook, use PHX_MAGIC_ID for matching.
163
206
  // This ensures morphdom can match elements even when JS modifies their IDs.
164
207
  if (DOM.private(node, "clientsideIdAttribute")) {
165
- return node.getAttribute && node.getAttribute(PHX_MAGIC_ID);
208
+ return node.getAttribute(PHX_MAGIC_ID);
166
209
  }
167
210
 
168
- return (
169
- node.id || (node.getAttribute && node.getAttribute(PHX_MAGIC_ID))
170
- );
211
+ return node.id || node.getAttribute(PHX_MAGIC_ID);
171
212
  },
172
213
  // skip indexing from children when container is stream
173
214
  skipFromChildren: (from) => {
174
215
  return from.getAttribute(phxUpdate) === PHX_STREAM;
175
216
  },
176
217
  // tell morphdom how to add a child
177
- addChild: (parent, child) => {
218
+ addChild: (parent: Element, child: Element) => {
178
219
  const { ref, streamAt } = this.getStreamInsert(child);
179
220
  if (ref === undefined) {
180
221
  return parent.appendChild(child);
@@ -191,7 +232,7 @@ export default class DOMPatch {
191
232
  const nonStreamChild = Array.from(parent.children).find(
192
233
  (c) => !c.hasAttribute(PHX_STREAM_REF),
193
234
  );
194
- parent.insertBefore(child, nonStreamChild);
235
+ parent.insertBefore(child, nonStreamChild ?? null);
195
236
  } else {
196
237
  parent.appendChild(child);
197
238
  }
@@ -201,6 +242,10 @@ export default class DOMPatch {
201
242
  }
202
243
  },
203
244
  onBeforeNodeAdded: (el) => {
245
+ if (!(el instanceof Element)) {
246
+ return el;
247
+ }
248
+
204
249
  // don't add update_only nodes if they did not already exist
205
250
  if (
206
251
  this.getStreamInsert(el)?.updateOnly &&
@@ -210,7 +255,6 @@ export default class DOMPatch {
210
255
  }
211
256
 
212
257
  DOM.maintainPrivateHooks(el, el, phxViewportTop, phxViewportBottom);
213
- this.trackBefore("added", el);
214
258
 
215
259
  let morphedEl = el;
216
260
  // this is a stream item that was kept on reset, recursively morph it
@@ -223,9 +267,13 @@ export default class DOMPatch {
223
267
  return morphedEl;
224
268
  },
225
269
  onNodeAdded: (el) => {
226
- if (el.getAttribute) {
227
- this.maybeReOrderStream(el, true);
270
+ if (!(el instanceof Element)) {
271
+ added.push(el);
272
+ return;
228
273
  }
274
+
275
+ this.maybeReOrderStream(el, true);
276
+
229
277
  // phx-portal handling
230
278
  if (DOM.isPortalTemplate(el)) {
231
279
  portalCallbacks.push(() => this.teleport(el, morph));
@@ -247,19 +295,24 @@ export default class DOMPatch {
247
295
  (DOM.isPhxChild(el) && view.ownsElement(el)) ||
248
296
  (DOM.isPhxSticky(el) && view.ownsElement(el.parentNode))
249
297
  ) {
250
- this.trackAfter("phxChildAdded", el);
298
+ this.trackAfterPhxChildAdded(el);
251
299
  }
252
300
 
253
301
  // data-phx-runtime-hook
254
302
  if (el.nodeName === "SCRIPT" && el.hasAttribute(PHX_RUNTIME_HOOK)) {
255
- this.handleRuntimeHook(el, source);
303
+ this.handleRuntimeHook(el as HTMLScriptElement, source);
256
304
  }
257
305
 
258
306
  added.push(el);
259
307
  },
260
308
  onNodeDiscarded: (el) => this.onNodeDiscarded(el),
261
309
  onBeforeNodeDiscarded: (el) => {
262
- if (el.getAttribute && el.getAttribute(PHX_PRUNE) !== null) {
310
+ // Non-element nodes can always be discarded
311
+ if (!(el instanceof Element)) {
312
+ return true;
313
+ }
314
+
315
+ if (el.getAttribute(PHX_PRUNE) !== null) {
263
316
  return true;
264
317
  }
265
318
  if (
@@ -274,7 +327,7 @@ export default class DOMPatch {
274
327
  return false;
275
328
  }
276
329
  // don't remove teleported elements
277
- if (el.getAttribute && el.getAttribute(PHX_TELEPORTED_REF)) {
330
+ if (el.getAttribute(PHX_TELEPORTED_REF)) {
278
331
  return false;
279
332
  }
280
333
  if (this.maybePendingRemove(el)) {
@@ -288,11 +341,11 @@ export default class DOMPatch {
288
341
  // if the portal template itself is removed, remove the teleported element as well;
289
342
  // we also perform a check after morphdom is finished to catch parent removals
290
343
  const teleportedEl = document.getElementById(
291
- el.content.firstElementChild.id,
344
+ el.content.firstElementChild?.id || "",
292
345
  );
293
346
  if (teleportedEl) {
294
347
  teleportedEl.remove();
295
- morphCallbacks.onNodeDiscarded(teleportedEl);
348
+ morphCallbacks.onNodeDiscarded!(teleportedEl);
296
349
  this.view.dropPortalElementId(teleportedEl.id);
297
350
  }
298
351
  }
@@ -314,9 +367,9 @@ export default class DOMPatch {
314
367
  fromEl.isSameNode(targetContainer) &&
315
368
  fromEl.id !== toEl.id
316
369
  ) {
317
- morphCallbacks.onNodeDiscarded(fromEl);
370
+ morphCallbacks.onNodeDiscarded!(fromEl);
318
371
  fromEl.replaceWith(toEl);
319
- return morphCallbacks.onNodeAdded(toEl);
372
+ return morphCallbacks.onNodeAdded!(toEl);
320
373
  }
321
374
  DOM.syncPendingAttrs(fromEl, toEl);
322
375
  DOM.maintainPrivateHooks(
@@ -327,7 +380,9 @@ export default class DOMPatch {
327
380
  );
328
381
  DOM.cleanChildNodes(toEl, phxUpdate);
329
382
  const isFocusedFormEl =
330
- focused && fromEl.isSameNode(focused) && DOM.isFormInput(fromEl);
383
+ focused &&
384
+ fromEl.isSameNode(focused) &&
385
+ DOM.isEditableInput(fromEl);
331
386
  const focusedSelectChanged =
332
387
  isFocusedFormEl && this.isChangedSelect(fromEl, toEl);
333
388
  if (this.skipCIDSibling(toEl)) {
@@ -359,7 +414,7 @@ export default class DOMPatch {
359
414
  DOM.isIgnored(fromEl, phxUpdate) ||
360
415
  (fromEl.form && fromEl.form.isSameNode(externalFormTriggered))
361
416
  ) {
362
- this.trackBefore("updated", fromEl, toEl);
417
+ this.trackBeforeUpdated(fromEl, toEl);
363
418
  DOM.mergeAttrs(fromEl, toEl, {
364
419
  isIgnored: DOM.isIgnored(fromEl, phxUpdate),
365
420
  });
@@ -419,7 +474,7 @@ export default class DOMPatch {
419
474
  fromEl.type !== "hidden" &&
420
475
  !focusedSelectChanged
421
476
  ) {
422
- this.trackBefore("updated", fromEl, toEl);
477
+ this.trackBeforeUpdated(fromEl, toEl);
423
478
  DOM.mergeFocusedInput(fromEl, toEl);
424
479
  DOM.syncAttrsToProps(fromEl);
425
480
  updates.push(fromEl);
@@ -442,7 +497,7 @@ export default class DOMPatch {
442
497
 
443
498
  DOM.syncAttrsToProps(toEl);
444
499
  DOM.applyStickyOperations(toEl);
445
- this.trackBefore("updated", fromEl, toEl);
500
+ this.trackBeforeUpdated(fromEl, toEl);
446
501
  return fromEl;
447
502
  }
448
503
  },
@@ -451,8 +506,7 @@ export default class DOMPatch {
451
506
  morphdom(targetContainer, source, morphCallbacks);
452
507
  };
453
508
 
454
- this.trackBefore("added", container);
455
- this.trackBefore("updated", container, container);
509
+ this.trackBeforeUpdated(container, container);
456
510
 
457
511
  liveSocket.time("morphdom", () => {
458
512
  this.streams.forEach(([ref, inserts, deleteIds, reset]) => {
@@ -506,13 +560,14 @@ export default class DOMPatch {
506
560
  this.view.portalElementIds.forEach((id) => {
507
561
  const el = document.getElementById(id);
508
562
  if (el) {
509
- const source = document.getElementById(
510
- el.getAttribute(PHX_TELEPORTED_SRC),
511
- );
512
- if (!source) {
513
- el.remove();
514
- this.onNodeDiscarded(el);
515
- this.view.dropPortalElementId(id);
563
+ const srcId = el.getAttribute(PHX_TELEPORTED_SRC);
564
+ if (srcId) {
565
+ const source = document.getElementById(srcId);
566
+ if (!source) {
567
+ el.remove();
568
+ this.onNodeDiscarded(el);
569
+ this.view.dropPortalElementId(id);
570
+ }
516
571
  }
517
572
  }
518
573
  });
@@ -544,8 +599,8 @@ export default class DOMPatch {
544
599
  DOM.restoreFocus(focused, selectionStart, selectionEnd),
545
600
  );
546
601
  DOM.dispatchEvent(document, "phx:update");
547
- added.forEach((el) => this.trackAfter("added", el));
548
- updates.forEach((el) => this.trackAfter("updated", el));
602
+ added.forEach((el) => this.trackAfterAdded(el));
603
+ updates.forEach((el) => this.trackAfterUpdated(el));
549
604
 
550
605
  this.transitionPendingRemoves();
551
606
 
@@ -575,15 +630,39 @@ export default class DOMPatch {
575
630
  return true;
576
631
  }
577
632
 
578
- onNodeDiscarded(el) {
633
+ private trackBeforeUpdated(fromEl: Element, toEl: Element) {
634
+ this.beforeUpdatedCallbacks.forEach((cb) => cb(fromEl, toEl));
635
+ }
636
+
637
+ private trackAfterAdded(el: Node) {
638
+ this.afterAddedCallbacks.forEach((cb) => cb(el));
639
+ }
640
+
641
+ private trackAfterUpdated(el: Element) {
642
+ this.afterUpdatedCallbacks.forEach((cb) => cb(el));
643
+ }
644
+
645
+ private trackAfterPhxChildAdded(el: Element) {
646
+ this.afterPhxChildAddedCallbacks.forEach((cb) => cb(el));
647
+ }
648
+
649
+ private trackAfterDiscarded(el: Node) {
650
+ this.afterDiscardedCallbacks.forEach((cb) => cb(el));
651
+ }
652
+
653
+ private trackAfterTransitionsDiscarded(els: Element[]) {
654
+ this.afterTransitionsDiscardedCallbacks.forEach((cb) => cb(els));
655
+ }
656
+
657
+ private onNodeDiscarded(el) {
579
658
  // nested view handling
580
659
  if (DOM.isPhxChild(el) || DOM.isPhxSticky(el)) {
581
660
  this.liveSocket.destroyViewByEl(el);
582
661
  }
583
- this.trackAfter("discarded", el);
662
+ this.trackAfterDiscarded(el);
584
663
  }
585
664
 
586
- maybePendingRemove(node) {
665
+ private maybePendingRemove(node) {
587
666
  if (node.getAttribute && node.getAttribute(this.phxRemove) !== null) {
588
667
  this.pendingRemoves.push(node);
589
668
  return true;
@@ -592,7 +671,7 @@ export default class DOMPatch {
592
671
  }
593
672
  }
594
673
 
595
- removeStreamChildElement(child, force = false) {
674
+ private removeStreamChildElement(child, force = false) {
596
675
  // make sure to only remove elements owned by the current view
597
676
  // see https://github.com/phoenixframework/phoenix_live_view/issues/3047
598
677
  // and https://github.com/phoenixframework/phoenix_live_view/issues/3681
@@ -614,18 +693,18 @@ export default class DOMPatch {
614
693
  }
615
694
  }
616
695
 
617
- getStreamInsert(el) {
696
+ private getStreamInsert(el) {
618
697
  const insert = el.id ? this.streamInserts[el.id] : {};
619
698
  return insert || {};
620
699
  }
621
700
 
622
- setStreamRef(el, ref) {
701
+ private setStreamRef(el, ref) {
623
702
  DOM.putSticky(el, PHX_STREAM_REF, (el) =>
624
703
  el.setAttribute(PHX_STREAM_REF, ref),
625
704
  );
626
705
  }
627
706
 
628
- maybeReOrderStream(el, isNew) {
707
+ private maybeReOrderStream(el: Element, isNew = false) {
629
708
  const { ref, streamAt, reset } = this.getStreamInsert(el);
630
709
  if (streamAt === undefined) {
631
710
  return;
@@ -681,7 +760,7 @@ export default class DOMPatch {
681
760
  // are not disconnected and reconnected by the move. Falls back to
682
761
  // insertBefore otherwise. Passing `ref === null` moves to the end.
683
762
  // See also https://github.com/phoenixframework/phoenix_live_view/issues/4212.
684
- moveOrInsertBefore(parent, child, ref) {
763
+ private moveOrInsertBefore(parent, child, ref) {
685
764
  if (typeof parent.moveBefore === "function") {
686
765
  try {
687
766
  parent.moveBefore(child, ref);
@@ -694,21 +773,23 @@ export default class DOMPatch {
694
773
  parent.insertBefore(child, ref);
695
774
  }
696
775
 
697
- maybeLimitStream(el) {
776
+ private maybeLimitStream(el) {
698
777
  const { limit } = this.getStreamInsert(el);
699
- const children = limit !== null && Array.from(el.parentElement.children);
700
- if (limit && limit < 0 && children.length > limit * -1) {
701
- children
702
- .slice(0, children.length + limit)
703
- .forEach((child) => this.removeStreamChildElement(child));
704
- } else if (limit && limit >= 0 && children.length > limit) {
705
- children
706
- .slice(limit)
707
- .forEach((child) => this.removeStreamChildElement(child));
778
+ if (limit !== null) {
779
+ const children = Array.from(el.parentElement.children);
780
+ if (limit < 0 && children.length > limit * -1) {
781
+ children
782
+ .slice(0, children.length + limit)
783
+ .forEach((child) => this.removeStreamChildElement(child));
784
+ } else if (limit >= 0 && children.length > limit) {
785
+ children
786
+ .slice(limit)
787
+ .forEach((child) => this.removeStreamChildElement(child));
788
+ }
708
789
  }
709
790
  }
710
791
 
711
- transitionPendingRemoves() {
792
+ private transitionPendingRemoves() {
712
793
  const { pendingRemoves, liveSocket } = this;
713
794
  if (pendingRemoves.length > 0) {
714
795
  liveSocket.transitionRemoves(pendingRemoves, () => {
@@ -719,12 +800,12 @@ export default class DOMPatch {
719
800
  }
720
801
  el.remove();
721
802
  });
722
- this.trackAfter("transitionsDiscarded", pendingRemoves);
803
+ this.trackAfterTransitionsDiscarded(pendingRemoves);
723
804
  });
724
805
  }
725
806
  }
726
807
 
727
- isChangedSelect(fromEl, toEl) {
808
+ private isChangedSelect(fromEl, toEl) {
728
809
  if (!(fromEl instanceof HTMLSelectElement) || fromEl.multiple) {
729
810
  return false;
730
811
  }
@@ -740,23 +821,19 @@ export default class DOMPatch {
740
821
  return !fromEl.isEqualNode(toEl);
741
822
  }
742
823
 
743
- isCIDPatch() {
744
- return this.cidPatch;
745
- }
746
-
747
- skipCIDSibling(el) {
824
+ private skipCIDSibling(el) {
748
825
  return el.nodeType === Node.ELEMENT_NODE && el.hasAttribute(PHX_SKIP);
749
826
  }
750
827
 
751
- maybeCloneLockedElement(fromEl, isFocusedFormEl) {
828
+ private maybeCloneLockedElement(fromEl, isFocusedFormEl) {
752
829
  if (!fromEl.hasAttribute(PHX_REF_SRC)) return fromEl;
753
830
 
754
831
  const ref = new ElementRef(fromEl);
755
- // Only perform the clone step while the element remains locked. lockRef can
756
- // be 0 for the first event, so compare against null/undefined explicitly.
832
+ // Only perform the clone step while the element remains locked. lockRef and
833
+ // undoRef can be 0 for the first event, so compare against null explicitly.
757
834
  if (
758
- ref.lockRef === null ||
759
- (this.undoRef !== undefined && ref.isLockUndoneBy(this.undoRef))
835
+ !fromEl.hasAttribute(PHX_REF_LOCK) ||
836
+ (this.undoRef !== null && ref.isLockUndoneBy(this.undoRef))
760
837
  ) {
761
838
  return fromEl;
762
839
  }
@@ -771,36 +848,21 @@ export default class DOMPatch {
771
848
  return isFocusedFormEl ? fromEl : clone;
772
849
  }
773
850
 
774
- copyNestedPrivateLock(fromEl, toEl) {
851
+ private copyNestedPrivateLock(fromEl, toEl) {
775
852
  // During unlock morphs, toEl may be the private clone that accumulated a
776
853
  // nested locked subtree. Copy that private clone back to fromEl before the
777
854
  // outer unlock finishes so the nested element can apply its own ack later.
778
- // undoRef can be 0, so presence is checked with undefined.
779
- if (this.undoRef === undefined || !DOM.private(toEl, PHX_REF_LOCK)) return;
855
+ // undoRef can be 0, so presence is checked against null.
856
+ if (this.undoRef === null || !DOM.private(toEl, PHX_REF_LOCK)) return;
780
857
 
781
858
  DOM.putPrivate(fromEl, PHX_REF_LOCK, DOM.private(toEl, PHX_REF_LOCK));
782
859
  }
783
860
 
784
- targetCIDContainer(html) {
785
- if (!this.isCIDPatch()) {
786
- return;
787
- }
788
- const [first, ...rest] = DOM.findComponentNodeList(
789
- this.view.id,
790
- this.targetCID,
791
- );
792
- if (rest.length === 0 && DOM.childNodeLength(html) === 1) {
793
- return first;
794
- } else {
795
- return first && first.parentNode;
796
- }
797
- }
798
-
799
- indexOf(parent, child) {
861
+ private indexOf(parent, child) {
800
862
  return Array.from(parent.children).indexOf(child);
801
863
  }
802
864
 
803
- teleport(el, morph) {
865
+ private teleport(el, morph) {
804
866
  const targetSelector = el.getAttribute(PHX_PORTAL);
805
867
  const portalContainer = document.querySelector(targetSelector);
806
868
  if (!portalContainer) {
@@ -850,17 +912,18 @@ export default class DOMPatch {
850
912
  this.view.pushPortalElementId(toTeleport.id);
851
913
  }
852
914
 
853
- handleRuntimeHook(el, source) {
915
+ private handleRuntimeHook(el: HTMLScriptElement, source: string) {
854
916
  // usually, scripts are not executed when morphdom adds them to the DOM
855
917
  // we special case runtime colocated hooks
856
- const name = el.getAttribute(PHX_RUNTIME_HOOK);
918
+ const name = el.getAttribute(PHX_RUNTIME_HOOK)!;
857
919
  let nonce = el.hasAttribute("nonce") ? el.getAttribute("nonce") : null;
858
920
  if (el.hasAttribute("nonce")) {
859
921
  const template = document.createElement("template");
860
922
  template.innerHTML = source;
861
- nonce = template.content
862
- .querySelector(`script[${PHX_RUNTIME_HOOK}="${CSS.escape(name)}"]`)
863
- .getAttribute("nonce");
923
+ nonce =
924
+ template.content
925
+ .querySelector(`script[${PHX_RUNTIME_HOOK}="${CSS.escape(name)}"]`)
926
+ ?.getAttribute("nonce") ?? null;
864
927
  }
865
928
  const script = document.createElement("script");
866
929
  script.textContent = el.textContent;
@@ -3,13 +3,28 @@ import { maybe } from "./utils";
3
3
  import DOM from "./dom";
4
4
 
5
5
  export default class DOMPostMorphRestorer {
6
- constructor(containerBefore, containerAfter, updateType) {
6
+ private containerId: string;
7
+ private updateType: "append" | "prepend";
8
+ private elementsToModify: {
9
+ elementId: string;
10
+ previousElementId: string | null;
11
+ }[];
12
+ private elementIdsToAdd: string[];
13
+
14
+ constructor(
15
+ containerBefore: Element,
16
+ containerAfter: Element,
17
+ updateType: "append" | "prepend",
18
+ ) {
7
19
  const idsBefore = new Set();
8
20
  const idsAfter = new Set(
9
21
  [...containerAfter.children].map((child) => child.id),
10
22
  );
11
23
 
12
- const elementsToModify = [];
24
+ const elementsToModify: Array<{
25
+ elementId: string;
26
+ previousElementId: string | null;
27
+ }> = [];
13
28
 
14
29
  Array.from(containerBefore.children).forEach((child) => {
15
30
  if (child.id) {