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.
- package/README.md +5 -5
- package/assets/js/phoenix_live_view/README.md +3 -0
- package/assets/js/phoenix_live_view/{aria.js → aria.ts} +18 -10
- package/assets/js/phoenix_live_view/{browser.js → browser.ts} +12 -8
- package/assets/js/phoenix_live_view/{dom.js → dom.ts} +107 -34
- package/assets/js/phoenix_live_view/{dom_patch.js → dom_patch.ts} +187 -124
- package/assets/js/phoenix_live_view/{dom_post_morph_restorer.js → dom_post_morph_restorer.ts} +17 -2
- package/assets/js/phoenix_live_view/{element_ref.js → element_ref.ts} +17 -11
- package/assets/js/phoenix_live_view/entry_uploader.js +4 -4
- package/assets/js/phoenix_live_view/{hooks.js → hooks.ts} +108 -91
- package/assets/js/phoenix_live_view/index.ts +14 -301
- package/assets/js/phoenix_live_view/js.js +2 -1
- package/assets/js/phoenix_live_view/js_commands.ts +12 -9
- package/assets/js/phoenix_live_view/{live_socket.js → live_socket.ts} +582 -114
- package/assets/js/phoenix_live_view/live_uploader.js +1 -1
- package/assets/js/phoenix_live_view/rendered.js +3 -0
- package/assets/js/phoenix_live_view/{utils.js → utils.ts} +35 -6
- package/assets/js/phoenix_live_view/{view.js → view.ts} +221 -110
- package/assets/js/phoenix_live_view/view_hook.ts +92 -32
- package/package.json +5 -2
- package/priv/static/phoenix_live_view.cjs.js +577 -314
- package/priv/static/phoenix_live_view.cjs.js.map +4 -4
- package/priv/static/phoenix_live_view.esm.js +577 -314
- package/priv/static/phoenix_live_view.esm.js.map +4 -4
- package/priv/static/phoenix_live_view.js +584 -314
- package/priv/static/phoenix_live_view.min.js +7 -7
- /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
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
66
|
-
this.
|
|
106
|
+
afterAdded(callback: AfterAddedCallback) {
|
|
107
|
+
this.afterAddedCallbacks.push(callback);
|
|
67
108
|
}
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
this.
|
|
118
|
+
afterDiscarded(callback: AfterDiscardedCallback) {
|
|
119
|
+
this.afterDiscardedCallbacks.push(callback);
|
|
74
120
|
}
|
|
75
121
|
|
|
76
|
-
|
|
77
|
-
this.
|
|
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.
|
|
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
|
|
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
|
|
227
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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 &&
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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.
|
|
548
|
-
updates.forEach((el) => this.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
700
|
-
|
|
701
|
-
children
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
children
|
|
706
|
-
|
|
707
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
756
|
-
// be 0 for the first event, so compare against null
|
|
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
|
-
|
|
759
|
-
(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
|
|
779
|
-
if (this.undoRef ===
|
|
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
|
-
|
|
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 =
|
|
862
|
-
.
|
|
863
|
-
|
|
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;
|
package/assets/js/phoenix_live_view/{dom_post_morph_restorer.js → dom_post_morph_restorer.ts}
RENAMED
|
@@ -3,13 +3,28 @@ import { maybe } from "./utils";
|
|
|
3
3
|
import DOM from "./dom";
|
|
4
4
|
|
|
5
5
|
export default class DOMPostMorphRestorer {
|
|
6
|
-
|
|
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) {
|