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 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 straighforward. There are two main differences.
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 compatiblity with latest Plug
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 compatiblity with pre-0.13 behaviour, you can provide the following metadata option:
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` targetting a sibling LiveView component with the same component ID
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
- - Automatially clear the flash on live navigation examples - only the newly assigned flash is persisted
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 specfic css classes
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 targetting LiveView and LiveComponent events from the client
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 PHX_REMOVE = "data-phx-remove"
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-disconnected"
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] = clone(source[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
- PHX_REMOVE,
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(PHX_REMOVE, "")
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(PHX_REMOVE) !== null){ return true }
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 && !this.forceFocusedSelectUpdate(fromEl, toEl)){
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