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
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Phoenix LiveView
|
|
2
2
|
|
|
3
|
-
[](https://github.com/phoenixframework/phoenix_live_view/actions?query=workflow%3ACI) [](https://hex.pm/packages/phoenix_live_view) [](https://hexdocs.pm
|
|
3
|
+
[](https://github.com/phoenixframework/phoenix_live_view/actions?query=workflow%3ACI) [](https://hex.pm/packages/phoenix_live_view) [](https://phoenix-live-view.hexdocs.pm)
|
|
4
4
|
|
|
5
5
|
Phoenix LiveView enables rich, real-time user experiences with server-rendered HTML.
|
|
6
6
|
|
|
@@ -34,7 +34,7 @@ model while keeping your code closer to your data (and ultimately your source of
|
|
|
34
34
|
|
|
35
35
|
* **Diffs over the wire:** Instead of sending "HTML over the wire", LiveView knows exactly which parts of your templates change, sending minimal diffs over the wire after the initial render, reducing latency and bandwidth usage. The client leverages this information and optimizes the browser with 5-10x faster updates, compared to solutions that replace whole HTML fragments.
|
|
36
36
|
|
|
37
|
-
* **Live form validation:** LiveView supports real-time form validation out of the box. Create rich user interfaces with features like uploads, nested inputs, and [specialized recovery](https://hexdocs.pm/
|
|
37
|
+
* **Live form validation:** LiveView supports real-time form validation out of the box. Create rich user interfaces with features like uploads, nested inputs, and [specialized recovery](https://phoenix-live-view.hexdocs.pm/form-bindings.html#recovery-following-crashes-or-disconnects).
|
|
38
38
|
|
|
39
39
|
* **File uploads:** Real-time file uploads with progress indicators and image previews. Process your uploads on the fly or submit them to your desired cloud service.
|
|
40
40
|
|
|
@@ -52,9 +52,9 @@ model while keeping your code closer to your data (and ultimately your source of
|
|
|
52
52
|
|
|
53
53
|
## Learning
|
|
54
54
|
|
|
55
|
-
Check our [comprehensive docs](https://hexdocs.pm
|
|
55
|
+
Check our [comprehensive docs](https://phoenix-live-view.hexdocs.pm) to get started.
|
|
56
56
|
|
|
57
|
-
The Phoenix framework documentation also keeps a list of [community resources](https://hexdocs.pm/
|
|
57
|
+
The Phoenix framework documentation also keeps a list of [community resources](https://phoenix.hexdocs.pm/community.html), including books, videos, and other materials, and some include LiveView too.
|
|
58
58
|
|
|
59
59
|
Also follow these announcements from the Phoenix team on LiveView for more examples and rationale:
|
|
60
60
|
|
|
@@ -103,7 +103,7 @@ which provides quick times for "First Meaningful Paint", in addition to
|
|
|
103
103
|
helping search and indexing engines.
|
|
104
104
|
|
|
105
105
|
Then LiveView uses a persistent connection between client and server.
|
|
106
|
-
This allows LiveView applications to react faster to user events as
|
|
106
|
+
This allows LiveView applications to react faster to user events, as
|
|
107
107
|
there is less work to be done and less data to be sent compared to
|
|
108
108
|
stateless requests that have to authenticate, decode, load, and encode
|
|
109
109
|
data on every request.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
## LiveView JavaScript Client
|
|
2
|
+
|
|
3
|
+
This is the documentation for the LiveView JavaScript client. It is a more low-level API documentation for advanced users. For a higher-level overview, [see the page on JavaScript interoperability](https://phoenix-live-view.hexdocs.pm/js-interop.html) instead.
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
const ARIA = {
|
|
2
|
-
anyOf(
|
|
3
|
-
|
|
2
|
+
anyOf(
|
|
3
|
+
instance: unknown,
|
|
4
|
+
classes: (new (...args: any[]) => unknown)[],
|
|
5
|
+
): boolean {
|
|
6
|
+
return classes.some((name) => instance instanceof name);
|
|
4
7
|
},
|
|
5
8
|
|
|
6
|
-
isFocusable(el, interactiveOnly) {
|
|
9
|
+
isFocusable(el: Element, interactiveOnly = false): boolean {
|
|
7
10
|
return (
|
|
8
11
|
(el instanceof HTMLAnchorElement && el.rel !== "ignore") ||
|
|
9
12
|
(el instanceof HTMLAreaElement && el.href !== undefined) ||
|
|
10
|
-
(!el.disabled &&
|
|
13
|
+
(!("disabled" in el && el.disabled) &&
|
|
11
14
|
this.anyOf(el, [
|
|
12
15
|
HTMLInputElement,
|
|
13
16
|
HTMLSelectElement,
|
|
@@ -15,17 +18,19 @@ const ARIA = {
|
|
|
15
18
|
HTMLButtonElement,
|
|
16
19
|
])) ||
|
|
17
20
|
el instanceof HTMLIFrameElement ||
|
|
18
|
-
(el
|
|
21
|
+
(el instanceof HTMLElement &&
|
|
22
|
+
el.tabIndex >= 0 &&
|
|
23
|
+
el.getAttribute("aria-hidden") !== "true") ||
|
|
19
24
|
(!interactiveOnly &&
|
|
20
25
|
el.getAttribute("tabindex") !== null &&
|
|
21
26
|
el.getAttribute("aria-hidden") !== "true")
|
|
22
27
|
);
|
|
23
28
|
},
|
|
24
29
|
|
|
25
|
-
attemptFocus(el, interactiveOnly) {
|
|
30
|
+
attemptFocus(el: Element, interactiveOnly = false): boolean {
|
|
26
31
|
if (this.isFocusable(el, interactiveOnly)) {
|
|
27
32
|
try {
|
|
28
|
-
el.focus();
|
|
33
|
+
(el as HTMLElement).focus();
|
|
29
34
|
} catch {
|
|
30
35
|
// that's fine
|
|
31
36
|
}
|
|
@@ -33,7 +38,7 @@ const ARIA = {
|
|
|
33
38
|
return !!document.activeElement && document.activeElement.isSameNode(el);
|
|
34
39
|
},
|
|
35
40
|
|
|
36
|
-
focusFirstInteractive(el) {
|
|
41
|
+
focusFirstInteractive(el: Element): boolean {
|
|
37
42
|
let child = el.firstElementChild;
|
|
38
43
|
while (child) {
|
|
39
44
|
if (this.attemptFocus(child, true) || this.focusFirstInteractive(child)) {
|
|
@@ -41,9 +46,10 @@ const ARIA = {
|
|
|
41
46
|
}
|
|
42
47
|
child = child.nextElementSibling;
|
|
43
48
|
}
|
|
49
|
+
return false;
|
|
44
50
|
},
|
|
45
51
|
|
|
46
|
-
focusFirst(el) {
|
|
52
|
+
focusFirst(el: Element): boolean {
|
|
47
53
|
let child = el.firstElementChild;
|
|
48
54
|
while (child) {
|
|
49
55
|
if (this.attemptFocus(child) || this.focusFirst(child)) {
|
|
@@ -51,9 +57,10 @@ const ARIA = {
|
|
|
51
57
|
}
|
|
52
58
|
child = child.nextElementSibling;
|
|
53
59
|
}
|
|
60
|
+
return false;
|
|
54
61
|
},
|
|
55
62
|
|
|
56
|
-
focusLast(el) {
|
|
63
|
+
focusLast(el: Element): boolean {
|
|
57
64
|
let child = el.lastElementChild;
|
|
58
65
|
while (child) {
|
|
59
66
|
if (this.attemptFocus(child) || this.focusLast(child)) {
|
|
@@ -61,6 +68,7 @@ const ARIA = {
|
|
|
61
68
|
}
|
|
62
69
|
child = child.previousElementSibling;
|
|
63
70
|
}
|
|
71
|
+
return false;
|
|
64
72
|
},
|
|
65
73
|
};
|
|
66
74
|
export default ARIA;
|
|
@@ -30,7 +30,11 @@ const Browser = {
|
|
|
30
30
|
);
|
|
31
31
|
},
|
|
32
32
|
|
|
33
|
-
pushState(
|
|
33
|
+
pushState(
|
|
34
|
+
kind: "replace" | "push",
|
|
35
|
+
meta: { type: string; scroll?: number; id?: string; position?: number },
|
|
36
|
+
to?: string,
|
|
37
|
+
) {
|
|
34
38
|
if (this.canPushState()) {
|
|
35
39
|
if (to !== window.location.href) {
|
|
36
40
|
if (meta.type == "redirect" && meta.scroll) {
|
|
@@ -57,32 +61,32 @@ const Browser = {
|
|
|
57
61
|
}
|
|
58
62
|
});
|
|
59
63
|
}
|
|
60
|
-
} else {
|
|
64
|
+
} else if (to) {
|
|
61
65
|
this.redirect(to);
|
|
62
66
|
}
|
|
63
67
|
},
|
|
64
68
|
|
|
65
|
-
setCookie(name, value, maxAgeSeconds) {
|
|
69
|
+
setCookie(name: string, value: string | number, maxAgeSeconds?: number) {
|
|
66
70
|
const expires =
|
|
67
71
|
typeof maxAgeSeconds === "number" ? ` max-age=${maxAgeSeconds};` : "";
|
|
68
72
|
document.cookie = `${name}=${value};${expires} path=/`;
|
|
69
73
|
},
|
|
70
74
|
|
|
71
|
-
getCookie(name) {
|
|
75
|
+
getCookie(name: string) {
|
|
72
76
|
return document.cookie.replace(
|
|
73
77
|
new RegExp(`(?:(?:^|.*;\s*)${name}\s*\=\s*([^;]*).*$)|^.*$`),
|
|
74
78
|
"$1",
|
|
75
79
|
);
|
|
76
80
|
},
|
|
77
81
|
|
|
78
|
-
deleteCookie(name) {
|
|
82
|
+
deleteCookie(name: string) {
|
|
79
83
|
document.cookie = `${name}=; max-age=-1; path=/`;
|
|
80
84
|
},
|
|
81
85
|
|
|
82
86
|
redirect(
|
|
83
|
-
toURL,
|
|
84
|
-
flash,
|
|
85
|
-
navigate = (url) => {
|
|
87
|
+
toURL: string,
|
|
88
|
+
flash: string | null = null,
|
|
89
|
+
navigate: (url: string) => void = (url) => {
|
|
86
90
|
window.location.href = url;
|
|
87
91
|
},
|
|
88
92
|
) {
|
|
@@ -28,11 +28,31 @@ import {
|
|
|
28
28
|
|
|
29
29
|
import { logError } from "./utils";
|
|
30
30
|
|
|
31
|
+
export type FormInputLike = HTMLElement & {
|
|
32
|
+
readonly form?: HTMLFormElement | null;
|
|
33
|
+
readonly type?: string;
|
|
34
|
+
readonly validity?: ValidityState;
|
|
35
|
+
readonly name?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type QueryableNode = Element | Document | DocumentFragment;
|
|
39
|
+
|
|
31
40
|
const DOM = {
|
|
32
41
|
byId(id) {
|
|
33
42
|
return document.getElementById(id) || logError(`no id found for ${id}`);
|
|
34
43
|
},
|
|
35
44
|
|
|
45
|
+
elementFromTarget(target: EventTarget): Element | null {
|
|
46
|
+
if (!(target instanceof Node)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (target.nodeType === Node.ELEMENT_NODE) {
|
|
50
|
+
return target as Element;
|
|
51
|
+
} else {
|
|
52
|
+
return target.parentElement;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
|
|
36
56
|
removeClass(el, className) {
|
|
37
57
|
el.classList.remove(className);
|
|
38
58
|
if (el.classList.length === 0) {
|
|
@@ -40,7 +60,11 @@ const DOM = {
|
|
|
40
60
|
}
|
|
41
61
|
},
|
|
42
62
|
|
|
43
|
-
all(
|
|
63
|
+
all(
|
|
64
|
+
node: QueryableNode | null,
|
|
65
|
+
query: string,
|
|
66
|
+
callback?: (el: Element) => void,
|
|
67
|
+
): Element[] {
|
|
44
68
|
if (!node) {
|
|
45
69
|
return [];
|
|
46
70
|
}
|
|
@@ -57,7 +81,7 @@ const DOM = {
|
|
|
57
81
|
return template.content.childElementCount;
|
|
58
82
|
},
|
|
59
83
|
|
|
60
|
-
isUploadInput(el) {
|
|
84
|
+
isUploadInput(el): el is HTMLInputElement {
|
|
61
85
|
return el.type === "file" && el.getAttribute(PHX_UPLOAD_REF) !== null;
|
|
62
86
|
},
|
|
63
87
|
|
|
@@ -65,7 +89,7 @@ const DOM = {
|
|
|
65
89
|
return inputEl.hasAttribute("data-phx-auto-upload");
|
|
66
90
|
},
|
|
67
91
|
|
|
68
|
-
findUploadInputs(node) {
|
|
92
|
+
findUploadInputs(node): HTMLInputElement[] {
|
|
69
93
|
const formId = node.id;
|
|
70
94
|
const inputsOutsideForm = this.all(
|
|
71
95
|
document,
|
|
@@ -73,16 +97,33 @@ const DOM = {
|
|
|
73
97
|
);
|
|
74
98
|
return this.all(node, `input[type="file"][${PHX_UPLOAD_REF}]`).concat(
|
|
75
99
|
inputsOutsideForm,
|
|
76
|
-
);
|
|
100
|
+
) as HTMLInputElement[];
|
|
77
101
|
},
|
|
78
102
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
103
|
+
findComponent(
|
|
104
|
+
viewId: string,
|
|
105
|
+
cid: string | number,
|
|
106
|
+
doc: QueryableNode = document,
|
|
107
|
+
): Element | null {
|
|
108
|
+
return doc.querySelector(
|
|
82
109
|
`[${PHX_VIEW_REF}="${viewId}"][${PHX_COMPONENT}="${cid}"]`,
|
|
83
110
|
);
|
|
84
111
|
},
|
|
85
112
|
|
|
113
|
+
getComponent(
|
|
114
|
+
viewId: string,
|
|
115
|
+
cid: number,
|
|
116
|
+
doc: QueryableNode = document,
|
|
117
|
+
): Element {
|
|
118
|
+
const el = this.findComponent(viewId, cid, doc);
|
|
119
|
+
if (!el) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`no component found matching viewId ${viewId} and cid ${cid}`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return el;
|
|
125
|
+
},
|
|
126
|
+
|
|
86
127
|
isPhxDestroyed(node) {
|
|
87
128
|
return node.id && DOM.private(node, "destroyed") ? true : false;
|
|
88
129
|
},
|
|
@@ -208,7 +249,7 @@ const DOM = {
|
|
|
208
249
|
).forEach((parent) => {
|
|
209
250
|
parentCids.add(cid);
|
|
210
251
|
this.all(parent, `[${PHX_VIEW_REF}="${viewId}"][${PHX_COMPONENT}]`)
|
|
211
|
-
.map((el) => parseInt(el.getAttribute(PHX_COMPONENT)))
|
|
252
|
+
.map((el) => parseInt(el.getAttribute(PHX_COMPONENT)!))
|
|
212
253
|
.forEach((childCID) => childrenCids.add(childCID));
|
|
213
254
|
});
|
|
214
255
|
});
|
|
@@ -376,7 +417,7 @@ const DOM = {
|
|
|
376
417
|
}
|
|
377
418
|
},
|
|
378
419
|
|
|
379
|
-
triggerCycle(el, key, currentCycle) {
|
|
420
|
+
triggerCycle(el, key, currentCycle?) {
|
|
380
421
|
const [cycle, trigger] = this.private(el, key);
|
|
381
422
|
if (!currentCycle) {
|
|
382
423
|
currentCycle = cycle;
|
|
@@ -470,7 +511,7 @@ const DOM = {
|
|
|
470
511
|
return this.isPhxChild(el) ? el : this.all(el, `[${PHX_PARENT_ID}]`)[0];
|
|
471
512
|
},
|
|
472
513
|
|
|
473
|
-
isPortalTemplate(el) {
|
|
514
|
+
isPortalTemplate(el): el is HTMLTemplateElement {
|
|
474
515
|
return el.tagName === "TEMPLATE" && el.hasAttribute(PHX_PORTAL);
|
|
475
516
|
},
|
|
476
517
|
|
|
@@ -491,7 +532,7 @@ const DOM = {
|
|
|
491
532
|
return null;
|
|
492
533
|
},
|
|
493
534
|
|
|
494
|
-
dispatchEvent(target, name, opts = {}) {
|
|
535
|
+
dispatchEvent(target, name, opts: { bubbles?: boolean; detail?: any } = {}) {
|
|
495
536
|
let defaultBubble = true;
|
|
496
537
|
const isUploadTarget =
|
|
497
538
|
target.nodeName === "INPUT" && target.type === "file";
|
|
@@ -524,7 +565,11 @@ const DOM = {
|
|
|
524
565
|
// merge attributes from source to target
|
|
525
566
|
// if an element is ignored, we only merge data attributes
|
|
526
567
|
// including removing data attributes that are no longer in the source
|
|
527
|
-
mergeAttrs(
|
|
568
|
+
mergeAttrs(
|
|
569
|
+
target,
|
|
570
|
+
source,
|
|
571
|
+
opts: { exclude?: string[]; isIgnored?: boolean } = {},
|
|
572
|
+
) {
|
|
528
573
|
const exclude = new Set(opts.exclude || []);
|
|
529
574
|
const isIgnored = opts.isIgnored;
|
|
530
575
|
const sourceAttrs = source.attributes;
|
|
@@ -588,7 +633,7 @@ const DOM = {
|
|
|
588
633
|
}
|
|
589
634
|
},
|
|
590
635
|
|
|
591
|
-
hasSelectionRange(el) {
|
|
636
|
+
hasSelectionRange(el): el is HTMLInputElement | HTMLTextAreaElement {
|
|
592
637
|
return (
|
|
593
638
|
el.setSelectionRange && (el.type === "text" || el.type === "textarea")
|
|
594
639
|
);
|
|
@@ -611,21 +656,41 @@ const DOM = {
|
|
|
611
656
|
}
|
|
612
657
|
},
|
|
613
658
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
}
|
|
659
|
+
/**
|
|
660
|
+
* Returns true if the element is an input that can be focused and edited by the user,
|
|
661
|
+
* so we can skip patching it if it has focus.
|
|
662
|
+
*/
|
|
663
|
+
isEditableInput(el: Element | EventTarget | null): el is FormInputLike {
|
|
664
|
+
return (
|
|
665
|
+
this.isFormAssociated(el) &&
|
|
666
|
+
!(el instanceof HTMLButtonElement) &&
|
|
667
|
+
!(el instanceof HTMLInputElement && el.type === "button")
|
|
668
|
+
);
|
|
669
|
+
},
|
|
626
670
|
|
|
671
|
+
isFormAssociated(el: Element | EventTarget | null): el is FormInputLike {
|
|
672
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
673
|
+
if (el.localName) {
|
|
674
|
+
const customEl = customElements.get(el.localName);
|
|
675
|
+
if (customEl) {
|
|
676
|
+
// Custom Elements may be form associated. This allows them
|
|
677
|
+
// to participate within a form's lifecycle, including form
|
|
678
|
+
// validity and form submissions.
|
|
679
|
+
// The spec for Form Associated custom elements requires the
|
|
680
|
+
// custom element's class to contain a static boolean value of `formAssociated`
|
|
681
|
+
// which identifies this class as allowed to associate to a form.
|
|
682
|
+
// See https://html.spec.whatwg.org/dev/custom-elements.html#custom-elements-face-example
|
|
683
|
+
// for details.
|
|
684
|
+
return (
|
|
685
|
+
(customEl as { formAssociated?: boolean }).formAssociated === true
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
627
689
|
return (
|
|
628
|
-
|
|
690
|
+
el instanceof HTMLInputElement ||
|
|
691
|
+
el instanceof HTMLSelectElement ||
|
|
692
|
+
el instanceof HTMLTextAreaElement ||
|
|
693
|
+
el instanceof HTMLButtonElement
|
|
629
694
|
);
|
|
630
695
|
},
|
|
631
696
|
|
|
@@ -650,21 +715,22 @@ const DOM = {
|
|
|
650
715
|
);
|
|
651
716
|
},
|
|
652
717
|
|
|
653
|
-
cleanChildNodes(container, phxUpdate) {
|
|
718
|
+
cleanChildNodes(container: Element, phxUpdate: string) {
|
|
654
719
|
if (
|
|
655
720
|
DOM.isPhxUpdate(container, phxUpdate, ["append", "prepend", PHX_STREAM])
|
|
656
721
|
) {
|
|
657
|
-
const toRemove = [];
|
|
722
|
+
const toRemove: Array<ChildNode> = [];
|
|
658
723
|
container.childNodes.forEach((childNode) => {
|
|
659
|
-
if (!childNode.id) {
|
|
724
|
+
if (!("id" in childNode) || !childNode.id) {
|
|
660
725
|
// Skip warning if it's an empty text node (e.g. a new-line)
|
|
661
726
|
const isEmptyTextNode =
|
|
662
727
|
childNode.nodeType === Node.TEXT_NODE &&
|
|
728
|
+
childNode.nodeValue &&
|
|
663
729
|
childNode.nodeValue.trim() === "";
|
|
664
730
|
if (!isEmptyTextNode && childNode.nodeType !== Node.COMMENT_NODE) {
|
|
665
731
|
logError(
|
|
666
732
|
"only HTML element tags with an id are allowed inside containers with phx-update.\n\n" +
|
|
667
|
-
`removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"\n\n`,
|
|
733
|
+
`removing illegal node: "${(("outerHTML" in childNode && (childNode.outerHTML as string)) || childNode.nodeValue || "").trim()}"\n\n`,
|
|
668
734
|
);
|
|
669
735
|
}
|
|
670
736
|
toRemove.push(childNode);
|
|
@@ -674,7 +740,11 @@ const DOM = {
|
|
|
674
740
|
}
|
|
675
741
|
},
|
|
676
742
|
|
|
677
|
-
replaceRootContainer(
|
|
743
|
+
replaceRootContainer(
|
|
744
|
+
container: Element,
|
|
745
|
+
tagName: string,
|
|
746
|
+
attrs: Record<string, string>,
|
|
747
|
+
) {
|
|
678
748
|
const retainedAttrs = new Set([
|
|
679
749
|
"id",
|
|
680
750
|
PHX_SESSION,
|
|
@@ -697,9 +767,12 @@ const DOM = {
|
|
|
697
767
|
Object.keys(attrs).forEach((attr) =>
|
|
698
768
|
newContainer.setAttribute(attr, attrs[attr]),
|
|
699
769
|
);
|
|
700
|
-
retainedAttrs.forEach((attr) =>
|
|
701
|
-
|
|
702
|
-
|
|
770
|
+
retainedAttrs.forEach((attr) => {
|
|
771
|
+
const value = container.getAttribute(attr);
|
|
772
|
+
if (value !== null) {
|
|
773
|
+
newContainer.setAttribute(attr, value);
|
|
774
|
+
}
|
|
775
|
+
});
|
|
703
776
|
newContainer.innerHTML = container.innerHTML;
|
|
704
777
|
container.replaceWith(newContainer);
|
|
705
778
|
return newContainer;
|