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
@@ -29,13 +29,17 @@ export default class ElementRef {
29
29
  );
30
30
  }
31
31
 
32
- constructor(el) {
32
+ private el: Element;
33
+ private loadingRef: number | null;
34
+ private lockRef: number | null;
35
+
36
+ constructor(el: Element) {
33
37
  this.el = el;
34
38
  this.loadingRef = el.hasAttribute(PHX_REF_LOADING)
35
- ? parseInt(el.getAttribute(PHX_REF_LOADING), 10)
39
+ ? parseInt(el.getAttribute(PHX_REF_LOADING)!, 10)
36
40
  : null;
37
41
  this.lockRef = el.hasAttribute(PHX_REF_LOCK)
38
- ? parseInt(el.getAttribute(PHX_REF_LOCK), 10)
42
+ ? parseInt(el.getAttribute(PHX_REF_LOCK)!, 10)
39
43
  : null;
40
44
  }
41
45
 
@@ -89,7 +93,7 @@ export default class ElementRef {
89
93
 
90
94
  // private
91
95
 
92
- isWithin(ref) {
96
+ private isWithin(ref) {
93
97
  return !(
94
98
  this.loadingRef !== null &&
95
99
  this.loadingRef > ref &&
@@ -104,7 +108,7 @@ export default class ElementRef {
104
108
  //
105
109
  // 1. execute pending mounted hooks for nodes now in the DOM
106
110
  // 2. undo any ref inside the cloned tree that has since been ack'd
107
- undoLocks(ref, phxEvent, eachCloneCallback) {
111
+ private undoLocks(ref, phxEvent, eachCloneCallback) {
108
112
  if (!this.isLockUndoneBy(ref)) {
109
113
  return;
110
114
  }
@@ -126,7 +130,7 @@ export default class ElementRef {
126
130
  );
127
131
  }
128
132
 
129
- undoLoading(ref, phxEvent) {
133
+ private undoLoading(ref, phxEvent) {
130
134
  if (!this.isLoadingUndoneBy(ref)) {
131
135
  if (
132
136
  this.canUndoLoading(ref) &&
@@ -142,11 +146,11 @@ export default class ElementRef {
142
146
  const disabledVal = this.el.getAttribute(PHX_DISABLED);
143
147
  const readOnlyVal = this.el.getAttribute(PHX_READONLY);
144
148
  // restore inputs
145
- if (readOnlyVal !== null) {
149
+ if (readOnlyVal !== null && "readOnly" in this.el) {
146
150
  this.el.readOnly = readOnlyVal === "true" ? true : false;
147
151
  this.el.removeAttribute(PHX_READONLY);
148
152
  }
149
- if (disabledVal !== null) {
153
+ if (disabledVal !== null && "disabled" in this.el) {
150
154
  this.el.disabled = disabledVal === "true" ? true : false;
151
155
  this.el.removeAttribute(PHX_DISABLED);
152
156
  }
@@ -175,14 +179,16 @@ export default class ElementRef {
175
179
  });
176
180
  }
177
181
 
178
- isLoadingUndoneBy(ref) {
182
+ private isLoadingUndoneBy(ref) {
179
183
  return this.loadingRef === null ? false : this.loadingRef <= ref;
180
184
  }
185
+
186
+ /** @internal */
181
187
  isLockUndoneBy(ref) {
182
188
  return this.lockRef === null ? false : this.lockRef <= ref;
183
189
  }
184
190
 
185
- isFullyResolvedBy(ref) {
191
+ private isFullyResolvedBy(ref) {
186
192
  return (
187
193
  (this.loadingRef === null || this.loadingRef <= ref) &&
188
194
  (this.lockRef === null || this.lockRef <= ref)
@@ -190,7 +196,7 @@ export default class ElementRef {
190
196
  }
191
197
 
192
198
  // only remove the phx-submit-loading class if we are not locked
193
- canUndoLoading(ref) {
199
+ private canUndoLoading(ref) {
194
200
  return this.lockRef === null || this.lockRef <= ref;
195
201
  }
196
202
  }
@@ -21,7 +21,7 @@ export default class EntryUploader {
21
21
  }
22
22
  this.uploadChannel.leave();
23
23
  this.errored = true;
24
- clearTimeout(this.chunkTimer);
24
+ this.chunkTimer != null && clearTimeout(this.chunkTimer);
25
25
  this.entry.error(reason);
26
26
  }
27
27
 
@@ -30,7 +30,7 @@ export default class EntryUploader {
30
30
  this.uploadChannel
31
31
  .join()
32
32
  .receive("ok", (_data) => this.readNextChunk())
33
- .receive("error", (reason) => this.error(reason));
33
+ .receive("error", ({ reason }) => this.error(reason));
34
34
  }
35
35
 
36
36
  isDone() {
@@ -44,11 +44,11 @@ export default class EntryUploader {
44
44
  this.chunkSize + this.offset,
45
45
  );
46
46
  reader.onload = (e) => {
47
- if (e.target.error === null) {
47
+ if (e.target?.error === null) {
48
48
  this.offset += /** @type {ArrayBuffer} */ (e.target.result).byteLength;
49
49
  this.pushChunk(/** @type {ArrayBuffer} */ (e.target.result));
50
50
  } else {
51
- return logError("Read error: " + e.target.error);
51
+ return logError("Read error: " + e.target?.error);
52
52
  }
53
53
  };
54
54
  reader.readAsArrayBuffer(blob);
@@ -6,89 +6,12 @@ import {
6
6
  PHX_VIEWPORT_OVERRUN_TARGET,
7
7
  } from "./constants";
8
8
 
9
+ import type { Hook } from "./view_hook";
10
+
9
11
  import LiveUploader from "./live_uploader";
10
12
  import ARIA from "./aria";
11
13
 
12
- const Hooks = {
13
- LiveFileUpload: {
14
- activeRefs() {
15
- return this.el.getAttribute(PHX_ACTIVE_ENTRY_REFS);
16
- },
17
-
18
- preflightedRefs() {
19
- return this.el.getAttribute(PHX_PREFLIGHTED_REFS);
20
- },
21
-
22
- mounted() {
23
- this.js().ignoreAttributes(this.el, ["value"]);
24
- this.preflightedWas = this.preflightedRefs();
25
- },
26
-
27
- updated() {
28
- const newPreflights = this.preflightedRefs();
29
- if (this.preflightedWas !== newPreflights) {
30
- this.preflightedWas = newPreflights;
31
- if (newPreflights === "") {
32
- this.__view().cancelSubmit(this.el.form);
33
- }
34
- }
35
-
36
- if (this.activeRefs() === "") {
37
- this.el.value = null;
38
- }
39
- this.el.dispatchEvent(new CustomEvent(PHX_LIVE_FILE_UPDATED));
40
- },
41
- },
42
-
43
- LiveImgPreview: {
44
- mounted() {
45
- this.ref = this.el.getAttribute("data-phx-entry-ref");
46
- this.inputEl = document.getElementById(
47
- this.el.getAttribute(PHX_UPLOAD_REF),
48
- );
49
- this.url = LiveUploader.getEntryDataURL(this.inputEl, this.ref);
50
- this.el.src = this.url;
51
- },
52
- destroyed() {
53
- URL.revokeObjectURL(this.url);
54
- },
55
- },
56
- FocusWrap: {
57
- mounted() {
58
- this.focusStart = this.el.firstElementChild;
59
- this.focusEnd = this.el.lastElementChild;
60
- this.focusStart.addEventListener("focus", (e) => {
61
- if (!e.relatedTarget || !this.el.contains(e.relatedTarget)) {
62
- // Handle focus entering from outside (e.g. Tab when body is focused)
63
- // https://github.com/phoenixframework/phoenix_live_view/issues/3636
64
- const nextFocus = e.target.nextElementSibling;
65
- ARIA.attemptFocus(nextFocus) || ARIA.focusFirst(nextFocus);
66
- } else {
67
- ARIA.focusLast(this.el);
68
- }
69
- });
70
- this.focusEnd.addEventListener("focus", (e) => {
71
- if (!e.relatedTarget || !this.el.contains(e.relatedTarget)) {
72
- // Handle focus entering from outside (e.g. Shift+Tab when body is focused)
73
- // https://github.com/phoenixframework/phoenix_live_view/issues/3636
74
- const nextFocus = e.target.previousElementSibling;
75
- ARIA.attemptFocus(nextFocus) || ARIA.focusLast(nextFocus);
76
- } else {
77
- ARIA.focusFirst(this.el);
78
- }
79
- });
80
- // only try to change the focus if it is not already inside
81
- if (!this.el.contains(document.activeElement)) {
82
- this.el.addEventListener("phx:show-end", () => this.el.focus());
83
- if (window.getComputedStyle(this.el).display !== "none") {
84
- ARIA.focusFirst(this.el);
85
- }
86
- }
87
- },
88
- },
89
- };
90
-
91
- const findScrollContainer = (el) => {
14
+ const findScrollContainer = (el): HTMLElement | null => {
92
15
  // the scroll event won't be fired on the html/body element even if overflow is set
93
16
  // therefore we return null to instead listen for scroll events on document
94
17
  if (["HTML", "BODY"].indexOf(el.nodeName.toUpperCase()) >= 0) return null;
@@ -97,7 +20,7 @@ const findScrollContainer = (el) => {
97
20
  return findScrollContainer(el.parentElement);
98
21
  };
99
22
 
100
- const scrollTop = (scrollContainer) => {
23
+ const scrollTop = (scrollContainer: HTMLElement | null) => {
101
24
  if (scrollContainer) {
102
25
  return scrollContainer.scrollTop;
103
26
  } else {
@@ -105,7 +28,7 @@ const scrollTop = (scrollContainer) => {
105
28
  }
106
29
  };
107
30
 
108
- const bottom = (scrollContainer) => {
31
+ const bottom = (scrollContainer: HTMLElement | null) => {
109
32
  if (scrollContainer) {
110
33
  return scrollContainer.getBoundingClientRect().bottom;
111
34
  } else {
@@ -115,7 +38,7 @@ const bottom = (scrollContainer) => {
115
38
  }
116
39
  };
117
40
 
118
- const top = (scrollContainer) => {
41
+ const top = (scrollContainer: HTMLElement | null) => {
119
42
  if (scrollContainer) {
120
43
  return scrollContainer.getBoundingClientRect().top;
121
44
  } else {
@@ -125,7 +48,7 @@ const top = (scrollContainer) => {
125
48
  }
126
49
  };
127
50
 
128
- const isAtViewportTop = (el, scrollContainer) => {
51
+ const isAtViewportTop = (el: Element, scrollContainer: HTMLElement | null) => {
129
52
  const rect = el.getBoundingClientRect();
130
53
  return (
131
54
  Math.ceil(rect.top) >= top(scrollContainer) &&
@@ -133,7 +56,10 @@ const isAtViewportTop = (el, scrollContainer) => {
133
56
  );
134
57
  };
135
58
 
136
- const isAtViewportBottom = (el, scrollContainer) => {
59
+ const isAtViewportBottom = (
60
+ el: Element,
61
+ scrollContainer: HTMLElement | null,
62
+ ) => {
137
63
  const rect = el.getBoundingClientRect();
138
64
  return (
139
65
  Math.ceil(rect.bottom) >= top(scrollContainer) &&
@@ -141,7 +67,7 @@ const isAtViewportBottom = (el, scrollContainer) => {
141
67
  );
142
68
  };
143
69
 
144
- const isWithinViewport = (el, scrollContainer) => {
70
+ const isWithinViewport = (el: Element, scrollContainer: HTMLElement | null) => {
145
71
  const rect = el.getBoundingClientRect();
146
72
  return (
147
73
  Math.ceil(rect.top) >= top(scrollContainer) &&
@@ -149,13 +75,18 @@ const isWithinViewport = (el, scrollContainer) => {
149
75
  );
150
76
  };
151
77
 
152
- Hooks.InfiniteScroll = {
78
+ const InfiniteScroll: Hook<
79
+ {
80
+ scrollContainer: HTMLElement | null;
81
+ },
82
+ HTMLElement
83
+ > = {
153
84
  mounted() {
154
85
  this.scrollContainer = findScrollContainer(this.el);
155
86
  let scrollBefore = scrollTop(this.scrollContainer);
156
87
  let topOverran = false;
157
88
  const throttleInterval = 500;
158
- let pendingOp = null;
89
+ let pendingOp: (() => void) | null = null;
159
90
 
160
91
  const onTopOverrun = this.throttle(
161
92
  throttleInterval,
@@ -208,7 +139,7 @@ Hooks.InfiniteScroll = {
208
139
  },
209
140
  );
210
141
 
211
- this.onScroll = (_e) => {
142
+ this.onScroll = (_e: Event) => {
212
143
  const scrollNow = scrollTop(this.scrollContainer);
213
144
 
214
145
  if (pendingOp) {
@@ -239,12 +170,14 @@ Hooks.InfiniteScroll = {
239
170
  if (
240
171
  topEvent &&
241
172
  isScrollingUp &&
173
+ firstChild &&
242
174
  isAtViewportTop(firstChild, this.scrollContainer)
243
175
  ) {
244
176
  onFirstChildAtTop(topEvent, firstChild);
245
177
  } else if (
246
178
  bottomEvent &&
247
179
  isScrollingDown &&
180
+ lastChild &&
248
181
  isAtViewportBottom(lastChild, this.scrollContainer)
249
182
  ) {
250
183
  onLastChildAtBottom(bottomEvent, lastChild);
@@ -263,8 +196,8 @@ Hooks.InfiniteScroll = {
263
196
  // Check if the scroll container still exists
264
197
  // https://github.com/phoenixframework/phoenix_live_view/issues/4169.
265
198
  if (this.scrollContainer && !this.scrollContainer.isConnected) {
266
- this.destroyed();
267
- this.mounted();
199
+ this.destroyed!();
200
+ this.mounted!();
268
201
  }
269
202
  },
270
203
 
@@ -319,4 +252,88 @@ Hooks.InfiniteScroll = {
319
252
  return rect;
320
253
  },
321
254
  };
255
+
256
+ const LiveFileUpload: Hook<object, HTMLInputElement> = {
257
+ activeRefs() {
258
+ return this.el.getAttribute(PHX_ACTIVE_ENTRY_REFS);
259
+ },
260
+
261
+ preflightedRefs() {
262
+ return this.el.getAttribute(PHX_PREFLIGHTED_REFS);
263
+ },
264
+
265
+ mounted() {
266
+ this.js().ignoreAttributes(this.el, ["value"]);
267
+ this.preflightedWas = this.preflightedRefs();
268
+ },
269
+
270
+ updated() {
271
+ const newPreflights = this.preflightedRefs();
272
+ if (this.preflightedWas !== newPreflights) {
273
+ this.preflightedWas = newPreflights;
274
+ if (newPreflights === "") {
275
+ this.__view().cancelSubmit(this.el.form);
276
+ }
277
+ }
278
+
279
+ if (this.activeRefs() === "") {
280
+ this.el.value = "";
281
+ }
282
+ this.el.dispatchEvent(new CustomEvent(PHX_LIVE_FILE_UPDATED));
283
+ },
284
+ };
285
+
286
+ const LiveImgPreview: Hook<object, HTMLImageElement> = {
287
+ mounted() {
288
+ this.ref = this.el.getAttribute("data-phx-entry-ref");
289
+ this.inputEl = document.getElementById(
290
+ this.el.getAttribute(PHX_UPLOAD_REF)!,
291
+ );
292
+ this.url = LiveUploader.getEntryDataURL(this.inputEl, this.ref);
293
+ this.el.src = this.url;
294
+ },
295
+ destroyed() {
296
+ URL.revokeObjectURL(this.url);
297
+ },
298
+ };
299
+
300
+ const Hooks: Record<string, Hook<any, any>> = {
301
+ LiveFileUpload,
302
+ LiveImgPreview,
303
+ FocusWrap: {
304
+ mounted() {
305
+ this.focusStart = this.el.firstElementChild;
306
+ this.focusEnd = this.el.lastElementChild;
307
+ this.focusStart.addEventListener("focus", (e) => {
308
+ if (!e.relatedTarget || !this.el.contains(e.relatedTarget)) {
309
+ // Handle focus entering from outside (e.g. Tab when body is focused)
310
+ // https://github.com/phoenixframework/phoenix_live_view/issues/3636
311
+ const nextFocus = e.target.nextElementSibling;
312
+ ARIA.attemptFocus(nextFocus) || ARIA.focusFirst(nextFocus);
313
+ } else {
314
+ ARIA.focusLast(this.el);
315
+ }
316
+ });
317
+ this.focusEnd.addEventListener("focus", (e) => {
318
+ if (!e.relatedTarget || !this.el.contains(e.relatedTarget)) {
319
+ // Handle focus entering from outside (e.g. Shift+Tab when body is focused)
320
+ // https://github.com/phoenixframework/phoenix_live_view/issues/3636
321
+ const nextFocus = e.target.previousElementSibling;
322
+ ARIA.attemptFocus(nextFocus) || ARIA.focusLast(nextFocus);
323
+ } else {
324
+ ARIA.focusFirst(this.el);
325
+ }
326
+ });
327
+ // only try to change the focus if it is not already inside
328
+ if (!this.el.contains(document.activeElement)) {
329
+ this.el.addEventListener("phx:show-end", () => this.el.focus());
330
+ if (window.getComputedStyle(this.el).display !== "none") {
331
+ ARIA.focusFirst(this.el);
332
+ }
333
+ }
334
+ },
335
+ },
336
+ InfiniteScroll,
337
+ };
338
+
322
339
  export default Hooks;