phoenix_live_view 1.1.27 → 1.2.0-rc.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/assets/js/phoenix_live_view/constants.js +1 -0
- package/assets/js/phoenix_live_view/hooks.js +9 -3
- package/assets/js/phoenix_live_view/index.ts +6 -2
- package/assets/js/phoenix_live_view/js.js +3 -2
- package/assets/js/phoenix_live_view/js_commands.ts +16 -7
- package/assets/js/phoenix_live_view/live_socket.js +1 -1
- package/assets/js/phoenix_live_view/view.js +115 -89
- package/assets/js/phoenix_live_view/view_hook.ts +2 -2
- package/package.json +2 -2
- package/priv/static/phoenix_live_view.cjs.js +91 -73
- package/priv/static/phoenix_live_view.cjs.js.map +3 -3
- package/priv/static/phoenix_live_view.esm.js +91 -73
- package/priv/static/phoenix_live_view.esm.js.map +3 -3
- package/priv/static/phoenix_live_view.js +91 -73
- package/priv/static/phoenix_live_view.min.js +6 -6
- package/assets/js/types/aria.d.ts +0 -9
- package/assets/js/types/browser.d.ts +0 -15
- package/assets/js/types/constants.d.ts +0 -97
- package/assets/js/types/dom.d.ts +0 -63
- package/assets/js/types/dom_patch.d.ts +0 -50
- package/assets/js/types/dom_post_morph_restorer.d.ts +0 -8
- package/assets/js/types/element_ref.d.ts +0 -15
- package/assets/js/types/entry_uploader.d.ts +0 -16
- package/assets/js/types/hooks.d.ts +0 -9
- package/assets/js/types/index.d.ts +0 -313
- package/assets/js/types/js.d.ts +0 -99
- package/assets/js/types/js_commands.d.ts +0 -217
- package/assets/js/types/live_socket.d.ts +0 -149
- package/assets/js/types/live_uploader.d.ts +0 -29
- package/assets/js/types/rendered.d.ts +0 -49
- package/assets/js/types/upload_entry.d.ts +0 -42
- package/assets/js/types/utils.d.ts +0 -14
- package/assets/js/types/view.d.ts +0 -151
- package/assets/js/types/view_hook.d.ts +0 -233
|
@@ -85,6 +85,7 @@ export const PHX_LV_PID = "data-phx-pid";
|
|
|
85
85
|
export const PHX_KEY = "key";
|
|
86
86
|
export const PHX_PRIVATE = "phxPrivate";
|
|
87
87
|
export const PHX_AUTO_RECOVER = "auto-recover";
|
|
88
|
+
export const PHX_NO_UNUSED_FIELD = "no-unused-field";
|
|
88
89
|
export const PHX_LV_DEBUG = "phx:live-socket:debug";
|
|
89
90
|
export const PHX_LV_PROFILE = "phx:live-socket:profiling";
|
|
90
91
|
export const PHX_LV_LATENCY_SIM = "phx:live-socket:latency-sim";
|
|
@@ -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);
|
|
@@ -12,7 +12,7 @@ import { ViewHook } from "./view_hook";
|
|
|
12
12
|
import View from "./view";
|
|
13
13
|
import { logError } from "./utils";
|
|
14
14
|
|
|
15
|
-
import type { LiveSocketJSCommands } from "./js_commands";
|
|
15
|
+
import type { EncodedJS, LiveSocketJSCommands } from "./js_commands";
|
|
16
16
|
import type { Hook, HooksOptions } from "./view_hook";
|
|
17
17
|
import type { Socket as PhoenixSocket } from "phoenix";
|
|
18
18
|
|
|
@@ -266,7 +266,11 @@ export interface LiveSocketInstanceInterface {
|
|
|
266
266
|
*
|
|
267
267
|
* See [`Phoenix.LiveView.JS`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.JS.html) for more information.
|
|
268
268
|
*/
|
|
269
|
-
execJS(
|
|
269
|
+
execJS(
|
|
270
|
+
el: HTMLElement,
|
|
271
|
+
encodedJS: EncodedJS,
|
|
272
|
+
eventType?: string | null,
|
|
273
|
+
): void;
|
|
270
274
|
/**
|
|
271
275
|
* Returns an object with methods to manipulate the DOM and execute JavaScript.
|
|
272
276
|
* The applied changes integrate with server DOM patching.
|
|
@@ -11,8 +11,9 @@ const JS = {
|
|
|
11
11
|
null,
|
|
12
12
|
{ callback: defaults && defaults.callback },
|
|
13
13
|
];
|
|
14
|
-
const commands =
|
|
15
|
-
phxEvent
|
|
14
|
+
const commands = Array.isArray(phxEvent)
|
|
15
|
+
? phxEvent
|
|
16
|
+
: typeof phxEvent === "string" && phxEvent.startsWith("[")
|
|
16
17
|
? JSON.parse(phxEvent)
|
|
17
18
|
: [[defaultKind, defaultArgs]];
|
|
18
19
|
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import JS from "./js";
|
|
2
2
|
import LiveSocket from "./live_socket";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* An encoded JS command. Use functions in the `Phoenix.LiveView.JS` module on
|
|
6
|
+
* the server to create and compose JS commands.
|
|
7
|
+
*
|
|
8
|
+
* The underlying primitive type is considered opaque, and may change in future
|
|
9
|
+
* versions.
|
|
10
|
+
*/
|
|
11
|
+
export type EncodedJS = string | Array<any>;
|
|
12
|
+
|
|
4
13
|
type Transition = string | string[];
|
|
5
14
|
|
|
6
15
|
// Base options for commands involving transitions and timing
|
|
@@ -76,13 +85,13 @@ type NavigationOpts = {
|
|
|
76
85
|
*/
|
|
77
86
|
interface AllJSCommands {
|
|
78
87
|
/**
|
|
79
|
-
* Executes encoded
|
|
88
|
+
* Executes an encoded JS command in the context of an element.
|
|
80
89
|
* This version is for general use via liveSocket.js().
|
|
81
90
|
*
|
|
82
|
-
* @param el - The element in whose context to execute the
|
|
83
|
-
* @param encodedJS - The encoded
|
|
91
|
+
* @param el - The element in whose context to execute the JS command.
|
|
92
|
+
* @param encodedJS - The encoded JS command with operations to execute.
|
|
84
93
|
*/
|
|
85
|
-
exec(el: HTMLElement, encodedJS:
|
|
94
|
+
exec(el: HTMLElement, encodedJS: EncodedJS): void;
|
|
86
95
|
|
|
87
96
|
/**
|
|
88
97
|
* Shows an element.
|
|
@@ -382,9 +391,9 @@ export type LiveSocketJSCommands = AllJSCommands;
|
|
|
382
391
|
*/
|
|
383
392
|
export interface HookJSCommands extends Omit<AllJSCommands, "exec"> {
|
|
384
393
|
/**
|
|
385
|
-
* Executes
|
|
394
|
+
* Executes a JS command in the context of the hook's element.
|
|
386
395
|
*
|
|
387
|
-
* @param
|
|
396
|
+
* @param encodedJS - The encoded JS command with operations to execute.
|
|
388
397
|
*/
|
|
389
|
-
exec(encodedJS:
|
|
398
|
+
exec(encodedJS: EncodedJS): void;
|
|
390
399
|
}
|
|
@@ -224,7 +224,7 @@ export default class LiveSocket {
|
|
|
224
224
|
|
|
225
225
|
/**
|
|
226
226
|
* @param {HTMLElement} el
|
|
227
|
-
* @param {
|
|
227
|
+
* @param {import("./js_commands").EncodedJS} encodedJS
|
|
228
228
|
* @param {string | null} [eventType]
|
|
229
229
|
*/
|
|
230
230
|
execJS(el, encodedJS, eventType = null) {
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
PHX_VIEWPORT_BOTTOM,
|
|
39
39
|
MAX_CHILD_JOIN_ATTEMPTS,
|
|
40
40
|
PHX_LV_PID,
|
|
41
|
+
PHX_NO_UNUSED_FIELD,
|
|
41
42
|
PHX_PORTAL,
|
|
42
43
|
PHX_TELEPORTED_REF,
|
|
43
44
|
PHX_TELEPORTED_SRC,
|
|
@@ -77,90 +78,6 @@ export const prependFormDataKey = (key, prefix) => {
|
|
|
77
78
|
return baseKey;
|
|
78
79
|
};
|
|
79
80
|
|
|
80
|
-
const serializeForm = (form, opts, onlyNames = []) => {
|
|
81
|
-
const { submitter } = opts;
|
|
82
|
-
|
|
83
|
-
// We must inject the submitter in the order that it exists in the DOM
|
|
84
|
-
// relative to other inputs. For example, for checkbox groups, the order must be maintained.
|
|
85
|
-
let injectedElement;
|
|
86
|
-
if (submitter && submitter.name) {
|
|
87
|
-
const input = document.createElement("input");
|
|
88
|
-
input.type = "hidden";
|
|
89
|
-
// set the form attribute if the submitter has one;
|
|
90
|
-
// this can happen if the element is outside the actual form element
|
|
91
|
-
const formId = submitter.getAttribute("form");
|
|
92
|
-
if (formId) {
|
|
93
|
-
input.setAttribute("form", formId);
|
|
94
|
-
}
|
|
95
|
-
input.name = submitter.name;
|
|
96
|
-
input.value = submitter.value;
|
|
97
|
-
submitter.parentElement.insertBefore(input, submitter);
|
|
98
|
-
injectedElement = input;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const formData = new FormData(form);
|
|
102
|
-
const toRemove = [];
|
|
103
|
-
|
|
104
|
-
formData.forEach((val, key, _index) => {
|
|
105
|
-
if (val instanceof File) {
|
|
106
|
-
toRemove.push(key);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// Cleanup after building fileData
|
|
111
|
-
toRemove.forEach((key) => formData.delete(key));
|
|
112
|
-
|
|
113
|
-
const params = new URLSearchParams();
|
|
114
|
-
|
|
115
|
-
const { inputsUnused, onlyHiddenInputs } = Array.from(form.elements).reduce(
|
|
116
|
-
(acc, input) => {
|
|
117
|
-
const { inputsUnused, onlyHiddenInputs } = acc;
|
|
118
|
-
const key = input.name;
|
|
119
|
-
if (!key) {
|
|
120
|
-
return acc;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (inputsUnused[key] === undefined) {
|
|
124
|
-
inputsUnused[key] = true;
|
|
125
|
-
}
|
|
126
|
-
if (onlyHiddenInputs[key] === undefined) {
|
|
127
|
-
onlyHiddenInputs[key] = true;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const isUsed =
|
|
131
|
-
DOM.private(input, PHX_HAS_FOCUSED) ||
|
|
132
|
-
DOM.private(input, PHX_HAS_SUBMITTED);
|
|
133
|
-
const isHidden = input.type === "hidden";
|
|
134
|
-
inputsUnused[key] = inputsUnused[key] && !isUsed;
|
|
135
|
-
onlyHiddenInputs[key] = onlyHiddenInputs[key] && isHidden;
|
|
136
|
-
|
|
137
|
-
return acc;
|
|
138
|
-
},
|
|
139
|
-
{ inputsUnused: {}, onlyHiddenInputs: {} },
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
for (const [key, val] of formData.entries()) {
|
|
143
|
-
if (onlyNames.length === 0 || onlyNames.indexOf(key) >= 0) {
|
|
144
|
-
const isUnused = inputsUnused[key];
|
|
145
|
-
const hidden = onlyHiddenInputs[key];
|
|
146
|
-
if (isUnused && !(submitter && submitter.name == key) && !hidden) {
|
|
147
|
-
params.append(prependFormDataKey(key, "_unused_"), "");
|
|
148
|
-
}
|
|
149
|
-
if (typeof val === "string") {
|
|
150
|
-
params.append(key, val);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// remove the injected element again
|
|
156
|
-
// (it would be removed by the next dom patch anyway, but this is cleaner)
|
|
157
|
-
if (submitter && injectedElement) {
|
|
158
|
-
submitter.parentElement.removeChild(injectedElement);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return params.toString();
|
|
162
|
-
};
|
|
163
|
-
|
|
164
81
|
export default class View {
|
|
165
82
|
static closestView(el) {
|
|
166
83
|
const liveViewEl = el.closest(PHX_VIEW_SELECTOR);
|
|
@@ -193,6 +110,11 @@ export default class View {
|
|
|
193
110
|
// bind the view to the element
|
|
194
111
|
DOM.putPrivate(this.el, "view", this);
|
|
195
112
|
this.id = this.el.id;
|
|
113
|
+
// destroyViewByEl requires the root set, so we need to set it early
|
|
114
|
+
// otherwise it could happen that we try to apply a join result for a
|
|
115
|
+
// view whose DOM node was already removed
|
|
116
|
+
// See https://github.com/phoenixframework/phoenix_live_view/issues/4177.
|
|
117
|
+
this.el.setAttribute(PHX_ROOT_ID, this.root.id);
|
|
196
118
|
this.ref = 0;
|
|
197
119
|
this.lastAckRef = null;
|
|
198
120
|
this.childJoins = 0;
|
|
@@ -684,7 +606,8 @@ export default class View {
|
|
|
684
606
|
|
|
685
607
|
patch.after("updated", (el) => {
|
|
686
608
|
if (updatedHookIds.has(el.id)) {
|
|
687
|
-
this.getHook(el)
|
|
609
|
+
const hook = this.getHook(el);
|
|
610
|
+
hook && hook.__updated();
|
|
688
611
|
}
|
|
689
612
|
});
|
|
690
613
|
|
|
@@ -1660,6 +1583,107 @@ export default class View {
|
|
|
1660
1583
|
return meta;
|
|
1661
1584
|
}
|
|
1662
1585
|
|
|
1586
|
+
serializeForm(form, opts, onlyNames = []) {
|
|
1587
|
+
const { submitter } = opts;
|
|
1588
|
+
|
|
1589
|
+
// We must inject the submitter in the order that it exists in the DOM
|
|
1590
|
+
// relative to other inputs. For example, for checkbox groups, the order must be maintained.
|
|
1591
|
+
let injectedElement;
|
|
1592
|
+
if (submitter && submitter.name) {
|
|
1593
|
+
const input = document.createElement("input");
|
|
1594
|
+
input.type = "hidden";
|
|
1595
|
+
// set the form attribute if the submitter has one;
|
|
1596
|
+
// this can happen if the element is outside the actual form element
|
|
1597
|
+
const formId = submitter.getAttribute("form");
|
|
1598
|
+
if (formId) {
|
|
1599
|
+
input.setAttribute("form", formId);
|
|
1600
|
+
}
|
|
1601
|
+
input.name = submitter.name;
|
|
1602
|
+
input.value = submitter.value;
|
|
1603
|
+
submitter.parentElement.insertBefore(input, submitter);
|
|
1604
|
+
injectedElement = input;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
const formData = new FormData(form);
|
|
1608
|
+
const toRemove = [];
|
|
1609
|
+
|
|
1610
|
+
formData.forEach((val, key, _index) => {
|
|
1611
|
+
if (val instanceof File) {
|
|
1612
|
+
toRemove.push(key);
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1616
|
+
// Cleanup after building fileData
|
|
1617
|
+
toRemove.forEach((key) => formData.delete(key));
|
|
1618
|
+
|
|
1619
|
+
const params = new URLSearchParams();
|
|
1620
|
+
|
|
1621
|
+
const { inputsUnused, onlyHiddenInputs } = Array.from(form.elements).reduce(
|
|
1622
|
+
(acc, input) => {
|
|
1623
|
+
const { inputsUnused, onlyHiddenInputs } = acc;
|
|
1624
|
+
const key = input.name;
|
|
1625
|
+
if (!key) {
|
|
1626
|
+
return acc;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
if (inputsUnused[key] === undefined) {
|
|
1630
|
+
inputsUnused[key] = true;
|
|
1631
|
+
}
|
|
1632
|
+
if (onlyHiddenInputs[key] === undefined) {
|
|
1633
|
+
onlyHiddenInputs[key] = true;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
const inputSkipUnusedField = input.hasAttribute(
|
|
1637
|
+
this.binding(PHX_NO_UNUSED_FIELD),
|
|
1638
|
+
);
|
|
1639
|
+
|
|
1640
|
+
const isUsed =
|
|
1641
|
+
DOM.private(input, PHX_HAS_FOCUSED) ||
|
|
1642
|
+
DOM.private(input, PHX_HAS_SUBMITTED) ||
|
|
1643
|
+
inputSkipUnusedField;
|
|
1644
|
+
|
|
1645
|
+
const isHidden = input.type === "hidden";
|
|
1646
|
+
inputsUnused[key] = inputsUnused[key] && !isUsed;
|
|
1647
|
+
onlyHiddenInputs[key] = onlyHiddenInputs[key] && isHidden;
|
|
1648
|
+
|
|
1649
|
+
return acc;
|
|
1650
|
+
},
|
|
1651
|
+
{ inputsUnused: {}, onlyHiddenInputs: {} },
|
|
1652
|
+
);
|
|
1653
|
+
|
|
1654
|
+
const formSkipUnusedFields = form.hasAttribute(
|
|
1655
|
+
this.binding(PHX_NO_UNUSED_FIELD),
|
|
1656
|
+
);
|
|
1657
|
+
|
|
1658
|
+
for (const [key, val] of formData.entries()) {
|
|
1659
|
+
if (onlyNames.length === 0 || onlyNames.indexOf(key) >= 0) {
|
|
1660
|
+
const isUnused = inputsUnused[key];
|
|
1661
|
+
const hidden = onlyHiddenInputs[key];
|
|
1662
|
+
const skipUnusedCheck = formSkipUnusedFields;
|
|
1663
|
+
|
|
1664
|
+
if (
|
|
1665
|
+
!skipUnusedCheck &&
|
|
1666
|
+
isUnused &&
|
|
1667
|
+
!(submitter && submitter.name == key) &&
|
|
1668
|
+
!hidden
|
|
1669
|
+
) {
|
|
1670
|
+
params.append(prependFormDataKey(key, "_unused_"), "");
|
|
1671
|
+
}
|
|
1672
|
+
if (typeof val === "string") {
|
|
1673
|
+
params.append(key, val);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
// remove the injected element again
|
|
1679
|
+
// (it would be removed by the next dom patch anyway, but this is cleaner)
|
|
1680
|
+
if (submitter && injectedElement) {
|
|
1681
|
+
submitter.parentElement.removeChild(injectedElement);
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
return params.toString();
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1663
1687
|
pushEvent(type, el, targetCtx, phxEvent, meta, opts = {}, onReply) {
|
|
1664
1688
|
this.pushWithReply(
|
|
1665
1689
|
(maybePayload) =>
|
|
@@ -1721,9 +1745,11 @@ export default class View {
|
|
|
1721
1745
|
serializeOpts.submitter = inputEl;
|
|
1722
1746
|
}
|
|
1723
1747
|
if (inputEl.getAttribute(this.binding("change"))) {
|
|
1724
|
-
formData = serializeForm(inputEl.form, serializeOpts, [
|
|
1748
|
+
formData = this.serializeForm(inputEl.form, serializeOpts, [
|
|
1749
|
+
inputEl.name,
|
|
1750
|
+
]);
|
|
1725
1751
|
} else {
|
|
1726
|
-
formData = serializeForm(inputEl.form, serializeOpts);
|
|
1752
|
+
formData = this.serializeForm(inputEl.form, serializeOpts);
|
|
1727
1753
|
}
|
|
1728
1754
|
if (
|
|
1729
1755
|
DOM.isUploadInput(inputEl) &&
|
|
@@ -1900,7 +1926,7 @@ export default class View {
|
|
|
1900
1926
|
return this.undoRefs(ref, phxEvent);
|
|
1901
1927
|
}
|
|
1902
1928
|
const meta = this.extractMeta(formEl, {}, opts.value);
|
|
1903
|
-
const formData = serializeForm(formEl, { submitter });
|
|
1929
|
+
const formData = this.serializeForm(formEl, { submitter });
|
|
1904
1930
|
this.pushWithReply(proxyRefGen, "event", {
|
|
1905
1931
|
type: "form",
|
|
1906
1932
|
event: phxEvent,
|
|
@@ -1918,7 +1944,7 @@ export default class View {
|
|
|
1918
1944
|
)
|
|
1919
1945
|
) {
|
|
1920
1946
|
const meta = this.extractMeta(formEl, {}, opts.value);
|
|
1921
|
-
const formData = serializeForm(formEl, { submitter });
|
|
1947
|
+
const formData = this.serializeForm(formEl, { submitter });
|
|
1922
1948
|
this.pushWithReply(refGenerator, "event", {
|
|
1923
1949
|
type: "form",
|
|
1924
1950
|
event: phxEvent,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import jsCommands, { HookJSCommands } from "./js_commands";
|
|
1
|
+
import jsCommands, { EncodedJS, HookJSCommands } from "./js_commands";
|
|
2
2
|
import DOM from "./dom";
|
|
3
3
|
import LiveSocket from "./live_socket";
|
|
4
4
|
import View from "./view";
|
|
@@ -395,7 +395,7 @@ export class ViewHook<E extends HTMLElement = HTMLElement>
|
|
|
395
395
|
js(): HookJSCommands {
|
|
396
396
|
return {
|
|
397
397
|
...jsCommands(this.__view().liveSocket, "hook"),
|
|
398
|
-
exec: (encodedJS:
|
|
398
|
+
exec: (encodedJS: EncodedJS) => {
|
|
399
399
|
this.__view().liveSocket.execJS(this.el, encodedJS, "hook");
|
|
400
400
|
},
|
|
401
401
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "phoenix_live_view",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0-rc.0",
|
|
4
4
|
"description": "The Phoenix LiveView JavaScript client.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@babel/preset-env": "7.27.2",
|
|
38
38
|
"@babel/preset-typescript": "^7.27.1",
|
|
39
39
|
"@eslint/js": "^9.29.0",
|
|
40
|
-
"@playwright/test": "^1.
|
|
40
|
+
"@playwright/test": "^1.59.1",
|
|
41
41
|
"@types/jest": "^30.0.0",
|
|
42
42
|
"@types/phoenix": "^1.6.6",
|
|
43
43
|
"css.escape": "^1.5.1",
|
|
@@ -114,6 +114,7 @@ var PHX_LV_PID = "data-phx-pid";
|
|
|
114
114
|
var PHX_KEY = "key";
|
|
115
115
|
var PHX_PRIVATE = "phxPrivate";
|
|
116
116
|
var PHX_AUTO_RECOVER = "auto-recover";
|
|
117
|
+
var PHX_NO_UNUSED_FIELD = "no-unused-field";
|
|
117
118
|
var PHX_LV_DEBUG = "phx:live-socket:debug";
|
|
118
119
|
var PHX_LV_PROFILE = "phx:live-socket:profiling";
|
|
119
120
|
var PHX_LV_LATENCY_SIM = "phx:live-socket:latency-sim";
|
|
@@ -1359,15 +1360,15 @@ var top = (scrollContainer) => {
|
|
|
1359
1360
|
};
|
|
1360
1361
|
var isAtViewportTop = (el, scrollContainer) => {
|
|
1361
1362
|
const rect = el.getBoundingClientRect();
|
|
1362
|
-
return Math.ceil(rect.top) >= top(scrollContainer) && Math.
|
|
1363
|
+
return Math.ceil(rect.top) >= top(scrollContainer) && Math.floor(rect.top) <= bottom(scrollContainer);
|
|
1363
1364
|
};
|
|
1364
1365
|
var isAtViewportBottom = (el, scrollContainer) => {
|
|
1365
1366
|
const rect = el.getBoundingClientRect();
|
|
1366
|
-
return Math.ceil(rect.bottom) >= top(scrollContainer) && Math.
|
|
1367
|
+
return Math.ceil(rect.bottom) >= top(scrollContainer) && Math.floor(rect.bottom) <= bottom(scrollContainer);
|
|
1367
1368
|
};
|
|
1368
1369
|
var isWithinViewport = (el, scrollContainer) => {
|
|
1369
1370
|
const rect = el.getBoundingClientRect();
|
|
1370
|
-
return Math.ceil(rect.top) >= top(scrollContainer) && Math.
|
|
1371
|
+
return Math.ceil(rect.top) >= top(scrollContainer) && Math.floor(rect.top) <= bottom(scrollContainer);
|
|
1371
1372
|
};
|
|
1372
1373
|
Hooks.InfiniteScroll = {
|
|
1373
1374
|
mounted() {
|
|
@@ -1458,6 +1459,12 @@ Hooks.InfiniteScroll = {
|
|
|
1458
1459
|
window.addEventListener("scroll", this.onScroll);
|
|
1459
1460
|
}
|
|
1460
1461
|
},
|
|
1462
|
+
updated() {
|
|
1463
|
+
if (!this.scrollContainer.isConnected) {
|
|
1464
|
+
this.destroyed();
|
|
1465
|
+
this.mounted();
|
|
1466
|
+
}
|
|
1467
|
+
},
|
|
1461
1468
|
destroyed() {
|
|
1462
1469
|
if (this.scrollContainer) {
|
|
1463
1470
|
this.scrollContainer.removeEventListener("scroll", this.onScroll);
|
|
@@ -3264,7 +3271,7 @@ var JS = {
|
|
|
3264
3271
|
null,
|
|
3265
3272
|
{ callback: defaults && defaults.callback }
|
|
3266
3273
|
];
|
|
3267
|
-
const commands =
|
|
3274
|
+
const commands = Array.isArray(phxEvent) ? phxEvent : typeof phxEvent === "string" && phxEvent.startsWith("[") ? JSON.parse(phxEvent) : [[defaultKind, defaultArgs]];
|
|
3268
3275
|
commands.forEach(([kind, args]) => {
|
|
3269
3276
|
if (kind === defaultKind) {
|
|
3270
3277
|
args = { ...defaultArgs, ...args };
|
|
@@ -4099,68 +4106,6 @@ var prependFormDataKey = (key, prefix) => {
|
|
|
4099
4106
|
}
|
|
4100
4107
|
return baseKey;
|
|
4101
4108
|
};
|
|
4102
|
-
var serializeForm = (form, opts, onlyNames = []) => {
|
|
4103
|
-
const { submitter } = opts;
|
|
4104
|
-
let injectedElement;
|
|
4105
|
-
if (submitter && submitter.name) {
|
|
4106
|
-
const input = document.createElement("input");
|
|
4107
|
-
input.type = "hidden";
|
|
4108
|
-
const formId = submitter.getAttribute("form");
|
|
4109
|
-
if (formId) {
|
|
4110
|
-
input.setAttribute("form", formId);
|
|
4111
|
-
}
|
|
4112
|
-
input.name = submitter.name;
|
|
4113
|
-
input.value = submitter.value;
|
|
4114
|
-
submitter.parentElement.insertBefore(input, submitter);
|
|
4115
|
-
injectedElement = input;
|
|
4116
|
-
}
|
|
4117
|
-
const formData = new FormData(form);
|
|
4118
|
-
const toRemove = [];
|
|
4119
|
-
formData.forEach((val, key, _index) => {
|
|
4120
|
-
if (val instanceof File) {
|
|
4121
|
-
toRemove.push(key);
|
|
4122
|
-
}
|
|
4123
|
-
});
|
|
4124
|
-
toRemove.forEach((key) => formData.delete(key));
|
|
4125
|
-
const params = new URLSearchParams();
|
|
4126
|
-
const { inputsUnused, onlyHiddenInputs } = Array.from(form.elements).reduce(
|
|
4127
|
-
(acc, input) => {
|
|
4128
|
-
const { inputsUnused: inputsUnused2, onlyHiddenInputs: onlyHiddenInputs2 } = acc;
|
|
4129
|
-
const key = input.name;
|
|
4130
|
-
if (!key) {
|
|
4131
|
-
return acc;
|
|
4132
|
-
}
|
|
4133
|
-
if (inputsUnused2[key] === void 0) {
|
|
4134
|
-
inputsUnused2[key] = true;
|
|
4135
|
-
}
|
|
4136
|
-
if (onlyHiddenInputs2[key] === void 0) {
|
|
4137
|
-
onlyHiddenInputs2[key] = true;
|
|
4138
|
-
}
|
|
4139
|
-
const isUsed = dom_default.private(input, PHX_HAS_FOCUSED) || dom_default.private(input, PHX_HAS_SUBMITTED);
|
|
4140
|
-
const isHidden = input.type === "hidden";
|
|
4141
|
-
inputsUnused2[key] = inputsUnused2[key] && !isUsed;
|
|
4142
|
-
onlyHiddenInputs2[key] = onlyHiddenInputs2[key] && isHidden;
|
|
4143
|
-
return acc;
|
|
4144
|
-
},
|
|
4145
|
-
{ inputsUnused: {}, onlyHiddenInputs: {} }
|
|
4146
|
-
);
|
|
4147
|
-
for (const [key, val] of formData.entries()) {
|
|
4148
|
-
if (onlyNames.length === 0 || onlyNames.indexOf(key) >= 0) {
|
|
4149
|
-
const isUnused = inputsUnused[key];
|
|
4150
|
-
const hidden = onlyHiddenInputs[key];
|
|
4151
|
-
if (isUnused && !(submitter && submitter.name == key) && !hidden) {
|
|
4152
|
-
params.append(prependFormDataKey(key, "_unused_"), "");
|
|
4153
|
-
}
|
|
4154
|
-
if (typeof val === "string") {
|
|
4155
|
-
params.append(key, val);
|
|
4156
|
-
}
|
|
4157
|
-
}
|
|
4158
|
-
}
|
|
4159
|
-
if (submitter && injectedElement) {
|
|
4160
|
-
submitter.parentElement.removeChild(injectedElement);
|
|
4161
|
-
}
|
|
4162
|
-
return params.toString();
|
|
4163
|
-
};
|
|
4164
4109
|
var View = class _View {
|
|
4165
4110
|
static closestView(el) {
|
|
4166
4111
|
const liveViewEl = el.closest(PHX_VIEW_SELECTOR);
|
|
@@ -4189,6 +4134,7 @@ var View = class _View {
|
|
|
4189
4134
|
}
|
|
4190
4135
|
dom_default.putPrivate(this.el, "view", this);
|
|
4191
4136
|
this.id = this.el.id;
|
|
4137
|
+
this.el.setAttribute(PHX_ROOT_ID, this.root.id);
|
|
4192
4138
|
this.ref = 0;
|
|
4193
4139
|
this.lastAckRef = null;
|
|
4194
4140
|
this.childJoins = 0;
|
|
@@ -4592,7 +4538,8 @@ var View = class _View {
|
|
|
4592
4538
|
});
|
|
4593
4539
|
patch.after("updated", (el) => {
|
|
4594
4540
|
if (updatedHookIds.has(el.id)) {
|
|
4595
|
-
this.getHook(el)
|
|
4541
|
+
const hook = this.getHook(el);
|
|
4542
|
+
hook && hook.__updated();
|
|
4596
4543
|
}
|
|
4597
4544
|
});
|
|
4598
4545
|
patch.after("discarded", (el) => {
|
|
@@ -5387,6 +5334,75 @@ var View = class _View {
|
|
|
5387
5334
|
}
|
|
5388
5335
|
return meta;
|
|
5389
5336
|
}
|
|
5337
|
+
serializeForm(form, opts, onlyNames = []) {
|
|
5338
|
+
const { submitter } = opts;
|
|
5339
|
+
let injectedElement;
|
|
5340
|
+
if (submitter && submitter.name) {
|
|
5341
|
+
const input = document.createElement("input");
|
|
5342
|
+
input.type = "hidden";
|
|
5343
|
+
const formId = submitter.getAttribute("form");
|
|
5344
|
+
if (formId) {
|
|
5345
|
+
input.setAttribute("form", formId);
|
|
5346
|
+
}
|
|
5347
|
+
input.name = submitter.name;
|
|
5348
|
+
input.value = submitter.value;
|
|
5349
|
+
submitter.parentElement.insertBefore(input, submitter);
|
|
5350
|
+
injectedElement = input;
|
|
5351
|
+
}
|
|
5352
|
+
const formData = new FormData(form);
|
|
5353
|
+
const toRemove = [];
|
|
5354
|
+
formData.forEach((val, key, _index) => {
|
|
5355
|
+
if (val instanceof File) {
|
|
5356
|
+
toRemove.push(key);
|
|
5357
|
+
}
|
|
5358
|
+
});
|
|
5359
|
+
toRemove.forEach((key) => formData.delete(key));
|
|
5360
|
+
const params = new URLSearchParams();
|
|
5361
|
+
const { inputsUnused, onlyHiddenInputs } = Array.from(form.elements).reduce(
|
|
5362
|
+
(acc, input) => {
|
|
5363
|
+
const { inputsUnused: inputsUnused2, onlyHiddenInputs: onlyHiddenInputs2 } = acc;
|
|
5364
|
+
const key = input.name;
|
|
5365
|
+
if (!key) {
|
|
5366
|
+
return acc;
|
|
5367
|
+
}
|
|
5368
|
+
if (inputsUnused2[key] === void 0) {
|
|
5369
|
+
inputsUnused2[key] = true;
|
|
5370
|
+
}
|
|
5371
|
+
if (onlyHiddenInputs2[key] === void 0) {
|
|
5372
|
+
onlyHiddenInputs2[key] = true;
|
|
5373
|
+
}
|
|
5374
|
+
const inputSkipUnusedField = input.hasAttribute(
|
|
5375
|
+
this.binding(PHX_NO_UNUSED_FIELD)
|
|
5376
|
+
);
|
|
5377
|
+
const isUsed = dom_default.private(input, PHX_HAS_FOCUSED) || dom_default.private(input, PHX_HAS_SUBMITTED) || inputSkipUnusedField;
|
|
5378
|
+
const isHidden = input.type === "hidden";
|
|
5379
|
+
inputsUnused2[key] = inputsUnused2[key] && !isUsed;
|
|
5380
|
+
onlyHiddenInputs2[key] = onlyHiddenInputs2[key] && isHidden;
|
|
5381
|
+
return acc;
|
|
5382
|
+
},
|
|
5383
|
+
{ inputsUnused: {}, onlyHiddenInputs: {} }
|
|
5384
|
+
);
|
|
5385
|
+
const formSkipUnusedFields = form.hasAttribute(
|
|
5386
|
+
this.binding(PHX_NO_UNUSED_FIELD)
|
|
5387
|
+
);
|
|
5388
|
+
for (const [key, val] of formData.entries()) {
|
|
5389
|
+
if (onlyNames.length === 0 || onlyNames.indexOf(key) >= 0) {
|
|
5390
|
+
const isUnused = inputsUnused[key];
|
|
5391
|
+
const hidden = onlyHiddenInputs[key];
|
|
5392
|
+
const skipUnusedCheck = formSkipUnusedFields;
|
|
5393
|
+
if (!skipUnusedCheck && isUnused && !(submitter && submitter.name == key) && !hidden) {
|
|
5394
|
+
params.append(prependFormDataKey(key, "_unused_"), "");
|
|
5395
|
+
}
|
|
5396
|
+
if (typeof val === "string") {
|
|
5397
|
+
params.append(key, val);
|
|
5398
|
+
}
|
|
5399
|
+
}
|
|
5400
|
+
}
|
|
5401
|
+
if (submitter && injectedElement) {
|
|
5402
|
+
submitter.parentElement.removeChild(injectedElement);
|
|
5403
|
+
}
|
|
5404
|
+
return params.toString();
|
|
5405
|
+
}
|
|
5390
5406
|
pushEvent(type, el, targetCtx, phxEvent, meta, opts = {}, onReply) {
|
|
5391
5407
|
this.pushWithReply(
|
|
5392
5408
|
(maybePayload) => this.putRef([{ el, loading: true, lock: true }], phxEvent, type, {
|
|
@@ -5438,9 +5454,11 @@ var View = class _View {
|
|
|
5438
5454
|
serializeOpts.submitter = inputEl;
|
|
5439
5455
|
}
|
|
5440
5456
|
if (inputEl.getAttribute(this.binding("change"))) {
|
|
5441
|
-
formData = serializeForm(inputEl.form, serializeOpts, [
|
|
5457
|
+
formData = this.serializeForm(inputEl.form, serializeOpts, [
|
|
5458
|
+
inputEl.name
|
|
5459
|
+
]);
|
|
5442
5460
|
} else {
|
|
5443
|
-
formData = serializeForm(inputEl.form, serializeOpts);
|
|
5461
|
+
formData = this.serializeForm(inputEl.form, serializeOpts);
|
|
5444
5462
|
}
|
|
5445
5463
|
if (dom_default.isUploadInput(inputEl) && inputEl.files && inputEl.files.length > 0) {
|
|
5446
5464
|
LiveUploader.trackFiles(inputEl, Array.from(inputEl.files));
|
|
@@ -5581,7 +5599,7 @@ var View = class _View {
|
|
|
5581
5599
|
return this.undoRefs(ref, phxEvent);
|
|
5582
5600
|
}
|
|
5583
5601
|
const meta = this.extractMeta(formEl, {}, opts.value);
|
|
5584
|
-
const formData = serializeForm(formEl, { submitter });
|
|
5602
|
+
const formData = this.serializeForm(formEl, { submitter });
|
|
5585
5603
|
this.pushWithReply(proxyRefGen, "event", {
|
|
5586
5604
|
type: "form",
|
|
5587
5605
|
event: phxEvent,
|
|
@@ -5592,7 +5610,7 @@ var View = class _View {
|
|
|
5592
5610
|
});
|
|
5593
5611
|
} else if (!(formEl.hasAttribute(PHX_REF_SRC) && formEl.classList.contains("phx-submit-loading"))) {
|
|
5594
5612
|
const meta = this.extractMeta(formEl, {}, opts.value);
|
|
5595
|
-
const formData = serializeForm(formEl, { submitter });
|
|
5613
|
+
const formData = this.serializeForm(formEl, { submitter });
|
|
5596
5614
|
this.pushWithReply(refGenerator, "event", {
|
|
5597
5615
|
type: "form",
|
|
5598
5616
|
event: phxEvent,
|
|
@@ -5934,7 +5952,7 @@ var LiveSocket = class {
|
|
|
5934
5952
|
}
|
|
5935
5953
|
// public
|
|
5936
5954
|
version() {
|
|
5937
|
-
return "1.
|
|
5955
|
+
return "1.2.0-rc.0";
|
|
5938
5956
|
}
|
|
5939
5957
|
isProfileEnabled() {
|
|
5940
5958
|
return this.sessionStorage.getItem(PHX_LV_PROFILE) === "true";
|
|
@@ -6011,7 +6029,7 @@ var LiveSocket = class {
|
|
|
6011
6029
|
}
|
|
6012
6030
|
/**
|
|
6013
6031
|
* @param {HTMLElement} el
|
|
6014
|
-
* @param {
|
|
6032
|
+
* @param {import("./js_commands").EncodedJS} encodedJS
|
|
6015
6033
|
* @param {string | null} [eventType]
|
|
6016
6034
|
*/
|
|
6017
6035
|
execJS(el, encodedJS, eventType = null) {
|