phoenix_live_view 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/js/phoenix_live_view/constants.js +1 -0
- package/assets/js/phoenix_live_view/dom.js +4 -3
- package/assets/js/phoenix_live_view/dom_patch.js +12 -9
- package/assets/js/phoenix_live_view/hooks.js +3 -6
- package/assets/js/phoenix_live_view/js.js +44 -21
- package/assets/js/phoenix_live_view/live_socket.js +4 -0
- package/assets/js/phoenix_live_view/view.js +47 -17
- package/package.json +1 -1
- package/priv/static/phoenix_live_view.cjs.js +100 -60
- package/priv/static/phoenix_live_view.cjs.js.map +3 -3
- package/priv/static/phoenix_live_view.esm.js +100 -60
- package/priv/static/phoenix_live_view.esm.js.map +3 -3
- package/priv/static/phoenix_live_view.js +100 -72
- package/priv/static/phoenix_live_view.min.js +5 -5
|
@@ -67,6 +67,7 @@ export const PHX_RELOAD_STATUS = "__phoenix_reload_status__"
|
|
|
67
67
|
export const LOADER_TIMEOUT = 1
|
|
68
68
|
export const MAX_CHILD_JOIN_ATTEMPTS = 3
|
|
69
69
|
export const BEFORE_UNLOAD_LOADER_TIMEOUT = 200
|
|
70
|
+
export const DISCONNECTED_TIMEOUT = 500
|
|
70
71
|
export const BINDING_PREFIX = "phx-"
|
|
71
72
|
export const PUSH_TIMEOUT = 30000
|
|
72
73
|
export const LINK_HEADER = "x-requested-with"
|
|
@@ -234,10 +234,11 @@ let DOM = {
|
|
|
234
234
|
case null: return callback()
|
|
235
235
|
|
|
236
236
|
case "blur":
|
|
237
|
+
this.incCycle(el, "debounce-blur-cycle", () => {
|
|
238
|
+
if(asyncFilter()){ callback() }
|
|
239
|
+
})
|
|
237
240
|
if(this.once(el, "debounce-blur")){
|
|
238
|
-
el.addEventListener("blur", () =>
|
|
239
|
-
if(asyncFilter()){ callback() }
|
|
240
|
-
})
|
|
241
|
+
el.addEventListener("blur", () => this.triggerCycle(el, "debounce-blur-cycle"))
|
|
241
242
|
}
|
|
242
243
|
return
|
|
243
244
|
|
|
@@ -245,7 +245,11 @@ export default class DOMPatch {
|
|
|
245
245
|
return false
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
//
|
|
248
|
+
// if we are undoing a lock, copy potentially nested clones over
|
|
249
|
+
if(this.undoRef && DOM.private(toEl, PHX_REF_LOCK)){
|
|
250
|
+
DOM.putPrivate(fromEl, PHX_REF_LOCK, DOM.private(toEl, PHX_REF_LOCK))
|
|
251
|
+
}
|
|
252
|
+
// now copy regular DOM.private data
|
|
249
253
|
DOM.copyPrivates(toEl, fromEl)
|
|
250
254
|
|
|
251
255
|
// skip patching focused inputs unless focus is a select that has changed options
|
|
@@ -295,14 +299,8 @@ export default class DOMPatch {
|
|
|
295
299
|
// clear stream items from the dead render if they are not inserted again
|
|
296
300
|
if(isJoinPatch){
|
|
297
301
|
DOM.all(this.container, `[${phxUpdate}=${PHX_STREAM}]`, el => {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
this.liveSocket.owner(el, (view) => {
|
|
301
|
-
if(view === this.view){
|
|
302
|
-
Array.from(el.children).forEach(child => {
|
|
303
|
-
this.removeStreamChildElement(child)
|
|
304
|
-
})
|
|
305
|
-
}
|
|
302
|
+
Array.from(el.children).forEach(child => {
|
|
303
|
+
this.removeStreamChildElement(child)
|
|
306
304
|
})
|
|
307
305
|
})
|
|
308
306
|
}
|
|
@@ -359,6 +357,11 @@ export default class DOMPatch {
|
|
|
359
357
|
}
|
|
360
358
|
|
|
361
359
|
removeStreamChildElement(child){
|
|
360
|
+
// make sure to only remove elements owned by the current view
|
|
361
|
+
// see https://github.com/phoenixframework/phoenix_live_view/issues/3047
|
|
362
|
+
// and https://github.com/phoenixframework/phoenix_live_view/issues/3681
|
|
363
|
+
if(!this.view.ownsElement(child)){ return }
|
|
364
|
+
|
|
362
365
|
// we need to store the node if it is actually re-added in the same patch
|
|
363
366
|
// we do NOT want to execute phx-remove, we do NOT want to call onNodeDiscarded
|
|
364
367
|
if(this.streamInserts[child.id]){
|
|
@@ -67,12 +67,9 @@ let Hooks = {
|
|
|
67
67
|
ARIA.focusFirst(this.el)
|
|
68
68
|
}
|
|
69
69
|
})
|
|
70
|
-
|
|
71
|
-
if(
|
|
72
|
-
|
|
73
|
-
if(window.getComputedStyle(this.el).display !== "none"){
|
|
74
|
-
ARIA.focusFirst(this.el)
|
|
75
|
-
}
|
|
70
|
+
this.el.addEventListener("phx:show-end", () => this.el.focus())
|
|
71
|
+
if(window.getComputedStyle(this.el).display !== "none"){
|
|
72
|
+
ARIA.focusFirst(this.el)
|
|
76
73
|
}
|
|
77
74
|
}
|
|
78
75
|
}
|
|
@@ -46,12 +46,9 @@ let JS = {
|
|
|
46
46
|
// commands
|
|
47
47
|
|
|
48
48
|
exec_exec(e, eventType, phxEvent, view, sourceEl, el, {attr, to}){
|
|
49
|
-
let
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if(!encodedJS){ throw new Error(`expected ${attr} to contain JS command on "${to}"`) }
|
|
53
|
-
view.liveSocket.execJS(node, encodedJS, eventType)
|
|
54
|
-
})
|
|
49
|
+
let encodedJS = el.getAttribute(attr)
|
|
50
|
+
if(!encodedJS){ throw new Error(`expected ${attr} to contain JS command on "${to}"`) }
|
|
51
|
+
view.liveSocket.execJS(el, encodedJS, eventType)
|
|
55
52
|
},
|
|
56
53
|
|
|
57
54
|
exec_dispatch(e, eventType, phxEvent, view, sourceEl, el, {event, detail, bubbles}){
|
|
@@ -98,21 +95,35 @@ let JS = {
|
|
|
98
95
|
|
|
99
96
|
exec_focus(e, eventType, phxEvent, view, sourceEl, el){
|
|
100
97
|
ARIA.attemptFocus(el)
|
|
98
|
+
// in case the JS.focus command is in a JS.show/hide/toggle chain, for show we need
|
|
99
|
+
// to wait for JS.show to have updated the element's display property (see exec_toggle)
|
|
100
|
+
// but that run in nested animation frames, therefore we need to use them here as well
|
|
101
|
+
window.requestAnimationFrame(() => {
|
|
102
|
+
window.requestAnimationFrame(() => ARIA.attemptFocus(el))
|
|
103
|
+
})
|
|
101
104
|
},
|
|
102
105
|
|
|
103
106
|
exec_focus_first(e, eventType, phxEvent, view, sourceEl, el){
|
|
104
107
|
ARIA.focusFirstInteractive(el) || ARIA.focusFirst(el)
|
|
108
|
+
// if you wonder about the nested animation frames, see exec_focus
|
|
109
|
+
window.requestAnimationFrame(() => {
|
|
110
|
+
window.requestAnimationFrame(() => ARIA.focusFirstInteractive(el) || ARIA.focusFirst(el))
|
|
111
|
+
})
|
|
105
112
|
},
|
|
106
113
|
|
|
107
114
|
exec_push_focus(e, eventType, phxEvent, view, sourceEl, el){
|
|
108
|
-
|
|
115
|
+
focusStack.push(el || sourceEl)
|
|
109
116
|
},
|
|
110
117
|
|
|
111
118
|
exec_pop_focus(_e, _eventType, _phxEvent, _view, _sourceEl, _el){
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
const el = focusStack.pop()
|
|
120
|
+
if(el){
|
|
121
|
+
el.focus()
|
|
122
|
+
// if you wonder about the nested animation frames, see exec_focus
|
|
123
|
+
window.requestAnimationFrame(() => {
|
|
124
|
+
window.requestAnimationFrame(() => el.focus())
|
|
125
|
+
})
|
|
126
|
+
}
|
|
116
127
|
},
|
|
117
128
|
|
|
118
129
|
exec_add_class(e, eventType, phxEvent, view, sourceEl, el, {names, transition, time, blocking}){
|
|
@@ -198,11 +209,19 @@ let JS = {
|
|
|
198
209
|
if(eventType === "remove"){ return }
|
|
199
210
|
let onStart = () => {
|
|
200
211
|
this.addOrRemoveClasses(el, inStartClasses, outClasses.concat(outStartClasses).concat(outEndClasses))
|
|
201
|
-
|
|
202
|
-
DOM.putSticky(el, "toggle", currentEl => currentEl.style.display = stickyDisplay)
|
|
212
|
+
const stickyDisplay = display || this.defaultDisplay(el)
|
|
203
213
|
window.requestAnimationFrame(() => {
|
|
214
|
+
// first add the starting + active class, THEN make the element visible
|
|
215
|
+
// otherwise if we toggled the visibility earlier css animations
|
|
216
|
+
// would flicker, as the element becomes visible before the active animation
|
|
217
|
+
// class is set (see https://github.com/phoenixframework/phoenix_live_view/issues/3456)
|
|
204
218
|
this.addOrRemoveClasses(el, inClasses, [])
|
|
205
|
-
|
|
219
|
+
// addOrRemoveClasses uses a requestAnimationFrame itself, therefore we need to move the putSticky
|
|
220
|
+
// into the next requestAnimationFrame...
|
|
221
|
+
window.requestAnimationFrame(() => {
|
|
222
|
+
DOM.putSticky(el, "toggle", currentEl => currentEl.style.display = stickyDisplay)
|
|
223
|
+
this.addOrRemoveClasses(el, inEndClasses, inStartClasses)
|
|
224
|
+
})
|
|
206
225
|
})
|
|
207
226
|
}
|
|
208
227
|
let onEnd = () => {
|
|
@@ -219,14 +238,18 @@ let JS = {
|
|
|
219
238
|
}
|
|
220
239
|
} else {
|
|
221
240
|
if(this.isVisible(el)){
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
241
|
+
window.requestAnimationFrame(() => {
|
|
242
|
+
el.dispatchEvent(new Event("phx:hide-start"))
|
|
243
|
+
DOM.putSticky(el, "toggle", currentEl => currentEl.style.display = "none")
|
|
244
|
+
el.dispatchEvent(new Event("phx:hide-end"))
|
|
245
|
+
})
|
|
225
246
|
} else {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
247
|
+
window.requestAnimationFrame(() => {
|
|
248
|
+
el.dispatchEvent(new Event("phx:show-start"))
|
|
249
|
+
let stickyDisplay = display || this.defaultDisplay(el)
|
|
250
|
+
DOM.putSticky(el, "toggle", currentEl => currentEl.style.display = stickyDisplay)
|
|
251
|
+
el.dispatchEvent(new Event("phx:show-end"))
|
|
252
|
+
})
|
|
230
253
|
}
|
|
231
254
|
}
|
|
232
255
|
},
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
* @param {Object} [opts.uploaders] - The optional object for referencing LiveView uploader callbacks.
|
|
29
29
|
* @param {integer} [opts.loaderTimeout] - The optional delay in milliseconds to wait before apply
|
|
30
30
|
* loading states.
|
|
31
|
+
* @param {integer} [opts.disconnectedTimeout] - The delay in milliseconds to wait before
|
|
32
|
+
* executing phx-disconnected commands. Defaults to 500.
|
|
31
33
|
* @param {integer} [opts.maxReloads] - The maximum reloads before entering failsafe mode.
|
|
32
34
|
* @param {integer} [opts.reloadJitterMin] - The minimum time between normal reload attempts.
|
|
33
35
|
* @param {integer} [opts.reloadJitterMax] - The maximum time between normal reload attempts.
|
|
@@ -78,6 +80,7 @@ import {
|
|
|
78
80
|
DEFAULTS,
|
|
79
81
|
FAILSAFE_JITTER,
|
|
80
82
|
LOADER_TIMEOUT,
|
|
83
|
+
DISCONNECTED_TIMEOUT,
|
|
81
84
|
MAX_RELOADS,
|
|
82
85
|
PHX_DEBOUNCE,
|
|
83
86
|
PHX_DROP_TARGET,
|
|
@@ -152,6 +155,7 @@ export default class LiveSocket {
|
|
|
152
155
|
this.hooks = opts.hooks || {}
|
|
153
156
|
this.uploaders = opts.uploaders || {}
|
|
154
157
|
this.loaderTimeout = opts.loaderTimeout || LOADER_TIMEOUT
|
|
158
|
+
this.disconnectedTimeout = opts.disconnectedTimeout || DISCONNECTED_TIMEOUT
|
|
155
159
|
this.reloadWithJitterTimer = null
|
|
156
160
|
this.maxReloads = opts.maxReloads || MAX_RELOADS
|
|
157
161
|
this.reloadJitterMin = opts.reloadJitterMin || RELOAD_JITTER_MIN
|
|
@@ -67,8 +67,8 @@ export let prependFormDataKey = (key, prefix) => {
|
|
|
67
67
|
return baseKey
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
let serializeForm = (form,
|
|
71
|
-
const {submitter
|
|
70
|
+
let serializeForm = (form, opts, onlyNames = []) => {
|
|
71
|
+
const {submitter} = opts
|
|
72
72
|
|
|
73
73
|
// We must inject the submitter in the order that it exists in the DOM
|
|
74
74
|
// relative to other inputs. For example, for checkbox groups, the order must be maintained.
|
|
@@ -100,12 +100,26 @@ let serializeForm = (form, metadata, onlyNames = []) => {
|
|
|
100
100
|
|
|
101
101
|
const params = new URLSearchParams()
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
const {inputsUnused, onlyHiddenInputs} = Array.from(form.elements).reduce((acc, input) => {
|
|
104
|
+
const {inputsUnused, onlyHiddenInputs} = acc
|
|
105
|
+
const key = input.name
|
|
106
|
+
if(!key){ return acc }
|
|
107
|
+
|
|
108
|
+
if(inputsUnused[key] === undefined){ inputsUnused[key] = true }
|
|
109
|
+
if(onlyHiddenInputs[key] === undefined){ onlyHiddenInputs[key] = true }
|
|
110
|
+
|
|
111
|
+
const isUsed = DOM.private(input, PHX_HAS_FOCUSED) || DOM.private(input, PHX_HAS_SUBMITTED)
|
|
112
|
+
const isHidden = input.type === "hidden"
|
|
113
|
+
inputsUnused[key] = inputsUnused[key] && !isUsed
|
|
114
|
+
onlyHiddenInputs[key] = onlyHiddenInputs[key] && isHidden
|
|
115
|
+
|
|
116
|
+
return acc
|
|
117
|
+
}, {inputsUnused: {}, onlyHiddenInputs: {}})
|
|
118
|
+
|
|
104
119
|
for(let [key, val] of formData.entries()){
|
|
105
120
|
if(onlyNames.length === 0 || onlyNames.indexOf(key) >= 0){
|
|
106
|
-
let
|
|
107
|
-
let
|
|
108
|
-
let hidden = inputs.every(input => input.type === "hidden")
|
|
121
|
+
let isUnused = inputsUnused[key]
|
|
122
|
+
let hidden = onlyHiddenInputs[key]
|
|
109
123
|
if(isUnused && !(submitter && submitter.name == key) && !hidden){
|
|
110
124
|
params.append(prependFormDataKey(key, "_unused_"), "")
|
|
111
125
|
}
|
|
@@ -119,8 +133,6 @@ let serializeForm = (form, metadata, onlyNames = []) => {
|
|
|
119
133
|
submitter.parentElement.removeChild(injectedElement)
|
|
120
134
|
}
|
|
121
135
|
|
|
122
|
-
for(let metaKey in meta){ params.append(metaKey, meta[metaKey]) }
|
|
123
|
-
|
|
124
136
|
return params.toString()
|
|
125
137
|
}
|
|
126
138
|
|
|
@@ -143,6 +155,7 @@ export default class View {
|
|
|
143
155
|
this.lastAckRef = null
|
|
144
156
|
this.childJoins = 0
|
|
145
157
|
this.loaderTimer = null
|
|
158
|
+
this.disconnectedTimer = null
|
|
146
159
|
this.pendingDiffs = []
|
|
147
160
|
this.pendingForms = new Set()
|
|
148
161
|
this.redirect = false
|
|
@@ -254,6 +267,7 @@ export default class View {
|
|
|
254
267
|
|
|
255
268
|
hideLoader(){
|
|
256
269
|
clearTimeout(this.loaderTimer)
|
|
270
|
+
clearTimeout(this.disconnectedTimer)
|
|
257
271
|
this.setContainerClasses(PHX_CONNECTED_CLASS)
|
|
258
272
|
this.execAll(this.binding("connected"))
|
|
259
273
|
}
|
|
@@ -740,6 +754,12 @@ export default class View {
|
|
|
740
754
|
}
|
|
741
755
|
|
|
742
756
|
applyPendingUpdates(){
|
|
757
|
+
// prevent race conditions where we might still be pending a new
|
|
758
|
+
// navigation after applying the current one;
|
|
759
|
+
// if we call update and a pendingDiff is not applied, it would
|
|
760
|
+
// be silently dropped otherwise, as update would push it back to
|
|
761
|
+
// pendingDiffs, but we clear it immediately after
|
|
762
|
+
if(this.liveSocket.hasPendingLink() && this.root.isMain()){ return }
|
|
743
763
|
this.pendingDiffs.forEach(({diff, events}) => this.update(diff, events))
|
|
744
764
|
this.pendingDiffs = []
|
|
745
765
|
this.eachChild(child => child.applyPendingUpdates())
|
|
@@ -891,7 +911,13 @@ export default class View {
|
|
|
891
911
|
if(this.isMain()){ DOM.dispatchEvent(window, "phx:page-loading-start", {detail: {to: this.href, kind: "error"}}) }
|
|
892
912
|
this.showLoader()
|
|
893
913
|
this.setContainerClasses(...classes)
|
|
894
|
-
this.
|
|
914
|
+
this.delayedDisconnected()
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
delayedDisconnected(){
|
|
918
|
+
this.disconnectedTimer = setTimeout(() => {
|
|
919
|
+
this.execAll(this.binding("disconnected"))
|
|
920
|
+
}, this.liveSocket.disconnectedTimeout)
|
|
895
921
|
}
|
|
896
922
|
|
|
897
923
|
wrapPush(callerPush, receives){
|
|
@@ -1171,12 +1197,13 @@ export default class View {
|
|
|
1171
1197
|
], phxEvent, "change", opts)
|
|
1172
1198
|
}
|
|
1173
1199
|
let formData
|
|
1174
|
-
let meta
|
|
1175
|
-
|
|
1200
|
+
let meta = this.extractMeta(inputEl.form, {}, opts.value)
|
|
1201
|
+
let serializeOpts = {}
|
|
1202
|
+
if(inputEl instanceof HTMLButtonElement){ serializeOpts.submitter = inputEl }
|
|
1176
1203
|
if(inputEl.getAttribute(this.binding("change"))){
|
|
1177
|
-
formData = serializeForm(inputEl.form,
|
|
1204
|
+
formData = serializeForm(inputEl.form, serializeOpts, [inputEl.name])
|
|
1178
1205
|
} else {
|
|
1179
|
-
formData = serializeForm(inputEl.form,
|
|
1206
|
+
formData = serializeForm(inputEl.form, serializeOpts)
|
|
1180
1207
|
}
|
|
1181
1208
|
if(DOM.isUploadInput(inputEl) && inputEl.files && inputEl.files.length > 0){
|
|
1182
1209
|
LiveUploader.trackFiles(inputEl, Array.from(inputEl.files))
|
|
@@ -1187,6 +1214,7 @@ export default class View {
|
|
|
1187
1214
|
type: "form",
|
|
1188
1215
|
event: phxEvent,
|
|
1189
1216
|
value: formData,
|
|
1217
|
+
meta: {_target: opts._target, ...meta},
|
|
1190
1218
|
uploads: uploads,
|
|
1191
1219
|
cid: cid
|
|
1192
1220
|
}
|
|
@@ -1300,22 +1328,24 @@ export default class View {
|
|
|
1300
1328
|
if(LiveUploader.inputsAwaitingPreflight(formEl).length > 0){
|
|
1301
1329
|
return this.undoRefs(ref, phxEvent)
|
|
1302
1330
|
}
|
|
1303
|
-
let meta = this.extractMeta(formEl)
|
|
1304
|
-
let formData = serializeForm(formEl, {submitter
|
|
1331
|
+
let meta = this.extractMeta(formEl, {}, opts.value)
|
|
1332
|
+
let formData = serializeForm(formEl, {submitter})
|
|
1305
1333
|
this.pushWithReply(proxyRefGen, "event", {
|
|
1306
1334
|
type: "form",
|
|
1307
1335
|
event: phxEvent,
|
|
1308
1336
|
value: formData,
|
|
1337
|
+
meta: meta,
|
|
1309
1338
|
cid: cid
|
|
1310
1339
|
}).then(({resp}) => onReply(resp))
|
|
1311
1340
|
})
|
|
1312
1341
|
} else if(!(formEl.hasAttribute(PHX_REF_SRC) && formEl.classList.contains("phx-submit-loading"))){
|
|
1313
|
-
let meta = this.extractMeta(formEl)
|
|
1314
|
-
let formData = serializeForm(formEl, {submitter
|
|
1342
|
+
let meta = this.extractMeta(formEl, {}, opts.value)
|
|
1343
|
+
let formData = serializeForm(formEl, {submitter})
|
|
1315
1344
|
this.pushWithReply(refGenerator, "event", {
|
|
1316
1345
|
type: "form",
|
|
1317
1346
|
event: phxEvent,
|
|
1318
1347
|
value: formData,
|
|
1348
|
+
meta: meta,
|
|
1319
1349
|
cid: cid
|
|
1320
1350
|
}).then(({resp}) => onReply(resp))
|
|
1321
1351
|
}
|