phoenix_live_view 1.1.27 → 1.1.29

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.
@@ -54,7 +54,11 @@ export default class DOMPatch {
54
54
  afterphxChildAdded: [],
55
55
  aftertransitionsDiscarded: [],
56
56
  };
57
- this.withChildren = opts.withChildren || opts.undoRef || false;
57
+ // unlock patches pass undoRef and must morph the locked element itself, not
58
+ // only its children. The first client ref is 0, so this must check for the
59
+ // option's presence rather than truthiness.
60
+ this.withChildren =
61
+ opts.withChildren || opts.undoRef !== undefined || false;
58
62
  this.undoRef = opts.undoRef;
59
63
  }
60
64
 
@@ -105,6 +109,12 @@ export default class DOMPatch {
105
109
  targetContainer = clonedTree.querySelector(
106
110
  `[data-phx-component="${this.targetCID}"]`,
107
111
  );
112
+ // The visible DOM can still contain the target CID while the locked
113
+ // clone has gone stale and no longer does. In that case there is no
114
+ // safe clone target for this component diff, so leave the visible DOM
115
+ // locked and wait for a later patch instead of throwing or patching
116
+ // outside the locked tree.
117
+ if (!targetContainer) return;
108
118
  }
109
119
  }
110
120
  }
@@ -148,6 +158,13 @@ export default class DOMPatch {
148
158
  if (isJoinPatch) {
149
159
  return node.id;
150
160
  }
161
+
162
+ // If ID was touched by JavaScript hook, use PHX_MAGIC_ID for matching.
163
+ // This ensures morphdom can match elements even when JS modifies their IDs.
164
+ if (DOM.private(node, "clientsideIdAttribute")) {
165
+ return node.getAttribute && node.getAttribute(PHX_MAGIC_ID);
166
+ }
167
+
151
168
  return (
152
169
  node.id || (node.getAttribute && node.getAttribute(PHX_MAGIC_ID))
153
170
  );
@@ -309,7 +326,16 @@ export default class DOMPatch {
309
326
  phxViewportBottom,
310
327
  );
311
328
  DOM.cleanChildNodes(toEl, phxUpdate);
329
+ const isFocusedFormEl =
330
+ focused && fromEl.isSameNode(focused) && DOM.isFormInput(fromEl);
331
+ const focusedSelectChanged =
332
+ isFocusedFormEl && this.isChangedSelect(fromEl, toEl);
312
333
  if (this.skipCIDSibling(toEl)) {
334
+ // A skipped update returns before the normal update path below, so
335
+ // it must still perform the lock bookkeeping that keeps private
336
+ // cloned trees in sync.
337
+ this.maybeCloneLockedElement(fromEl, isFocusedFormEl);
338
+ this.copyNestedPrivateLock(fromEl, toEl);
313
339
  // if this is a live component used in a stream, we may need to reorder it
314
340
  this.maybeReOrderStream(fromEl);
315
341
  return false;
@@ -354,30 +380,7 @@ export default class DOMPatch {
354
380
  // We keep a reference to the cloned tree in the element's private data, and
355
381
  // on ack (view.undoRefs), we morph the cloned tree with the true fromEl in the DOM to
356
382
  // apply any changes that happened while the element was locked.
357
- const isFocusedFormEl =
358
- focused && fromEl.isSameNode(focused) && DOM.isFormInput(fromEl);
359
- const focusedSelectChanged =
360
- isFocusedFormEl && this.isChangedSelect(fromEl, toEl);
361
- if (fromEl.hasAttribute(PHX_REF_SRC)) {
362
- const ref = new ElementRef(fromEl);
363
- // only perform the clone step if this is not a patch that unlocks
364
- if (
365
- ref.lockRef &&
366
- (!this.undoRef || !ref.isLockUndoneBy(this.undoRef))
367
- ) {
368
- DOM.applyStickyOperations(fromEl);
369
- const isLocked = fromEl.hasAttribute(PHX_REF_LOCK);
370
- const clone = isLocked
371
- ? DOM.private(fromEl, PHX_REF_LOCK) || fromEl.cloneNode(true)
372
- : null;
373
- if (clone) {
374
- DOM.putPrivate(fromEl, PHX_REF_LOCK, clone);
375
- if (!isFocusedFormEl) {
376
- fromEl = clone;
377
- }
378
- }
379
- }
380
- }
383
+ fromEl = this.maybeCloneLockedElement(fromEl, isFocusedFormEl);
381
384
 
382
385
  // nested view handling
383
386
  if (DOM.isPhxChild(toEl)) {
@@ -391,14 +394,10 @@ export default class DOMPatch {
391
394
  return false;
392
395
  }
393
396
 
394
- // if we are undoing a lock, copy potentially nested clones over
395
- if (this.undoRef && DOM.private(toEl, PHX_REF_LOCK)) {
396
- DOM.putPrivate(
397
- fromEl,
398
- PHX_REF_LOCK,
399
- DOM.private(toEl, PHX_REF_LOCK),
400
- );
401
- }
397
+ // If we are undoing a lock, copy potentially nested clones over.
398
+ // This keeps an inner locked subtree's private clone alive while an
399
+ // ancestor lock is being reconciled.
400
+ this.copyNestedPrivateLock(fromEl, toEl);
402
401
  // now copy regular DOM.private data
403
402
  DOM.copyPrivates(toEl, fromEl);
404
403
 
@@ -649,18 +648,26 @@ export default class DOMPatch {
649
648
  }
650
649
 
651
650
  if (streamAt === 0) {
652
- el.parentElement.insertBefore(el, el.parentElement.firstElementChild);
651
+ this.moveOrInsertBefore(
652
+ el.parentElement,
653
+ el,
654
+ el.parentElement.firstElementChild,
655
+ );
653
656
  } else if (streamAt > 0) {
654
657
  const children = Array.from(el.parentElement.children);
655
658
  const oldIndex = children.indexOf(el);
656
659
  if (streamAt >= children.length - 1) {
657
- el.parentElement.appendChild(el);
660
+ this.moveOrInsertBefore(el.parentElement, el, null);
658
661
  } else {
659
662
  const sibling = children[streamAt];
660
663
  if (oldIndex > streamAt) {
661
- el.parentElement.insertBefore(el, sibling);
664
+ this.moveOrInsertBefore(el.parentElement, el, sibling);
662
665
  } else {
663
- el.parentElement.insertBefore(el, sibling.nextElementSibling);
666
+ this.moveOrInsertBefore(
667
+ el.parentElement,
668
+ el,
669
+ sibling.nextElementSibling,
670
+ );
664
671
  }
665
672
  }
666
673
  }
@@ -668,6 +675,25 @@ export default class DOMPatch {
668
675
  this.maybeLimitStream(el);
669
676
  }
670
677
 
678
+ // Reorder a child within its parent. When supported, use the atomic
679
+ // moveBefore (https://developer.mozilla.org/en-US/docs/Web/API/Node/moveBefore)
680
+ // so connected custom elements (and other state-bearing nodes like iframes)
681
+ // are not disconnected and reconnected by the move. Falls back to
682
+ // insertBefore otherwise. Passing `ref === null` moves to the end.
683
+ // See also https://github.com/phoenixframework/phoenix_live_view/issues/4212.
684
+ moveOrInsertBefore(parent, child, ref) {
685
+ if (typeof parent.moveBefore === "function") {
686
+ try {
687
+ parent.moveBefore(child, ref);
688
+ return;
689
+ } catch {
690
+ // moveBefore can throw (e.g. HierarchyRequestError) in cases where
691
+ // an atomic move is not possible; fall back to insertBefore.
692
+ }
693
+ }
694
+ parent.insertBefore(child, ref);
695
+ }
696
+
671
697
  maybeLimitStream(el) {
672
698
  const { limit } = this.getStreamInsert(el);
673
699
  const children = limit !== null && Array.from(el.parentElement.children);
@@ -722,6 +748,39 @@ export default class DOMPatch {
722
748
  return el.nodeType === Node.ELEMENT_NODE && el.hasAttribute(PHX_SKIP);
723
749
  }
724
750
 
751
+ maybeCloneLockedElement(fromEl, isFocusedFormEl) {
752
+ if (!fromEl.hasAttribute(PHX_REF_SRC)) return fromEl;
753
+
754
+ 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.
757
+ if (
758
+ ref.lockRef === null ||
759
+ (this.undoRef !== undefined && ref.isLockUndoneBy(this.undoRef))
760
+ ) {
761
+ return fromEl;
762
+ }
763
+
764
+ DOM.applyStickyOperations(fromEl);
765
+ const clone = fromEl.hasAttribute(PHX_REF_LOCK)
766
+ ? DOM.private(fromEl, PHX_REF_LOCK) || fromEl.cloneNode(true)
767
+ : null;
768
+ if (!clone) return fromEl;
769
+
770
+ DOM.putPrivate(fromEl, PHX_REF_LOCK, clone);
771
+ return isFocusedFormEl ? fromEl : clone;
772
+ }
773
+
774
+ copyNestedPrivateLock(fromEl, toEl) {
775
+ // During unlock morphs, toEl may be the private clone that accumulated a
776
+ // nested locked subtree. Copy that private clone back to fromEl before the
777
+ // 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;
780
+
781
+ DOM.putPrivate(fromEl, PHX_REF_LOCK, DOM.private(toEl, PHX_REF_LOCK));
782
+ }
783
+
725
784
  targetCIDContainer(html) {
726
785
  if (!this.isCIDPatch()) {
727
786
  return;
@@ -131,7 +131,6 @@ const isAtViewportTop = (el, scrollContainer) => {
131
131
  const rect = el.getBoundingClientRect();
132
132
  return (
133
133
  Math.ceil(rect.top) >= top(scrollContainer) &&
134
- Math.ceil(rect.left) >= 0 &&
135
134
  Math.floor(rect.top) <= bottom(scrollContainer)
136
135
  );
137
136
  };
@@ -140,7 +139,6 @@ const isAtViewportBottom = (el, scrollContainer) => {
140
139
  const rect = el.getBoundingClientRect();
141
140
  return (
142
141
  Math.ceil(rect.bottom) >= top(scrollContainer) &&
143
- Math.ceil(rect.left) >= 0 &&
144
142
  Math.floor(rect.bottom) <= bottom(scrollContainer)
145
143
  );
146
144
  };
@@ -149,7 +147,6 @@ const isWithinViewport = (el, scrollContainer) => {
149
147
  const rect = el.getBoundingClientRect();
150
148
  return (
151
149
  Math.ceil(rect.top) >= top(scrollContainer) &&
152
- Math.ceil(rect.left) >= 0 &&
153
150
  Math.floor(rect.top) <= bottom(scrollContainer)
154
151
  );
155
152
  };
@@ -264,6 +261,15 @@ Hooks.InfiniteScroll = {
264
261
  }
265
262
  },
266
263
 
264
+ updated() {
265
+ // Check if the scroll container still exists
266
+ // https://github.com/phoenixframework/phoenix_live_view/issues/4169.
267
+ if (!this.scrollContainer.isConnected) {
268
+ this.destroyed();
269
+ this.mounted();
270
+ }
271
+ },
272
+
267
273
  destroyed() {
268
274
  if (this.scrollContainer) {
269
275
  this.scrollContainer.removeEventListener("scroll", this.onScroll);
@@ -599,6 +599,11 @@ const JS = {
599
599
  .filter((attr) => !alteredAttrs.includes(attr))
600
600
  .concat(removes);
601
601
 
602
+ // If element ID is touched via JavaScript, mark it for cheap lookup during morphdom
603
+ if (sets.some(([attr, _val]) => attr === "id")) {
604
+ DOM.putPrivate(el, "clientsideIdAttribute", true);
605
+ }
606
+
602
607
  DOM.putSticky(el, "attrs", (currentEl) => {
603
608
  newRemoves.forEach((attr) => currentEl.removeAttribute(attr));
604
609
  newSets.forEach(([attr, val]) => currentEl.setAttribute(attr, val));
@@ -193,6 +193,11 @@ export default class View {
193
193
  // bind the view to the element
194
194
  DOM.putPrivate(this.el, "view", this);
195
195
  this.id = this.el.id;
196
+ // destroyViewByEl requires the root set, so we need to set it early
197
+ // otherwise it could happen that we try to apply a join result for a
198
+ // view whose DOM node was already removed
199
+ // See https://github.com/phoenixframework/phoenix_live_view/issues/4177.
200
+ this.el.setAttribute(PHX_ROOT_ID, this.root.id);
196
201
  this.ref = 0;
197
202
  this.lastAckRef = null;
198
203
  this.childJoins = 0;
@@ -684,7 +689,8 @@ export default class View {
684
689
 
685
690
  patch.after("updated", (el) => {
686
691
  if (updatedHookIds.has(el.id)) {
687
- this.getHook(el).__updated();
692
+ const hook = this.getHook(el);
693
+ hook && hook.__updated();
688
694
  }
689
695
  });
690
696
 
@@ -38,11 +38,14 @@ export default class DOMPatch {
38
38
  getStreamInsert(el: any): any;
39
39
  setStreamRef(el: any, ref: any): void;
40
40
  maybeReOrderStream(el: any, isNew: any): void;
41
+ moveOrInsertBefore(parent: any, child: any, ref: any): void;
41
42
  maybeLimitStream(el: any): void;
42
43
  transitionPendingRemoves(): void;
43
44
  isChangedSelect(fromEl: any, toEl: any): boolean;
44
45
  isCIDPatch(): boolean;
45
46
  skipCIDSibling(el: any): any;
47
+ maybeCloneLockedElement(fromEl: any, isFocusedFormEl: any): any;
48
+ copyNestedPrivateLock(fromEl: any, toEl: any): void;
46
49
  targetCIDContainer(html: any): any;
47
50
  indexOf(parent: any, child: any): number;
48
51
  teleport(el: any, morph: any): void;
@@ -2,6 +2,7 @@ export default Hooks;
2
2
  declare namespace Hooks {
3
3
  namespace InfiniteScroll {
4
4
  function mounted(): void;
5
+ function updated(): void;
5
6
  function destroyed(): void;
6
7
  function throttle(interval: any, callback: any): (...args: any[]) => void;
7
8
  function findOverrunTarget(): any;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phoenix_live_view",
3
- "version": "1.1.27",
3
+ "version": "1.1.29",
4
4
  "description": "The Phoenix LiveView JavaScript client.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1359,15 +1359,15 @@ var top = (scrollContainer) => {
1359
1359
  };
1360
1360
  var isAtViewportTop = (el, scrollContainer) => {
1361
1361
  const rect = el.getBoundingClientRect();
1362
- return Math.ceil(rect.top) >= top(scrollContainer) && Math.ceil(rect.left) >= 0 && Math.floor(rect.top) <= bottom(scrollContainer);
1362
+ return Math.ceil(rect.top) >= top(scrollContainer) && Math.floor(rect.top) <= bottom(scrollContainer);
1363
1363
  };
1364
1364
  var isAtViewportBottom = (el, scrollContainer) => {
1365
1365
  const rect = el.getBoundingClientRect();
1366
- return Math.ceil(rect.bottom) >= top(scrollContainer) && Math.ceil(rect.left) >= 0 && Math.floor(rect.bottom) <= bottom(scrollContainer);
1366
+ return Math.ceil(rect.bottom) >= top(scrollContainer) && Math.floor(rect.bottom) <= bottom(scrollContainer);
1367
1367
  };
1368
1368
  var isWithinViewport = (el, scrollContainer) => {
1369
1369
  const rect = el.getBoundingClientRect();
1370
- return Math.ceil(rect.top) >= top(scrollContainer) && Math.ceil(rect.left) >= 0 && Math.floor(rect.top) <= bottom(scrollContainer);
1370
+ return Math.ceil(rect.top) >= top(scrollContainer) && Math.floor(rect.top) <= bottom(scrollContainer);
1371
1371
  };
1372
1372
  Hooks.InfiniteScroll = {
1373
1373
  mounted() {
@@ -1458,6 +1458,12 @@ Hooks.InfiniteScroll = {
1458
1458
  window.addEventListener("scroll", this.onScroll);
1459
1459
  }
1460
1460
  },
1461
+ updated() {
1462
+ if (!this.scrollContainer.isConnected) {
1463
+ this.destroyed();
1464
+ this.mounted();
1465
+ }
1466
+ },
1461
1467
  destroyed() {
1462
1468
  if (this.scrollContainer) {
1463
1469
  this.scrollContainer.removeEventListener("scroll", this.onScroll);
@@ -2264,7 +2270,7 @@ var DOMPatch = class {
2264
2270
  afterphxChildAdded: [],
2265
2271
  aftertransitionsDiscarded: []
2266
2272
  };
2267
- this.withChildren = opts.withChildren || opts.undoRef || false;
2273
+ this.withChildren = opts.withChildren || opts.undoRef !== void 0 || false;
2268
2274
  this.undoRef = opts.undoRef;
2269
2275
  }
2270
2276
  before(kind, callback) {
@@ -2303,6 +2309,8 @@ var DOMPatch = class {
2303
2309
  targetContainer = clonedTree.querySelector(
2304
2310
  `[data-phx-component="${this.targetCID}"]`
2305
2311
  );
2312
+ if (!targetContainer)
2313
+ return;
2306
2314
  }
2307
2315
  }
2308
2316
  }
@@ -2331,6 +2339,9 @@ var DOMPatch = class {
2331
2339
  if (isJoinPatch) {
2332
2340
  return node.id;
2333
2341
  }
2342
+ if (dom_default.private(node, "clientsideIdAttribute")) {
2343
+ return node.getAttribute && node.getAttribute(PHX_MAGIC_ID);
2344
+ }
2334
2345
  return node.id || node.getAttribute && node.getAttribute(PHX_MAGIC_ID);
2335
2346
  },
2336
2347
  // skip indexing from children when container is stream
@@ -2452,7 +2463,11 @@ var DOMPatch = class {
2452
2463
  phxViewportBottom
2453
2464
  );
2454
2465
  dom_default.cleanChildNodes(toEl, phxUpdate);
2466
+ const isFocusedFormEl = focused && fromEl.isSameNode(focused) && dom_default.isFormInput(fromEl);
2467
+ const focusedSelectChanged = isFocusedFormEl && this.isChangedSelect(fromEl, toEl);
2455
2468
  if (this.skipCIDSibling(toEl)) {
2469
+ this.maybeCloneLockedElement(fromEl, isFocusedFormEl);
2470
+ this.copyNestedPrivateLock(fromEl, toEl);
2456
2471
  this.maybeReOrderStream(fromEl);
2457
2472
  return false;
2458
2473
  }
@@ -2480,22 +2495,7 @@ var DOMPatch = class {
2480
2495
  if (fromEl.type === "number" && fromEl.validity && fromEl.validity.badInput) {
2481
2496
  return false;
2482
2497
  }
2483
- const isFocusedFormEl = focused && fromEl.isSameNode(focused) && dom_default.isFormInput(fromEl);
2484
- const focusedSelectChanged = isFocusedFormEl && this.isChangedSelect(fromEl, toEl);
2485
- if (fromEl.hasAttribute(PHX_REF_SRC)) {
2486
- const ref = new ElementRef(fromEl);
2487
- if (ref.lockRef && (!this.undoRef || !ref.isLockUndoneBy(this.undoRef))) {
2488
- dom_default.applyStickyOperations(fromEl);
2489
- const isLocked = fromEl.hasAttribute(PHX_REF_LOCK);
2490
- const clone2 = isLocked ? dom_default.private(fromEl, PHX_REF_LOCK) || fromEl.cloneNode(true) : null;
2491
- if (clone2) {
2492
- dom_default.putPrivate(fromEl, PHX_REF_LOCK, clone2);
2493
- if (!isFocusedFormEl) {
2494
- fromEl = clone2;
2495
- }
2496
- }
2497
- }
2498
- }
2498
+ fromEl = this.maybeCloneLockedElement(fromEl, isFocusedFormEl);
2499
2499
  if (dom_default.isPhxChild(toEl)) {
2500
2500
  const prevSession = fromEl.getAttribute(PHX_SESSION);
2501
2501
  dom_default.mergeAttrs(fromEl, toEl, { exclude: [PHX_STATIC] });
@@ -2506,13 +2506,7 @@ var DOMPatch = class {
2506
2506
  dom_default.applyStickyOperations(fromEl);
2507
2507
  return false;
2508
2508
  }
2509
- if (this.undoRef && dom_default.private(toEl, PHX_REF_LOCK)) {
2510
- dom_default.putPrivate(
2511
- fromEl,
2512
- PHX_REF_LOCK,
2513
- dom_default.private(toEl, PHX_REF_LOCK)
2514
- );
2515
- }
2509
+ this.copyNestedPrivateLock(fromEl, toEl);
2516
2510
  dom_default.copyPrivates(toEl, fromEl);
2517
2511
  if (dom_default.isPortalTemplate(toEl)) {
2518
2512
  portalCallbacks.push(() => this.teleport(toEl, morph));
@@ -2694,23 +2688,47 @@ var DOMPatch = class {
2694
2688
  return;
2695
2689
  }
2696
2690
  if (streamAt === 0) {
2697
- el.parentElement.insertBefore(el, el.parentElement.firstElementChild);
2691
+ this.moveOrInsertBefore(
2692
+ el.parentElement,
2693
+ el,
2694
+ el.parentElement.firstElementChild
2695
+ );
2698
2696
  } else if (streamAt > 0) {
2699
2697
  const children = Array.from(el.parentElement.children);
2700
2698
  const oldIndex = children.indexOf(el);
2701
2699
  if (streamAt >= children.length - 1) {
2702
- el.parentElement.appendChild(el);
2700
+ this.moveOrInsertBefore(el.parentElement, el, null);
2703
2701
  } else {
2704
2702
  const sibling = children[streamAt];
2705
2703
  if (oldIndex > streamAt) {
2706
- el.parentElement.insertBefore(el, sibling);
2704
+ this.moveOrInsertBefore(el.parentElement, el, sibling);
2707
2705
  } else {
2708
- el.parentElement.insertBefore(el, sibling.nextElementSibling);
2706
+ this.moveOrInsertBefore(
2707
+ el.parentElement,
2708
+ el,
2709
+ sibling.nextElementSibling
2710
+ );
2709
2711
  }
2710
2712
  }
2711
2713
  }
2712
2714
  this.maybeLimitStream(el);
2713
2715
  }
2716
+ // Reorder a child within its parent. When supported, use the atomic
2717
+ // moveBefore (https://developer.mozilla.org/en-US/docs/Web/API/Node/moveBefore)
2718
+ // so connected custom elements (and other state-bearing nodes like iframes)
2719
+ // are not disconnected and reconnected by the move. Falls back to
2720
+ // insertBefore otherwise. Passing `ref === null` moves to the end.
2721
+ // See also https://github.com/phoenixframework/phoenix_live_view/issues/4212.
2722
+ moveOrInsertBefore(parent, child, ref) {
2723
+ if (typeof parent.moveBefore === "function") {
2724
+ try {
2725
+ parent.moveBefore(child, ref);
2726
+ return;
2727
+ } catch {
2728
+ }
2729
+ }
2730
+ parent.insertBefore(child, ref);
2731
+ }
2714
2732
  maybeLimitStream(el) {
2715
2733
  const { limit } = this.getStreamInsert(el);
2716
2734
  const children = limit !== null && Array.from(el.parentElement.children);
@@ -2751,6 +2769,25 @@ var DOMPatch = class {
2751
2769
  skipCIDSibling(el) {
2752
2770
  return el.nodeType === Node.ELEMENT_NODE && el.hasAttribute(PHX_SKIP);
2753
2771
  }
2772
+ maybeCloneLockedElement(fromEl, isFocusedFormEl) {
2773
+ if (!fromEl.hasAttribute(PHX_REF_SRC))
2774
+ return fromEl;
2775
+ const ref = new ElementRef(fromEl);
2776
+ if (ref.lockRef === null || this.undoRef !== void 0 && ref.isLockUndoneBy(this.undoRef)) {
2777
+ return fromEl;
2778
+ }
2779
+ dom_default.applyStickyOperations(fromEl);
2780
+ const clone2 = fromEl.hasAttribute(PHX_REF_LOCK) ? dom_default.private(fromEl, PHX_REF_LOCK) || fromEl.cloneNode(true) : null;
2781
+ if (!clone2)
2782
+ return fromEl;
2783
+ dom_default.putPrivate(fromEl, PHX_REF_LOCK, clone2);
2784
+ return isFocusedFormEl ? fromEl : clone2;
2785
+ }
2786
+ copyNestedPrivateLock(fromEl, toEl) {
2787
+ if (this.undoRef === void 0 || !dom_default.private(toEl, PHX_REF_LOCK))
2788
+ return;
2789
+ dom_default.putPrivate(fromEl, PHX_REF_LOCK, dom_default.private(toEl, PHX_REF_LOCK));
2790
+ }
2754
2791
  targetCIDContainer(html) {
2755
2792
  if (!this.isCIDPatch()) {
2756
2793
  return;
@@ -3688,6 +3725,9 @@ var JS = {
3688
3725
  const alteredAttrs = sets.map(([attr, _val]) => attr).concat(removes);
3689
3726
  const newSets = prevSets.filter(([attr, _val]) => !alteredAttrs.includes(attr)).concat(sets);
3690
3727
  const newRemoves = prevRemoves.filter((attr) => !alteredAttrs.includes(attr)).concat(removes);
3728
+ if (sets.some(([attr, _val]) => attr === "id")) {
3729
+ dom_default.putPrivate(el, "clientsideIdAttribute", true);
3730
+ }
3691
3731
  dom_default.putSticky(el, "attrs", (currentEl) => {
3692
3732
  newRemoves.forEach((attr) => currentEl.removeAttribute(attr));
3693
3733
  newSets.forEach(([attr, val]) => currentEl.setAttribute(attr, val));
@@ -4189,6 +4229,7 @@ var View = class _View {
4189
4229
  }
4190
4230
  dom_default.putPrivate(this.el, "view", this);
4191
4231
  this.id = this.el.id;
4232
+ this.el.setAttribute(PHX_ROOT_ID, this.root.id);
4192
4233
  this.ref = 0;
4193
4234
  this.lastAckRef = null;
4194
4235
  this.childJoins = 0;
@@ -4592,7 +4633,8 @@ var View = class _View {
4592
4633
  });
4593
4634
  patch.after("updated", (el) => {
4594
4635
  if (updatedHookIds.has(el.id)) {
4595
- this.getHook(el).__updated();
4636
+ const hook = this.getHook(el);
4637
+ hook && hook.__updated();
4596
4638
  }
4597
4639
  });
4598
4640
  patch.after("discarded", (el) => {
@@ -5934,7 +5976,7 @@ var LiveSocket = class {
5934
5976
  }
5935
5977
  // public
5936
5978
  version() {
5937
- return "1.1.27";
5979
+ return "1.1.29";
5938
5980
  }
5939
5981
  isProfileEnabled() {
5940
5982
  return this.sessionStorage.getItem(PHX_LV_PROFILE) === "true";