phoenix_live_view 0.20.14 → 0.20.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -1
- package/assets/js/phoenix_live_view/dom.js +9 -16
- package/assets/js/phoenix_live_view/dom_patch.js +18 -6
- package/assets/js/phoenix_live_view/hooks.js +3 -1
- package/assets/js/phoenix_live_view/js.js +5 -5
- package/assets/js/phoenix_live_view/live_socket.js +27 -18
- package/assets/js/phoenix_live_view/view.js +164 -77
- package/assets/package.json +2 -1
- package/package.json +10 -4
- package/priv/static/phoenix_live_view.cjs.js +161 -111
- package/priv/static/phoenix_live_view.cjs.js.map +2 -2
- package/priv/static/phoenix_live_view.esm.js +161 -111
- package/priv/static/phoenix_live_view.esm.js.map +2 -2
- package/priv/static/phoenix_live_view.js +164 -111
- package/priv/static/phoenix_live_view.min.js +6 -5
package/README.md
CHANGED
|
@@ -166,7 +166,12 @@ $ mix deps.get
|
|
|
166
166
|
$ mix test
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
-
Running
|
|
169
|
+
Running all JavaScript tests:
|
|
170
|
+
```bash
|
|
171
|
+
$ npm run test
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Running the JavaScript unit tests:
|
|
170
175
|
|
|
171
176
|
```bash
|
|
172
177
|
$ cd assets
|
|
@@ -176,4 +181,23 @@ $ npm run test
|
|
|
176
181
|
$ npm run test.watch
|
|
177
182
|
```
|
|
178
183
|
|
|
184
|
+
or simply:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
$ npm run js:test
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Running the JavaScript end-to-end tests:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
$ npm run e2e:test
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Checking test coverage:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
$ npm run cover
|
|
200
|
+
$ npm run cover:report
|
|
201
|
+
```
|
|
202
|
+
|
|
179
203
|
JS contributions are very welcome, but please do not include an updated `priv/static/phoenix_live_view.js` in pull requests. The maintainers will update it as part of the release process.
|
|
@@ -70,7 +70,8 @@ let DOM = {
|
|
|
70
70
|
let wantsNewTab = e.ctrlKey || e.shiftKey || e.metaKey || (e.button && e.button === 1)
|
|
71
71
|
let isDownload = (e.target instanceof HTMLAnchorElement && e.target.hasAttribute("download"))
|
|
72
72
|
let isTargetBlank = e.target.hasAttribute("target") && e.target.getAttribute("target").toLowerCase() === "_blank"
|
|
73
|
-
|
|
73
|
+
let isTargetNamedTab = e.target.hasAttribute("target") && !e.target.getAttribute("target").startsWith("_")
|
|
74
|
+
return wantsNewTab || isTargetBlank || isDownload || isTargetNamedTab
|
|
74
75
|
},
|
|
75
76
|
|
|
76
77
|
isUnloadableFormSubmit(e){
|
|
@@ -222,7 +223,9 @@ let DOM = {
|
|
|
222
223
|
|
|
223
224
|
case "blur":
|
|
224
225
|
if(this.once(el, "debounce-blur")){
|
|
225
|
-
el.addEventListener("blur", () =>
|
|
226
|
+
el.addEventListener("blur", () => {
|
|
227
|
+
if(asyncFilter()){ callback() }
|
|
228
|
+
})
|
|
226
229
|
}
|
|
227
230
|
return
|
|
228
231
|
|
|
@@ -479,7 +482,6 @@ let DOM = {
|
|
|
479
482
|
if(!DOM.isTextualInput(focused)){ return }
|
|
480
483
|
|
|
481
484
|
let wasFocused = focused.matches(":focus")
|
|
482
|
-
if(focused.readOnly){ focused.blur() }
|
|
483
485
|
if(!wasFocused){ focused.focus() }
|
|
484
486
|
if(this.hasSelectionRange(focused)){
|
|
485
487
|
focused.setSelectionRange(selectionStart, selectionEnd)
|
|
@@ -505,18 +507,9 @@ let DOM = {
|
|
|
505
507
|
if(ref === null){ return true }
|
|
506
508
|
let refSrc = fromEl.getAttribute(PHX_REF_SRC)
|
|
507
509
|
|
|
508
|
-
if(DOM.
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
return false
|
|
512
|
-
} else {
|
|
513
|
-
PHX_EVENT_CLASSES.forEach(className => {
|
|
514
|
-
fromEl.classList.contains(className) && toEl.classList.add(className)
|
|
515
|
-
})
|
|
516
|
-
toEl.setAttribute(PHX_REF, ref)
|
|
517
|
-
toEl.setAttribute(PHX_REF_SRC, refSrc)
|
|
518
|
-
return true
|
|
519
|
-
}
|
|
510
|
+
if(DOM.isUploadInput(fromEl)){ DOM.mergeAttrs(fromEl, toEl, {isIgnored: true}) }
|
|
511
|
+
DOM.putPrivate(fromEl, PHX_REF, toEl)
|
|
512
|
+
return false
|
|
520
513
|
},
|
|
521
514
|
|
|
522
515
|
cleanChildNodes(container, phxUpdate){
|
|
@@ -526,7 +519,7 @@ let DOM = {
|
|
|
526
519
|
if(!childNode.id){
|
|
527
520
|
// Skip warning if it's an empty text node (e.g. a new-line)
|
|
528
521
|
let isEmptyTextNode = childNode.nodeType === Node.TEXT_NODE && childNode.nodeValue.trim() === ""
|
|
529
|
-
if(!isEmptyTextNode){
|
|
522
|
+
if(!isEmptyTextNode && childNode.nodeType !== Node.COMMENT_NODE){
|
|
530
523
|
logError("only HTML element tags with an id are allowed inside containers with phx-update.\n\n" +
|
|
531
524
|
`removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"\n\n`)
|
|
532
525
|
}
|
|
@@ -99,9 +99,13 @@ export default class DOMPatch {
|
|
|
99
99
|
|
|
100
100
|
let externalFormTriggered = null
|
|
101
101
|
|
|
102
|
-
function morph(targetContainer, source){
|
|
102
|
+
function morph(targetContainer, source, withChildren=false){
|
|
103
103
|
morphdom(targetContainer, source, {
|
|
104
|
-
childrenOnly
|
|
104
|
+
// normally, we are running with childrenOnly, as the patch HTML for a LV
|
|
105
|
+
// does not include the LV attrs (data-phx-session, etc.)
|
|
106
|
+
// when we are patching a live component, we do want to patch the root element as well;
|
|
107
|
+
// another case is the recursive patch of a stream item that was kept on reset (-> onBeforeNodeAdded)
|
|
108
|
+
childrenOnly: targetContainer.getAttribute(PHX_COMPONENT) === null && !withChildren,
|
|
105
109
|
getNodeKey: (node) => {
|
|
106
110
|
if(DOM.isPhxDestroyed(node)){ return null }
|
|
107
111
|
// If we have a join patch, then by definition there was no PHX_MAGIC_ID.
|
|
@@ -137,7 +141,7 @@ export default class DOMPatch {
|
|
|
137
141
|
if(!isJoinPatch && this.streamComponentRestore[el.id]){
|
|
138
142
|
morphedEl = this.streamComponentRestore[el.id]
|
|
139
143
|
delete this.streamComponentRestore[el.id]
|
|
140
|
-
morph.
|
|
144
|
+
morph.call(this, morphedEl, el, true)
|
|
141
145
|
}
|
|
142
146
|
|
|
143
147
|
return morphedEl
|
|
@@ -198,7 +202,7 @@ export default class DOMPatch {
|
|
|
198
202
|
if(DOM.isPhxSticky(fromEl)){ return false }
|
|
199
203
|
if(DOM.isIgnored(fromEl, phxUpdate) || (fromEl.form && fromEl.form.isSameNode(externalFormTriggered))){
|
|
200
204
|
this.trackBefore("updated", fromEl, toEl)
|
|
201
|
-
DOM.mergeAttrs(fromEl, toEl, {isIgnored:
|
|
205
|
+
DOM.mergeAttrs(fromEl, toEl, {isIgnored: DOM.isIgnored(fromEl, phxUpdate)})
|
|
202
206
|
updates.push(fromEl)
|
|
203
207
|
DOM.applyStickyOperations(fromEl)
|
|
204
208
|
return false
|
|
@@ -286,10 +290,18 @@ export default class DOMPatch {
|
|
|
286
290
|
})
|
|
287
291
|
}
|
|
288
292
|
|
|
289
|
-
morph.
|
|
293
|
+
morph.call(this, targetContainer, html)
|
|
290
294
|
})
|
|
291
295
|
|
|
292
|
-
if(liveSocket.isDebugEnabled()){
|
|
296
|
+
if(liveSocket.isDebugEnabled()){
|
|
297
|
+
detectDuplicateIds()
|
|
298
|
+
// warn if there are any inputs named "id"
|
|
299
|
+
Array.from(document.querySelectorAll("input[name=id]")).forEach(node => {
|
|
300
|
+
if(node.form){
|
|
301
|
+
console.error("Detected an input with name=\"id\" inside a form! This will cause problems when patching the DOM.\n", node)
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
}
|
|
293
305
|
|
|
294
306
|
if(appendPrependUpdates.length > 0){
|
|
295
307
|
liveSocket.time("post-morph append/prepend restoration", () => {
|
|
@@ -58,8 +58,10 @@ let Hooks = {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
let findScrollContainer = (el) => {
|
|
61
|
+
// the scroll event won't be fired on the html/body element even if overflow is set
|
|
62
|
+
// therefore we return null to instead listen for scroll events on document
|
|
63
|
+
if (["HTML", "BODY"].indexOf(el.nodeName.toUpperCase()) >= 0) return null
|
|
61
64
|
if(["scroll", "auto"].indexOf(getComputedStyle(el).overflowY) >= 0) return el
|
|
62
|
-
if(document.documentElement === el) return null
|
|
63
65
|
return findScrollContainer(el.parentElement)
|
|
64
66
|
}
|
|
65
67
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import DOM from "./dom"
|
|
2
2
|
import ARIA from "./aria"
|
|
3
3
|
|
|
4
|
-
let focusStack =
|
|
4
|
+
let focusStack = []
|
|
5
5
|
let default_transition_time = 200
|
|
6
6
|
|
|
7
7
|
let JS = {
|
|
@@ -96,13 +96,13 @@ let JS = {
|
|
|
96
96
|
},
|
|
97
97
|
|
|
98
98
|
exec_push_focus(eventType, phxEvent, view, sourceEl, el){
|
|
99
|
-
window.requestAnimationFrame(() => focusStack
|
|
99
|
+
window.requestAnimationFrame(() => focusStack.push(el || sourceEl))
|
|
100
100
|
},
|
|
101
101
|
|
|
102
102
|
exec_pop_focus(eventType, phxEvent, view, sourceEl, el){
|
|
103
103
|
window.requestAnimationFrame(() => {
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
const el = focusStack.pop()
|
|
105
|
+
if(el){ el.focus() }
|
|
106
106
|
})
|
|
107
107
|
},
|
|
108
108
|
|
|
@@ -115,7 +115,7 @@ let JS = {
|
|
|
115
115
|
},
|
|
116
116
|
|
|
117
117
|
exec_toggle_class(eventType, phxEvent, view, sourceEl, el, {to, names, transition, time}){
|
|
118
|
-
this.toggleClasses(el, names, transition, view)
|
|
118
|
+
this.toggleClasses(el, names, transition, time, view)
|
|
119
119
|
},
|
|
120
120
|
|
|
121
121
|
exec_toggle_attr(eventType, phxEvent, view, sourceEl, el, {attr: [attr, val1, val2]}){
|
|
@@ -158,7 +158,13 @@ export default class LiveSocket {
|
|
|
158
158
|
this.localStorage = opts.localStorage || window.localStorage
|
|
159
159
|
this.sessionStorage = opts.sessionStorage || window.sessionStorage
|
|
160
160
|
this.boundTopLevelEvents = false
|
|
161
|
-
this.
|
|
161
|
+
this.serverCloseRef = null
|
|
162
|
+
this.domCallbacks = Object.assign({
|
|
163
|
+
onPatchStart: closure(),
|
|
164
|
+
onPatchEnd: closure(),
|
|
165
|
+
onNodeAdded: closure(),
|
|
166
|
+
onBeforeElUpdated: closure()},
|
|
167
|
+
opts.dom || {})
|
|
162
168
|
this.transitions = new TransitionSet()
|
|
163
169
|
window.addEventListener("pagehide", _e => {
|
|
164
170
|
this.unloaded = true
|
|
@@ -173,6 +179,8 @@ export default class LiveSocket {
|
|
|
173
179
|
|
|
174
180
|
// public
|
|
175
181
|
|
|
182
|
+
version(){ return LV_VSN }
|
|
183
|
+
|
|
176
184
|
isProfileEnabled(){ return this.sessionStorage.getItem(PHX_LV_PROFILE) === "true" }
|
|
177
185
|
|
|
178
186
|
isDebugEnabled(){ return this.sessionStorage.getItem(PHX_LV_DEBUG) === "true" }
|
|
@@ -225,6 +233,12 @@ export default class LiveSocket {
|
|
|
225
233
|
|
|
226
234
|
disconnect(callback){
|
|
227
235
|
clearTimeout(this.reloadWithJitterTimer)
|
|
236
|
+
// remove the socket close listener to avoid trying to handle
|
|
237
|
+
// a server close event when it is actually caused by us disconnecting
|
|
238
|
+
if(this.serverCloseRef){
|
|
239
|
+
this.socket.off(this.serverCloseRef)
|
|
240
|
+
this.serverCloseRef = null
|
|
241
|
+
}
|
|
228
242
|
this.socket.disconnect(callback)
|
|
229
243
|
}
|
|
230
244
|
|
|
@@ -512,7 +526,7 @@ export default class LiveSocket {
|
|
|
512
526
|
|
|
513
527
|
this.boundTopLevelEvents = true
|
|
514
528
|
// enter failsafe reload if server has gone away intentionally, such as "disconnect" broadcast
|
|
515
|
-
this.socket.onClose(event => {
|
|
529
|
+
this.serverCloseRef = this.socket.onClose(event => {
|
|
516
530
|
// failsafe reload if normal closure and we still have a main LV
|
|
517
531
|
if(event && event.code === 1000 && this.main){ return this.reloadWithJitter(this.main) }
|
|
518
532
|
})
|
|
@@ -625,28 +639,23 @@ export default class LiveSocket {
|
|
|
625
639
|
|
|
626
640
|
bindClicks(){
|
|
627
641
|
window.addEventListener("mousedown", e => this.clickStartedAtTarget = e.target)
|
|
628
|
-
this.bindClick("click", "click"
|
|
629
|
-
this.bindClick("mousedown", "capture-click", true)
|
|
642
|
+
this.bindClick("click", "click")
|
|
630
643
|
}
|
|
631
644
|
|
|
632
|
-
bindClick(eventName, bindingName
|
|
645
|
+
bindClick(eventName, bindingName){
|
|
633
646
|
let click = this.binding(bindingName)
|
|
634
647
|
window.addEventListener(eventName, e => {
|
|
635
648
|
let target = null
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
target = closestPhxBinding(clickStartedAtTarget, click)
|
|
644
|
-
this.dispatchClickAway(e, clickStartedAtTarget)
|
|
645
|
-
this.clickStartedAtTarget = null
|
|
646
|
-
}
|
|
649
|
+
// a synthetic click event (detail 0) will not have caused a mousedown event,
|
|
650
|
+
// therefore the clickStartedAtTarget is stale
|
|
651
|
+
if(e.detail === 0) this.clickStartedAtTarget = e.target
|
|
652
|
+
let clickStartedAtTarget = this.clickStartedAtTarget || e.target
|
|
653
|
+
target = closestPhxBinding(clickStartedAtTarget, click)
|
|
654
|
+
this.dispatchClickAway(e, clickStartedAtTarget)
|
|
655
|
+
this.clickStartedAtTarget = null
|
|
647
656
|
let phxEvent = target && target.getAttribute(click)
|
|
648
657
|
if(!phxEvent){
|
|
649
|
-
if(
|
|
658
|
+
if(DOM.isNewPageClick(e, window.location)){ this.unload() }
|
|
650
659
|
return
|
|
651
660
|
}
|
|
652
661
|
|
|
@@ -660,7 +669,7 @@ export default class LiveSocket {
|
|
|
660
669
|
JS.exec("click", phxEvent, view, target, ["push", {data: this.eventMeta("click", e, target)}])
|
|
661
670
|
})
|
|
662
671
|
})
|
|
663
|
-
},
|
|
672
|
+
}, false)
|
|
664
673
|
}
|
|
665
674
|
|
|
666
675
|
dispatchClickAway(e, clickStartedAt){
|