phoenix_live_view 0.20.1 → 0.20.3
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 +10 -9
- package/assets/js/phoenix_live_view/aria.js +1 -1
- package/assets/js/phoenix_live_view/constants.js +3 -1
- package/assets/js/phoenix_live_view/dom.js +12 -7
- package/assets/js/phoenix_live_view/dom_patch.js +31 -49
- package/assets/js/phoenix_live_view/entry_uploader.js +1 -0
- package/assets/js/phoenix_live_view/js.js +24 -4
- package/assets/js/phoenix_live_view/live_socket.js +9 -7
- package/assets/js/phoenix_live_view/rendered.js +197 -71
- package/assets/js/phoenix_live_view/view.js +11 -7
- package/assets/package.json +2 -2
- package/package.json +1 -1
- package/priv/static/phoenix_live_view.cjs.js +233 -135
- package/priv/static/phoenix_live_view.cjs.js.map +2 -2
- package/priv/static/phoenix_live_view.esm.js +233 -135
- package/priv/static/phoenix_live_view.esm.js.map +2 -2
- package/priv/static/phoenix_live_view.js +233 -135
- package/priv/static/phoenix_live_view.min.js +4 -10
package/README.md
CHANGED
|
@@ -29,10 +29,10 @@ model while keeping your code closer to your data (and ultimately your source of
|
|
|
29
29
|
|
|
30
30
|
* **Rich templating language:** Enjoy HEEx: a templating language that supports function components, slots, HTML validation, verified routes, and more.
|
|
31
31
|
|
|
32
|
-
* **
|
|
32
|
+
* **Diffs over the wire:** Instead of sending "HTML over the wire", LiveView knows exactly which parts of your templates change, sending minimal diffs over the wire after the initial render, reducing latency and bandwidth usage. The client leverages this information and optimizes the browser with 5-10x faster updates, compared to solutions that replace whole HTML fragments.
|
|
33
33
|
|
|
34
34
|
* **Live form validation:** LiveView supports real-time form validation out of the box. Create rich user interfaces with features like uploads, nested inputs, and [specialized recovery](https://hexdocs.pm/phoenix_live_view/form-bindings.html#recovery-following-crashes-or-disconnects).
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
* **File uploads:** Real-time file uploads with progress indicators and image previews. Process your uploads on the fly or submit them to your desired cloud service.
|
|
37
37
|
|
|
38
38
|
* **Rich integration API:** Use the rich integration API to interact with the client, with `phx-click`, `phx-focus`, `phx-blur`, `phx-submit`, and `phx-hook` included for cases where you have to write JavaScript.
|
|
@@ -47,9 +47,13 @@ model while keeping your code closer to your data (and ultimately your source of
|
|
|
47
47
|
|
|
48
48
|
* **Robust test suite:** Write tests with confidence alongside Phoenix LiveView built-in testing tools. No more running a whole browser alongside your tests.
|
|
49
49
|
|
|
50
|
-
##
|
|
50
|
+
## Learning
|
|
51
|
+
|
|
52
|
+
Check our [comprehensive docs](https://hexdocs.pm/phoenix_live_view) to get started.
|
|
53
|
+
|
|
54
|
+
The Phoenix framework documentation also keeps a list of [community resources](https://hexdocs.pm/phoenix/community.html), including books, videos, and other materials, and some include LiveView too.
|
|
51
55
|
|
|
52
|
-
|
|
56
|
+
Also follow these announcements from the Phoenix team on LiveView for more examples and rationale:
|
|
53
57
|
|
|
54
58
|
* [LiveBeats: Building a Social Music App With Phoenix LiveView](https://fly.io/blog/livebeats/)
|
|
55
59
|
|
|
@@ -57,15 +61,11 @@ News from the Phoenix team on LiveView:
|
|
|
57
61
|
|
|
58
62
|
* [Initial announcement](https://dockyard.com/blog/2018/12/12/phoenix-liveview-interactive-real-time-apps-no-need-to-write-javascript)
|
|
59
63
|
|
|
60
|
-
## Learning
|
|
61
|
-
|
|
62
|
-
See our existing comprehensive [docs](https://hexdocs.pm/phoenix_live_view) and [guides](https://hexdocs.pm/phoenix_live_view/api-reference.html) for more information.
|
|
63
|
-
|
|
64
64
|
## Installation
|
|
65
65
|
|
|
66
66
|
LiveView is included by default in all new Phoenix v1.6+ applications and
|
|
67
67
|
later. If you have an older existing Phoenix app and you wish to add
|
|
68
|
-
LiveView, see [the installation guide
|
|
68
|
+
LiveView, see [the previous installation guide](https://github.com/phoenixframework/phoenix_live_view/blob/v0.20.1/guides/introduction/installation.md).
|
|
69
69
|
|
|
70
70
|
## What makes LiveView unique?
|
|
71
71
|
|
|
@@ -167,6 +167,7 @@ $ mix test
|
|
|
167
167
|
```
|
|
168
168
|
|
|
169
169
|
Running the Javascript tests:
|
|
170
|
+
|
|
170
171
|
```bash
|
|
171
172
|
$ cd assets
|
|
172
173
|
$ npm run test
|
|
@@ -17,7 +17,7 @@ let ARIA = {
|
|
|
17
17
|
(el instanceof HTMLAreaElement && el.href !== undefined) ||
|
|
18
18
|
(!el.disabled && (this.anyOf(el, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, HTMLButtonElement]))) ||
|
|
19
19
|
(el instanceof HTMLIFrameElement) ||
|
|
20
|
-
(el.tabIndex > 0 || (!interactiveOnly && el.
|
|
20
|
+
(el.tabIndex > 0 || (!interactiveOnly && el.getAttribute("tabindex") !== null && el.getAttribute("aria-hidden") !== "true"))
|
|
21
21
|
)
|
|
22
22
|
},
|
|
23
23
|
|
|
@@ -21,6 +21,7 @@ export const PHX_DROP_TARGET = "drop-target"
|
|
|
21
21
|
export const PHX_ACTIVE_ENTRY_REFS = "data-phx-active-refs"
|
|
22
22
|
export const PHX_LIVE_FILE_UPDATED = "phx:live-file:updated"
|
|
23
23
|
export const PHX_SKIP = "data-phx-skip"
|
|
24
|
+
export const PHX_MAGIC_ID = "data-phx-id"
|
|
24
25
|
export const PHX_PRUNE = "data-phx-prune"
|
|
25
26
|
export const PHX_PAGE_LOADING = "page-loading"
|
|
26
27
|
export const PHX_CONNECTED_CLASS = "phx-connected"
|
|
@@ -79,9 +80,10 @@ export const DEFAULTS = {
|
|
|
79
80
|
// Rendered
|
|
80
81
|
export const DYNAMICS = "d"
|
|
81
82
|
export const STATIC = "s"
|
|
83
|
+
export const ROOT = "r"
|
|
82
84
|
export const COMPONENTS = "c"
|
|
83
85
|
export const EVENTS = "e"
|
|
84
86
|
export const REPLY = "r"
|
|
85
87
|
export const TITLE = "t"
|
|
86
88
|
export const TEMPLATES = "p"
|
|
87
|
-
export const STREAM = "stream"
|
|
89
|
+
export const STREAM = "stream"
|
|
@@ -285,10 +285,15 @@ let DOM = {
|
|
|
285
285
|
}
|
|
286
286
|
},
|
|
287
287
|
|
|
288
|
-
maybeHideFeedback(container,
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if(
|
|
288
|
+
maybeHideFeedback(container, inputs, phxFeedbackFor){
|
|
289
|
+
let feedbacks = []
|
|
290
|
+
inputs.forEach(input => {
|
|
291
|
+
if(!(this.private(input, PHX_HAS_FOCUSED) || this.private(input, PHX_HAS_SUBMITTED))){
|
|
292
|
+
feedbacks.push(input.name)
|
|
293
|
+
if(input.name.endsWith("[]")){ feedbacks.push(input.name.slice(0, -2)) }
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
if(feedbacks.length > 0){
|
|
292
297
|
let selector = feedbacks.map(f => `[${phxFeedbackFor}="${f}"]`).join(", ")
|
|
293
298
|
DOM.all(container, selector, el => el.classList.add(PHX_NO_FEEDBACK_CLASS))
|
|
294
299
|
}
|
|
@@ -358,7 +363,7 @@ let DOM = {
|
|
|
358
363
|
for(let i = targetAttrs.length - 1; i >= 0; i--){
|
|
359
364
|
let name = targetAttrs[i].name
|
|
360
365
|
if(isIgnored){
|
|
361
|
-
if(name.startsWith("data-") && !source.hasAttribute(name)){ target.removeAttribute(name) }
|
|
366
|
+
if(name.startsWith("data-") && !source.hasAttribute(name) && ![PHX_REF, PHX_REF_SRC].includes(name)){ target.removeAttribute(name) }
|
|
362
367
|
} else {
|
|
363
368
|
if(!source.hasAttribute(name)){ target.removeAttribute(name) }
|
|
364
369
|
}
|
|
@@ -366,8 +371,8 @@ let DOM = {
|
|
|
366
371
|
},
|
|
367
372
|
|
|
368
373
|
mergeFocusedInput(target, source){
|
|
369
|
-
|
|
370
|
-
|
|
374
|
+
DOM.mergeAttrs(target, source, {exclude: ["value"]})
|
|
375
|
+
|
|
371
376
|
if(source.readOnly){
|
|
372
377
|
target.setAttribute("readonly", true)
|
|
373
378
|
} else {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
PHX_ROOT_ID,
|
|
7
7
|
PHX_SESSION,
|
|
8
8
|
PHX_SKIP,
|
|
9
|
+
PHX_MAGIC_ID,
|
|
9
10
|
PHX_STATIC,
|
|
10
11
|
PHX_TRIGGER_ACTION,
|
|
11
12
|
PHX_UPDATE,
|
|
@@ -76,7 +77,7 @@ export default class DOMPatch {
|
|
|
76
77
|
})
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
perform(){
|
|
80
|
+
perform(isJoinPatch){
|
|
80
81
|
let {view, liveSocket, container, html} = this
|
|
81
82
|
let targetContainer = this.isCIDPatch() ? this.targetCIDContainer(html) : container
|
|
82
83
|
if(this.isCIDPatch() && !targetContainer){ return }
|
|
@@ -96,21 +97,19 @@ export default class DOMPatch {
|
|
|
96
97
|
|
|
97
98
|
let externalFormTriggered = null
|
|
98
99
|
|
|
99
|
-
let diffHTML = liveSocket.time("premorph container prep", () => {
|
|
100
|
-
return this.buildDiffHTML(container, html, phxUpdate, targetContainer)
|
|
101
|
-
})
|
|
102
|
-
|
|
103
100
|
this.trackBefore("added", container)
|
|
104
101
|
this.trackBefore("updated", container, container)
|
|
105
102
|
|
|
106
103
|
liveSocket.time("morphdom", () => {
|
|
107
104
|
this.streams.forEach(([ref, inserts, deleteIds, reset]) => {
|
|
108
105
|
Object.entries(inserts).forEach(([key, [streamAt, limit]]) => {
|
|
109
|
-
this.streamInserts[key] = {ref, streamAt, limit}
|
|
106
|
+
this.streamInserts[key] = {ref, streamAt, limit, resetKept: false}
|
|
110
107
|
})
|
|
111
108
|
if(reset !== undefined){
|
|
112
109
|
DOM.all(container, `[${PHX_STREAM_REF}="${ref}"]`, child => {
|
|
113
|
-
if(
|
|
110
|
+
if(inserts[child.id]){
|
|
111
|
+
this.streamInserts[child.id].resetKept = true
|
|
112
|
+
} else {
|
|
114
113
|
this.removeStreamChildElement(child)
|
|
115
114
|
}
|
|
116
115
|
})
|
|
@@ -121,10 +120,14 @@ export default class DOMPatch {
|
|
|
121
120
|
})
|
|
122
121
|
})
|
|
123
122
|
|
|
124
|
-
morphdom(targetContainer,
|
|
123
|
+
morphdom(targetContainer, html, {
|
|
125
124
|
childrenOnly: targetContainer.getAttribute(PHX_COMPONENT) === null,
|
|
126
125
|
getNodeKey: (node) => {
|
|
127
|
-
|
|
126
|
+
if(DOM.isPhxDestroyed(node)){ return null }
|
|
127
|
+
// If we have a join patch, then by definition there was no PHX_MAGIC_ID.
|
|
128
|
+
// This is important to reduce the amount of elements morphdom discards.
|
|
129
|
+
if(isJoinPatch){ return node.id }
|
|
130
|
+
return node.id || (node.getAttribute && node.getAttribute(PHX_MAGIC_ID))
|
|
128
131
|
},
|
|
129
132
|
// skip indexing from children when container is stream
|
|
130
133
|
skipFromChildren: (from) => { return from.getAttribute(phxUpdate) === PHX_STREAM },
|
|
@@ -164,7 +167,7 @@ export default class DOMPatch {
|
|
|
164
167
|
return el
|
|
165
168
|
},
|
|
166
169
|
onNodeAdded: (el) => {
|
|
167
|
-
if(el.getAttribute){ this.maybeReOrderStream(el) }
|
|
170
|
+
if(el.getAttribute){ this.maybeReOrderStream(el, true) }
|
|
168
171
|
|
|
169
172
|
// hack to fix Safari handling of img srcset and video tags
|
|
170
173
|
if(el instanceof HTMLImageElement && el.srcset){
|
|
@@ -185,6 +188,19 @@ export default class DOMPatch {
|
|
|
185
188
|
}
|
|
186
189
|
added.push(el)
|
|
187
190
|
},
|
|
191
|
+
onBeforeElChildrenUpdated: (fromEl, toEl) => {
|
|
192
|
+
// before we update the children, we need to set existing stream children
|
|
193
|
+
// into the new order from the server if they were kept during a stream reset
|
|
194
|
+
if(fromEl.getAttribute(phxUpdate) === PHX_STREAM){
|
|
195
|
+
let toIds = Array.from(toEl.children).map(child => child.id)
|
|
196
|
+
Array.from(fromEl.children).filter(child => {
|
|
197
|
+
let {resetKept} = this.getStreamInsert(child)
|
|
198
|
+
return resetKept
|
|
199
|
+
}).forEach((child) => {
|
|
200
|
+
this.streamInserts[child.id].streamAt = toIds.indexOf(child.id)
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
},
|
|
188
204
|
onNodeDiscarded: (el) => this.onNodeDiscarded(el),
|
|
189
205
|
onBeforeNodeDiscarded: (el) => {
|
|
190
206
|
if(el.getAttribute && el.getAttribute(PHX_PRUNE) !== null){ return true }
|
|
@@ -202,7 +218,7 @@ export default class DOMPatch {
|
|
|
202
218
|
externalFormTriggered = el
|
|
203
219
|
}
|
|
204
220
|
updates.push(el)
|
|
205
|
-
this.maybeReOrderStream(el)
|
|
221
|
+
this.maybeReOrderStream(el, false)
|
|
206
222
|
},
|
|
207
223
|
onBeforeElUpdated: (fromEl, toEl) => {
|
|
208
224
|
DOM.maybeAddPrivateHooks(toEl, phxViewportTop, phxViewportBottom)
|
|
@@ -273,9 +289,7 @@ export default class DOMPatch {
|
|
|
273
289
|
})
|
|
274
290
|
}
|
|
275
291
|
|
|
276
|
-
|
|
277
|
-
DOM.maybeHideFeedback(targetContainer, input, phxFeedbackFor)
|
|
278
|
-
})
|
|
292
|
+
DOM.maybeHideFeedback(targetContainer, trackedInputs, phxFeedbackFor)
|
|
279
293
|
|
|
280
294
|
liveSocket.silenceEvents(() => DOM.restoreFocus(focused, selectionStart, selectionEnd))
|
|
281
295
|
DOM.dispatchEvent(document, "phx:update")
|
|
@@ -320,9 +334,9 @@ export default class DOMPatch {
|
|
|
320
334
|
return insert || {}
|
|
321
335
|
}
|
|
322
336
|
|
|
323
|
-
maybeReOrderStream(el){
|
|
337
|
+
maybeReOrderStream(el, isNew){
|
|
324
338
|
let {ref, streamAt, limit} = this.getStreamInsert(el)
|
|
325
|
-
if(streamAt === undefined){ return }
|
|
339
|
+
if(streamAt === undefined || (streamAt === 0 && !isNew)){ return }
|
|
326
340
|
|
|
327
341
|
// we need to the PHX_STREAM_REF here as well as addChild is invoked only for parents
|
|
328
342
|
DOM.putSticky(el, PHX_STREAM_REF, el => el.setAttribute(PHX_STREAM_REF, ref))
|
|
@@ -363,7 +377,7 @@ export default class DOMPatch {
|
|
|
363
377
|
isCIDPatch(){ return this.cidPatch }
|
|
364
378
|
|
|
365
379
|
skipCIDSibling(el){
|
|
366
|
-
return el.nodeType === Node.ELEMENT_NODE && el.
|
|
380
|
+
return el.nodeType === Node.ELEMENT_NODE && el.hasAttribute(PHX_SKIP)
|
|
367
381
|
}
|
|
368
382
|
|
|
369
383
|
targetCIDContainer(html){
|
|
@@ -376,37 +390,5 @@ export default class DOMPatch {
|
|
|
376
390
|
}
|
|
377
391
|
}
|
|
378
392
|
|
|
379
|
-
// builds HTML for morphdom patch
|
|
380
|
-
// - for full patches of LiveView or a component with a single
|
|
381
|
-
// root node, simply returns the HTML
|
|
382
|
-
// - for patches of a component with multiple root nodes, the
|
|
383
|
-
// parent node becomes the target container and non-component
|
|
384
|
-
// siblings are marked as skip.
|
|
385
|
-
buildDiffHTML(container, html, phxUpdate, targetContainer){
|
|
386
|
-
let isCIDPatch = this.isCIDPatch()
|
|
387
|
-
let isCIDWithSingleRoot = isCIDPatch && targetContainer.getAttribute(PHX_COMPONENT) === this.targetCID.toString()
|
|
388
|
-
if(!isCIDPatch || isCIDWithSingleRoot){
|
|
389
|
-
return html
|
|
390
|
-
} else {
|
|
391
|
-
// component patch with multiple CID roots
|
|
392
|
-
let diffContainer = null
|
|
393
|
-
let template = document.createElement("template")
|
|
394
|
-
diffContainer = DOM.cloneNode(targetContainer)
|
|
395
|
-
let [firstComponent, ...rest] = DOM.findComponentNodeList(diffContainer, this.targetCID)
|
|
396
|
-
template.innerHTML = html
|
|
397
|
-
rest.forEach(el => el.remove())
|
|
398
|
-
Array.from(diffContainer.childNodes).forEach(child => {
|
|
399
|
-
// we can only skip trackable nodes with an ID
|
|
400
|
-
if(child.id && child.nodeType === Node.ELEMENT_NODE && child.getAttribute(PHX_COMPONENT) !== this.targetCID.toString()){
|
|
401
|
-
child.setAttribute(PHX_SKIP, "")
|
|
402
|
-
child.innerHTML = ""
|
|
403
|
-
}
|
|
404
|
-
})
|
|
405
|
-
Array.from(template.content.childNodes).forEach(el => diffContainer.insertBefore(el, firstComponent))
|
|
406
|
-
firstComponent.remove()
|
|
407
|
-
return diffContainer.outerHTML
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
393
|
indexOf(parent, child){ return Array.from(parent.children).indexOf(child) }
|
|
412
394
|
}
|
|
@@ -9,8 +9,6 @@ let JS = {
|
|
|
9
9
|
let commands = phxEvent.charAt(0) === "[" ?
|
|
10
10
|
JSON.parse(phxEvent) : [[defaultKind, defaultArgs]]
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
12
|
commands.forEach(([kind, args]) => {
|
|
15
13
|
if(kind === defaultKind && defaultArgs.data){
|
|
16
14
|
args.data = Object.assign(args.data || {}, defaultArgs.data)
|
|
@@ -26,6 +24,16 @@ let JS = {
|
|
|
26
24
|
return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length > 0)
|
|
27
25
|
},
|
|
28
26
|
|
|
27
|
+
isInViewport(el){
|
|
28
|
+
const rect = el.getBoundingClientRect()
|
|
29
|
+
return (
|
|
30
|
+
rect.top >= 0 &&
|
|
31
|
+
rect.left >= 0 &&
|
|
32
|
+
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
33
|
+
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
|
34
|
+
)
|
|
35
|
+
},
|
|
36
|
+
|
|
29
37
|
// private
|
|
30
38
|
|
|
31
39
|
// commands
|
|
@@ -46,13 +54,12 @@ let JS = {
|
|
|
46
54
|
},
|
|
47
55
|
|
|
48
56
|
exec_push(eventType, phxEvent, view, sourceEl, el, args){
|
|
49
|
-
if(!view.isConnected()){ return }
|
|
50
|
-
|
|
51
57
|
let {event, data, target, page_loading, loading, value, dispatcher, callback} = args
|
|
52
58
|
let pushOpts = {loading, value, target, page_loading: !!page_loading}
|
|
53
59
|
let targetSrc = eventType === "change" && dispatcher ? dispatcher : sourceEl
|
|
54
60
|
let phxTarget = target || targetSrc.getAttribute(view.binding("target")) || targetSrc
|
|
55
61
|
view.withinTargets(phxTarget, (targetView, targetCtx) => {
|
|
62
|
+
if(!targetView.isConnected()){ return }
|
|
56
63
|
if(eventType === "change"){
|
|
57
64
|
let {newCid, _target} = args
|
|
58
65
|
_target = _target || (DOM.isFormInput(sourceEl) ? sourceEl.name : undefined)
|
|
@@ -102,6 +109,10 @@ let JS = {
|
|
|
102
109
|
this.addOrRemoveClasses(el, [], names, transition, time, view)
|
|
103
110
|
},
|
|
104
111
|
|
|
112
|
+
exec_toggle_class(eventType, phxEvent, view, sourceEl, el, {to, names, transition, time}){
|
|
113
|
+
this.toggleClasses(el, names, transition, view)
|
|
114
|
+
},
|
|
115
|
+
|
|
105
116
|
exec_transition(eventType, phxEvent, view, sourceEl, el, {time, transition}){
|
|
106
117
|
this.addOrRemoveClasses(el, [], [], transition, time, view)
|
|
107
118
|
},
|
|
@@ -193,6 +204,15 @@ let JS = {
|
|
|
193
204
|
}
|
|
194
205
|
},
|
|
195
206
|
|
|
207
|
+
toggleClasses(el, classes, transition, time, view){
|
|
208
|
+
window.requestAnimationFrame(() => {
|
|
209
|
+
let [prevAdds, prevRemoves] = DOM.getSticky(el, "classes", [[], []])
|
|
210
|
+
let newAdds = classes.filter(name => prevAdds.indexOf(name) < 0 && !el.classList.contains(name))
|
|
211
|
+
let newRemoves = classes.filter(name => prevRemoves.indexOf(name) < 0 && el.classList.contains(name))
|
|
212
|
+
this.addOrRemoveClasses(el, newAdds, newRemoves, transition, time, view)
|
|
213
|
+
})
|
|
214
|
+
},
|
|
215
|
+
|
|
196
216
|
addOrRemoveClasses(el, adds, removes, transition, time, view){
|
|
197
217
|
let [transitionRun, transitionStart, transitionEnd] = transition || [[], [], []]
|
|
198
218
|
if(transitionRun.length > 0){
|
|
@@ -522,7 +522,7 @@ export default class LiveSocket {
|
|
|
522
522
|
if(!dead){ this.bindNav() }
|
|
523
523
|
this.bindClicks()
|
|
524
524
|
if(!dead){ this.bindForms() }
|
|
525
|
-
this.bind({keyup: "keyup", keydown: "keydown"}, (e, type, view, targetEl, phxEvent,
|
|
525
|
+
this.bind({keyup: "keyup", keydown: "keydown"}, (e, type, view, targetEl, phxEvent, phxTarget) => {
|
|
526
526
|
let matchKey = targetEl.getAttribute(this.binding(PHX_KEY))
|
|
527
527
|
let pressedKey = e.key && e.key.toLowerCase() // chrome clicked autocompletes send a keydown without key
|
|
528
528
|
if(matchKey && matchKey.toLowerCase() !== pressedKey){ return }
|
|
@@ -530,13 +530,13 @@ export default class LiveSocket {
|
|
|
530
530
|
let data = {key: e.key, ...this.eventMeta(type, e, targetEl)}
|
|
531
531
|
JS.exec(type, phxEvent, view, targetEl, ["push", {data}])
|
|
532
532
|
})
|
|
533
|
-
this.bind({blur: "focusout", focus: "focusin"}, (e, type, view, targetEl, phxEvent,
|
|
534
|
-
if(!
|
|
533
|
+
this.bind({blur: "focusout", focus: "focusin"}, (e, type, view, targetEl, phxEvent, phxTarget) => {
|
|
534
|
+
if(!phxTarget){
|
|
535
535
|
let data = {key: e.key, ...this.eventMeta(type, e, targetEl)}
|
|
536
536
|
JS.exec(type, phxEvent, view, targetEl, ["push", {data}])
|
|
537
537
|
}
|
|
538
538
|
})
|
|
539
|
-
this.bind({blur: "blur", focus: "focus"}, (e, type, view, targetEl,
|
|
539
|
+
this.bind({blur: "blur", focus: "focus"}, (e, type, view, targetEl, phxEvent, phxTarget) => {
|
|
540
540
|
// blur and focus are triggered on document and window. Discard one to avoid dups
|
|
541
541
|
if(phxTarget === "window"){
|
|
542
542
|
let data = this.eventMeta(type, e, targetEl)
|
|
@@ -619,7 +619,7 @@ export default class LiveSocket {
|
|
|
619
619
|
}
|
|
620
620
|
|
|
621
621
|
bindClicks(){
|
|
622
|
-
window.addEventListener("
|
|
622
|
+
window.addEventListener("mousedown", e => this.clickStartedAtTarget = e.target)
|
|
623
623
|
this.bindClick("click", "click", false)
|
|
624
624
|
this.bindClick("mousedown", "capture-click", true)
|
|
625
625
|
}
|
|
@@ -661,7 +661,7 @@ export default class LiveSocket {
|
|
|
661
661
|
if(!(el.isSameNode(clickStartedAt) || el.contains(clickStartedAt))){
|
|
662
662
|
this.withinOwners(e.target, view => {
|
|
663
663
|
let phxEvent = el.getAttribute(phxClickAway)
|
|
664
|
-
if(JS.isVisible(el)){
|
|
664
|
+
if(JS.isVisible(el) && JS.isInViewport(el)){
|
|
665
665
|
JS.exec("click", phxEvent, view, el, ["push", {data: this.eventMeta("click", e, e.target)}])
|
|
666
666
|
}
|
|
667
667
|
})
|
|
@@ -703,7 +703,9 @@ export default class LiveSocket {
|
|
|
703
703
|
let type = target && target.getAttribute(PHX_LIVE_LINK)
|
|
704
704
|
if(!type || !this.isConnected() || !this.main || DOM.wantsNewTab(e)){ return }
|
|
705
705
|
|
|
706
|
-
|
|
706
|
+
// When wrapping an SVG element in an anchor tag, the href can be an SVGAnimatedString
|
|
707
|
+
let href = target.href instanceof SVGAnimatedString ? target.href.baseVal : target.href
|
|
708
|
+
|
|
707
709
|
let linkState = target.getAttribute(PHX_LINK_STATE)
|
|
708
710
|
e.preventDefault()
|
|
709
711
|
e.stopImmediatePropagation() // do not bubble click to regular phx-click bindings
|