phoenix_live_view 0.20.2 → 0.20.4
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 +3 -1
- package/assets/js/phoenix_live_view/dom.js +78 -12
- package/assets/js/phoenix_live_view/dom_patch.js +96 -58
- package/assets/js/phoenix_live_view/hooks.js +71 -17
- package/assets/js/phoenix_live_view/js.js +39 -8
- package/assets/js/phoenix_live_view/live_socket.js +22 -12
- package/assets/js/phoenix_live_view/live_uploader.js +8 -1
- package/assets/js/phoenix_live_view/rendered.js +52 -49
- package/assets/js/phoenix_live_view/upload_entry.js +10 -1
- package/assets/js/phoenix_live_view/view.js +52 -21
- package/assets/package.json +11 -9
- package/package.json +9 -2
- package/priv/static/phoenix_live_view.cjs.js +333 -167
- package/priv/static/phoenix_live_view.cjs.js.map +2 -2
- package/priv/static/phoenix_live_view.esm.js +333 -167
- package/priv/static/phoenix_live_view.esm.js.map +2 -2
- package/priv/static/phoenix_live_view.js +333 -167
- package/priv/static/phoenix_live_view.min.js +4 -5
|
@@ -2,6 +2,7 @@ import DOM from "./dom"
|
|
|
2
2
|
import ARIA from "./aria"
|
|
3
3
|
|
|
4
4
|
let focusStack = null
|
|
5
|
+
let default_transition_time = 200
|
|
5
6
|
|
|
6
7
|
let JS = {
|
|
7
8
|
exec(eventType, phxEvent, view, sourceEl, defaults){
|
|
@@ -9,8 +10,6 @@ let JS = {
|
|
|
9
10
|
let commands = phxEvent.charAt(0) === "[" ?
|
|
10
11
|
JSON.parse(phxEvent) : [[defaultKind, defaultArgs]]
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
13
|
commands.forEach(([kind, args]) => {
|
|
15
14
|
if(kind === defaultKind && defaultArgs.data){
|
|
16
15
|
args.data = Object.assign(args.data || {}, defaultArgs.data)
|
|
@@ -40,7 +39,7 @@ let JS = {
|
|
|
40
39
|
|
|
41
40
|
// commands
|
|
42
41
|
|
|
43
|
-
exec_exec(eventType, phxEvent, view, sourceEl, el,
|
|
42
|
+
exec_exec(eventType, phxEvent, view, sourceEl, el, {attr, to}){
|
|
44
43
|
let nodes = to ? DOM.all(document, to) : [sourceEl]
|
|
45
44
|
nodes.forEach(node => {
|
|
46
45
|
let encodedJS = node.getAttribute(attr)
|
|
@@ -56,13 +55,12 @@ let JS = {
|
|
|
56
55
|
},
|
|
57
56
|
|
|
58
57
|
exec_push(eventType, phxEvent, view, sourceEl, el, args){
|
|
59
|
-
if(!view.isConnected()){ return }
|
|
60
|
-
|
|
61
58
|
let {event, data, target, page_loading, loading, value, dispatcher, callback} = args
|
|
62
59
|
let pushOpts = {loading, value, target, page_loading: !!page_loading}
|
|
63
60
|
let targetSrc = eventType === "change" && dispatcher ? dispatcher : sourceEl
|
|
64
61
|
let phxTarget = target || targetSrc.getAttribute(view.binding("target")) || targetSrc
|
|
65
62
|
view.withinTargets(phxTarget, (targetView, targetCtx) => {
|
|
63
|
+
if(!targetView.isConnected()){ return }
|
|
66
64
|
if(eventType === "change"){
|
|
67
65
|
let {newCid, _target} = args
|
|
68
66
|
_target = _target || (DOM.isFormInput(sourceEl) ? sourceEl.name : undefined)
|
|
@@ -112,6 +110,28 @@ let JS = {
|
|
|
112
110
|
this.addOrRemoveClasses(el, [], names, transition, time, view)
|
|
113
111
|
},
|
|
114
112
|
|
|
113
|
+
exec_toggle_class(eventType, phxEvent, view, sourceEl, el, {to, names, transition, time}){
|
|
114
|
+
this.toggleClasses(el, names, transition, view)
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
exec_toggle_attr(eventType, phxEvent, view, sourceEl, el, {attr: [attr, val1, val2]}){
|
|
118
|
+
if(el.hasAttribute(attr)){
|
|
119
|
+
if(val2 !== undefined){
|
|
120
|
+
// toggle between val1 and val2
|
|
121
|
+
if(el.getAttribute(attr) === val1){
|
|
122
|
+
this.setOrRemoveAttrs(el, [[attr, val2]], [])
|
|
123
|
+
} else {
|
|
124
|
+
this.setOrRemoveAttrs(el, [[attr, val1]], [])
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
// remove attr
|
|
128
|
+
this.setOrRemoveAttrs(el, [], [attr])
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
this.setOrRemoveAttrs(el, [[attr, val1]], [])
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
|
|
115
135
|
exec_transition(eventType, phxEvent, view, sourceEl, el, {time, transition}){
|
|
116
136
|
this.addOrRemoveClasses(el, [], [], transition, time, view)
|
|
117
137
|
},
|
|
@@ -151,6 +171,7 @@ let JS = {
|
|
|
151
171
|
},
|
|
152
172
|
|
|
153
173
|
toggle(eventType, view, el, display, ins, outs, time){
|
|
174
|
+
time = time || default_transition_time
|
|
154
175
|
let [inClasses, inStartClasses, inEndClasses] = ins || [[], [], []]
|
|
155
176
|
let [outClasses, outStartClasses, outEndClasses] = outs || [[], [], []]
|
|
156
177
|
if(inClasses.length > 0 || outClasses.length > 0){
|
|
@@ -203,7 +224,17 @@ let JS = {
|
|
|
203
224
|
}
|
|
204
225
|
},
|
|
205
226
|
|
|
227
|
+
toggleClasses(el, classes, transition, time, view){
|
|
228
|
+
window.requestAnimationFrame(() => {
|
|
229
|
+
let [prevAdds, prevRemoves] = DOM.getSticky(el, "classes", [[], []])
|
|
230
|
+
let newAdds = classes.filter(name => prevAdds.indexOf(name) < 0 && !el.classList.contains(name))
|
|
231
|
+
let newRemoves = classes.filter(name => prevRemoves.indexOf(name) < 0 && el.classList.contains(name))
|
|
232
|
+
this.addOrRemoveClasses(el, newAdds, newRemoves, transition, time, view)
|
|
233
|
+
})
|
|
234
|
+
},
|
|
235
|
+
|
|
206
236
|
addOrRemoveClasses(el, adds, removes, transition, time, view){
|
|
237
|
+
time = time || default_transition_time
|
|
207
238
|
let [transitionRun, transitionStart, transitionEnd] = transition || [[], [], []]
|
|
208
239
|
if(transitionRun.length > 0){
|
|
209
240
|
let onStart = () => {
|
|
@@ -235,9 +266,9 @@ let JS = {
|
|
|
235
266
|
setOrRemoveAttrs(el, sets, removes){
|
|
236
267
|
let [prevSets, prevRemoves] = DOM.getSticky(el, "attrs", [[], []])
|
|
237
268
|
|
|
238
|
-
let alteredAttrs = sets.map(([attr, _val]) => attr).concat(removes)
|
|
239
|
-
let newSets = prevSets.filter(([attr, _val]) => !alteredAttrs.includes(attr)).concat(sets)
|
|
240
|
-
let newRemoves = prevRemoves.filter((attr) => !alteredAttrs.includes(attr)).concat(removes)
|
|
269
|
+
let alteredAttrs = sets.map(([attr, _val]) => attr).concat(removes)
|
|
270
|
+
let newSets = prevSets.filter(([attr, _val]) => !alteredAttrs.includes(attr)).concat(sets)
|
|
271
|
+
let newRemoves = prevRemoves.filter((attr) => !alteredAttrs.includes(attr)).concat(removes)
|
|
241
272
|
|
|
242
273
|
DOM.putSticky(el, "attrs", currentEl => {
|
|
243
274
|
newRemoves.forEach(attr => currentEl.removeAttribute(attr))
|
|
@@ -106,7 +106,6 @@ import {
|
|
|
106
106
|
closestPhxBinding,
|
|
107
107
|
closure,
|
|
108
108
|
debug,
|
|
109
|
-
isObject,
|
|
110
109
|
maybe
|
|
111
110
|
} from "./utils"
|
|
112
111
|
|
|
@@ -400,23 +399,28 @@ export default class LiveSocket {
|
|
|
400
399
|
|
|
401
400
|
this.main = this.newRootView(newMainEl, flash, liveReferer)
|
|
402
401
|
this.main.setRedirect(href)
|
|
403
|
-
this.transitionRemoves()
|
|
402
|
+
this.transitionRemoves(null, true)
|
|
404
403
|
this.main.join((joinCount, onDone) => {
|
|
405
404
|
if(joinCount === 1 && this.commitPendingLink(linkRef)){
|
|
406
405
|
this.requestDOMUpdate(() => {
|
|
407
406
|
DOM.findPhxSticky(document).forEach(el => newMainEl.appendChild(el))
|
|
408
407
|
this.outgoingMainEl.replaceWith(newMainEl)
|
|
409
408
|
this.outgoingMainEl = null
|
|
410
|
-
callback &&
|
|
409
|
+
callback && callback(linkRef)
|
|
411
410
|
onDone()
|
|
412
411
|
})
|
|
413
412
|
}
|
|
414
413
|
})
|
|
415
414
|
}
|
|
416
415
|
|
|
417
|
-
transitionRemoves(elements){
|
|
416
|
+
transitionRemoves(elements, skipSticky){
|
|
418
417
|
let removeAttr = this.binding("remove")
|
|
419
418
|
elements = elements || DOM.all(document, `[${removeAttr}]`)
|
|
419
|
+
|
|
420
|
+
if(skipSticky){
|
|
421
|
+
const stickies = DOM.findPhxSticky(document) || []
|
|
422
|
+
elements = elements.filter(el => !DOM.isChildOfAny(el, stickies))
|
|
423
|
+
}
|
|
420
424
|
elements.forEach(el => {
|
|
421
425
|
this.execJS(el, el.getAttribute(removeAttr), "remove")
|
|
422
426
|
})
|
|
@@ -631,6 +635,9 @@ export default class LiveSocket {
|
|
|
631
635
|
if(capture){
|
|
632
636
|
target = e.target.matches(`[${click}]`) ? e.target : e.target.querySelector(`[${click}]`)
|
|
633
637
|
} else {
|
|
638
|
+
// a synthetic click event (detail 0) will not have caused a mousedown event,
|
|
639
|
+
// therefore the clickStartedAtTarget is stale
|
|
640
|
+
if(e.detail === 0) this.clickStartedAtTarget = e.target
|
|
634
641
|
let clickStartedAtTarget = this.clickStartedAtTarget || e.target
|
|
635
642
|
target = closestPhxBinding(clickStartedAtTarget, click)
|
|
636
643
|
this.dispatchClickAway(e, clickStartedAtTarget)
|
|
@@ -659,7 +666,7 @@ export default class LiveSocket {
|
|
|
659
666
|
let phxClickAway = this.binding("click-away")
|
|
660
667
|
DOM.all(document, `[${phxClickAway}]`, el => {
|
|
661
668
|
if(!(el.isSameNode(clickStartedAt) || el.contains(clickStartedAt))){
|
|
662
|
-
this.withinOwners(
|
|
669
|
+
this.withinOwners(el, view => {
|
|
663
670
|
let phxEvent = el.getAttribute(phxClickAway)
|
|
664
671
|
if(JS.isVisible(el) && JS.isInViewport(el)){
|
|
665
672
|
JS.exec("click", phxEvent, view, el, ["push", {data: this.eventMeta("click", e, e.target)}])
|
|
@@ -727,7 +734,7 @@ export default class LiveSocket {
|
|
|
727
734
|
}, false)
|
|
728
735
|
}
|
|
729
736
|
|
|
730
|
-
maybeScroll(scroll)
|
|
737
|
+
maybeScroll(scroll){
|
|
731
738
|
if(typeof(scroll) === "number"){
|
|
732
739
|
requestAnimationFrame(() => {
|
|
733
740
|
window.scrollTo(0, scroll)
|
|
@@ -750,7 +757,7 @@ export default class LiveSocket {
|
|
|
750
757
|
}
|
|
751
758
|
|
|
752
759
|
pushHistoryPatch(href, linkState, targetEl){
|
|
753
|
-
if(!this.isConnected()){ return Browser.redirect(href) }
|
|
760
|
+
if(!this.isConnected() || !this.main.isMain()){ return Browser.redirect(href) }
|
|
754
761
|
|
|
755
762
|
this.withPageLoading({to: href, kind: "patch"}, done => {
|
|
756
763
|
this.main.pushLinkPatch(href, targetEl, linkRef => {
|
|
@@ -769,8 +776,9 @@ export default class LiveSocket {
|
|
|
769
776
|
}
|
|
770
777
|
|
|
771
778
|
historyRedirect(href, linkState, flash){
|
|
779
|
+
if(!this.isConnected() || !this.main.isMain()){ return Browser.redirect(href, flash) }
|
|
780
|
+
|
|
772
781
|
// convert to full href if only path prefix
|
|
773
|
-
if(!this.isConnected()){ return Browser.redirect(href, flash) }
|
|
774
782
|
if(/^\/$|^\/[^\/]+.*$/.test(href)){
|
|
775
783
|
let {protocol, host} = window.location
|
|
776
784
|
href = `${protocol}//${host}${href}`
|
|
@@ -873,10 +881,12 @@ export default class LiveSocket {
|
|
|
873
881
|
let form = e.target
|
|
874
882
|
DOM.resetForm(form, this.binding(PHX_FEEDBACK_FOR))
|
|
875
883
|
let input = Array.from(form.elements).find(el => el.type === "reset")
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
884
|
+
if(input){
|
|
885
|
+
// wait until next tick to get updated input value
|
|
886
|
+
window.requestAnimationFrame(() => {
|
|
887
|
+
input.dispatchEvent(new Event("input", {bubbles: true, cancelable: false}))
|
|
888
|
+
})
|
|
889
|
+
}
|
|
880
890
|
})
|
|
881
891
|
}
|
|
882
892
|
|
|
@@ -94,7 +94,11 @@ export default class LiveUploader {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
static filesAwaitingPreflight(input){
|
|
97
|
-
return this.activeFiles(input).filter(f => !UploadEntry.isPreflighted(input, f))
|
|
97
|
+
return this.activeFiles(input).filter(f => !UploadEntry.isPreflighted(input, f) && !UploadEntry.isPreflightInProgress(f))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
static markPreflightInProgress(entries){
|
|
101
|
+
entries.forEach(entry => UploadEntry.markPreflightInProgress(entry.file))
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
constructor(inputEl, view, onComplete){
|
|
@@ -104,6 +108,9 @@ export default class LiveUploader {
|
|
|
104
108
|
Array.from(LiveUploader.filesAwaitingPreflight(inputEl) || [])
|
|
105
109
|
.map(file => new UploadEntry(inputEl, file, view))
|
|
106
110
|
|
|
111
|
+
// prevent sending duplicate preflight requests
|
|
112
|
+
LiveUploader.markPreflightInProgress(this._entries)
|
|
113
|
+
|
|
107
114
|
this.numEntriesInProgress = this._entries.length
|
|
108
115
|
}
|
|
109
116
|
|
|
@@ -37,60 +37,41 @@ const VOID_TAGS = new Set([
|
|
|
37
37
|
"track",
|
|
38
38
|
"wbr"
|
|
39
39
|
])
|
|
40
|
-
const endingTagNameChars = new Set([">", "/", " ", "\n", "\t", "\r"])
|
|
41
40
|
const quoteChars = new Set(["'", '"'])
|
|
42
41
|
|
|
43
42
|
export let modifyRoot = (html, attrs, clearInnerHTML) => {
|
|
44
43
|
let i = 0
|
|
45
44
|
let insideComment = false
|
|
46
45
|
let beforeTag, afterTag, tag, tagNameEndsAt, id, newHTML
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
|
|
47
|
+
let lookahead = html.match(/^(\s*(?:<!--.*?-->\s*)*)<([^\s\/>]+)/)
|
|
48
|
+
if(lookahead === null) { throw new Error(`malformed html ${html}`) }
|
|
49
|
+
|
|
50
|
+
i = lookahead[0].length
|
|
51
|
+
beforeTag = lookahead[1]
|
|
52
|
+
tag = lookahead[2]
|
|
53
|
+
tagNameEndsAt = i
|
|
54
|
+
|
|
55
|
+
// Scan the opening tag for id, if there is any
|
|
56
|
+
for(i; i < html.length; i++){
|
|
57
|
+
if(html.charAt(i) === ">" ){ break }
|
|
58
|
+
if(html.charAt(i) === "="){
|
|
59
|
+
let isId = html.slice(i - 3, i) === " id"
|
|
60
|
+
i++;
|
|
61
|
+
let char = html.charAt(i)
|
|
62
|
+
if (quoteChars.has(char)) {
|
|
63
|
+
let attrStartsAt = i
|
|
54
64
|
i++
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
let iAtOpen = i
|
|
62
|
-
i++
|
|
63
|
-
for(i; i < html.length; i++){
|
|
64
|
-
if(endingTagNameChars.has(html.charAt(i))){ break }
|
|
65
|
-
}
|
|
66
|
-
tagNameEndsAt = i
|
|
67
|
-
tag = html.slice(iAtOpen + 1, tagNameEndsAt)
|
|
68
|
-
// Scan the opening tag for id, if there is any
|
|
69
|
-
for(i; i < html.length; i++){
|
|
70
|
-
if(html.charAt(i) === ">" ){ break }
|
|
71
|
-
if(html.charAt(i) === "="){
|
|
72
|
-
let isId = html.slice(i - 3, i) === " id"
|
|
73
|
-
i++;
|
|
74
|
-
let char = html.charAt(i)
|
|
75
|
-
if (quoteChars.has(char)) {
|
|
76
|
-
let attrStartsAt = i
|
|
77
|
-
i++
|
|
78
|
-
for(i; i < html.length; i++){
|
|
79
|
-
if(html.charAt(i) === char){ break }
|
|
80
|
-
}
|
|
81
|
-
if (isId) {
|
|
82
|
-
id = html.slice(attrStartsAt + 1, i)
|
|
83
|
-
break
|
|
84
|
-
}
|
|
85
|
-
}
|
|
65
|
+
for(i; i < html.length; i++){
|
|
66
|
+
if(html.charAt(i) === char){ break }
|
|
67
|
+
}
|
|
68
|
+
if (isId) {
|
|
69
|
+
id = html.slice(attrStartsAt + 1, i)
|
|
70
|
+
break
|
|
86
71
|
}
|
|
87
72
|
}
|
|
88
|
-
break
|
|
89
|
-
} else {
|
|
90
|
-
i++
|
|
91
73
|
}
|
|
92
74
|
}
|
|
93
|
-
if(!tag){ throw new Error(`malformed html ${html}`) }
|
|
94
75
|
|
|
95
76
|
let closeAt = html.length - 1
|
|
96
77
|
insideComment = false
|
|
@@ -174,6 +155,14 @@ export default class Rendered {
|
|
|
174
155
|
|
|
175
156
|
getComponent(diff, cid){ return diff[COMPONENTS][cid] }
|
|
176
157
|
|
|
158
|
+
resetRender(cid){
|
|
159
|
+
// we are racing a component destroy, it could not exist, so
|
|
160
|
+
// make sure that we don't try to set reset on undefined
|
|
161
|
+
if(this.rendered[COMPONENTS][cid]){
|
|
162
|
+
this.rendered[COMPONENTS][cid].reset = true
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
177
166
|
mergeDiff(diff){
|
|
178
167
|
let newc = diff[COMPONENTS]
|
|
179
168
|
let cache = {}
|
|
@@ -305,7 +294,7 @@ export default class Rendered {
|
|
|
305
294
|
//
|
|
306
295
|
// changeTracking controls if we can apply the PHX_SKIP optimization.
|
|
307
296
|
// It is disabled for comprehensions since we must re-render the entire collection
|
|
308
|
-
// and no
|
|
297
|
+
// and no individual element is tracked inside the comprehension.
|
|
309
298
|
toOutputBuffer(rendered, templates, output, changeTracking, rootAttrs = {}){
|
|
310
299
|
if(rendered[DYNAMICS]){ return this.comprehensionToBuffer(rendered, templates, output) }
|
|
311
300
|
let {[STATIC]: statics} = rendered
|
|
@@ -314,6 +303,8 @@ export default class Rendered {
|
|
|
314
303
|
let prevBuffer = output.buffer
|
|
315
304
|
if(isRoot){ output.buffer = "" }
|
|
316
305
|
|
|
306
|
+
// this condition is called when first rendering an optimizable function component.
|
|
307
|
+
// LC have their magicId previously set
|
|
317
308
|
if(changeTracking && isRoot && !rendered.magicId){
|
|
318
309
|
rendered.newRender = true
|
|
319
310
|
rendered.magicId = this.nextMagicID()
|
|
@@ -332,13 +323,17 @@ export default class Rendered {
|
|
|
332
323
|
if(isRoot){
|
|
333
324
|
let skip = false
|
|
334
325
|
let attrs
|
|
335
|
-
|
|
336
|
-
|
|
326
|
+
// when a LC is added on the page, we need to re-render the entire LC tree,
|
|
327
|
+
// therefore changeTracking is false; however, we need to keep all the magicIds
|
|
328
|
+
// from any function component so the next time the LC is updated, we can apply
|
|
329
|
+
// the skip optimization
|
|
330
|
+
if(changeTracking || rendered.magicId){
|
|
331
|
+
skip = changeTracking && !rendered.newRender
|
|
337
332
|
attrs = {[PHX_MAGIC_ID]: rendered.magicId, ...rootAttrs}
|
|
338
333
|
} else {
|
|
339
334
|
attrs = rootAttrs
|
|
340
335
|
}
|
|
341
|
-
if(skip){ attrs[PHX_SKIP] = true}
|
|
336
|
+
if(skip){ attrs[PHX_SKIP] = true }
|
|
342
337
|
let [newRoot, commentBefore, commentAfter] = modifyRoot(output.buffer, attrs, skip)
|
|
343
338
|
rendered.newRender = false
|
|
344
339
|
output.buffer = prevBuffer + commentBefore + newRoot + commentAfter
|
|
@@ -395,15 +390,23 @@ export default class Rendered {
|
|
|
395
390
|
//
|
|
396
391
|
// 2. The root PHX_SKIP optimization generalizes to all HEEx function components, and
|
|
397
392
|
// works in the same PHX_SKIP attribute fashion as 1, but the newRender tracking is done
|
|
398
|
-
// at the general diff merge level. If we merge a diff with new dynamics, we
|
|
393
|
+
// at the general diff merge level. If we merge a diff with new dynamics, we necessarily have
|
|
399
394
|
// experienced a change which must be a newRender, and thus we can't skip the render.
|
|
400
395
|
//
|
|
401
396
|
// Both optimization flows apply here. newRender is set based on the onlyCids optimization, and
|
|
402
397
|
// we track a deterministic magicId based on the cid.
|
|
398
|
+
//
|
|
399
|
+
// By default changeTracking is enabled, but we special case the flow where the client is pruning
|
|
400
|
+
// cids and the server adds the component back. In such cases, we explicitly disable changeTracking
|
|
401
|
+
// with resetRender for this cid, then re-enable it after the recursive call to skip the optimization
|
|
402
|
+
// for the entire component tree.
|
|
403
403
|
component.newRender = !skip
|
|
404
404
|
component.magicId = `${this.parentViewId()}-c-${cid}`
|
|
405
|
-
|
|
405
|
+
// enable change tracking as long as the component hasn't been reset
|
|
406
|
+
let changeTracking = !component.reset
|
|
406
407
|
let [html, streams] = this.recursiveToString(component, components, onlyCids, changeTracking, attrs)
|
|
408
|
+
// disable reset after we've rendered
|
|
409
|
+
delete component.reset
|
|
407
410
|
|
|
408
411
|
return [html, streams]
|
|
409
412
|
}
|
|
@@ -15,9 +15,10 @@ import DOM from "./dom"
|
|
|
15
15
|
export default class UploadEntry {
|
|
16
16
|
static isActive(fileEl, file){
|
|
17
17
|
let isNew = file._phxRef === undefined
|
|
18
|
+
let isPreflightInProgress = UploadEntry.isPreflightInProgress(file)
|
|
18
19
|
let activeRefs = fileEl.getAttribute(PHX_ACTIVE_ENTRY_REFS).split(",")
|
|
19
20
|
let isActive = activeRefs.indexOf(LiveUploader.genFileRef(file)) >= 0
|
|
20
|
-
return file.size > 0 && (isNew || isActive)
|
|
21
|
+
return file.size > 0 && (isNew || isActive || !isPreflightInProgress)
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
static isPreflighted(fileEl, file){
|
|
@@ -26,6 +27,14 @@ export default class UploadEntry {
|
|
|
26
27
|
return isPreflighted && this.isActive(fileEl, file)
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
static isPreflightInProgress(file){
|
|
31
|
+
return file._preflightInProgress === true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static markPreflightInProgress(file){
|
|
35
|
+
file._preflightInProgress = true
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
constructor(fileEl, file, view){
|
|
30
39
|
this.ref = LiveUploader.genFileRef(file)
|
|
31
40
|
this.fileEl = fileEl
|
|
@@ -56,18 +56,28 @@ import ViewHook from "./view_hook"
|
|
|
56
56
|
import JS from "./js"
|
|
57
57
|
|
|
58
58
|
let serializeForm = (form, metadata, onlyNames = []) => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
let
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
const {submitter, ...meta} = metadata
|
|
60
|
+
|
|
61
|
+
// We must inject the submitter in the order that it exists in the DOM
|
|
62
|
+
// releative to other inputs. For example, for checkbox groups, the order must be maintained.
|
|
63
|
+
let injectedElement
|
|
64
|
+
if(submitter && submitter.name){
|
|
65
|
+
const input = document.createElement("input")
|
|
66
|
+
input.type = "hidden"
|
|
67
|
+
// set the form attribute if the submitter has one;
|
|
68
|
+
// this can happen if the element is outside the actual form element
|
|
69
|
+
const formId = submitter.getAttribute("form")
|
|
70
|
+
if(formId){
|
|
71
|
+
input.setAttribute("form", form)
|
|
72
|
+
}
|
|
73
|
+
input.name = submitter.name
|
|
74
|
+
input.value = submitter.value
|
|
75
|
+
submitter.parentElement.insertBefore(input, submitter)
|
|
76
|
+
injectedElement = input
|
|
68
77
|
}
|
|
69
78
|
|
|
70
|
-
|
|
79
|
+
const formData = new FormData(form)
|
|
80
|
+
const toRemove = []
|
|
71
81
|
|
|
72
82
|
formData.forEach((val, key, _index) => {
|
|
73
83
|
if(val instanceof File){ toRemove.push(key) }
|
|
@@ -76,12 +86,20 @@ let serializeForm = (form, metadata, onlyNames = []) => {
|
|
|
76
86
|
// Cleanup after building fileData
|
|
77
87
|
toRemove.forEach(key => formData.delete(key))
|
|
78
88
|
|
|
79
|
-
|
|
89
|
+
const params = new URLSearchParams()
|
|
90
|
+
|
|
80
91
|
for(let [key, val] of formData.entries()){
|
|
81
92
|
if(onlyNames.length === 0 || onlyNames.indexOf(key) >= 0){
|
|
82
93
|
params.append(key, val)
|
|
83
94
|
}
|
|
84
95
|
}
|
|
96
|
+
|
|
97
|
+
// remove the injected element again
|
|
98
|
+
// (it would be removed by the next dom patch anyway, but this is cleaner)
|
|
99
|
+
if(submitter && injectedElement){
|
|
100
|
+
submitter.parentElement.removeChild(injectedElement)
|
|
101
|
+
}
|
|
102
|
+
|
|
85
103
|
for(let metaKey in meta){ params.append(metaKey, meta[metaKey]) }
|
|
86
104
|
|
|
87
105
|
return params.toString()
|
|
@@ -303,6 +321,9 @@ export default class View {
|
|
|
303
321
|
let fromEl = toEl.id && this.el.querySelector(`[id="${toEl.id}"]`)
|
|
304
322
|
let phxStatic = fromEl && fromEl.getAttribute(PHX_STATIC)
|
|
305
323
|
if(phxStatic){ toEl.setAttribute(PHX_STATIC, phxStatic) }
|
|
324
|
+
// set PHX_ROOT_ID to prevent events from being dispatched to the root view
|
|
325
|
+
// while the child join is still pending
|
|
326
|
+
if(fromEl){ fromEl.setAttribute(PHX_ROOT_ID, this.root.id) }
|
|
306
327
|
return this.joinChild(toEl)
|
|
307
328
|
})
|
|
308
329
|
|
|
@@ -389,6 +410,9 @@ export default class View {
|
|
|
389
410
|
|
|
390
411
|
patch.after("added", el => {
|
|
391
412
|
this.liveSocket.triggerDOM("onNodeAdded", [el])
|
|
413
|
+
let phxViewportTop = this.binding(PHX_VIEWPORT_TOP)
|
|
414
|
+
let phxViewportBottom = this.binding(PHX_VIEWPORT_BOTTOM)
|
|
415
|
+
DOM.maybeAddPrivateHooks(el, phxViewportTop, phxViewportBottom)
|
|
392
416
|
this.maybeAddNewHook(el)
|
|
393
417
|
if(el.getAttribute){ this.maybeMounted(el) }
|
|
394
418
|
})
|
|
@@ -750,12 +774,13 @@ export default class View {
|
|
|
750
774
|
|
|
751
775
|
DOM.all(document, `[${PHX_REF_SRC}="${this.id}"][${PHX_REF}="${ref}"]`, el => {
|
|
752
776
|
let disabledVal = el.getAttribute(PHX_DISABLED)
|
|
777
|
+
let readOnlyVal = el.getAttribute(PHX_READONLY)
|
|
753
778
|
// remove refs
|
|
754
779
|
el.removeAttribute(PHX_REF)
|
|
755
780
|
el.removeAttribute(PHX_REF_SRC)
|
|
756
781
|
// restore inputs
|
|
757
|
-
if(
|
|
758
|
-
el.readOnly = false
|
|
782
|
+
if(readOnlyVal !== null){
|
|
783
|
+
el.readOnly = readOnlyVal === "true" ? true : false
|
|
759
784
|
el.removeAttribute(PHX_READONLY)
|
|
760
785
|
}
|
|
761
786
|
if(disabledVal !== null){
|
|
@@ -795,6 +820,8 @@ export default class View {
|
|
|
795
820
|
el.setAttribute(PHX_DISABLE_WITH_RESTORE, el.innerText)
|
|
796
821
|
}
|
|
797
822
|
if(disableText !== ""){ el.innerText = disableText }
|
|
823
|
+
// PHX_DISABLED could have already been set in disableForm
|
|
824
|
+
el.setAttribute(PHX_DISABLED, el.getAttribute(PHX_DISABLED) || el.disabled)
|
|
798
825
|
el.setAttribute("disabled", "")
|
|
799
826
|
}
|
|
800
827
|
})
|
|
@@ -895,6 +922,7 @@ export default class View {
|
|
|
895
922
|
let refGenerator = () => this.putRef([inputEl, inputEl.form], "change", opts)
|
|
896
923
|
let formData
|
|
897
924
|
let meta = this.extractMeta(inputEl.form)
|
|
925
|
+
if(inputEl instanceof HTMLButtonElement){ meta.submitter = inputEl }
|
|
898
926
|
if(inputEl.getAttribute(this.binding("change"))){
|
|
899
927
|
formData = serializeForm(inputEl.form, {_target: opts._target, ...meta}, [inputEl.name])
|
|
900
928
|
} else {
|
|
@@ -920,6 +948,7 @@ export default class View {
|
|
|
920
948
|
this.uploadFiles(inputEl.form, targetCtx, ref, cid, (_uploads) => {
|
|
921
949
|
callback && callback(resp)
|
|
922
950
|
this.triggerAwaitingSubmit(inputEl.form)
|
|
951
|
+
this.undoRefs(ref)
|
|
923
952
|
})
|
|
924
953
|
}
|
|
925
954
|
} else {
|
|
@@ -1064,7 +1093,7 @@ export default class View {
|
|
|
1064
1093
|
}
|
|
1065
1094
|
|
|
1066
1095
|
dispatchUploads(targetCtx, name, filesOrBlobs){
|
|
1067
|
-
let targetElement = this.targetCtxElement(targetCtx) || this.el
|
|
1096
|
+
let targetElement = this.targetCtxElement(targetCtx) || this.el
|
|
1068
1097
|
let inputs = DOM.findUploadInputs(targetElement).filter(el => el.name === name)
|
|
1069
1098
|
if(inputs.length === 0){ logError(`no live file inputs found matching the name "${name}"`) }
|
|
1070
1099
|
else if(inputs.length > 1){ logError(`duplicate live file inputs found matching the name "${name}"`) }
|
|
@@ -1139,7 +1168,7 @@ export default class View {
|
|
|
1139
1168
|
.map(form => {
|
|
1140
1169
|
// attribute given via JS module needs to be escaped as it contains the symbols []",
|
|
1141
1170
|
// which result in an invalid css selector otherwise.
|
|
1142
|
-
const phxChangeValue = form.getAttribute(phxChange)
|
|
1171
|
+
const phxChangeValue = CSS.escape(form.getAttribute(phxChange))
|
|
1143
1172
|
let newForm = template.content.querySelector(`form[id="${form.id}"][${phxChange}="${phxChangeValue}"]`)
|
|
1144
1173
|
if(newForm){
|
|
1145
1174
|
return [form, newForm, this.targetComponentID(newForm)]
|
|
@@ -1152,17 +1181,18 @@ export default class View {
|
|
|
1152
1181
|
}
|
|
1153
1182
|
|
|
1154
1183
|
maybePushComponentsDestroyed(destroyedCIDs){
|
|
1155
|
-
let willDestroyCIDs = destroyedCIDs.filter(cid => {
|
|
1184
|
+
let willDestroyCIDs = destroyedCIDs.concat(this.pruningCIDs).filter(cid => {
|
|
1156
1185
|
return DOM.findComponentNodeList(this.el, cid).length === 0
|
|
1157
1186
|
})
|
|
1187
|
+
// make sure this is a copy and not a reference
|
|
1188
|
+
this.pruningCIDs = willDestroyCIDs.concat([])
|
|
1189
|
+
|
|
1158
1190
|
if(willDestroyCIDs.length > 0){
|
|
1159
|
-
|
|
1191
|
+
// we must reset the render change tracking for cids that
|
|
1192
|
+
// could be added back from the server so we don't skip them
|
|
1193
|
+
willDestroyCIDs.forEach(cid => this.rendered.resetRender(cid))
|
|
1160
1194
|
|
|
1161
1195
|
this.pushWithReply(null, "cids_will_destroy", {cids: willDestroyCIDs}, () => {
|
|
1162
|
-
// The cids are either back on the page or they will be fully removed,
|
|
1163
|
-
// so we can remove them from the pruningCIDs.
|
|
1164
|
-
this.pruningCIDs = this.pruningCIDs.filter(cid => willDestroyCIDs.indexOf(cid) !== -1)
|
|
1165
|
-
|
|
1166
1196
|
// See if any of the cids we wanted to destroy were added back,
|
|
1167
1197
|
// if they were added back, we don't actually destroy them.
|
|
1168
1198
|
let completelyDestroyCIDs = willDestroyCIDs.filter(cid => {
|
|
@@ -1171,6 +1201,7 @@ export default class View {
|
|
|
1171
1201
|
|
|
1172
1202
|
if(completelyDestroyCIDs.length > 0){
|
|
1173
1203
|
this.pushWithReply(null, "cids_destroyed", {cids: completelyDestroyCIDs}, (resp) => {
|
|
1204
|
+
this.pruningCIDs = this.pruningCIDs.filter(cid => resp.cids.indexOf(cid) === -1)
|
|
1174
1205
|
this.rendered.pruneCIDs(resp.cids)
|
|
1175
1206
|
})
|
|
1176
1207
|
}
|
package/assets/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "phoenix_live_view",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.4",
|
|
4
4
|
"description": "The Phoenix LiveView JavaScript client.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {},
|
|
@@ -10,15 +10,17 @@
|
|
|
10
10
|
"test.watch": "jest --watch"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"morphdom": "2.7.
|
|
13
|
+
"morphdom": "2.7.2"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@babel/cli": "7.
|
|
17
|
-
"@babel/core": "7.
|
|
18
|
-
"@babel/preset-env": "7.
|
|
19
|
-
"
|
|
20
|
-
"eslint
|
|
21
|
-
"jest": "
|
|
22
|
-
"
|
|
16
|
+
"@babel/cli": "7.23.4",
|
|
17
|
+
"@babel/core": "7.23.7",
|
|
18
|
+
"@babel/preset-env": "7.23.8",
|
|
19
|
+
"css.escape": "^1.5.1",
|
|
20
|
+
"eslint": "8.56.0",
|
|
21
|
+
"eslint-plugin-jest": "27.6.3",
|
|
22
|
+
"jest": "^29.7.0",
|
|
23
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
24
|
+
"phoenix": "1.7.10"
|
|
23
25
|
}
|
|
24
26
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "phoenix_live_view",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.4",
|
|
4
4
|
"description": "The Phoenix LiveView JavaScript client.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"module": "./priv/static/phoenix_live_view.esm.js",
|
|
@@ -22,5 +22,12 @@
|
|
|
22
22
|
"package.json",
|
|
23
23
|
"priv/static/*",
|
|
24
24
|
"assets/js/phoenix_live_view/*"
|
|
25
|
-
]
|
|
25
|
+
],
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@playwright/test": "^1.41.0"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"e2e:server": "MIX_ENV=e2e mix run test/e2e/test_helper.exs",
|
|
31
|
+
"e2e:test": "mix assets.build && cd test/e2e && npx playwright test"
|
|
32
|
+
}
|
|
26
33
|
}
|