-
Notifications
You must be signed in to change notification settings - Fork 65
/
Copy pathDom.elm
384 lines (266 loc) · 11.5 KB
/
Dom.elm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
module Browser.Dom exposing
( focus, blur, Error(..)
, getViewport, Viewport, getViewportOf
, setViewport, setViewportOf
, getElement, Element
)
{-| This module allows you to manipulate the DOM in various ways. It covers:
- Focus and blur input elements.
- Get the `width` and `height` of elements.
- Get the `x` and `y` coordinates of elements.
- Figure out the scroll position.
- Change the scroll position!
We use different terminology than JavaScript though...
# Terminology
Have you ever thought about how “scrolling” is a metaphor about
scrolls? Like hanging scrolls of caligraphy made during the Han Dynasty
in China?
This metaphor falls apart almost immediately though. For example, many scrolls
read horizontally! Like a [Sefer Torah][torah] or [Chinese Handscrolls][hand].
The two sides move independently, sometimes kept in place with stones. What is
a scroll bar in this world? And [hanging scrolls][hang] (which _are_ displayed
vertically) do not “scroll” at all! They hang!
So in JavaScript, we start with a badly stretched metaphor and add a bunch of
DOM details like padding, borders, and margins. How do those relate to scrolls?
For example, JavaScript has `clientWidth`. Client like a feudal state that pays
tribute to the emperor? And `offsetHeight`. Can an offset even have height? And
what has that got to do with scrolls?
So instead of inheriting this metaphorical hodge-podge, we use terminology from
3D graphics. You have a **scene** containing all your elements and a **viewport**
into the scene. I think it ends up being a lot clearer, but you can evaluate
for yourself when you see the diagrams later!
**Note:** For more scroll facts, I recommend [A Day on the Grand Canal with
the Emperor of China or: Surface Is Illusion But So Is Depth][doc] where David
Hockney explores the history of _perspective_ in art. Really interesting!
[torah]: https://en.wikipedia.org/wiki/Sefer_Torah
[hand]: https://www.metmuseum.org/toah/hd/chhs/hd_chhs.htm
[hang]: https://en.wikipedia.org/wiki/Hanging_scroll
[doc]: https://www.imdb.com/title/tt0164525/
# Focus
@docs focus, blur, Error
# Get Viewport
@docs getViewport, Viewport, getViewportOf
# Set Viewport
@docs setViewport, setViewportOf
# Position
@docs getElement, Element
-}
import Elm.Kernel.Browser
import Task exposing (Task)
-- FOCUS
{-| Find a DOM node by `id` and focus on it. So if you wanted to focus a node
like `<input type="text" id="search-box">` you could say:
import Browser.Dom as Dom
import Task
type Msg
= NoOp
focusSearchBox : Cmd Msg
focusSearchBox =
Task.attempt (\_ -> NoOp) (Dom.focus "search-box")
Notice that this code ignores the possibility that `search-box` is not used
as an `id` by any node, failing silently in that case. It would be better to
log the failure with whatever error reporting system you use.
-}
focus : String -> Task Error ()
focus =
Elm.Kernel.Browser.call "focus"
{-| Find a DOM node by `id` and make it lose focus. So if you wanted a node
like `<input type="text" id="search-box">` to lose focus you could say:
import Browser.Dom as Dom
import Task
type Msg
= NoOp
unfocusSearchBox : Cmd Msg
unfocusSearchBox =
Task.attempt (\_ -> NoOp) (Dom.blur "search-box")
Notice that this code ignores the possibility that `search-box` is not used
as an `id` by any node, failing silently in that case. It would be better to
log the failure with whatever error reporting system you use.
-}
blur : String -> Task Error ()
blur =
Elm.Kernel.Browser.call "blur"
-- ERROR
{-| Many functions in this module look up DOM nodes up by their `id`. If you
ask for an `id` that is not in the DOM, you will get this error.
-}
type Error
= NotFound String
-- VIEWPORT
{-| Get information on the current viewport of the browser.

If you want to move the viewport around (i.e. change the scroll position) you
can use [`setViewport`](#setViewport) which change the `x` and `y` of the
viewport.
-}
getViewport : Task x Viewport
getViewport =
Elm.Kernel.Browser.withWindow Elm.Kernel.Browser.getViewport
{-| All the information about the current viewport.

-}
type alias Viewport =
{ scene :
{ width : Float
, height : Float
}
, viewport :
{ x : Float
, y : Float
, width : Float
, height : Float
}
}
{-| Just like `getViewport`, but for any scrollable DOM node. Say we have an
application with a chat box in the bottom right corner like this:

There are probably a whole bunch of messages that are not being shown. You
could scroll up to see them all. Well, we can think of that chat box is a
viewport into a scene!

This can be useful with [`setViewportOf`](#setViewportOf) to make sure new
messages always appear on the bottom.
The viewport size _does not_ include the border or margins.
**Note:** This data is collected from specific fields in JavaScript, so it
may be helpful to know that:
- `scene.width` = [`scrollWidth`][sw]
- `scene.height` = [`scrollHeight`][sh]
- `viewport.x` = [`scrollLeft`][sl]
- `viewport.y` = [`scrollTop`][st]
- `viewport.width` = [`clientWidth`][cw]
- `viewport.height` = [`clientHeight`][ch]
Neither [`offsetWidth`][ow] nor [`offsetHeight`][oh] are available. The theory
is that (1) the information can always be obtained by using `getElement` on a
node without margins, (2) no cases came to mind where you actually care in the
first place, and (3) it is available through ports if it is really needed.
If you have a case that really needs it though, please share your specific
scenario in an issue! Nicely presented case studies are the raw ingredients for
API improvements!
[sw]: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollWidth
[sh]: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight
[st]: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTop
[sl]: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft
[cw]: https://developer.mozilla.org/en-US/docs/Web/API/Element/clientWidth
[ch]: https://developer.mozilla.org/en-US/docs/Web/API/Element/clientHeight
[ow]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetWidth
[oh]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetHeight
-}
getViewportOf : String -> Task Error Viewport
getViewportOf =
Elm.Kernel.Browser.getViewportOf
-- SET VIEWPORT
{-| Change the `x` and `y` offset of the browser viewport immediately. For
example, you could make a command to jump to the top of the page:
import Browser.Dom as Dom
import Task
type Msg
= NoOp
resetViewport : Cmd Msg
resetViewport =
Task.perform (\_ -> NoOp) (Dom.setViewport 0 0)
This sets the viewport offset to zero.
This could be useful with `Browser.application` where you may want to reset
the viewport when the URL changes. Maybe you go to a “new page”
and want people to start at the top!
-}
setViewport : Float -> Float -> Task x ()
setViewport =
Elm.Kernel.Browser.setViewport
{-| Change the `x` and `y` offset of a DOM node’s viewport by ID. This
is common in text messaging and chat rooms, where once the messages fill the
screen, you want to always be at the very bottom of the message chain. This
way the latest message is always on screen! You could do this:
import Browser.Dom as Dom
import Task
type Msg
= NoOp
jumpToBottom : String -> Cmd Msg
jumpToBottom id =
Dom.getViewportOf id
|> Task.andThen (\info -> Dom.setViewportOf id 0 info.scene.height)
|> Task.attempt (\_ -> NoOp)
So you could call `jumpToBottom "chat-box"` whenever you add a new message.
**Note 1:** What happens if the viewport is placed out of bounds? Where there
is no `scene` to show? To avoid this question, the `x` and `y` offsets are
clamped such that the viewport is always fully within the `scene`. So when
`jumpToBottom` sets the `y` offset of the viewport to the `height` of the
`scene` (i.e. too far!) it relies on this clamping behavior to put the viewport
back in bounds.
**Note 2:** The example ignores when the element ID is not found, but it would
be great to log that information. It means there may be a bug or a dead link
somewhere!
-}
setViewportOf : String -> Float -> Float -> Task Error ()
setViewportOf =
Elm.Kernel.Browser.setViewportOf
{--SLIDE VIEWPORT
{-| Change the `x` and `y` offset of the viewport with an animation. In JS,
this corresponds to setting [`scroll-behavior`][sb] to `smooth`.
This can definitely be overused, so try to use it specifically when you want
the user to be spatially situated in a scene. For example, a “back to
top” button might use it:
import Browser.Dom as Dom
import Task
type Msg = NoOp
backToTop : Cmd Msg
backToTop =
Task.perform (\_ -> NoOp) (Dom.slideViewport 0 0)
Be careful when paring this with `Browser.application`. When the URL changes
and a whole new scene is going to be rendered, using `setViewport` is probably
best. If you are moving within a scene, you may benefit from a mix of
`setViewport` and `slideViewport`. Sliding to the top is nice, but sliding
around everywhere is probably annoying.
[sb]: https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-behavior
-}
slideViewport : Float -> Float -> Task x ()
slideViewport =
Debug.todo "slideViewport"
slideViewportOf : String -> Float -> Float -> Task Error ()
slideViewportOf =
Debug.todo "slideViewportOf"
--}
-- ELEMENT
{-| Get position information about specific elements. Say we put
`id "jesting-aside"` on the seventh paragraph of the text. When we call
`getElement "jesting-aside"` we would get the following information:

This can be useful for:
- **Scrolling** — Pair this information with `setViewport` to scroll
specific elements into view. This gives you a lot of control over where exactly
the element would be after the viewport moved.
- **Drag and Drop** — As of this writing, `touchmove` events do not tell
you which element you are currently above. To figure out if you have dragged
something over the target, you could see if the `pageX` and `pageY` of the
touch are inside the `x`, `y`, `width`, and `height` of the target element.
**Note:** This corresponds to JavaScript’s [`getBoundingClientRect`][gbcr],
so **the element’s margins are included in its `width` and `height`**.
With scrolling, maybe you want to include the margins. With drag-and-drop, you
probably do not, so some folks set the margins to zero and put the target
element in a `<div>` that adds the spacing. Just something to be aware of!
[gbcr]: https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
-}
getElement : String -> Task Error Element
getElement =
Elm.Kernel.Browser.getElement
{-| A bunch of information about the position and size of an element relative
to the overall scene.

-}
type alias Element =
{ scene :
{ width : Float
, height : Float
}
, viewport :
{ x : Float
, y : Float
, width : Float
, height : Float
}
, element :
{ x : Float
, y : Float
, width : Float
, height : Float
}
}