phoenix_live_view 0.16.3 → 0.17.2
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/CHANGELOG.md +128 -7
- package/assets/js/phoenix_live_view/constants.js +2 -2
- package/assets/js/phoenix_live_view/dom.js +49 -13
- package/assets/js/phoenix_live_view/dom_patch.js +31 -11
- package/assets/js/phoenix_live_view/js.js +169 -0
- package/assets/js/phoenix_live_view/live_socket.js +143 -50
- package/assets/js/phoenix_live_view/view.js +113 -87
- package/assets/js/phoenix_live_view/view_hook.js +2 -2
- package/package.json +5 -3
- package/priv/static/phoenix_live_view.cjs.js +3737 -0
- package/priv/static/phoenix_live_view.cjs.js.map +7 -0
- package/priv/static/phoenix_live_view.esm.js +495 -167
- package/priv/static/phoenix_live_view.esm.js.map +3 -3
- package/priv/static/phoenix_live_view.js +495 -167
- package/priv/static/phoenix_live_view.min.js +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,122 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.17.2 (2021-10-22)
|
|
4
|
+
|
|
5
|
+
### Bug fixes
|
|
6
|
+
- Fix HTML engine bug causing attribute expressions to be incorrectly evaluated in certain cases
|
|
7
|
+
- Fix show/hide/toggle custom display not being restored.
|
|
8
|
+
- Fix default `to` target for `JS.show|hide|dispatch`
|
|
9
|
+
- Fix form input targetting
|
|
10
|
+
|
|
11
|
+
## 0.17.1 (2021-10-21)
|
|
12
|
+
|
|
13
|
+
### Bug fixes
|
|
14
|
+
- Fix SVG element support for phx binding interactions
|
|
15
|
+
|
|
16
|
+
## 0.17.0 (2021-10-21)
|
|
17
|
+
|
|
18
|
+
### Breaking Changes
|
|
19
|
+
|
|
20
|
+
#### on_mount changes
|
|
21
|
+
|
|
22
|
+
The hook API introduced in LiveView 0.16 has been improved based on feedback.
|
|
23
|
+
LiveView 0.17 removes the custom module-function callbacks for the
|
|
24
|
+
`Phoenix.LiveView.on_mount/1` macro and the `:on_mount` option for
|
|
25
|
+
`Phoenix.LiveView.Router.live_session/3` in favor of supporting a custom
|
|
26
|
+
argument. For clarity, the module function to be invoked during the mount
|
|
27
|
+
lifecycle stage will always be named `on_mount/4`.
|
|
28
|
+
|
|
29
|
+
For example, if you had invoked `on_mount/1` like so:
|
|
30
|
+
|
|
31
|
+
```elixir
|
|
32
|
+
on_mount MyAppWeb.MyHook
|
|
33
|
+
on_mount {MyAppWeb.MyHook, :assign_current_user}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
and defined your callbacks as:
|
|
37
|
+
|
|
38
|
+
```elixir
|
|
39
|
+
# my_hook.ex
|
|
40
|
+
|
|
41
|
+
def mount(_params, _session, _socket) do
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def assign_current_user(_params, _session, _socket) do
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Change the callback to:
|
|
49
|
+
|
|
50
|
+
```elixir
|
|
51
|
+
# my_hook.ex
|
|
52
|
+
|
|
53
|
+
def on_mount(:default, _params, _session, _socket) do
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def on_mount(:assign_current_user, _params, _session, _socket) do
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
When given only a module name, the first argument to `on_mount/4` will be the
|
|
61
|
+
atom `:default`.
|
|
62
|
+
|
|
63
|
+
#### LEEx templates in stateful LiveComponents
|
|
64
|
+
|
|
65
|
+
Stateful LiveComponents (where an `:id` is given) must now return HEEx templates
|
|
66
|
+
(`~H` sigil or `.heex` extension). LEEx temlates (`~L` sigil or `.leex` extension)
|
|
67
|
+
are no longer supported. This addresses bugs and allows stateful components
|
|
68
|
+
to be rendered more efficiently client-side.
|
|
69
|
+
|
|
70
|
+
#### phx-disconnected class has been replaced with phx-loading
|
|
71
|
+
|
|
72
|
+
Due to a bug in the newly released Safari 15, the previously used `.phx-disconnected` class has been replaced by a new `.phx-loading` class. The reason for the change is `phx.new` included a `.phx-disconnected` rule in the generated `app.css` which triggers the Safari bug. Renaming the class avoids applying the erronous rule for existing applications. Folks can upgrade by simply renaming their `.phx-disconnected` rules to `.phx-loading`.
|
|
73
|
+
|
|
74
|
+
#### phx-capture-click has been deprecated in favor of phx-click-away
|
|
75
|
+
|
|
76
|
+
The new phx-click-away binding replaces phx-capture-click and is much more versatile because it can detect "click focus" being lost on containers.
|
|
77
|
+
|
|
78
|
+
#### Removal of previously deprecated functionality
|
|
79
|
+
|
|
80
|
+
Some functionality that was previously deprecated has been removed:
|
|
81
|
+
|
|
82
|
+
- Implicit assigns in `live_component` do-blocks is no longer supported
|
|
83
|
+
- Passing a `@socket` to `live_component` will now raise if possible
|
|
84
|
+
|
|
85
|
+
### Enhancements
|
|
86
|
+
- Allow slots in function components: they are marked as `<:slot_name>` and can be rendered with `<%= render_slot @slot_name %>`
|
|
87
|
+
- Add JS command for executing JavaScript utility operations on the client with an extended push API
|
|
88
|
+
- Optimize string attributes:
|
|
89
|
+
- If the attribute is a string interpolation, such as `<div class={"foo bar #{@baz}"}>`, only the interpolation part is marked as dynamic
|
|
90
|
+
- If the attribute can be empty, such as "class" and "style", keep the attribute name as static
|
|
91
|
+
- Add a function component for rendering `Phoenix.LiveComponent`. Instead of `<%= live_component FormComponent, id: "form" %>`, you must now do: `<.live_component module={FormComponent} id="form" />`
|
|
92
|
+
|
|
93
|
+
### Bug fixes
|
|
94
|
+
- Fix LiveViews with form recovery failing to properly mount following a reconnect when preceeded by a live redirect
|
|
95
|
+
- Fix stale session causing full redirect fallback when issuing a `push_redirect` from mount
|
|
96
|
+
- Add workaround for Safari bug causing img tags with srcset and video with autoplay to fail to render
|
|
97
|
+
- Support EEx interpolation inside HTML comments in HEEx templates
|
|
98
|
+
- Support HTML tags inside script tags (as in regular HTML)
|
|
99
|
+
- Raise if using quotes in attribute names
|
|
100
|
+
- Include the filename in error messages when it is not possible to parse interpolated attributes
|
|
101
|
+
- Make sure the test client always sends the full URL on `live_patch`/`live_redirect`. This mirrors the behaviour of the JavaScript client
|
|
102
|
+
- Do not reload flash from session on `live_redirect`s
|
|
103
|
+
- Fix select drop-down flashes in chrome when the DOM is patched during focus
|
|
104
|
+
|
|
105
|
+
### Deprecations
|
|
106
|
+
- `<%= live_component MyModule, id: @user.id, user: @user %>` is deprecated in favor of `<.live_component module={MyModule} id={@user.id} user={@user} />`. Notice the new API requires using HEEx templates. This change allows us to further improve LiveComponent and bring new features such as slots to them.
|
|
107
|
+
- `render_block/2` in deprecated in favor of `render_slot/2`
|
|
108
|
+
|
|
109
|
+
## 0.16.4 (2021-09-22)
|
|
110
|
+
|
|
111
|
+
### Enhancements
|
|
112
|
+
- Improve HEEx error messages
|
|
113
|
+
- Relax HTML tag validation to support mixed case tags
|
|
114
|
+
- Support self closing HTML tags
|
|
115
|
+
- Remove requirement for handle_params to be defined for lifecycle hooks
|
|
116
|
+
|
|
117
|
+
### Bug fixes
|
|
118
|
+
- Fix pushes failing to include channel `join_ref` on messages
|
|
119
|
+
|
|
3
120
|
## 0.16.3 (2021-09-03)
|
|
4
121
|
|
|
5
122
|
### Bug fixes
|
|
@@ -97,7 +214,7 @@ will emit warnings in future releases. We recommend using the `~H` sigil and the
|
|
|
97
214
|
extension for all future templates in your application. You should also plan to migrate
|
|
98
215
|
the old templates accordingly using the recommendations below.
|
|
99
216
|
|
|
100
|
-
Migrating from `LEEx` to `HEEx` is relatively
|
|
217
|
+
Migrating from `LEEx` to `HEEx` is relatively straightforward. There are two main differences.
|
|
101
218
|
First of all, HEEx does not allow interpolation inside tags. So instead of:
|
|
102
219
|
|
|
103
220
|
```elixir
|
|
@@ -181,6 +298,10 @@ Change it to:
|
|
|
181
298
|
import { LiveSocket } from "phoenix_live_view"
|
|
182
299
|
```
|
|
183
300
|
|
|
301
|
+
Additionally on the client, the root LiveView element no longer exposes the
|
|
302
|
+
LiveView module name, therefore the `phx-view` attribute is never set.
|
|
303
|
+
Similarly, the `viewName` property of client hooks has been removed.
|
|
304
|
+
|
|
184
305
|
### Enhancements
|
|
185
306
|
- Introduce HEEx templates
|
|
186
307
|
- Introduce `Phoenix.Component`
|
|
@@ -309,7 +430,7 @@ import { LiveSocket } from "phoenix_live_view"
|
|
|
309
430
|
## 0.14.8 (2020-10-30)
|
|
310
431
|
|
|
311
432
|
### Bug fixes
|
|
312
|
-
- Fix
|
|
433
|
+
- Fix compatibility with latest Plug
|
|
313
434
|
|
|
314
435
|
## 0.14.7 (2020-09-25)
|
|
315
436
|
|
|
@@ -433,7 +554,7 @@ import { LiveSocket } from "phoenix_live_view"
|
|
|
433
554
|
|
|
434
555
|
### Backwards incompatible changes
|
|
435
556
|
- No longer send event metadata by default. Metadata is now opt-in and user defined at the `LiveSocket` level.
|
|
436
|
-
To maintain backwards
|
|
557
|
+
To maintain backwards compatibility with pre-0.13 behaviour, you can provide the following metadata option:
|
|
437
558
|
|
|
438
559
|
```javascript
|
|
439
560
|
let liveSocket = new LiveSocket("/live", Socket, {
|
|
@@ -520,7 +641,7 @@ The new implementation will check there is a button at `#term .buttons a`, with
|
|
|
520
641
|
- Add `phx-trigger-action` form annotation to trigger an HTTP form submit on next DOM patch
|
|
521
642
|
|
|
522
643
|
### Bug fixes
|
|
523
|
-
- Fix `phx-target` `@myself`
|
|
644
|
+
- Fix `phx-target` `@myself` targeting a sibling LiveView component with the same component ID
|
|
524
645
|
- Fix `phx:page-loading-stop` firing before the DOM patch has been performed
|
|
525
646
|
- Fix `phx-update="prepend"` failing to properly patch the DOM when the same ID is updated back to back
|
|
526
647
|
- Fix redirects on mount failing to copy flash
|
|
@@ -626,7 +747,7 @@ The new implementation will check there is a button at `#term .buttons a`, with
|
|
|
626
747
|
- Allow the router to be accessed as `socket.router`
|
|
627
748
|
- Allow `MFArgs` as the `:session` option in the `live` router macro
|
|
628
749
|
- Trigger page loading event when main LV errors
|
|
629
|
-
-
|
|
750
|
+
- Automatically clear the flash on live navigation examples - only the newly assigned flash is persisted
|
|
630
751
|
|
|
631
752
|
## 0.8.1 (2020-02-27)
|
|
632
753
|
|
|
@@ -653,7 +774,7 @@ The new implementation will check there is a button at `#term .buttons a`, with
|
|
|
653
774
|
- Add `put_live_layout` plug to put the root layout used for live routes
|
|
654
775
|
- Allow `redirect` and `push_redirect` from mount
|
|
655
776
|
- Use acknowledgement tracking to avoid patching inputs until the server has processed the form event
|
|
656
|
-
- Add css loading states to all phx bound elements with event
|
|
777
|
+
- Add css loading states to all phx bound elements with event specific css classes
|
|
657
778
|
- Dispatch `phx:page-loading-start` and `phx:page-loading-stop` on window for live navigation, initial page loads, and form submits, for user controlled page loading integration
|
|
658
779
|
- Allow any phx bound element to specify `phx-page-loading` to dispatch loading events above when the event is pushed
|
|
659
780
|
- Add client side latency simulator with new `enableLatencySim(milliseconds)` and `disableLatencySim()`
|
|
@@ -779,7 +900,7 @@ Also note that **the session from now on will have string keys**. LiveView will
|
|
|
779
900
|
- Allow `live_link` and `live_redirect` to exist anywhere in the page and it will always target the main LiveView (the one defined at the router)
|
|
780
901
|
|
|
781
902
|
### Backwards incompatible changes
|
|
782
|
-
- `phx-target="window"` has been removed in favor of `phx-window-keydown`, `phx-window-focus`, etc, and the `phx-target` binding has been repurposed for
|
|
903
|
+
- `phx-target="window"` has been removed in favor of `phx-window-keydown`, `phx-window-focus`, etc, and the `phx-target` binding has been repurposed for targeting LiveView and LiveComponent events from the client
|
|
783
904
|
- `Phoenix.LiveView` no longer defined `live_render` and `live_link`. These functions have been moved to `Phoenix.LiveView.Helpers` which can now be fully imported in your views. In other words, replace `import Phoenix.LiveView, only: [live_render: ..., live_link: ...]` by `import Phoenix.LiveView.Helpers`
|
|
784
905
|
|
|
785
906
|
## 0.4.1 (2019-11-07)
|
|
@@ -20,10 +20,10 @@ export const PHX_DROP_TARGET = "drop-target"
|
|
|
20
20
|
export const PHX_ACTIVE_ENTRY_REFS = "data-phx-active-refs"
|
|
21
21
|
export const PHX_LIVE_FILE_UPDATED = "phx:live-file:updated"
|
|
22
22
|
export const PHX_SKIP = "data-phx-skip"
|
|
23
|
-
export const
|
|
23
|
+
export const PHX_PRUNE = "data-phx-prune"
|
|
24
24
|
export const PHX_PAGE_LOADING = "page-loading"
|
|
25
25
|
export const PHX_CONNECTED_CLASS = "phx-connected"
|
|
26
|
-
export const PHX_DISCONNECTED_CLASS = "phx-
|
|
26
|
+
export const PHX_DISCONNECTED_CLASS = "phx-loading"
|
|
27
27
|
export const PHX_NO_FEEDBACK_CLASS = "phx-no-feedback"
|
|
28
28
|
export const PHX_ERROR_CLASS = "phx-error"
|
|
29
29
|
export const PHX_PARENT_ID = "data-phx-parent-id"
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
PHX_PARENT_ID,
|
|
13
13
|
PHX_PRIVATE,
|
|
14
14
|
PHX_REF,
|
|
15
|
+
PHX_ROOT_ID,
|
|
15
16
|
PHX_SESSION,
|
|
16
17
|
PHX_STATIC,
|
|
17
18
|
PHX_UPLOAD_REF,
|
|
@@ -20,7 +21,6 @@ import {
|
|
|
20
21
|
} from "./constants"
|
|
21
22
|
|
|
22
23
|
import {
|
|
23
|
-
clone,
|
|
24
24
|
logError
|
|
25
25
|
} from "./utils"
|
|
26
26
|
|
|
@@ -57,7 +57,7 @@ let DOM = {
|
|
|
57
57
|
},
|
|
58
58
|
|
|
59
59
|
markPhxChildDestroyed(el){
|
|
60
|
-
el.setAttribute(PHX_SESSION, "")
|
|
60
|
+
if(this.isPhxChild(el)){ el.setAttribute(PHX_SESSION, "") }
|
|
61
61
|
this.putPrivate(el, "destroyed", true)
|
|
62
62
|
},
|
|
63
63
|
|
|
@@ -116,9 +116,18 @@ let DOM = {
|
|
|
116
116
|
el[PHX_PRIVATE][key] = value
|
|
117
117
|
},
|
|
118
118
|
|
|
119
|
+
updatePrivate(el, key, defaultVal, updateFunc){
|
|
120
|
+
let existing = this.private(el, key)
|
|
121
|
+
if(existing === undefined){
|
|
122
|
+
this.putPrivate(el, key, updateFunc(defaultVal))
|
|
123
|
+
} else {
|
|
124
|
+
this.putPrivate(el, key, updateFunc(existing))
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
|
|
119
128
|
copyPrivates(target, source){
|
|
120
129
|
if(source[PHX_PRIVATE]){
|
|
121
|
-
target[PHX_PRIVATE] =
|
|
130
|
+
target[PHX_PRIVATE] = source[PHX_PRIVATE]
|
|
122
131
|
}
|
|
123
132
|
},
|
|
124
133
|
|
|
@@ -296,15 +305,6 @@ let DOM = {
|
|
|
296
305
|
}
|
|
297
306
|
},
|
|
298
307
|
|
|
299
|
-
syncPropsToAttrs(el){
|
|
300
|
-
if(el instanceof HTMLSelectElement){
|
|
301
|
-
let selectedItem = el.options.item(el.selectedIndex)
|
|
302
|
-
if(selectedItem && selectedItem.getAttribute("selected") === null){
|
|
303
|
-
selectedItem.setAttribute("selected", "")
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
},
|
|
307
|
-
|
|
308
308
|
isTextualInput(el){ return FOCUSABLE_INPUTS.indexOf(el.type) >= 0 },
|
|
309
309
|
|
|
310
310
|
isNowTriggerFormExternal(el, phxTriggerExternal){
|
|
@@ -347,7 +347,7 @@ let DOM = {
|
|
|
347
347
|
},
|
|
348
348
|
|
|
349
349
|
replaceRootContainer(container, tagName, attrs){
|
|
350
|
-
let retainedAttrs = new Set(["id", PHX_SESSION, PHX_STATIC, PHX_MAIN])
|
|
350
|
+
let retainedAttrs = new Set(["id", PHX_SESSION, PHX_STATIC, PHX_MAIN, PHX_ROOT_ID])
|
|
351
351
|
if(container.tagName.toLowerCase() === tagName.toLowerCase()){
|
|
352
352
|
Array.from(container.attributes)
|
|
353
353
|
.filter(attr => !retainedAttrs.has(attr.name.toLowerCase()))
|
|
@@ -367,6 +367,42 @@ let DOM = {
|
|
|
367
367
|
container.replaceWith(newContainer)
|
|
368
368
|
return newContainer
|
|
369
369
|
}
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
getSticky(el, name, defaultVal){
|
|
373
|
+
let op = (DOM.private(el, "sticky") || []).find(([existingName, ]) => name === existingName)
|
|
374
|
+
if(op){
|
|
375
|
+
let [_name, _op, stashedResult] = op
|
|
376
|
+
return stashedResult
|
|
377
|
+
} else {
|
|
378
|
+
return typeof(defaultVal) === "function" ? defaultVal() : defaultVal
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
deleteSticky(el, name){
|
|
383
|
+
this.updatePrivate(el, "sticky", [], ops => {
|
|
384
|
+
return ops.filter(([existingName, _]) => existingName !== name)
|
|
385
|
+
})
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
putSticky(el, name, op){
|
|
389
|
+
let stashedResult = op(el)
|
|
390
|
+
this.updatePrivate(el, "sticky", [], ops => {
|
|
391
|
+
let existingIndex = ops.findIndex(([existingName, ]) => name === existingName)
|
|
392
|
+
if(existingIndex >= 0){
|
|
393
|
+
ops[existingIndex] = [name, op, stashedResult]
|
|
394
|
+
} else {
|
|
395
|
+
ops.push([name, op, stashedResult])
|
|
396
|
+
}
|
|
397
|
+
return ops
|
|
398
|
+
})
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
applyStickyOperations(el){
|
|
402
|
+
let ops = DOM.private(el, "sticky")
|
|
403
|
+
if(!ops){ return }
|
|
404
|
+
|
|
405
|
+
ops.forEach(([name, op, _stashed]) => this.putSticky(el, name, op))
|
|
370
406
|
}
|
|
371
407
|
}
|
|
372
408
|
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
PHX_COMPONENT,
|
|
3
3
|
PHX_DISABLE_WITH,
|
|
4
4
|
PHX_FEEDBACK_FOR,
|
|
5
|
-
|
|
5
|
+
PHX_PRUNE,
|
|
6
6
|
PHX_ROOT_ID,
|
|
7
7
|
PHX_SESSION,
|
|
8
8
|
PHX_SKIP,
|
|
@@ -44,7 +44,8 @@ export default class DOMPatch {
|
|
|
44
44
|
this.cidPatch = isCid(this.targetCID)
|
|
45
45
|
this.callbacks = {
|
|
46
46
|
beforeadded: [], beforeupdated: [], beforephxChildAdded: [],
|
|
47
|
-
afteradded: [], afterupdated: [], afterdiscarded: [], afterphxChildAdded: []
|
|
47
|
+
afteradded: [], afterupdated: [], afterdiscarded: [], afterphxChildAdded: [],
|
|
48
|
+
aftertransitionsDiscarded: []
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -61,7 +62,7 @@ export default class DOMPatch {
|
|
|
61
62
|
|
|
62
63
|
markPrunableContentForRemoval(){
|
|
63
64
|
DOM.all(this.container, "[phx-update=append] > *, [phx-update=prepend] > *", el => {
|
|
64
|
-
el.setAttribute(
|
|
65
|
+
el.setAttribute(PHX_PRUNE, "")
|
|
65
66
|
})
|
|
66
67
|
}
|
|
67
68
|
|
|
@@ -76,9 +77,11 @@ export default class DOMPatch {
|
|
|
76
77
|
let phxFeedbackFor = liveSocket.binding(PHX_FEEDBACK_FOR)
|
|
77
78
|
let disableWith = liveSocket.binding(PHX_DISABLE_WITH)
|
|
78
79
|
let phxTriggerExternal = liveSocket.binding(PHX_TRIGGER_ACTION)
|
|
80
|
+
let phxRemove = liveSocket.binding("remove")
|
|
79
81
|
let added = []
|
|
80
82
|
let updates = []
|
|
81
83
|
let appendPrependUpdates = []
|
|
84
|
+
let pendingRemoves = []
|
|
82
85
|
let externalFormTriggered = null
|
|
83
86
|
|
|
84
87
|
let diffHTML = liveSocket.time("premorph container prep", () => {
|
|
@@ -99,6 +102,12 @@ export default class DOMPatch {
|
|
|
99
102
|
return el
|
|
100
103
|
},
|
|
101
104
|
onNodeAdded: (el) => {
|
|
105
|
+
// hack to fix Safari handling of img srcset and video tags
|
|
106
|
+
if(el instanceof HTMLImageElement && el.srcset){
|
|
107
|
+
el.srcset = el.srcset
|
|
108
|
+
} else if(el instanceof HTMLVideoElement && el.autoplay){
|
|
109
|
+
el.play()
|
|
110
|
+
}
|
|
102
111
|
if(DOM.isNowTriggerFormExternal(el, phxTriggerExternal)){
|
|
103
112
|
externalFormTriggered = el
|
|
104
113
|
}
|
|
@@ -116,8 +125,12 @@ export default class DOMPatch {
|
|
|
116
125
|
this.trackAfter("discarded", el)
|
|
117
126
|
},
|
|
118
127
|
onBeforeNodeDiscarded: (el) => {
|
|
119
|
-
if(el.getAttribute && el.getAttribute(
|
|
128
|
+
if(el.getAttribute && el.getAttribute(PHX_PRUNE) !== null){ return true }
|
|
120
129
|
if(el.parentNode !== null && DOM.isPhxUpdate(el.parentNode, phxUpdate, ["append", "prepend"]) && el.id){ return false }
|
|
130
|
+
if(el.getAttribute && el.getAttribute(phxRemove)){
|
|
131
|
+
pendingRemoves.push(el)
|
|
132
|
+
return false
|
|
133
|
+
}
|
|
121
134
|
if(this.skipCIDSibling(el)){ return false }
|
|
122
135
|
return true
|
|
123
136
|
},
|
|
@@ -134,6 +147,7 @@ export default class DOMPatch {
|
|
|
134
147
|
this.trackBefore("updated", fromEl, toEl)
|
|
135
148
|
DOM.mergeAttrs(fromEl, toEl, {isIgnored: true})
|
|
136
149
|
updates.push(fromEl)
|
|
150
|
+
DOM.applyStickyOperations(fromEl)
|
|
137
151
|
return false
|
|
138
152
|
}
|
|
139
153
|
if(fromEl.type === "number" && (fromEl.validity && fromEl.validity.badInput)){ return false }
|
|
@@ -142,6 +156,7 @@ export default class DOMPatch {
|
|
|
142
156
|
this.trackBefore("updated", fromEl, toEl)
|
|
143
157
|
updates.push(fromEl)
|
|
144
158
|
}
|
|
159
|
+
DOM.applyStickyOperations(fromEl)
|
|
145
160
|
return false
|
|
146
161
|
}
|
|
147
162
|
|
|
@@ -151,26 +166,28 @@ export default class DOMPatch {
|
|
|
151
166
|
DOM.mergeAttrs(fromEl, toEl, {exclude: [PHX_STATIC]})
|
|
152
167
|
if(prevSession !== ""){ fromEl.setAttribute(PHX_SESSION, prevSession) }
|
|
153
168
|
fromEl.setAttribute(PHX_ROOT_ID, this.rootID)
|
|
169
|
+
DOM.applyStickyOperations(fromEl)
|
|
154
170
|
return false
|
|
155
171
|
}
|
|
156
172
|
|
|
157
173
|
// input handling
|
|
158
174
|
DOM.copyPrivates(toEl, fromEl)
|
|
159
175
|
DOM.discardError(targetContainer, toEl, phxFeedbackFor)
|
|
160
|
-
DOM.syncPropsToAttrs(toEl)
|
|
161
176
|
|
|
162
177
|
let isFocusedFormEl = focused && fromEl.isSameNode(focused) && DOM.isFormInput(fromEl)
|
|
163
|
-
if(isFocusedFormEl
|
|
178
|
+
if(isFocusedFormEl){
|
|
164
179
|
this.trackBefore("updated", fromEl, toEl)
|
|
165
180
|
DOM.mergeFocusedInput(fromEl, toEl)
|
|
166
181
|
DOM.syncAttrsToProps(fromEl)
|
|
167
182
|
updates.push(fromEl)
|
|
183
|
+
DOM.applyStickyOperations(fromEl)
|
|
168
184
|
return false
|
|
169
185
|
} else {
|
|
170
186
|
if(DOM.isPhxUpdate(toEl, phxUpdate, ["append", "prepend"])){
|
|
171
187
|
appendPrependUpdates.push(new DOMPostMorphRestorer(fromEl, toEl, toEl.getAttribute(phxUpdate)))
|
|
172
188
|
}
|
|
173
189
|
DOM.syncAttrsToProps(toEl)
|
|
190
|
+
DOM.applyStickyOperations(toEl)
|
|
174
191
|
this.trackBefore("updated", fromEl, toEl)
|
|
175
192
|
return true
|
|
176
193
|
}
|
|
@@ -191,6 +208,14 @@ export default class DOMPatch {
|
|
|
191
208
|
added.forEach(el => this.trackAfter("added", el))
|
|
192
209
|
updates.forEach(el => this.trackAfter("updated", el))
|
|
193
210
|
|
|
211
|
+
if(pendingRemoves.length > 0){
|
|
212
|
+
liveSocket.transitionRemoves(pendingRemoves)
|
|
213
|
+
liveSocket.requestDOMUpdate(() => {
|
|
214
|
+
pendingRemoves.forEach(el => el.remove())
|
|
215
|
+
this.trackAfter("transitionsDiscarded", pendingRemoves)
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
194
219
|
if(externalFormTriggered){
|
|
195
220
|
liveSocket.disconnect()
|
|
196
221
|
externalFormTriggered.submit()
|
|
@@ -198,11 +223,6 @@ export default class DOMPatch {
|
|
|
198
223
|
return true
|
|
199
224
|
}
|
|
200
225
|
|
|
201
|
-
forceFocusedSelectUpdate(fromEl, toEl){
|
|
202
|
-
let isSelect = ["select", "select-one", "select-multiple"].find((t) => t === fromEl.type)
|
|
203
|
-
return fromEl.multiple === true || (isSelect && fromEl.innerHTML != toEl.innerHTML)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
226
|
isCIDPatch(){ return this.cidPatch }
|
|
207
227
|
|
|
208
228
|
skipCIDSibling(el){
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import DOM from "./dom"
|
|
2
|
+
|
|
3
|
+
let JS = {
|
|
4
|
+
exec(eventType, phxEvent, view, el, defaults){
|
|
5
|
+
let [defaultKind, defaultArgs] = defaults || [null, {}]
|
|
6
|
+
let commands = phxEvent.charAt(0) === "[" ?
|
|
7
|
+
JSON.parse(phxEvent) : [[defaultKind, defaultArgs]]
|
|
8
|
+
|
|
9
|
+
commands.forEach(([kind, args]) => {
|
|
10
|
+
if(kind === defaultKind && defaultArgs.data){
|
|
11
|
+
args.data = Object.assign(args.data || {}, defaultArgs.data)
|
|
12
|
+
}
|
|
13
|
+
this[`exec_${kind}`](eventType, phxEvent, view, el, args)
|
|
14
|
+
})
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
// private
|
|
18
|
+
|
|
19
|
+
// commands
|
|
20
|
+
|
|
21
|
+
exec_dispatch(eventType, phxEvent, view, sourceEl, {to, event, detail}){
|
|
22
|
+
if(to){
|
|
23
|
+
DOM.all(document, to, el => DOM.dispatchEvent(el, event, detail))
|
|
24
|
+
} else {
|
|
25
|
+
DOM.dispatchEvent(sourceEl, event, detail)
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
exec_push(eventType, phxEvent, view, sourceEl, args){
|
|
30
|
+
let {event, data, target, page_loading, loading, value} = args
|
|
31
|
+
let pushOpts = {page_loading: !!page_loading, loading: loading, value: value}
|
|
32
|
+
let targetSrc = eventType === "change" ? sourceEl.form : sourceEl
|
|
33
|
+
let phxTarget = target || targetSrc.getAttribute(view.binding("target")) || targetSrc
|
|
34
|
+
view.withinTargets(phxTarget, (targetView, targetCtx) => {
|
|
35
|
+
if(eventType === "change"){
|
|
36
|
+
let {newCid, _target, callback} = args
|
|
37
|
+
if(_target){ pushOpts._target = _target }
|
|
38
|
+
targetView.pushInput(sourceEl, targetCtx, newCid, event || phxEvent, pushOpts, callback)
|
|
39
|
+
} else if(eventType === "submit"){
|
|
40
|
+
targetView.submitForm(sourceEl, targetCtx, event || phxEvent, pushOpts)
|
|
41
|
+
} else {
|
|
42
|
+
targetView.pushEvent(eventType, sourceEl, targetCtx, event || phxEvent, data, pushOpts)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
exec_add_class(eventType, phxEvent, view, sourceEl, {to, names, transition, time}){
|
|
48
|
+
if(to){
|
|
49
|
+
DOM.all(document, to, el => this.addOrRemoveClasses(el, names, [], transition, time, view))
|
|
50
|
+
} else {
|
|
51
|
+
this.addOrRemoveClasses(sourceEl, names, [], transition, view)
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
exec_remove_class(eventType, phxEvent, view, sourceEl, {to, names, transition, time}){
|
|
56
|
+
if(to){
|
|
57
|
+
DOM.all(document, to, el => this.addOrRemoveClasses(el, [], names, transition, time, view))
|
|
58
|
+
} else {
|
|
59
|
+
this.addOrRemoveClasses(sourceEl, [], names, transition, time, view)
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
exec_transition(eventType, phxEvent, view, sourceEl, {time, to, names}){
|
|
64
|
+
let els = to ? DOM.all(document, to) : [sourceEl]
|
|
65
|
+
els.forEach(el => {
|
|
66
|
+
this.addOrRemoveClasses(el, names, [])
|
|
67
|
+
view.transition(time, () => this.addOrRemoveClasses(el, [], names))
|
|
68
|
+
})
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
exec_toggle(eventType, phxEvent, view, sourceEl, {to, display, ins, outs, time}){
|
|
72
|
+
if(to){
|
|
73
|
+
DOM.all(document, to, el => this.toggle(eventType, view, el, display, ins || [], outs || [], time))
|
|
74
|
+
} else {
|
|
75
|
+
this.toggle(eventType, view, sourceEl, display, ins || [], outs || [], time)
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
exec_show(eventType, phxEvent, view, sourceEl, {to, display, transition, time}){
|
|
80
|
+
if(to){
|
|
81
|
+
DOM.all(document, to, el => this.show(eventType, view, el, display, transition, time))
|
|
82
|
+
} else {
|
|
83
|
+
this.show(eventType, view, sourceEl, transition, time)
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
exec_hide(eventType, phxEvent, view, sourceEl, {to, display, transition, time}){
|
|
88
|
+
if(to){
|
|
89
|
+
DOM.all(document, to, el => this.hide(eventType, view, el, display, transition, time))
|
|
90
|
+
} else {
|
|
91
|
+
this.hide(eventType, view, sourceEl, display, transition, time)
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
// utils for commands
|
|
96
|
+
|
|
97
|
+
show(eventType, view, el, display, transition, time){
|
|
98
|
+
let isVisible = this.isVisible(el)
|
|
99
|
+
if(transition.length > 0 && !isVisible){
|
|
100
|
+
this.toggle(eventType, view, el, display, transition, [], time)
|
|
101
|
+
} else if(!isVisible){
|
|
102
|
+
this.toggle(eventType, view, el, display, [], [], null)
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
hide(eventType, view, el, display, transition, time){
|
|
107
|
+
let isVisible = this.isVisible(el)
|
|
108
|
+
if(transition.length > 0 && isVisible){
|
|
109
|
+
this.toggle(eventType, view, el, display, [], transition, time)
|
|
110
|
+
} else if(isVisible){
|
|
111
|
+
this.toggle(eventType, view, el, display, [], [], time)
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
toggle(eventType, view, el, display, in_classes, out_classes, time){
|
|
116
|
+
if(in_classes.length > 0 || out_classes.length > 0){
|
|
117
|
+
if(this.isVisible(el)){
|
|
118
|
+
this.addOrRemoveClasses(el, out_classes, in_classes)
|
|
119
|
+
view.transition(time, () => {
|
|
120
|
+
DOM.putSticky(el, "toggle", currentEl => currentEl.style.display = "none")
|
|
121
|
+
this.addOrRemoveClasses(el, [], out_classes)
|
|
122
|
+
})
|
|
123
|
+
} else {
|
|
124
|
+
if(eventType === "remove"){ return }
|
|
125
|
+
this.addOrRemoveClasses(el, in_classes, out_classes)
|
|
126
|
+
DOM.putSticky(el, "toggle", currentEl => currentEl.style.display = (display || "block"))
|
|
127
|
+
view.transition(time, () => {
|
|
128
|
+
this.addOrRemoveClasses(el, [], in_classes)
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
let newDisplay = this.isVisible(el) ? "none" : (display || "block")
|
|
133
|
+
DOM.putSticky(el, "toggle", currentEl => currentEl.style.display = newDisplay)
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
addOrRemoveClasses(el, adds, removes, transition, time, view){
|
|
138
|
+
if(transition && transition.length > 0){
|
|
139
|
+
this.addOrRemoveClasses(el, transition, [])
|
|
140
|
+
return view.transition(time, () => this.addOrRemoveClasses(el, adds, removes.concat(transition)))
|
|
141
|
+
}
|
|
142
|
+
window.requestAnimationFrame(() => {
|
|
143
|
+
let [prevAdds, prevRemoves] = DOM.getSticky(el, "classes", [[], []])
|
|
144
|
+
let keepAdds = adds.filter(name => prevAdds.indexOf(name) < 0 && !el.classList.contains(name))
|
|
145
|
+
let keepRemoves = removes.filter(name => prevRemoves.indexOf(name) < 0 && el.classList.contains(name))
|
|
146
|
+
let newAdds = prevAdds.filter(name => removes.indexOf(name) < 0).concat(keepAdds)
|
|
147
|
+
let newRemoves = prevRemoves.filter(name => adds.indexOf(name) < 0).concat(keepRemoves)
|
|
148
|
+
|
|
149
|
+
DOM.putSticky(el, "classes", currentEl => {
|
|
150
|
+
currentEl.classList.remove(...newRemoves)
|
|
151
|
+
currentEl.classList.add(...newAdds)
|
|
152
|
+
return [newAdds, newRemoves]
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
hasAllClasses(el, classes){ return classes.every(name => el.classList.contains(name)) },
|
|
158
|
+
|
|
159
|
+
isVisible(el){
|
|
160
|
+
let style = window.getComputedStyle(el)
|
|
161
|
+
return !(style.opacity === 0 || style.display === "none")
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
isToggledOut(el, out_classes){
|
|
165
|
+
return !this.isVisible(el) || this.hasAllClasses(el, out_classes)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export default JS
|