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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type Socket } from "phoenix";
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
BINDING_PREFIX,
|
|
3
5
|
CONSECUTIVE_RELOADS,
|
|
@@ -48,13 +50,278 @@ import Hooks from "./hooks";
|
|
|
48
50
|
import LiveUploader from "./live_uploader";
|
|
49
51
|
import View from "./view";
|
|
50
52
|
import JS from "./js";
|
|
51
|
-
import jsCommands from "./js_commands";
|
|
52
|
-
|
|
53
|
+
import jsCommands, { EncodedJS, LiveSocketJSCommands } from "./js_commands";
|
|
54
|
+
import { HooksOptions } from "./view_hook";
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Returns true if the given element was touched by a user.
|
|
58
|
+
* @param {HTMLElement} el - The element to check.
|
|
59
|
+
* @returns {boolean} True if the element was touched by a user, false otherwise.
|
|
60
|
+
*/
|
|
53
61
|
export const isUsedInput = (el) => DOM.isUsedInput(el);
|
|
54
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Options for configuring the LiveSocket instance.
|
|
65
|
+
*/
|
|
66
|
+
export interface LiveSocketOptions {
|
|
67
|
+
/**
|
|
68
|
+
* Defaults for phx-debounce and phx-throttle.
|
|
69
|
+
*/
|
|
70
|
+
defaults?: {
|
|
71
|
+
/** The millisecond phx-debounce time. Defaults to `300`. */
|
|
72
|
+
debounce?: number;
|
|
73
|
+
/** The millisecond phx-throttle time. Defaults to `300`. */
|
|
74
|
+
throttle?: number;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* An object or function for passing connect params.
|
|
78
|
+
* The function receives the element associated with a given LiveView. For example:
|
|
79
|
+
*
|
|
80
|
+
* (el) => {view: el.getAttribute("data-my-view-name", token: window.myToken}
|
|
81
|
+
*
|
|
82
|
+
*/
|
|
83
|
+
params?:
|
|
84
|
+
| ((el: HTMLElement) => { [key: string]: any })
|
|
85
|
+
| { [key: string]: any };
|
|
86
|
+
/**
|
|
87
|
+
* The optional prefix to use for all phx DOM annotations.
|
|
88
|
+
*
|
|
89
|
+
* Defaults to `"phx-"`.
|
|
90
|
+
*/
|
|
91
|
+
bindingPrefix?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Callbacks for LiveView hooks.
|
|
94
|
+
*
|
|
95
|
+
* See [Client hooks via `phx-hook`](https://phoenix-live-view.hexdocs.pm/js-interop.html#client-hooks-via-phx-hook) for more information.
|
|
96
|
+
*/
|
|
97
|
+
hooks?: HooksOptions;
|
|
98
|
+
/** Callbacks for LiveView uploaders. */
|
|
99
|
+
uploaders?: { [key: string]: any }; // TODO: define more specifically
|
|
100
|
+
/** Delay in milliseconds before applying loading states. */
|
|
101
|
+
loaderTimeout?: number;
|
|
102
|
+
/** Delay in milliseconds before executing phx-disconnected commands. */
|
|
103
|
+
disconnectedTimeout?: number;
|
|
104
|
+
/** Maximum reloads before entering failsafe mode. */
|
|
105
|
+
maxReloads?: number;
|
|
106
|
+
/** Minimum time between normal reload attempts. */
|
|
107
|
+
reloadJitterMin?: number;
|
|
108
|
+
/** Maximum time between normal reload attempts. */
|
|
109
|
+
reloadJitterMax?: number;
|
|
110
|
+
/** Time between reload attempts in failsafe mode. */
|
|
111
|
+
failsafeJitter?: number;
|
|
112
|
+
/**
|
|
113
|
+
* Function to log debug information. For example:
|
|
114
|
+
*
|
|
115
|
+
* (view, kind, msg, obj) => console.log(`${view.id} ${kind}: ${msg} - `, obj)
|
|
116
|
+
*/
|
|
117
|
+
viewLogger?: (view: View, kind: string, msg: string, obj: any) => void;
|
|
118
|
+
/**
|
|
119
|
+
* Object mapping event names to functions for populating event metadata.
|
|
120
|
+
*
|
|
121
|
+
* metadata: {
|
|
122
|
+
* click: (e, el) => {
|
|
123
|
+
* return {
|
|
124
|
+
* ctrlKey: e.ctrlKey,
|
|
125
|
+
* metaKey: e.metaKey,
|
|
126
|
+
* detail: e.detail || 1,
|
|
127
|
+
* }
|
|
128
|
+
* },
|
|
129
|
+
* keydown: (e, el) => {
|
|
130
|
+
* return {
|
|
131
|
+
* key: e.key,
|
|
132
|
+
* ctrlKey: e.ctrlKey,
|
|
133
|
+
* metaKey: e.metaKey,
|
|
134
|
+
* shiftKey: e.shiftKey
|
|
135
|
+
* }
|
|
136
|
+
* }
|
|
137
|
+
* }
|
|
138
|
+
*
|
|
139
|
+
*/
|
|
140
|
+
metadata?: {
|
|
141
|
+
[K in keyof HTMLElementEventMap]?: (
|
|
142
|
+
e: HTMLElementEventMap[K],
|
|
143
|
+
el: HTMLElement,
|
|
144
|
+
) => object;
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* An optional Storage-compatible object.
|
|
148
|
+
* Useful when LiveView won't have access to `sessionStorage`. For example, this could
|
|
149
|
+
* happen if a site loads a cross-domain LiveView in an iframe.
|
|
150
|
+
*
|
|
151
|
+
* Example usage:
|
|
152
|
+
*
|
|
153
|
+
* class InMemoryStorage {
|
|
154
|
+
* constructor() { this.storage = {} }
|
|
155
|
+
* getItem(keyName) { return this.storage[keyName] || null }
|
|
156
|
+
* removeItem(keyName) { delete this.storage[keyName] }
|
|
157
|
+
* setItem(keyName, keyValue) { this.storage[keyName] = keyValue }
|
|
158
|
+
* }
|
|
159
|
+
*/
|
|
160
|
+
sessionStorage?: Storage;
|
|
161
|
+
/**
|
|
162
|
+
* An optional Storage-compatible object.
|
|
163
|
+
* Useful when LiveView won't have access to `localStorage`.
|
|
164
|
+
*
|
|
165
|
+
* See {@link sessionStorage} for an example.
|
|
166
|
+
*/
|
|
167
|
+
localStorage?: Storage;
|
|
168
|
+
/**
|
|
169
|
+
* If set to `true`, `phx-change` events will be blocked (will not fire)
|
|
170
|
+
* while the user is composing input using an IME (Input Method Editor).
|
|
171
|
+
* This is determined by the `e.isComposing` property on keyboard events,
|
|
172
|
+
* which is `true` when the user is in the process of entering composed characters (for example,
|
|
173
|
+
* when typing Japanese or Chinese using romaji or pinyin input methods).
|
|
174
|
+
* By default, `phx-change` will not be blocked during a composition session,
|
|
175
|
+
* but note that there were issues reported in older versions of Safari,
|
|
176
|
+
* where a LiveView patch to the input caused unexpected behavior.
|
|
177
|
+
*
|
|
178
|
+
* For more information, see
|
|
179
|
+
* - https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/isComposing
|
|
180
|
+
* - https://github.com/phoenixframework/phoenix_live_view/issues/3322
|
|
181
|
+
*
|
|
182
|
+
* Defaults to `false`.
|
|
183
|
+
*/
|
|
184
|
+
blockPhxChangeWhileComposing?: boolean;
|
|
185
|
+
/** DOM callbacks. */
|
|
186
|
+
dom?: {
|
|
187
|
+
/**
|
|
188
|
+
* An optional function to modify the behavior of querying elements in JS commands.
|
|
189
|
+
* @param sourceEl - The source element, e.g. the button that was clicked.
|
|
190
|
+
* @param query - The query value.
|
|
191
|
+
* @param defaultQuery - A default query function that can be used if no custom query should be applied.
|
|
192
|
+
* @returns A list of DOM elements.
|
|
193
|
+
*/
|
|
194
|
+
jsQuerySelectorAll?: (
|
|
195
|
+
sourceEl: HTMLElement,
|
|
196
|
+
query: string,
|
|
197
|
+
defaultQuery: () => Element[],
|
|
198
|
+
) => Element[];
|
|
199
|
+
/**
|
|
200
|
+
* When defined, called with a start callback that needs to be called
|
|
201
|
+
* to perform the actual patch. Failing to call the start callback causes
|
|
202
|
+
* the page to become stuck.
|
|
203
|
+
*
|
|
204
|
+
* This can be used to delay patches in order to perform view transitions,
|
|
205
|
+
* for example:
|
|
206
|
+
*
|
|
207
|
+
* ```javascript
|
|
208
|
+
* let liveSocket = new LiveSocket("/live", Socket, {
|
|
209
|
+
* dom: {
|
|
210
|
+
* onDocumentPatch(start) {
|
|
211
|
+
* document.startViewTransition(start);
|
|
212
|
+
* }
|
|
213
|
+
* }
|
|
214
|
+
* })
|
|
215
|
+
* ```
|
|
216
|
+
*
|
|
217
|
+
* It is strongly advised to call start as quickly as possible.
|
|
218
|
+
*/
|
|
219
|
+
onDocumentPatch?: (start: () => void) => void;
|
|
220
|
+
/**
|
|
221
|
+
* Called immediately before a DOM patch is applied.
|
|
222
|
+
*/
|
|
223
|
+
onPatchStart?: (container: HTMLElement) => void;
|
|
224
|
+
/**
|
|
225
|
+
* Called immediately after a DOM patch is applied.
|
|
226
|
+
*/
|
|
227
|
+
onPatchEnd?: (container: HTMLElement) => void;
|
|
228
|
+
/**
|
|
229
|
+
* Called when a new DOM node is added.
|
|
230
|
+
*/
|
|
231
|
+
onNodeAdded?: (node: Node) => void;
|
|
232
|
+
/**
|
|
233
|
+
* Called before an element is updated.
|
|
234
|
+
*/
|
|
235
|
+
onBeforeElUpdated?: (fromEl: Element, toEl: Element) => void;
|
|
236
|
+
};
|
|
237
|
+
/** Allow passthrough of other options to the Phoenix Socket constructor. */
|
|
238
|
+
[key: string]: any;
|
|
239
|
+
}
|
|
240
|
+
|
|
55
241
|
export default class LiveSocket {
|
|
56
|
-
|
|
57
|
-
|
|
242
|
+
socket: Socket;
|
|
243
|
+
|
|
244
|
+
/** @internal */
|
|
245
|
+
unloaded = false;
|
|
246
|
+
private bindingPrefix: string;
|
|
247
|
+
private viewLogger: any;
|
|
248
|
+
private metadataCallbacks: any;
|
|
249
|
+
private defaults: any;
|
|
250
|
+
private prevActive: any;
|
|
251
|
+
private silenced: boolean;
|
|
252
|
+
/** @internal */
|
|
253
|
+
main: View | null;
|
|
254
|
+
private outgoingMainEl: Element | null;
|
|
255
|
+
private clickStartedAtTarget: EventTarget | null;
|
|
256
|
+
private linkRef: number;
|
|
257
|
+
private roots: Record<string, View>;
|
|
258
|
+
private href: string;
|
|
259
|
+
private pendingLink: string | null;
|
|
260
|
+
private currentLocation: Location;
|
|
261
|
+
private hooks: HooksOptions;
|
|
262
|
+
/** @internal */
|
|
263
|
+
loaderTimeout: number;
|
|
264
|
+
private reloadWithJitterTimer: ReturnType<typeof setTimeout> | null;
|
|
265
|
+
private maxReloads: number;
|
|
266
|
+
private reloadJitterMin: number;
|
|
267
|
+
private reloadJitterMax: number;
|
|
268
|
+
private failsafeJitter: number;
|
|
269
|
+
/** @internal */
|
|
270
|
+
localStorage: Storage;
|
|
271
|
+
private sessionStorage: Storage;
|
|
272
|
+
private boundTopLevelEvents: boolean;
|
|
273
|
+
private boundEventNames: Set<string>;
|
|
274
|
+
private blockPhxChangeWhileComposing: boolean;
|
|
275
|
+
private serverCloseRef: string | null;
|
|
276
|
+
/** @internal */
|
|
277
|
+
domCallbacks: {
|
|
278
|
+
jsQuerySelectorAll:
|
|
279
|
+
| ((
|
|
280
|
+
sourceEl: HTMLElement,
|
|
281
|
+
query: string,
|
|
282
|
+
defaultQuery: () => Element[],
|
|
283
|
+
) => Element[])
|
|
284
|
+
| null;
|
|
285
|
+
onDocumentPatch?: (start: () => void) => void;
|
|
286
|
+
onPatchStart: (container: HTMLElement) => void;
|
|
287
|
+
onPatchEnd: (container: HTMLElement) => void;
|
|
288
|
+
onNodeAdded: (node: Node) => void;
|
|
289
|
+
onBeforeElUpdated: (fromEl: Element, toEl: Element) => void;
|
|
290
|
+
};
|
|
291
|
+
private transitions: TransitionSet;
|
|
292
|
+
/** @internal */
|
|
293
|
+
currentHistoryPosition: number;
|
|
294
|
+
|
|
295
|
+
/** @internal */
|
|
296
|
+
params: (el: Element) => Record<string, unknown>;
|
|
297
|
+
/** @internal */
|
|
298
|
+
uploaders: any;
|
|
299
|
+
/** @internal */
|
|
300
|
+
disconnectedTimeout: number;
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Creates a new LiveSocket instance.
|
|
304
|
+
*/
|
|
305
|
+
constructor(
|
|
306
|
+
/**
|
|
307
|
+
* The WebSocket endpoint URL, e.g., `"wss://example.com/live"`, or `"/live"` to inherit the host and protocol.
|
|
308
|
+
*/
|
|
309
|
+
url: string,
|
|
310
|
+
/**
|
|
311
|
+
* The required Phoenix Socket class imported from "phoenix". For example:
|
|
312
|
+
*
|
|
313
|
+
* ```javascript
|
|
314
|
+
* import {Socket} from "phoenix"
|
|
315
|
+
* import {LiveSocket} from "phoenix_live_view"
|
|
316
|
+
* let liveSocket = new LiveSocket("/live", Socket, {...})
|
|
317
|
+
* ```
|
|
318
|
+
*/
|
|
319
|
+
phxSocket: typeof Socket,
|
|
320
|
+
/**
|
|
321
|
+
* Optional configuration.
|
|
322
|
+
*/
|
|
323
|
+
opts: Partial<LiveSocketOptions> = {},
|
|
324
|
+
) {
|
|
58
325
|
if (!phxSocket || phxSocket.constructor.name === "Object") {
|
|
59
326
|
throw new Error(`
|
|
60
327
|
a phoenix Socket must be provided as the second argument to the LiveSocket constructor. For example:
|
|
@@ -66,7 +333,6 @@ export default class LiveSocket {
|
|
|
66
333
|
}
|
|
67
334
|
this.socket = new phxSocket(url, opts);
|
|
68
335
|
this.bindingPrefix = opts.bindingPrefix || BINDING_PREFIX;
|
|
69
|
-
this.opts = opts;
|
|
70
336
|
this.params = closure(opts.params || {});
|
|
71
337
|
this.viewLogger = opts.viewLogger;
|
|
72
338
|
this.metadataCallbacks = opts.metadata || {};
|
|
@@ -112,7 +378,8 @@ export default class LiveSocket {
|
|
|
112
378
|
);
|
|
113
379
|
this.transitions = new TransitionSet();
|
|
114
380
|
this.currentHistoryPosition =
|
|
115
|
-
parseInt(this.sessionStorage.getItem(PHX_LV_HISTORY_POSITION)
|
|
381
|
+
parseInt(this.sessionStorage.getItem(PHX_LV_HISTORY_POSITION) || "0") ||
|
|
382
|
+
0;
|
|
116
383
|
window.addEventListener("pagehide", (_e) => {
|
|
117
384
|
this.unloaded = true;
|
|
118
385
|
});
|
|
@@ -126,60 +393,107 @@ export default class LiveSocket {
|
|
|
126
393
|
|
|
127
394
|
// public
|
|
128
395
|
|
|
129
|
-
|
|
396
|
+
/**
|
|
397
|
+
* Returns the version of the LiveView client.
|
|
398
|
+
*/
|
|
399
|
+
version(): string {
|
|
130
400
|
return LV_VSN;
|
|
131
401
|
}
|
|
132
402
|
|
|
133
|
-
|
|
403
|
+
/**
|
|
404
|
+
* Returns true if profiling is enabled. See {@link enableProfiling} and {@link disableProfiling}.
|
|
405
|
+
*/
|
|
406
|
+
isProfileEnabled(): boolean {
|
|
134
407
|
return this.sessionStorage.getItem(PHX_LV_PROFILE) === "true";
|
|
135
408
|
}
|
|
136
409
|
|
|
137
|
-
|
|
410
|
+
/**
|
|
411
|
+
* Returns true if debugging is enabled. See {@link enableDebug} and {@link disableDebug}.
|
|
412
|
+
*/
|
|
413
|
+
isDebugEnabled(): boolean {
|
|
138
414
|
return this.sessionStorage.getItem(PHX_LV_DEBUG) === "true";
|
|
139
415
|
}
|
|
140
416
|
|
|
141
|
-
|
|
417
|
+
/**
|
|
418
|
+
* Returns true if debugging is disabled. See {@link enableDebug} and {@link disableDebug}.
|
|
419
|
+
*/
|
|
420
|
+
isDebugDisabled(): boolean {
|
|
142
421
|
return this.sessionStorage.getItem(PHX_LV_DEBUG) === "false";
|
|
143
422
|
}
|
|
144
423
|
|
|
145
|
-
|
|
424
|
+
/**
|
|
425
|
+
* Enables debugging.
|
|
426
|
+
*
|
|
427
|
+
* When debugging is enabled, the LiveView client will log debug information to the console.
|
|
428
|
+
* See [Debugging client events](https://phoenix-live-view.hexdocs.pm/js-interop.html#debugging-client-events) for more information.
|
|
429
|
+
*/
|
|
430
|
+
enableDebug(): void {
|
|
146
431
|
this.sessionStorage.setItem(PHX_LV_DEBUG, "true");
|
|
147
432
|
}
|
|
148
433
|
|
|
149
|
-
|
|
434
|
+
/**
|
|
435
|
+
* Enables profiling.
|
|
436
|
+
*
|
|
437
|
+
* When profiling is enabled, the LiveView client will log profiling information to the console.
|
|
438
|
+
*/
|
|
439
|
+
enableProfiling(): void {
|
|
150
440
|
this.sessionStorage.setItem(PHX_LV_PROFILE, "true");
|
|
151
441
|
}
|
|
152
442
|
|
|
153
|
-
|
|
443
|
+
/**
|
|
444
|
+
* Disables debugging.
|
|
445
|
+
*/
|
|
446
|
+
disableDebug(): void {
|
|
154
447
|
this.sessionStorage.setItem(PHX_LV_DEBUG, "false");
|
|
155
448
|
}
|
|
156
449
|
|
|
157
|
-
|
|
450
|
+
/**
|
|
451
|
+
* Disables profiling.
|
|
452
|
+
*/
|
|
453
|
+
disableProfiling(): void {
|
|
158
454
|
this.sessionStorage.removeItem(PHX_LV_PROFILE);
|
|
159
455
|
}
|
|
160
456
|
|
|
161
|
-
|
|
457
|
+
/**
|
|
458
|
+
* Enables latency simulation.
|
|
459
|
+
*
|
|
460
|
+
* When latency simulation is enabled, the LiveView client will add a delay to requests and responses from the server.
|
|
461
|
+
* See [Simulating Latency](https://phoenix-live-view.hexdocs.pm/js-interop.html#simulating-latency) for more information.
|
|
462
|
+
*/
|
|
463
|
+
enableLatencySim(upperBoundMs: number): void {
|
|
162
464
|
this.enableDebug();
|
|
163
465
|
console.log(
|
|
164
466
|
"latency simulator enabled for the duration of this browser session. Call disableLatencySim() to disable",
|
|
165
467
|
);
|
|
166
|
-
this.sessionStorage.setItem(PHX_LV_LATENCY_SIM, upperBoundMs);
|
|
468
|
+
this.sessionStorage.setItem(PHX_LV_LATENCY_SIM, upperBoundMs.toString());
|
|
167
469
|
}
|
|
168
470
|
|
|
169
|
-
|
|
471
|
+
/**
|
|
472
|
+
* Disables latency simulation.
|
|
473
|
+
*/
|
|
474
|
+
disableLatencySim(): void {
|
|
170
475
|
this.sessionStorage.removeItem(PHX_LV_LATENCY_SIM);
|
|
171
476
|
}
|
|
172
477
|
|
|
173
|
-
|
|
478
|
+
/**
|
|
479
|
+
* Returns the current latency simulation upper bound.
|
|
480
|
+
*/
|
|
481
|
+
getLatencySim(): number | null {
|
|
174
482
|
const str = this.sessionStorage.getItem(PHX_LV_LATENCY_SIM);
|
|
175
483
|
return str ? parseInt(str) : null;
|
|
176
484
|
}
|
|
177
485
|
|
|
178
|
-
|
|
486
|
+
/**
|
|
487
|
+
* Returns the Phoenix Socket instance.
|
|
488
|
+
*/
|
|
489
|
+
getSocket(): Socket {
|
|
179
490
|
return this.socket;
|
|
180
491
|
}
|
|
181
492
|
|
|
182
|
-
|
|
493
|
+
/**
|
|
494
|
+
* Connects to the LiveView server.
|
|
495
|
+
*/
|
|
496
|
+
connect(): void {
|
|
183
497
|
// enable debug by default if on localhost and not explicitly disabled
|
|
184
498
|
if (window.location.hostname === "localhost" && !this.isDebugDisabled()) {
|
|
185
499
|
this.enableDebug();
|
|
@@ -205,29 +519,41 @@ export default class LiveSocket {
|
|
|
205
519
|
}
|
|
206
520
|
}
|
|
207
521
|
|
|
208
|
-
|
|
209
|
-
|
|
522
|
+
/**
|
|
523
|
+
* Disconnects from the LiveView server.
|
|
524
|
+
*/
|
|
525
|
+
disconnect(callback?: () => void): void {
|
|
526
|
+
this.reloadWithJitterTimer != null &&
|
|
527
|
+
clearTimeout(this.reloadWithJitterTimer);
|
|
210
528
|
// remove the socket close listener to avoid trying to handle
|
|
211
529
|
// a server close event when it is actually caused by us disconnecting
|
|
212
530
|
if (this.serverCloseRef) {
|
|
213
|
-
this.socket.off(this.serverCloseRef);
|
|
531
|
+
this.socket.off([this.serverCloseRef]);
|
|
214
532
|
this.serverCloseRef = null;
|
|
215
533
|
}
|
|
216
534
|
this.socket.disconnect(callback);
|
|
217
535
|
}
|
|
218
536
|
|
|
219
|
-
|
|
220
|
-
|
|
537
|
+
/**
|
|
538
|
+
* Can be used to replace the transport used by the underlying Phoenix Socket.
|
|
539
|
+
*/
|
|
540
|
+
replaceTransport(transport: any): void {
|
|
541
|
+
this.reloadWithJitterTimer != null &&
|
|
542
|
+
clearTimeout(this.reloadWithJitterTimer);
|
|
221
543
|
this.socket.replaceTransport(transport);
|
|
222
544
|
this.connect();
|
|
223
545
|
}
|
|
224
546
|
|
|
225
547
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
548
|
+
* Executes an encoded JS command, targeting the given element.
|
|
549
|
+
*
|
|
550
|
+
* See [`Phoenix.LiveView.JS`](https://phoenix-live-view.hexdocs.pm/Phoenix.LiveView.JS.html) for more information.
|
|
229
551
|
*/
|
|
230
|
-
execJS(
|
|
552
|
+
execJS(
|
|
553
|
+
el: Element,
|
|
554
|
+
encodedJS: EncodedJS,
|
|
555
|
+
eventType: string | null = null,
|
|
556
|
+
): void {
|
|
231
557
|
const e = new CustomEvent("phx:exec", { detail: { sourceElement: el } });
|
|
232
558
|
this.owner(el, (view) => JS.exec(e, eventType, encodedJS, view, el));
|
|
233
559
|
}
|
|
@@ -236,14 +562,15 @@ export default class LiveSocket {
|
|
|
236
562
|
* Returns an object with methods to manipulate the DOM and execute JavaScript.
|
|
237
563
|
* The applied changes integrate with server DOM patching.
|
|
238
564
|
*
|
|
239
|
-
*
|
|
565
|
+
* See [JavaScript interoperability](https://phoenix-live-view.hexdocs.pm/js-interop.html) for more information.
|
|
240
566
|
*/
|
|
241
|
-
js() {
|
|
567
|
+
js(): LiveSocketJSCommands {
|
|
242
568
|
return jsCommands(this, "js");
|
|
243
569
|
}
|
|
244
570
|
|
|
245
571
|
// private
|
|
246
572
|
|
|
573
|
+
/** @internal */
|
|
247
574
|
unload() {
|
|
248
575
|
if (this.unloaded) {
|
|
249
576
|
return;
|
|
@@ -256,10 +583,12 @@ export default class LiveSocket {
|
|
|
256
583
|
this.disconnect();
|
|
257
584
|
}
|
|
258
585
|
|
|
586
|
+
/** @internal */
|
|
259
587
|
triggerDOM(kind, args) {
|
|
260
588
|
this.domCallbacks[kind](...args);
|
|
261
589
|
}
|
|
262
590
|
|
|
591
|
+
/** @internal */
|
|
263
592
|
time(name, func) {
|
|
264
593
|
if (!this.isProfileEnabled() || !console.time) {
|
|
265
594
|
return func();
|
|
@@ -270,6 +599,7 @@ export default class LiveSocket {
|
|
|
270
599
|
return result;
|
|
271
600
|
}
|
|
272
601
|
|
|
602
|
+
/** @internal */
|
|
273
603
|
log(view, kind, msgCallback) {
|
|
274
604
|
if (this.viewLogger) {
|
|
275
605
|
const [msg, obj] = msgCallback();
|
|
@@ -280,18 +610,22 @@ export default class LiveSocket {
|
|
|
280
610
|
}
|
|
281
611
|
}
|
|
282
612
|
|
|
613
|
+
/** @internal */
|
|
283
614
|
requestDOMUpdate(callback) {
|
|
284
615
|
this.transitions.after(callback);
|
|
285
616
|
}
|
|
286
617
|
|
|
618
|
+
/** @internal */
|
|
287
619
|
asyncTransition(promise) {
|
|
288
620
|
this.transitions.addAsyncTransition(promise);
|
|
289
621
|
}
|
|
290
622
|
|
|
623
|
+
/** @internal */
|
|
291
624
|
transition(time, onStart, onDone = function () {}) {
|
|
292
625
|
this.transitions.addTransition(time, onStart, onDone);
|
|
293
626
|
}
|
|
294
627
|
|
|
628
|
+
/** @internal */
|
|
295
629
|
onChannel(channel, event, cb) {
|
|
296
630
|
channel.on(event, (data) => {
|
|
297
631
|
const latency = this.getLatencySim();
|
|
@@ -303,8 +637,10 @@ export default class LiveSocket {
|
|
|
303
637
|
});
|
|
304
638
|
}
|
|
305
639
|
|
|
306
|
-
|
|
307
|
-
|
|
640
|
+
/** @internal */
|
|
641
|
+
reloadWithJitter(view, log?) {
|
|
642
|
+
this.reloadWithJitterTimer != null &&
|
|
643
|
+
clearTimeout(this.reloadWithJitterTimer);
|
|
308
644
|
this.disconnect();
|
|
309
645
|
const minMs = this.reloadJitterMin;
|
|
310
646
|
const maxMs = this.reloadJitterMax;
|
|
@@ -335,14 +671,15 @@ export default class LiveSocket {
|
|
|
335
671
|
`exceeded ${this.maxReloads} consecutive reloads. Entering failsafe mode`,
|
|
336
672
|
]);
|
|
337
673
|
}
|
|
338
|
-
if (this.
|
|
339
|
-
window.location = this.pendingLink;
|
|
674
|
+
if (this.pendingLink !== null) {
|
|
675
|
+
window.location.href = this.pendingLink;
|
|
340
676
|
} else {
|
|
341
677
|
window.location.reload();
|
|
342
678
|
}
|
|
343
679
|
}, afterMs);
|
|
344
680
|
}
|
|
345
681
|
|
|
682
|
+
/** @internal */
|
|
346
683
|
getHookDefinition(name) {
|
|
347
684
|
if (!name) {
|
|
348
685
|
return;
|
|
@@ -354,10 +691,12 @@ export default class LiveSocket {
|
|
|
354
691
|
);
|
|
355
692
|
}
|
|
356
693
|
|
|
694
|
+
/** @internal */
|
|
357
695
|
maybeInternalHook(name) {
|
|
358
696
|
return name && name.startsWith("Phoenix.") && Hooks[name.split(".")[1]];
|
|
359
697
|
}
|
|
360
698
|
|
|
699
|
+
/** @internal */
|
|
361
700
|
maybeRuntimeHook(name) {
|
|
362
701
|
const runtimeHook = document.querySelector(
|
|
363
702
|
`script[${PHX_RUNTIME_HOOK}="${CSS.escape(name)}"]`,
|
|
@@ -383,26 +722,32 @@ export default class LiveSocket {
|
|
|
383
722
|
);
|
|
384
723
|
}
|
|
385
724
|
|
|
725
|
+
/** @internal */
|
|
386
726
|
isUnloaded() {
|
|
387
727
|
return this.unloaded;
|
|
388
728
|
}
|
|
389
729
|
|
|
730
|
+
/** @internal */
|
|
390
731
|
isConnected() {
|
|
391
732
|
return this.socket.isConnected();
|
|
392
733
|
}
|
|
393
734
|
|
|
735
|
+
/** @internal */
|
|
394
736
|
getBindingPrefix() {
|
|
395
737
|
return this.bindingPrefix;
|
|
396
738
|
}
|
|
397
739
|
|
|
740
|
+
/** @internal */
|
|
398
741
|
binding(kind) {
|
|
399
742
|
return `${this.getBindingPrefix()}${kind}`;
|
|
400
743
|
}
|
|
401
744
|
|
|
745
|
+
/** @internal */
|
|
402
746
|
channel(topic, params) {
|
|
403
747
|
return this.socket.channel(topic, params);
|
|
404
748
|
}
|
|
405
749
|
|
|
750
|
+
/** @internal */
|
|
406
751
|
joinDeadView() {
|
|
407
752
|
const body = document.body;
|
|
408
753
|
if (
|
|
@@ -424,6 +769,7 @@ export default class LiveSocket {
|
|
|
424
769
|
}
|
|
425
770
|
}
|
|
426
771
|
|
|
772
|
+
/** @internal */
|
|
427
773
|
joinRootViews() {
|
|
428
774
|
let rootsFound = false;
|
|
429
775
|
DOM.all(
|
|
@@ -448,7 +794,8 @@ export default class LiveSocket {
|
|
|
448
794
|
return rootsFound;
|
|
449
795
|
}
|
|
450
796
|
|
|
451
|
-
|
|
797
|
+
/** @internal */
|
|
798
|
+
redirect(to: string, flash: string | null, reloadToken: string | null) {
|
|
452
799
|
if (reloadToken) {
|
|
453
800
|
Browser.setCookie(PHX_RELOAD_STATUS, reloadToken, 60);
|
|
454
801
|
}
|
|
@@ -456,18 +803,22 @@ export default class LiveSocket {
|
|
|
456
803
|
Browser.redirect(to, flash);
|
|
457
804
|
}
|
|
458
805
|
|
|
806
|
+
/** @internal */
|
|
459
807
|
replaceMain(
|
|
460
|
-
href,
|
|
461
|
-
flash,
|
|
462
|
-
callback = null,
|
|
808
|
+
href: string,
|
|
809
|
+
flash: string | null,
|
|
810
|
+
callback: ((linkRef: number) => void) | null = null,
|
|
463
811
|
linkRef = this.setPendingLink(href),
|
|
464
812
|
) {
|
|
813
|
+
if (!this.main) {
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
465
816
|
const liveReferer = this.currentLocation.href;
|
|
466
|
-
this.outgoingMainEl = this.outgoingMainEl || this.main
|
|
817
|
+
this.outgoingMainEl = this.outgoingMainEl || this.main!.el;
|
|
467
818
|
|
|
468
819
|
const stickies = DOM.findPhxSticky(document) || [];
|
|
469
820
|
const removeEls = DOM.all(
|
|
470
|
-
this.outgoingMainEl
|
|
821
|
+
this.outgoingMainEl!,
|
|
471
822
|
`[${this.binding("remove")}]`,
|
|
472
823
|
).filter((el) => !DOM.isChildOfAny(el, stickies));
|
|
473
824
|
|
|
@@ -484,7 +835,7 @@ export default class LiveSocket {
|
|
|
484
835
|
// remove phx-remove els right before we replace the main element
|
|
485
836
|
removeEls.forEach((el) => el.remove());
|
|
486
837
|
stickies.forEach((el) => newMainEl.appendChild(el));
|
|
487
|
-
this.outgoingMainEl
|
|
838
|
+
this.outgoingMainEl!.replaceWith(newMainEl);
|
|
488
839
|
this.outgoingMainEl = null;
|
|
489
840
|
callback && callback(linkRef);
|
|
490
841
|
onDone();
|
|
@@ -493,7 +844,8 @@ export default class LiveSocket {
|
|
|
493
844
|
});
|
|
494
845
|
}
|
|
495
846
|
|
|
496
|
-
|
|
847
|
+
/** @internal */
|
|
848
|
+
transitionRemoves(elements, callback?) {
|
|
497
849
|
const removeAttr = this.binding("remove");
|
|
498
850
|
const silenceEvents = (e) => {
|
|
499
851
|
e.preventDefault();
|
|
@@ -507,7 +859,7 @@ export default class LiveSocket {
|
|
|
507
859
|
}
|
|
508
860
|
this.execJS(el, el.getAttribute(removeAttr), "remove");
|
|
509
861
|
});
|
|
510
|
-
// remove the silenced listeners when transitions are done
|
|
862
|
+
// remove the silenced listeners when transitions are done in case the element is re-used
|
|
511
863
|
// and call caller's callback as soon as we are done with transitions
|
|
512
864
|
this.requestDOMUpdate(() => {
|
|
513
865
|
elements.forEach((el) => {
|
|
@@ -519,18 +871,21 @@ export default class LiveSocket {
|
|
|
519
871
|
});
|
|
520
872
|
}
|
|
521
873
|
|
|
874
|
+
/** @internal */
|
|
522
875
|
isPhxView(el) {
|
|
523
876
|
return el.getAttribute && el.getAttribute(PHX_SESSION) !== null;
|
|
524
877
|
}
|
|
525
878
|
|
|
526
|
-
|
|
879
|
+
/** @internal */
|
|
880
|
+
newRootView(el, flash?, liveReferer?) {
|
|
527
881
|
const view = new View(el, this, null, flash, liveReferer);
|
|
528
882
|
this.roots[view.id] = view;
|
|
529
883
|
return view;
|
|
530
884
|
}
|
|
531
885
|
|
|
532
|
-
|
|
533
|
-
|
|
886
|
+
/** @internal */
|
|
887
|
+
owner(childEl: Element, callback?: (view: View) => any) {
|
|
888
|
+
let view: View;
|
|
534
889
|
const viewEl = DOM.closestViewEl(childEl);
|
|
535
890
|
if (viewEl) {
|
|
536
891
|
// it can happen that we find a view that is already destroyed;
|
|
@@ -542,15 +897,17 @@ export default class LiveSocket {
|
|
|
542
897
|
// there's no owner and we should not do fall back
|
|
543
898
|
return null;
|
|
544
899
|
}
|
|
545
|
-
view = this.main
|
|
900
|
+
view = this.main!;
|
|
546
901
|
}
|
|
547
902
|
return view && callback ? callback(view) : view;
|
|
548
903
|
}
|
|
549
904
|
|
|
905
|
+
/** @internal */
|
|
550
906
|
withinOwners(childEl, callback) {
|
|
551
907
|
this.owner(childEl, (view) => callback(view, childEl));
|
|
552
908
|
}
|
|
553
909
|
|
|
910
|
+
/** @internal */
|
|
554
911
|
getViewByEl(el) {
|
|
555
912
|
const rootId = el.getAttribute(PHX_ROOT_ID);
|
|
556
913
|
return maybe(this.getRootById(rootId), (root) =>
|
|
@@ -558,10 +915,12 @@ export default class LiveSocket {
|
|
|
558
915
|
);
|
|
559
916
|
}
|
|
560
917
|
|
|
918
|
+
/** @internal */
|
|
561
919
|
getRootById(id) {
|
|
562
920
|
return this.roots[id];
|
|
563
921
|
}
|
|
564
922
|
|
|
923
|
+
/** @internal */
|
|
565
924
|
destroyAllViews() {
|
|
566
925
|
for (const id in this.roots) {
|
|
567
926
|
this.roots[id].destroy();
|
|
@@ -570,6 +929,7 @@ export default class LiveSocket {
|
|
|
570
929
|
this.main = null;
|
|
571
930
|
}
|
|
572
931
|
|
|
932
|
+
/** @internal */
|
|
573
933
|
destroyViewByEl(el) {
|
|
574
934
|
const root = this.getRootById(el.getAttribute(PHX_ROOT_ID));
|
|
575
935
|
if (root && root.id === el.id) {
|
|
@@ -580,16 +940,19 @@ export default class LiveSocket {
|
|
|
580
940
|
}
|
|
581
941
|
}
|
|
582
942
|
|
|
943
|
+
/** @internal */
|
|
583
944
|
getActiveElement() {
|
|
584
945
|
return document.activeElement;
|
|
585
946
|
}
|
|
586
947
|
|
|
948
|
+
/** @internal */
|
|
587
949
|
dropActiveElement(view) {
|
|
588
950
|
if (this.prevActive && view.ownsElement(this.prevActive)) {
|
|
589
951
|
this.prevActive = null;
|
|
590
952
|
}
|
|
591
953
|
}
|
|
592
954
|
|
|
955
|
+
/** @internal */
|
|
593
956
|
restorePreviouslyActiveFocus() {
|
|
594
957
|
if (
|
|
595
958
|
this.prevActive &&
|
|
@@ -600,6 +963,7 @@ export default class LiveSocket {
|
|
|
600
963
|
}
|
|
601
964
|
}
|
|
602
965
|
|
|
966
|
+
/** @internal */
|
|
603
967
|
blurActiveElement() {
|
|
604
968
|
this.prevActive = this.getActiveElement();
|
|
605
969
|
if (
|
|
@@ -610,10 +974,8 @@ export default class LiveSocket {
|
|
|
610
974
|
}
|
|
611
975
|
}
|
|
612
976
|
|
|
613
|
-
/**
|
|
614
|
-
|
|
615
|
-
*/
|
|
616
|
-
bindTopLevelEvents({ dead } = {}) {
|
|
977
|
+
/** @internal */
|
|
978
|
+
bindTopLevelEvents({ dead }: { dead?: boolean } = {}) {
|
|
617
979
|
if (this.boundTopLevelEvents) {
|
|
618
980
|
return;
|
|
619
981
|
}
|
|
@@ -663,7 +1025,7 @@ export default class LiveSocket {
|
|
|
663
1025
|
{ blur: "focusout", focus: "focusin" },
|
|
664
1026
|
(e, type, view, targetEl, phxEvent, phxTarget) => {
|
|
665
1027
|
if (!phxTarget) {
|
|
666
|
-
const data = {
|
|
1028
|
+
const data = { ...this.eventMeta(type, e, targetEl) };
|
|
667
1029
|
JS.exec(e, type, phxEvent, view, targetEl, ["push", { data }]);
|
|
668
1030
|
}
|
|
669
1031
|
},
|
|
@@ -680,10 +1042,11 @@ export default class LiveSocket {
|
|
|
680
1042
|
);
|
|
681
1043
|
this.on("dragover", (e) => e.preventDefault());
|
|
682
1044
|
this.on("dragenter", (e) => {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
1045
|
+
let target = e.target && DOM.elementFromTarget(e.target);
|
|
1046
|
+
if (!target) {
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
const dropzone = closestPhxBinding(target, this.binding(PHX_DROP_TARGET));
|
|
687
1050
|
|
|
688
1051
|
if (!dropzone || !(dropzone instanceof HTMLElement)) {
|
|
689
1052
|
return;
|
|
@@ -694,10 +1057,11 @@ export default class LiveSocket {
|
|
|
694
1057
|
}
|
|
695
1058
|
});
|
|
696
1059
|
this.on("dragleave", (e) => {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
1060
|
+
let target = e.target && DOM.elementFromTarget(e.target);
|
|
1061
|
+
if (!target) {
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
const dropzone = closestPhxBinding(target, this.binding(PHX_DROP_TARGET));
|
|
701
1065
|
|
|
702
1066
|
if (!dropzone || !(dropzone instanceof HTMLElement)) {
|
|
703
1067
|
return;
|
|
@@ -716,17 +1080,22 @@ export default class LiveSocket {
|
|
|
716
1080
|
}
|
|
717
1081
|
});
|
|
718
1082
|
this.on("drop", (e) => {
|
|
1083
|
+
let target = e.target && DOM.elementFromTarget(e.target);
|
|
1084
|
+
if (!target) {
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
719
1087
|
e.preventDefault();
|
|
720
1088
|
|
|
721
|
-
const dropzone = closestPhxBinding(
|
|
722
|
-
e.target,
|
|
723
|
-
this.binding(PHX_DROP_TARGET),
|
|
724
|
-
);
|
|
1089
|
+
const dropzone = closestPhxBinding(target, this.binding(PHX_DROP_TARGET));
|
|
725
1090
|
if (!dropzone || !(dropzone instanceof HTMLElement)) {
|
|
726
1091
|
return;
|
|
727
1092
|
}
|
|
728
1093
|
this.js().removeClass(dropzone, PHX_DROP_TARGET_ACTIVE_CLASS);
|
|
729
1094
|
|
|
1095
|
+
if (!e.dataTransfer) {
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
730
1099
|
const dropTargetId = dropzone.getAttribute(this.binding(PHX_DROP_TARGET));
|
|
731
1100
|
const dropTarget = dropTargetId && document.getElementById(dropTargetId);
|
|
732
1101
|
const files = Array.from(e.dataTransfer.files || []);
|
|
@@ -744,23 +1113,25 @@ export default class LiveSocket {
|
|
|
744
1113
|
dropTarget.dispatchEvent(new Event("input", { bubbles: true }));
|
|
745
1114
|
});
|
|
746
1115
|
this.on(PHX_TRACK_UPLOADS, (e) => {
|
|
747
|
-
const uploadTarget = e.target;
|
|
1116
|
+
const uploadTarget = e.target && DOM.elementFromTarget(e.target);
|
|
748
1117
|
if (!DOM.isUploadInput(uploadTarget)) {
|
|
749
1118
|
return;
|
|
750
1119
|
}
|
|
751
1120
|
const files = Array.from(e.detail.files || []).filter(
|
|
752
1121
|
(f) => f instanceof File || f instanceof Blob,
|
|
753
1122
|
);
|
|
754
|
-
LiveUploader.trackFiles(uploadTarget, files);
|
|
1123
|
+
LiveUploader.trackFiles(uploadTarget as HTMLInputElement, files);
|
|
755
1124
|
uploadTarget.dispatchEvent(new Event("input", { bubbles: true }));
|
|
756
1125
|
});
|
|
757
1126
|
}
|
|
758
1127
|
|
|
1128
|
+
/** @internal */
|
|
759
1129
|
eventMeta(eventName, e, targetEl) {
|
|
760
1130
|
const callback = this.metadataCallbacks[eventName];
|
|
761
1131
|
return callback ? callback(e, targetEl) : {};
|
|
762
1132
|
}
|
|
763
1133
|
|
|
1134
|
+
/** @internal */
|
|
764
1135
|
setPendingLink(href) {
|
|
765
1136
|
this.linkRef++;
|
|
766
1137
|
this.pendingLink = href;
|
|
@@ -768,31 +1139,49 @@ export default class LiveSocket {
|
|
|
768
1139
|
return this.linkRef;
|
|
769
1140
|
}
|
|
770
1141
|
|
|
771
|
-
|
|
772
|
-
|
|
1142
|
+
/**
|
|
1143
|
+
* @internal
|
|
1144
|
+
* anytime we are navigating or connecting, drop reload cookie in case
|
|
1145
|
+
* we issue the cookie but the next request was interrupted and the server never dropped it
|
|
1146
|
+
*/
|
|
773
1147
|
resetReloadStatus() {
|
|
774
1148
|
Browser.deleteCookie(PHX_RELOAD_STATUS);
|
|
775
1149
|
}
|
|
776
1150
|
|
|
1151
|
+
/** @internal */
|
|
777
1152
|
commitPendingLink(linkRef) {
|
|
778
1153
|
if (this.linkRef !== linkRef) {
|
|
779
1154
|
return false;
|
|
780
|
-
}
|
|
1155
|
+
}
|
|
1156
|
+
if (this.pendingLink !== null) {
|
|
781
1157
|
this.href = this.pendingLink;
|
|
782
1158
|
this.pendingLink = null;
|
|
783
|
-
return true;
|
|
784
1159
|
}
|
|
1160
|
+
return true;
|
|
785
1161
|
}
|
|
786
1162
|
|
|
1163
|
+
/** @internal */
|
|
787
1164
|
getHref() {
|
|
788
1165
|
return this.href;
|
|
789
1166
|
}
|
|
790
1167
|
|
|
1168
|
+
/** @internal */
|
|
791
1169
|
hasPendingLink() {
|
|
792
1170
|
return !!this.pendingLink;
|
|
793
1171
|
}
|
|
794
1172
|
|
|
795
|
-
|
|
1173
|
+
/** @internal */
|
|
1174
|
+
bind<E extends Record<string, keyof HTMLElementEventMap>>(
|
|
1175
|
+
events: E,
|
|
1176
|
+
callback: (
|
|
1177
|
+
e: HTMLElementEventMap[E[keyof E]],
|
|
1178
|
+
type: keyof E & string,
|
|
1179
|
+
view: View,
|
|
1180
|
+
targetEl: Element,
|
|
1181
|
+
phxEvent: string,
|
|
1182
|
+
phxTarget: "window" | null,
|
|
1183
|
+
) => void,
|
|
1184
|
+
) {
|
|
796
1185
|
for (const event in events) {
|
|
797
1186
|
const browserEventName = events[event];
|
|
798
1187
|
|
|
@@ -800,19 +1189,36 @@ export default class LiveSocket {
|
|
|
800
1189
|
const binding = this.binding(event);
|
|
801
1190
|
const windowBinding = this.binding(`window-${event}`);
|
|
802
1191
|
const targetPhxEvent =
|
|
803
|
-
e.target
|
|
1192
|
+
e.target instanceof Element && e.target.getAttribute(binding);
|
|
1193
|
+
if (!(e.target instanceof Element)) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
804
1196
|
if (targetPhxEvent) {
|
|
805
1197
|
this.debounce(e.target, e, browserEventName, () => {
|
|
806
1198
|
this.withinOwners(e.target, (view) => {
|
|
807
|
-
callback(
|
|
1199
|
+
callback(
|
|
1200
|
+
e as HTMLElementEventMap[E[keyof E]],
|
|
1201
|
+
event,
|
|
1202
|
+
view,
|
|
1203
|
+
e.target as Element,
|
|
1204
|
+
targetPhxEvent,
|
|
1205
|
+
null,
|
|
1206
|
+
);
|
|
808
1207
|
});
|
|
809
1208
|
});
|
|
810
1209
|
} else {
|
|
811
1210
|
DOM.all(document, `[${windowBinding}]`, (el) => {
|
|
812
|
-
const phxEvent = el.getAttribute(windowBinding)
|
|
1211
|
+
const phxEvent = el.getAttribute(windowBinding)!;
|
|
813
1212
|
this.debounce(el, e, browserEventName, () => {
|
|
814
1213
|
this.withinOwners(el, (view) => {
|
|
815
|
-
callback(
|
|
1214
|
+
callback(
|
|
1215
|
+
e as HTMLElementEventMap[E[keyof E]],
|
|
1216
|
+
event,
|
|
1217
|
+
view,
|
|
1218
|
+
el as Element,
|
|
1219
|
+
phxEvent,
|
|
1220
|
+
"window",
|
|
1221
|
+
);
|
|
816
1222
|
});
|
|
817
1223
|
});
|
|
818
1224
|
});
|
|
@@ -821,27 +1227,37 @@ export default class LiveSocket {
|
|
|
821
1227
|
}
|
|
822
1228
|
}
|
|
823
1229
|
|
|
1230
|
+
/** @internal */
|
|
824
1231
|
bindClicks() {
|
|
825
1232
|
this.on("mousedown", (e) => (this.clickStartedAtTarget = e.target));
|
|
826
|
-
this.bindClick(
|
|
1233
|
+
this.bindClick();
|
|
827
1234
|
}
|
|
828
1235
|
|
|
829
|
-
|
|
830
|
-
|
|
1236
|
+
/** @internal */
|
|
1237
|
+
bindClick() {
|
|
1238
|
+
const click = this.binding("click");
|
|
831
1239
|
window.addEventListener(
|
|
832
|
-
|
|
1240
|
+
"click",
|
|
833
1241
|
(e) => {
|
|
834
|
-
let target =
|
|
1242
|
+
let target = e.target && DOM.elementFromTarget(e.target);
|
|
1243
|
+
if (!target) {
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
835
1246
|
// a synthetic click event (detail 0) will not have caused a mousedown event,
|
|
836
1247
|
// therefore the clickStartedAtTarget is stale
|
|
837
|
-
if (e.detail === 0) this.clickStartedAtTarget =
|
|
838
|
-
const clickStartedAtTarget = this.clickStartedAtTarget ||
|
|
1248
|
+
if (e.detail === 0) this.clickStartedAtTarget = target;
|
|
1249
|
+
const clickStartedAtTarget = this.clickStartedAtTarget || target;
|
|
839
1250
|
// when searching the target for the click event, we always want to
|
|
840
1251
|
// use the actual event target, see #3372
|
|
841
|
-
target = closestPhxBinding(
|
|
1252
|
+
target = closestPhxBinding(target, click);
|
|
842
1253
|
this.dispatchClickAway(e, clickStartedAtTarget);
|
|
843
1254
|
this.clickStartedAtTarget = null;
|
|
844
|
-
|
|
1255
|
+
|
|
1256
|
+
if (!target) {
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const phxEvent = target.getAttribute(click);
|
|
845
1261
|
if (!phxEvent) {
|
|
846
1262
|
if (DOM.isNewPageClick(e, window.location)) {
|
|
847
1263
|
this.unload();
|
|
@@ -871,6 +1287,7 @@ export default class LiveSocket {
|
|
|
871
1287
|
);
|
|
872
1288
|
}
|
|
873
1289
|
|
|
1290
|
+
/** @internal */
|
|
874
1291
|
dispatchClickAway(e, clickStartedAt) {
|
|
875
1292
|
const phxClickAway = this.binding("click-away");
|
|
876
1293
|
const portal = clickStartedAt.closest(`[${PHX_TELEPORTED_SRC}]`);
|
|
@@ -911,6 +1328,7 @@ export default class LiveSocket {
|
|
|
911
1328
|
});
|
|
912
1329
|
}
|
|
913
1330
|
|
|
1331
|
+
/** @internal */
|
|
914
1332
|
bindNav() {
|
|
915
1333
|
if (!Browser.canPushState()) {
|
|
916
1334
|
return;
|
|
@@ -918,9 +1336,9 @@ export default class LiveSocket {
|
|
|
918
1336
|
if (history.scrollRestoration) {
|
|
919
1337
|
history.scrollRestoration = "manual";
|
|
920
1338
|
}
|
|
921
|
-
let scrollTimer = null;
|
|
1339
|
+
let scrollTimer: ReturnType<typeof setTimeout> | null = null;
|
|
922
1340
|
window.addEventListener("scroll", (_e) => {
|
|
923
|
-
clearTimeout(scrollTimer);
|
|
1341
|
+
scrollTimer != null && clearTimeout(scrollTimer);
|
|
924
1342
|
scrollTimer = setTimeout(() => {
|
|
925
1343
|
Browser.updateCurrentState((state) =>
|
|
926
1344
|
Object.assign(state, { scroll: window.scrollY }),
|
|
@@ -960,6 +1378,7 @@ export default class LiveSocket {
|
|
|
960
1378
|
this.maybeScroll(scroll);
|
|
961
1379
|
};
|
|
962
1380
|
if (
|
|
1381
|
+
this.main &&
|
|
963
1382
|
this.main.isConnected() &&
|
|
964
1383
|
navType === "patch" &&
|
|
965
1384
|
id === this.main.id
|
|
@@ -975,7 +1394,14 @@ export default class LiveSocket {
|
|
|
975
1394
|
window.addEventListener(
|
|
976
1395
|
"click",
|
|
977
1396
|
(e) => {
|
|
978
|
-
|
|
1397
|
+
let el = e.target && DOM.elementFromTarget(e.target);
|
|
1398
|
+
if (!el) {
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
const target = closestPhxBinding(
|
|
1402
|
+
el,
|
|
1403
|
+
PHX_LIVE_LINK,
|
|
1404
|
+
) as HTMLAnchorElement | null;
|
|
979
1405
|
const type = target && target.getAttribute(PHX_LIVE_LINK);
|
|
980
1406
|
if (!type || !this.isConnected() || !this.main || DOM.wantsNewTab(e)) {
|
|
981
1407
|
return;
|
|
@@ -983,11 +1409,16 @@ export default class LiveSocket {
|
|
|
983
1409
|
|
|
984
1410
|
// When wrapping an SVG element in an anchor tag, the href can be an SVGAnimatedString
|
|
985
1411
|
const href =
|
|
986
|
-
target.href instanceof SVGAnimatedString
|
|
987
|
-
? target.href.baseVal
|
|
1412
|
+
(target.href as unknown) instanceof SVGAnimatedString
|
|
1413
|
+
? (target.href as unknown as SVGAnimatedString).baseVal
|
|
988
1414
|
: target.href;
|
|
989
1415
|
|
|
990
1416
|
const linkState = target.getAttribute(PHX_LINK_STATE);
|
|
1417
|
+
if (linkState !== "replace" && linkState !== "push") {
|
|
1418
|
+
throw new Error(
|
|
1419
|
+
`expected ${PHX_LINK_STATE} to be "replace" or "push", got: ${linkState}`,
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
991
1422
|
e.preventDefault();
|
|
992
1423
|
e.stopImmediatePropagation(); // do not bubble click to regular phx-click bindings
|
|
993
1424
|
if (this.pendingLink === href) {
|
|
@@ -1014,6 +1445,7 @@ export default class LiveSocket {
|
|
|
1014
1445
|
);
|
|
1015
1446
|
}
|
|
1016
1447
|
|
|
1448
|
+
/** @internal */
|
|
1017
1449
|
maybeScroll(scroll) {
|
|
1018
1450
|
if (typeof scroll === "number") {
|
|
1019
1451
|
requestAnimationFrame(() => {
|
|
@@ -1022,34 +1454,39 @@ export default class LiveSocket {
|
|
|
1022
1454
|
}
|
|
1023
1455
|
}
|
|
1024
1456
|
|
|
1457
|
+
/** @internal */
|
|
1025
1458
|
dispatchEvent(event, payload = {}) {
|
|
1026
1459
|
DOM.dispatchEvent(window, `phx:${event}`, { detail: payload });
|
|
1027
1460
|
}
|
|
1028
1461
|
|
|
1462
|
+
/** @internal */
|
|
1029
1463
|
dispatchEvents(events) {
|
|
1030
1464
|
events.forEach(([event, payload]) => this.dispatchEvent(event, payload));
|
|
1031
1465
|
}
|
|
1032
1466
|
|
|
1033
|
-
|
|
1467
|
+
/** @internal */
|
|
1468
|
+
withPageLoading(info, callback?) {
|
|
1034
1469
|
DOM.dispatchEvent(window, "phx:page-loading-start", { detail: info });
|
|
1035
1470
|
const done = () =>
|
|
1036
1471
|
DOM.dispatchEvent(window, "phx:page-loading-stop", { detail: info });
|
|
1037
1472
|
return callback ? callback(done) : done;
|
|
1038
1473
|
}
|
|
1039
1474
|
|
|
1475
|
+
/** @internal */
|
|
1040
1476
|
pushHistoryPatch(e, href, linkState, targetEl) {
|
|
1041
|
-
if (!this.isConnected() || !this.main.isMain()) {
|
|
1477
|
+
if (!this.isConnected() || !(this.main && this.main.isMain())) {
|
|
1042
1478
|
return Browser.redirect(href);
|
|
1043
1479
|
}
|
|
1044
1480
|
|
|
1045
1481
|
this.withPageLoading({ to: href, kind: "patch" }, (done) => {
|
|
1046
|
-
this.main
|
|
1482
|
+
this.main!.pushLinkPatch(e, href, targetEl, (linkRef) => {
|
|
1047
1483
|
this.historyPatch(href, linkState, linkRef);
|
|
1048
1484
|
done();
|
|
1049
1485
|
});
|
|
1050
1486
|
});
|
|
1051
1487
|
}
|
|
1052
1488
|
|
|
1489
|
+
/** @internal */
|
|
1053
1490
|
historyPatch(href, linkState, linkRef = this.setPendingLink(href)) {
|
|
1054
1491
|
if (!this.commitPendingLink(linkRef)) {
|
|
1055
1492
|
return;
|
|
@@ -1069,7 +1506,7 @@ export default class LiveSocket {
|
|
|
1069
1506
|
linkState,
|
|
1070
1507
|
{
|
|
1071
1508
|
type: "patch",
|
|
1072
|
-
id: this.main
|
|
1509
|
+
id: this.main!.id,
|
|
1073
1510
|
position: this.currentHistoryPosition,
|
|
1074
1511
|
},
|
|
1075
1512
|
href,
|
|
@@ -1081,12 +1518,19 @@ export default class LiveSocket {
|
|
|
1081
1518
|
this.registerNewLocation(window.location);
|
|
1082
1519
|
}
|
|
1083
1520
|
|
|
1084
|
-
|
|
1521
|
+
/** @internal */
|
|
1522
|
+
historyRedirect(
|
|
1523
|
+
e: Event,
|
|
1524
|
+
href: string,
|
|
1525
|
+
linkState: "replace" | "push",
|
|
1526
|
+
flash: string | null,
|
|
1527
|
+
targetEl?: Element | null,
|
|
1528
|
+
) {
|
|
1085
1529
|
const clickLoading = targetEl && e.isTrusted && e.type !== "popstate";
|
|
1086
1530
|
if (clickLoading) {
|
|
1087
1531
|
targetEl.classList.add("phx-click-loading");
|
|
1088
1532
|
}
|
|
1089
|
-
if (!this.isConnected() || !this.main.isMain()) {
|
|
1533
|
+
if (!this.isConnected() || !(this.main && this.main.isMain())) {
|
|
1090
1534
|
return Browser.redirect(href, flash);
|
|
1091
1535
|
}
|
|
1092
1536
|
|
|
@@ -1116,7 +1560,7 @@ export default class LiveSocket {
|
|
|
1116
1560
|
linkState,
|
|
1117
1561
|
{
|
|
1118
1562
|
type: "redirect",
|
|
1119
|
-
id: this.main
|
|
1563
|
+
id: this.main!.id,
|
|
1120
1564
|
scroll: scroll,
|
|
1121
1565
|
position: this.currentHistoryPosition,
|
|
1122
1566
|
},
|
|
@@ -1138,6 +1582,7 @@ export default class LiveSocket {
|
|
|
1138
1582
|
});
|
|
1139
1583
|
}
|
|
1140
1584
|
|
|
1585
|
+
/** @internal */
|
|
1141
1586
|
registerNewLocation(newLocation) {
|
|
1142
1587
|
const { pathname, search } = this.currentLocation;
|
|
1143
1588
|
if (pathname + search === newLocation.pathname + newLocation.search) {
|
|
@@ -1148,12 +1593,14 @@ export default class LiveSocket {
|
|
|
1148
1593
|
}
|
|
1149
1594
|
}
|
|
1150
1595
|
|
|
1596
|
+
/** @internal */
|
|
1151
1597
|
bindForms() {
|
|
1152
1598
|
let iterations = 0;
|
|
1153
1599
|
let externalFormSubmitted = false;
|
|
1154
1600
|
|
|
1155
1601
|
// disable forms on submit that track phx-change but perform external submit
|
|
1156
1602
|
this.on("submit", (e) => {
|
|
1603
|
+
if (!(e.target instanceof HTMLFormElement)) return;
|
|
1157
1604
|
const phxSubmit = e.target.getAttribute(this.binding("submit"));
|
|
1158
1605
|
const phxChange = e.target.getAttribute(this.binding("change"));
|
|
1159
1606
|
if (!externalFormSubmitted && phxChange && !phxSubmit) {
|
|
@@ -1166,13 +1613,14 @@ export default class LiveSocket {
|
|
|
1166
1613
|
if (DOM.isUnloadableFormSubmit(e)) {
|
|
1167
1614
|
this.unload();
|
|
1168
1615
|
}
|
|
1169
|
-
e.target.submit();
|
|
1616
|
+
(e.target as HTMLFormElement).submit();
|
|
1170
1617
|
});
|
|
1171
1618
|
});
|
|
1172
1619
|
}
|
|
1173
1620
|
});
|
|
1174
1621
|
|
|
1175
1622
|
this.on("submit", (e) => {
|
|
1623
|
+
if (!(e.target instanceof HTMLFormElement)) return;
|
|
1176
1624
|
const phxEvent = e.target.getAttribute(this.binding("submit"));
|
|
1177
1625
|
if (!phxEvent) {
|
|
1178
1626
|
if (DOM.isUnloadableFormSubmit(e)) {
|
|
@@ -1190,15 +1638,13 @@ export default class LiveSocket {
|
|
|
1190
1638
|
});
|
|
1191
1639
|
});
|
|
1192
1640
|
|
|
1193
|
-
for (const type of ["change", "input"]) {
|
|
1641
|
+
for (const type of ["change" as const, "input" as const]) {
|
|
1194
1642
|
this.on(type, (e) => {
|
|
1195
|
-
if (
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
e.target.form === undefined
|
|
1201
|
-
) {
|
|
1643
|
+
if (!DOM.isFormAssociated(e.target)) {
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
if (e instanceof CustomEvent && e.target.form === undefined) {
|
|
1202
1648
|
// throw on invalid JS.dispatch target and noop if CustomEvent triggered outside JS.dispatch
|
|
1203
1649
|
if (e.detail && e.detail.dispatcher) {
|
|
1204
1650
|
throw new Error(
|
|
@@ -1207,9 +1653,14 @@ export default class LiveSocket {
|
|
|
1207
1653
|
}
|
|
1208
1654
|
return;
|
|
1209
1655
|
}
|
|
1210
|
-
|
|
1656
|
+
|
|
1211
1657
|
const input = e.target;
|
|
1212
|
-
|
|
1658
|
+
const phxChange = this.binding("change");
|
|
1659
|
+
if (
|
|
1660
|
+
this.blockPhxChangeWhileComposing &&
|
|
1661
|
+
e instanceof InputEvent &&
|
|
1662
|
+
e.isComposing
|
|
1663
|
+
) {
|
|
1213
1664
|
const key = `composition-listener-${type}`;
|
|
1214
1665
|
if (!DOM.private(input, key)) {
|
|
1215
1666
|
DOM.putPrivate(input, key, true);
|
|
@@ -1265,16 +1716,18 @@ export default class LiveSocket {
|
|
|
1265
1716
|
DOM.putPrivate(input, PHX_HAS_FOCUSED, true);
|
|
1266
1717
|
JS.exec(e, "change", phxEvent, view, input, [
|
|
1267
1718
|
"push",
|
|
1268
|
-
{ _target:
|
|
1719
|
+
{ _target: input.name, dispatcher: dispatcher },
|
|
1269
1720
|
]);
|
|
1270
1721
|
});
|
|
1271
1722
|
});
|
|
1272
1723
|
});
|
|
1273
1724
|
}
|
|
1274
|
-
this.on("reset", (e) => {
|
|
1275
|
-
const form = e.target;
|
|
1725
|
+
this.on("reset", (e: Event) => {
|
|
1726
|
+
const form = e.target as HTMLFormElement;
|
|
1276
1727
|
DOM.resetForm(form);
|
|
1277
|
-
const input = Array.from(form.elements).find(
|
|
1728
|
+
const input = Array.from(form.elements).find(
|
|
1729
|
+
(el) => "type" in el && el.type === "reset",
|
|
1730
|
+
) as HTMLInputElement | undefined;
|
|
1278
1731
|
if (input) {
|
|
1279
1732
|
// wait until next tick to get updated input value
|
|
1280
1733
|
window.requestAnimationFrame(() => {
|
|
@@ -1286,6 +1739,7 @@ export default class LiveSocket {
|
|
|
1286
1739
|
});
|
|
1287
1740
|
}
|
|
1288
1741
|
|
|
1742
|
+
/** @internal */
|
|
1289
1743
|
debounce(el, event, eventType, callback) {
|
|
1290
1744
|
if (eventType === "blur" || eventType === "focusout") {
|
|
1291
1745
|
return callback();
|
|
@@ -1314,21 +1768,31 @@ export default class LiveSocket {
|
|
|
1314
1768
|
});
|
|
1315
1769
|
}
|
|
1316
1770
|
|
|
1771
|
+
/** @internal */
|
|
1317
1772
|
silenceEvents(callback) {
|
|
1318
1773
|
this.silenced = true;
|
|
1319
1774
|
callback();
|
|
1320
1775
|
this.silenced = false;
|
|
1321
1776
|
}
|
|
1322
1777
|
|
|
1323
|
-
|
|
1778
|
+
/** @internal */
|
|
1779
|
+
on<K extends string>(
|
|
1780
|
+
event: K,
|
|
1781
|
+
callback: (
|
|
1782
|
+
e: K extends keyof HTMLElementEventMap
|
|
1783
|
+
? HTMLElementEventMap[K]
|
|
1784
|
+
: CustomEvent,
|
|
1785
|
+
) => void,
|
|
1786
|
+
) {
|
|
1324
1787
|
this.boundEventNames.add(event);
|
|
1325
1788
|
window.addEventListener(event, (e) => {
|
|
1326
1789
|
if (!this.silenced) {
|
|
1327
|
-
callback(e);
|
|
1790
|
+
callback(e as any);
|
|
1328
1791
|
}
|
|
1329
1792
|
});
|
|
1330
1793
|
}
|
|
1331
1794
|
|
|
1795
|
+
/** @internal */
|
|
1332
1796
|
jsQuerySelectorAll(sourceEl, query, defaultQuery) {
|
|
1333
1797
|
const all = this.domCallbacks.jsQuerySelectorAll;
|
|
1334
1798
|
return all ? all(sourceEl, query, defaultQuery) : defaultQuery();
|
|
@@ -1336,6 +1800,10 @@ export default class LiveSocket {
|
|
|
1336
1800
|
}
|
|
1337
1801
|
|
|
1338
1802
|
class TransitionSet {
|
|
1803
|
+
private transitions: Set<ReturnType<typeof setTimeout>>;
|
|
1804
|
+
private promises: Set<Promise<any>>;
|
|
1805
|
+
private pendingOps: Array<() => void>;
|
|
1806
|
+
|
|
1339
1807
|
constructor() {
|
|
1340
1808
|
this.transitions = new Set();
|
|
1341
1809
|
this.promises = new Set();
|