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.
Files changed (27) hide show
  1. package/README.md +5 -5
  2. package/assets/js/phoenix_live_view/README.md +3 -0
  3. package/assets/js/phoenix_live_view/{aria.js → aria.ts} +18 -10
  4. package/assets/js/phoenix_live_view/{browser.js → browser.ts} +12 -8
  5. package/assets/js/phoenix_live_view/{dom.js → dom.ts} +107 -34
  6. package/assets/js/phoenix_live_view/{dom_patch.js → dom_patch.ts} +187 -124
  7. package/assets/js/phoenix_live_view/{dom_post_morph_restorer.js → dom_post_morph_restorer.ts} +17 -2
  8. package/assets/js/phoenix_live_view/{element_ref.js → element_ref.ts} +17 -11
  9. package/assets/js/phoenix_live_view/entry_uploader.js +4 -4
  10. package/assets/js/phoenix_live_view/{hooks.js → hooks.ts} +108 -91
  11. package/assets/js/phoenix_live_view/index.ts +14 -301
  12. package/assets/js/phoenix_live_view/js.js +2 -1
  13. package/assets/js/phoenix_live_view/js_commands.ts +12 -9
  14. package/assets/js/phoenix_live_view/{live_socket.js → live_socket.ts} +582 -114
  15. package/assets/js/phoenix_live_view/live_uploader.js +1 -1
  16. package/assets/js/phoenix_live_view/rendered.js +3 -0
  17. package/assets/js/phoenix_live_view/{utils.js → utils.ts} +35 -6
  18. package/assets/js/phoenix_live_view/{view.js → view.ts} +221 -110
  19. package/assets/js/phoenix_live_view/view_hook.ts +92 -32
  20. package/package.json +5 -2
  21. package/priv/static/phoenix_live_view.cjs.js +577 -314
  22. package/priv/static/phoenix_live_view.cjs.js.map +4 -4
  23. package/priv/static/phoenix_live_view.esm.js +577 -314
  24. package/priv/static/phoenix_live_view.esm.js.map +4 -4
  25. package/priv/static/phoenix_live_view.js +584 -314
  26. package/priv/static/phoenix_live_view.min.js +7 -7
  27. /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
- constructor(url, phxSocket, opts = {}) {
57
- this.unloaded = false;
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)) || 0;
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
- version() {
396
+ /**
397
+ * Returns the version of the LiveView client.
398
+ */
399
+ version(): string {
130
400
  return LV_VSN;
131
401
  }
132
402
 
133
- isProfileEnabled() {
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
- isDebugEnabled() {
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
- isDebugDisabled() {
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
- enableDebug() {
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
- enableProfiling() {
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
- disableDebug() {
443
+ /**
444
+ * Disables debugging.
445
+ */
446
+ disableDebug(): void {
154
447
  this.sessionStorage.setItem(PHX_LV_DEBUG, "false");
155
448
  }
156
449
 
157
- disableProfiling() {
450
+ /**
451
+ * Disables profiling.
452
+ */
453
+ disableProfiling(): void {
158
454
  this.sessionStorage.removeItem(PHX_LV_PROFILE);
159
455
  }
160
456
 
161
- enableLatencySim(upperBoundMs) {
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
- disableLatencySim() {
471
+ /**
472
+ * Disables latency simulation.
473
+ */
474
+ disableLatencySim(): void {
170
475
  this.sessionStorage.removeItem(PHX_LV_LATENCY_SIM);
171
476
  }
172
477
 
173
- getLatencySim() {
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
- getSocket() {
486
+ /**
487
+ * Returns the Phoenix Socket instance.
488
+ */
489
+ getSocket(): Socket {
179
490
  return this.socket;
180
491
  }
181
492
 
182
- connect() {
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
- disconnect(callback) {
209
- clearTimeout(this.reloadWithJitterTimer);
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
- replaceTransport(transport) {
220
- clearTimeout(this.reloadWithJitterTimer);
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
- * @param {HTMLElement} el
227
- * @param {import("./js_commands").EncodedJS} encodedJS
228
- * @param {string | null} [eventType]
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(el, encodedJS, eventType = null) {
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
- * @returns {import("./js_commands").LiveSocketJSCommands}
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
- reloadWithJitter(view, log) {
307
- clearTimeout(this.reloadWithJitterTimer);
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.hasPendingLink()) {
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
- redirect(to, flash, reloadToken) {
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.el;
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.replaceWith(newMainEl);
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
- transitionRemoves(elements, callback) {
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 incase the element is re-used
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
- newRootView(el, flash, liveReferer) {
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
- owner(childEl, callback) {
533
- let view;
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
- * @param {{dead?: boolean}} [options={}]
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 = { key: e.key, ...this.eventMeta(type, e, targetEl) };
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
- const dropzone = closestPhxBinding(
684
- e.target,
685
- this.binding(PHX_DROP_TARGET),
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
- const dropzone = closestPhxBinding(
698
- e.target,
699
- this.binding(PHX_DROP_TARGET),
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
- // anytime we are navigating or connecting, drop reload cookie in case
772
- // we issue the cookie but the next request was interrupted and the server never dropped it
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
- } else {
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
- bind(events, callback) {
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.getAttribute && e.target.getAttribute(binding);
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(e, event, view, e.target, targetPhxEvent, null);
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(e, event, view, el, phxEvent, "window");
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("click", "click");
1233
+ this.bindClick();
827
1234
  }
828
1235
 
829
- bindClick(eventName, bindingName) {
830
- const click = this.binding(bindingName);
1236
+ /** @internal */
1237
+ bindClick() {
1238
+ const click = this.binding("click");
831
1239
  window.addEventListener(
832
- eventName,
1240
+ "click",
833
1241
  (e) => {
834
- let target = null;
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 = e.target;
838
- const clickStartedAtTarget = this.clickStartedAtTarget || e.target;
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(e.target, click);
1252
+ target = closestPhxBinding(target, click);
842
1253
  this.dispatchClickAway(e, clickStartedAtTarget);
843
1254
  this.clickStartedAtTarget = null;
844
- const phxEvent = target && target.getAttribute(click);
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
- const target = closestPhxBinding(e.target, PHX_LIVE_LINK);
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
- withPageLoading(info, callback) {
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.pushLinkPatch(e, href, targetEl, (linkRef) => {
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.id,
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
- historyRedirect(e, href, linkState, flash, targetEl) {
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.id,
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
- e instanceof CustomEvent &&
1197
- (e.target instanceof HTMLInputElement ||
1198
- e.target instanceof HTMLSelectElement ||
1199
- e.target instanceof HTMLTextAreaElement) &&
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
- const phxChange = this.binding("change");
1656
+
1211
1657
  const input = e.target;
1212
- if (this.blockPhxChangeWhileComposing && e.isComposing) {
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: e.target.name, dispatcher: dispatcher },
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((el) => el.type === "reset");
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
- on(event, callback) {
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();