├── .gitignore
├── LICENSE
├── README.md
├── elm.json
├── examples
├── README.md
├── elm.json
└── src
│ └── Drag.elm
├── notes
├── chat.svg
├── getElement.svg
├── getViewport.svg
├── getViewportOf.svg
├── keyboard.md
└── navigation-in-elements.md
└── src
├── Browser.elm
├── Browser
├── AnimationManager.elm
├── Dom.elm
├── Events.elm
└── Navigation.elm
├── Debugger
├── Expando.elm
├── History.elm
├── Main.elm
├── Metadata.elm
├── Overlay.elm
└── Report.elm
└── Elm
└── Kernel
├── Browser.js
├── Browser.server.js
└── Debugger.js
/.gitignore:
--------------------------------------------------------------------------------
1 | elm-stuff
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2017-present Evan Czaplicki
2 |
3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4 |
5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6 |
7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8 |
9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10 |
11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Elm in the Browser!
2 |
3 | This package allows you to create Elm programs that run in browsers.
4 |
5 |
6 | ## Learning Path
7 |
8 | **I highly recommend working through [guide.elm-lang.org][guide] to learn how to use Elm.** It is built around a learning path that introduces concepts gradually.
9 |
10 | [guide]: https://guide.elm-lang.org/
11 |
12 | You can see the outline of that learning path in the `Browser` module. It lets you create Elm programs with the following functions:
13 |
14 | 1. [`sandbox`](https://package.elm-lang.org/packages/elm/browser/latest/Browser#sandbox) — react to user input, like buttons and checkboxes
15 | 2. [`element`](https://package.elm-lang.org/packages/elm/browser/latest/Browser#element) — talk to the outside world, like HTTP and JS interop
16 | 3. [`document`](https://package.elm-lang.org/packages/elm/browser/latest/Browser#document) — control the `
` and ``
17 | 4. [`application`](https://package.elm-lang.org/packages/elm/browser/latest/Browser#application) — create single-page apps
18 |
19 | This order works well because important concepts and techniques are introduced at each stage. If you jump ahead, it is like building a house by starting with the roof! So again, **work through [guide.elm-lang.org][guide] to see examples and really *understand* how Elm works!**
20 |
21 | This order also works well because it mirrors how most people introduce Elm at work. Start small. Try using Elm in a single element in an existing JavaScript project. If that goes well, try doing a bit more. Etc.
22 |
--------------------------------------------------------------------------------
/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "package",
3 | "name": "elm/browser",
4 | "summary": "Run Elm in browsers, with access to browser history for single-page apps (SPAs)",
5 | "license": "BSD-3-Clause",
6 | "version": "1.0.2",
7 | "exposed-modules": [
8 | "Browser",
9 | "Browser.Dom",
10 | "Browser.Events",
11 | "Browser.Navigation"
12 | ],
13 | "elm-version": "0.19.0 <= v < 0.20.0",
14 | "dependencies": {
15 | "elm/core": "1.0.0 <= v < 2.0.0",
16 | "elm/html": "1.0.0 <= v < 2.0.0",
17 | "elm/json": "1.0.0 <= v < 2.0.0",
18 | "elm/time": "1.0.0 <= v < 2.0.0",
19 | "elm/url": "1.0.0 <= v < 2.0.0",
20 | "elm/virtual-dom": "1.0.2 <= v < 2.0.0"
21 | },
22 | "test-dependencies": {}
23 | }
24 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | To compile these examples locally, run the following terminal commands:
4 |
5 | ```
6 | git clone https://github.com/elm/browser.git
7 | cd browser/examples
8 | elm reactor
9 | ```
10 |
11 | Now go to [`http://localhost:8000`](http://localhost:8000) and navigate to whatever Elm file you want to see.
12 |
--------------------------------------------------------------------------------
/examples/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "application",
3 | "source-directories": [
4 | "src"
5 | ],
6 | "elm-version": "0.19.1",
7 | "dependencies": {
8 | "direct": {
9 | "elm/browser": "1.0.2",
10 | "elm/core": "1.0.2",
11 | "elm/html": "1.0.0",
12 | "elm/json": "1.1.3"
13 | },
14 | "indirect": {
15 | "elm/time": "1.0.0",
16 | "elm/url": "1.0.0",
17 | "elm/virtual-dom": "1.0.2"
18 | }
19 | },
20 | "test-dependencies": {
21 | "direct": {},
22 | "indirect": {}
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/examples/src/Drag.elm:
--------------------------------------------------------------------------------
1 | module Drag exposing (main)
2 |
3 | {- You know how the editor on the Elm website has two side-by-side panels that
4 | can be resized? This is a rough implementation of that sort of thing.
5 |
6 | APPROACH:
7 | 1. Have a normal "mousedown" event on the drag zone.
8 | 2. When a drag begins, listen for global onMouseMove and onMouseUp events.
9 | 3. Check which buttons are down on mouse moves to detect a weird scenario.
10 |
11 | -}
12 |
13 |
14 | import Browser
15 | import Browser.Events as E
16 | import Html exposing (..)
17 | import Html.Attributes exposing (..)
18 | import Html.Events exposing (..)
19 | import Json.Decode as D
20 |
21 |
22 |
23 | -- MAIN
24 |
25 |
26 | main =
27 | Browser.element
28 | { init = init
29 | , view = view
30 | , update = update
31 | , subscriptions = subscriptions
32 | }
33 |
34 |
35 |
36 | -- MODEL
37 |
38 |
39 | type alias Model =
40 | { dragState : DragState
41 | }
42 |
43 |
44 | type DragState
45 | = Static Float
46 | | Moving Float
47 |
48 |
49 | init : () -> (Model, Cmd Msg)
50 | init _ =
51 | ( { dragState = Static 0.5 }
52 | , Cmd.none
53 | )
54 |
55 |
56 |
57 | -- UPDATE
58 |
59 |
60 | type Msg
61 | = DragStart
62 | | DragMove Bool Float
63 | | DragStop Float
64 |
65 |
66 | update : Msg -> Model -> (Model, Cmd Msg)
67 | update msg model =
68 | case msg of
69 | DragStart ->
70 | ( { model | dragState = Moving (toFraction model.dragState) }
71 | , Cmd.none
72 | )
73 |
74 | DragMove isDown fraction ->
75 | ( { model | dragState = if isDown then Moving fraction else Static (toFraction model.dragState) }
76 | , Cmd.none
77 | )
78 |
79 | DragStop fraction ->
80 | ( { model | dragState = Static fraction }
81 | , Cmd.none
82 | )
83 |
84 |
85 | toFraction : DragState -> Float
86 | toFraction dragState =
87 | case dragState of
88 | Static fraction -> fraction
89 | Moving fraction -> fraction
90 |
91 |
92 |
93 | -- VIEW
94 |
95 |
96 | view : Model -> Html Msg
97 | view model =
98 | let
99 | fraction = toFraction model.dragState
100 | pointerEvents = toPointerEvents model.dragState
101 | in
102 | div
103 | [ style "margin" "0"
104 | , style "padding" "0"
105 | , style "width" "100vw"
106 | , style "height" "100vh"
107 | , style "font-family" "monospace"
108 | , style "display" "flex"
109 | , style "flex-direction" "row"
110 | ]
111 | [ viewPanel "#222" "#ddd" fraction pointerEvents
112 | , viewDragZone fraction
113 | , viewPanel "#ddd" "#222" (1 - fraction) pointerEvents
114 | ]
115 |
116 |
117 | toPointerEvents : DragState -> String
118 | toPointerEvents dragState =
119 | case dragState of
120 | Static _ -> "auto"
121 | Moving _ -> "none"
122 |
123 |
124 |
125 | {- The "user-select" and "pointer-event" properties are "none" when resizing,
126 | ensuring that text does not get highlighted as the mouse moves.
127 | -}
128 | viewPanel : String -> String -> Float -> String -> Html msg
129 | viewPanel background foreground fraction pointerEvents =
130 | div
131 | [ style "width" (String.fromFloat (100 * fraction) ++ "vw")
132 | , style "height" "100vh"
133 | , style "user-select" pointerEvents
134 | , style "pointer-events" pointerEvents
135 | --
136 | , style "background-color" background
137 | , style "color" foreground
138 | , style "font-size" "3em"
139 | --
140 | , style "display" "flex"
141 | , style "justify-content" "center"
142 | , style "align-items" "center"
143 | ]
144 | [ text (String.left 5 (String.fromFloat (100 * fraction)) ++ "%")
145 | ]
146 |
147 |
148 |
149 | -- VIEW DRAG ZONE
150 |
151 |
152 | {- This does a few tricks to create an invisible drag zone:
153 |
154 | 1. "z-index" is a high number so that this node is in front of both panels.
155 | 2. "width" is 10px so there is something to grab onto.
156 | 3. "position" is absolute so the "width" does not disrupt the panels.
157 | 4. "margin-left" is -5px such that this node overhangs both panels.
158 |
159 | You could avoid the 4th trick by setting "left" to "calc(50vw - 5px)" but I
160 | do not know if there is a strong benefit to one approach or the other.
161 | -}
162 | viewDragZone : Float -> Html Msg
163 | viewDragZone fraction =
164 | div
165 | [ style "position" "absolute"
166 | , style "top" "0"
167 | , style "left" (String.fromFloat (100 * fraction) ++ "vw")
168 | , style "width" "10px"
169 | , style "height" "100vh"
170 | , style "margin-left" "-5px"
171 | , style "cursor" "col-resize"
172 | , style "z-index" "10"
173 | , on "mousedown" (D.succeed DragStart)
174 | ]
175 | []
176 |
177 |
178 |
179 | -- SUBSCRIPTIONS
180 |
181 |
182 | {- We listen for the "mousemove" and "mouseup" events for the whole window.
183 | This way we catch all events, even if they are not on our drag zone.
184 |
185 | Listening for mouse moves is costly though, so we only listen if there is an
186 | ongoing drag.
187 | -}
188 | subscriptions : Model -> Sub Msg
189 | subscriptions model =
190 | case model.dragState of
191 | Static _ ->
192 | Sub.none
193 |
194 | Moving _ ->
195 | Sub.batch
196 | [ E.onMouseMove (D.map2 DragMove decodeButtons decodeFraction)
197 | , E.onMouseUp (D.map DragStop decodeFraction)
198 | ]
199 |
200 |
201 | {- The goal here is to get (mouse x / window width) on each mouse event. So if
202 | the mouse is at 500px and the screen is 1000px wide, we should get 0.5 from this.
203 |
204 | Getting the mouse x is not too hard, but getting window width is a bit tricky.
205 | We want the window.innerWidth value, which happens to be available at:
206 |
207 | event.currentTarget.defaultView.innerWidth
208 |
209 | The value at event.currentTarget is the document in these cases, but this will
210 | not work if you have a or a
with a normal elm/html event handler.
211 | So if currentTarget is NOT the document, you should instead get the value at:
212 |
213 | event.currentTarget.ownerDocument.defaultView.innerWidth
214 | ^^^^^^^^^^^^^
215 | -}
216 | decodeFraction : D.Decoder Float
217 | decodeFraction =
218 | D.map2 (/)
219 | (D.field "pageX" D.float)
220 | (D.at ["currentTarget","defaultView","innerWidth"] D.float)
221 |
222 |
223 | {- What happens when the user is dragging, but the "mouse up" occurs outside
224 | the browser window? We need to stop listening for mouse movement and end the
225 | drag. We use MouseEvent.buttons to detect this:
226 |
227 | https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
228 |
229 | The "buttons" value is 1 when "left-click" is pressed, so we use that to
230 | detect zombie drags.
231 | -}
232 | decodeButtons : D.Decoder Bool
233 | decodeButtons =
234 | D.field "buttons" (D.map (\buttons -> buttons == 1) D.int)
235 |
--------------------------------------------------------------------------------
/notes/chat.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/notes/keyboard.md:
--------------------------------------------------------------------------------
1 | # Which key was pressed?
2 |
3 | When you're listening for global keyboard events, you very likely want to know *which* key was pressed. Unfortunately different browsers implement the [`KeyboardEvent`][ke] values in different ways, so there is no one-size-fits-all solution.
4 |
5 | [ke]: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
6 |
7 | ## `charCode` vs `keyCode` vs `which` vs `key` vs `code`
8 |
9 | As of this writing, it seems that the `KeyboardEvent` API recommends using [`key`][key]. It can tell you which symbol was pressed, taking keyboard layout into account. So it will tell you if it was a `x`, `か`, `ø`, `β`, etc.
10 |
11 | [key]: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
12 |
13 | According to [the docs][ke], everything else is deprecated. So `charCode`, `keyCode`, and `which` are only useful if you need to support browsers besides [these](http://caniuse.com/#feat=keyboardevent-key).
14 |
15 |
16 | ## Writing a `key` decoder
17 |
18 | The simplest approach is to just decode the string value:
19 |
20 | ```elm
21 | import Json.Decode as Decode
22 |
23 | keyDecoder : Decode.Decoder String
24 | keyDecoder =
25 | Decode.field "key" Decode.string
26 | ```
27 |
28 | Depending on your scenario, you may want something more elaborate though!
29 |
30 |
31 | ### Decoding for User Input
32 |
33 | If you are handling user input, maybe you want to distinguish actual characters from all the different [key values](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values) that may be produced for non-character keys. This way pressing `h` then `i` then `Backspace` does not turn into `"hiBackspace"`. You could do this:
34 |
35 | ```elm
36 | import Json.Decode as Decode
37 |
38 | type Key
39 | = Character Char
40 | | Control String
41 |
42 | keyDecoder : Decode.Decoder Key
43 | keyDecoder =
44 | Decode.map toKey (Decode.field "key" Decode.string)
45 |
46 | toKey : String -> Key
47 | toKey string =
48 | case String.uncons string of
49 | Just (char, "") ->
50 | Character char
51 |
52 | _ ->
53 | Control string
54 | ```
55 |
56 | > **Note:** The `String.uncons` function chomps surrogate pairs properly, so it works with characters outside of the BMP. If that does not mean anything to you, you are lucky! In summary, a tricky character encoding problem of JavaScript is taken care of with this code and you do not need to worry about it. Congratulations!
57 |
58 |
59 | ### Decoding for Games
60 |
61 | Or maybe you want to handle left and right arrows specially for a game or a presentation viewer. You could do something like this:
62 |
63 | ```elm
64 | import Json.Decode as Decode
65 |
66 | type Direction
67 | = Left
68 | | Right
69 | | Other
70 |
71 | keyDecoder : Decode.Decoder Direction
72 | keyDecoder =
73 | Decode.map toDirection (Decode.field "key" Decode.string)
74 |
75 | toDirection : String -> Direction
76 | toDirection string =
77 | case string of
78 | "ArrowLeft" ->
79 | Left
80 |
81 | "ArrowRight" ->
82 | Right
83 |
84 | _ ->
85 | Other
86 | ```
87 |
88 | By converting to a specialized `Direction` type, the compiler can guarantee that you never forget to handle one of the valid inputs. If it was a `String`, new code could have typos or missing branches that would be hard to find.
89 |
90 | Hope that helps you write a decoder that works for your scenario!
91 |
--------------------------------------------------------------------------------
/notes/navigation-in-elements.md:
--------------------------------------------------------------------------------
1 | # How do I manage URL from a `Browser.element`?
2 |
3 | Many companies introduce Elm gradually. They use `Browser.element` to embed Elm in a larger codebase as a low-risk way to see if Elm is helpful. If so, great, do more! If not, just revert, no big deal.
4 |
5 | But at some companies the element has grown to manage _almost_ the whole page. Everything except the header and footer, which are produced by the server. And at that time, you may want Elm to start managing URL changes, showing different things in different cases. Well, `Browser.application` lets you do that in Elm, but maybe you have a bunch of legacy code that still needs the header and footer to be created on the server, so `Browser.element` is the only option.
6 |
7 | What do you do?
8 |
9 |
10 | ## Managing the URL from `Browser.element`
11 |
12 | You would initialize your element like this:
13 |
14 | ```javascript
15 | // Initialize your Elm program
16 | var app = Elm.Main.init({
17 | flags: location.href,
18 | node: document.getElementById('elm-main')
19 | });
20 |
21 | // Inform app of browser navigation (the BACK and FORWARD buttons)
22 | window.addEventListener('popstate', function () {
23 | app.ports.onUrlChange.send(location.href);
24 | });
25 |
26 | // Change the URL upon request, inform app of the change.
27 | app.ports.pushUrl.subscribe(function(url) {
28 | history.pushState({}, '', url);
29 | app.ports.onUrlChange.send(location.href);
30 | });
31 | ```
32 |
33 | Now the important thing is that you can handle other things in these two event listeners. Maybe your header is sensitive to the URL as well? This is where you manage
34 | anything like that.
35 |
36 | From there, your Elm code would look something like this:
37 |
38 | ```elm
39 | import Browser
40 | import Html exposing (..)
41 | import Html.Attributes exposing (..)
42 | import Html.Events exposing (..)
43 | import Json.Decode as D
44 | import Url
45 | import Url.Parser as Url
46 |
47 |
48 | main : Program String Model Msg
49 | main =
50 | Browser.element
51 | { init = init
52 | , view = view
53 | , update = update
54 | , subscriptions = subscriptions
55 | }
56 |
57 |
58 | type Msg = UrlChanged (Maybe Route) | ...
59 |
60 |
61 | -- INIT
62 |
63 | init : String -> ( Model, Cmd Msg )
64 | init locationHref =
65 | ...
66 |
67 |
68 | -- SUBSCRIPTION
69 |
70 | subscriptions : Model -> Sub Msg
71 | subscriptions model =
72 | onUrlChange (locationHrefToRoute >> UrlChanged)
73 |
74 |
75 | -- NAVIGATION
76 |
77 | port onUrlChange : (String -> msg) -> Sub msg
78 |
79 | port pushUrl : String -> Cmd msg
80 |
81 | link : msg -> List (Attribute msg) -> List (Html msg) -> Html msg
82 | link href attrs children =
83 | a (preventDefaultOn "click" (D.succeed (href, True)) :: attrs) children
84 |
85 | locationHrefToRoute : String -> Maybe Route
86 | locationHrefToRoute locationHref =
87 | case Url.fromString locationHref of
88 | Nothing -> Nothing
89 | Just url -> Url.parse myParser url
90 |
91 | -- myParser : Url.Parser (Route -> Route) Route
92 | ```
93 |
94 | So in contrast with `Browser.application`, you have to manage the URL yourself in JavaScript. What is up with that?!
95 |
96 |
97 | ## Justification
98 |
99 | The justification is that (1) this will lead to more reliable programs overall and (2) other designs do not save significant amounts of code. We will explore both in order.
100 |
101 |
102 | ### Reliability
103 |
104 | There are some Elm users that have many different technologies embedded in the same document. So imagine we have a header in React, charts in Elm, and a data entry interface in Angular.
105 |
106 | For URL management to work here, all three of these things need to agree on what page they are on. So the most reliable design is to have one `popstate` listener on the very outside. It would tell React, Elm, and Angular what to do. This gives you a guarantee that they are all in agreement about the current page. Similarly, they would all send messages out requesting a `pushState` such that everyone is informed of any changes.
107 |
108 | If each project was reacting to the URL internally, synchronization bugs would inevitably arise. Maybe it was a static page, but it upgraded to have the URL change. You added that to your Elm, but what about the Angular and React elements. What happens to them? Probably people forget and it is just a confusing bug. So having one `popstate` makes it obvious that there is a decision to make here. And what happens when React starts producing URLs that Angular and Elm have never heard of? Do those elements show some sort of 404 page?
109 |
110 | > **Note:** If you wanted you could send the `location.href` into a `Platform.worker` to do the URL parsing in Elm. Once you have nice data, you could send it out a port for all the different elements on your page.
111 |
112 |
113 | ### Lines of Code
114 |
115 | So the decision is primarily motivated by the fact that **URL management should happen at the highest possible level for reliability**, but what if Elm is the only thing on screen? How many lines extra are those people paying?
116 |
117 | Well, the JavaScript code would be something like this:
118 |
119 | ```javascript
120 | var app = Elm.Main.init({
121 | flags: ...
122 | });
123 | ```
124 |
125 | And in Elm:
126 |
127 | ```elm
128 | import Browser
129 | import Browser.Navigation as Nav
130 | import Url
131 | import Url.Parser as Url
132 |
133 |
134 | main : Program Flags Model Msg
135 | main =
136 | Browser.application
137 | { init = init
138 | , view = view
139 | , update = update
140 | , subscriptions = subscriptions
141 | , onUrlChange = UrlChanged
142 | , onUrlRequest = LinkClicked
143 | }
144 |
145 |
146 | type Msg = UrlChanged (Maybe Route) | ...
147 |
148 |
149 | -- INIT
150 |
151 | init : Flags -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
152 | init flags url key =
153 | ...
154 |
155 |
156 | -- SUBSCRIPTION
157 |
158 | subscriptions : Model -> Sub Msg
159 | subscriptions model =
160 | Sub.none
161 |
162 |
163 | -- NAVIGATION
164 |
165 | urlToRoute : Url.Url -> Maybe Route
166 | urlToRoute url =
167 | Url.parse myParser url
168 |
169 | -- myParser : Url.Parser (Route -> Route) Route
170 | ```
171 |
172 | So the main differences are:
173 |
174 | 1. You can delete the ports in JavaScript (seven lines)
175 | 2. `port onUrlChanged` becomes `onUrlChanged` in `main` (zero lines)
176 | 3. `locationHrefToRoute` becomes `urlToRoute` (three lines)
177 | 4. `link` becomes `onUrlRequest` and handling code in `update` (depends)
178 |
179 | So we are talking about maybe twenty lines of code that go away in the `application` version? And each line has a very clear purpose, allowing you to customize and synchronize based on your exact application. Maybe you only want the hash because you support certain IE browsers? Change the `popstate` listener to `hashchange`. Maybe you only want the last two segments of the URL because the rest is managed in React? Change `locationHrefToRoute` to be `whateverToRoute` based on what you need. Etc.
180 |
181 |
182 | ### Summary
183 |
184 | It seems appealing to “just do the same thing” in `Browser.element` as in `Browser.application`, but you quickly run into corner cases when you consider the broad range of people and companies using Elm. When Elm and React are on the same page, who owns the URL? When `history.pushState` is called in React, how does Elm hear about it? When `pushUrl` is called in Elm, how does React hear about it? It does not appear that there actually _is_ a simpler or shorter way for `Browser.element` to handle these questions. Special hooks on the JS side? And what about the folks using `Browser.element` who are not messing with the URL?
185 |
186 | By keeping it super simple (1) your attention is drawn to the fact that there are actually tricky situations to consider, (2) you have the flexibility to handle any situation that comes up, and (3) folks who are _not_ managing the URL from embedded Elm (the vast majority!) get a `Browser.element` with no extra details.
187 |
188 | The current design seems to balance all these competing concerns in a nice way, even if it may seem like one _particular_ scenario could be a bit better.
189 |
--------------------------------------------------------------------------------
/src/Browser.elm:
--------------------------------------------------------------------------------
1 | module Browser exposing
2 | ( sandbox
3 | , element
4 | , document, Document
5 | , application, UrlRequest(..)
6 | )
7 |
8 | {-| This module helps you set up an Elm `Program` with functions like
9 | [`sandbox`](#sandbox) and [`document`](#document).
10 |
11 |
12 | # Sandboxes
13 |
14 | @docs sandbox
15 |
16 |
17 | # Elements
18 |
19 | @docs element
20 |
21 |
22 | # Documents
23 |
24 | @docs document, Document
25 |
26 |
27 | # Applications
28 |
29 | @docs application, UrlRequest
30 |
31 | -}
32 |
33 | import Browser.Navigation as Navigation
34 | import Debugger.Main
35 | import Dict
36 | import Elm.Kernel.Browser
37 | import Html exposing (Html)
38 | import Url
39 |
40 |
41 |
42 | -- SANDBOX
43 |
44 |
45 | {-| Create a “sandboxed” program that cannot communicate with the outside
46 | world.
47 |
48 | This is great for learning the basics of [The Elm Architecture][tea]. You can
49 | see sandboxes in action in the following examples:
50 |
51 | - [Buttons](https://guide.elm-lang.org/architecture/buttons.html)
52 | - [Text Fields](https://guide.elm-lang.org/architecture/text_fields.html)
53 | - [Forms](https://guide.elm-lang.org/architecture/forms.html)
54 |
55 | Those are nice, but **I very highly recommend reading [this guide][guide]
56 | straight through** to really learn how Elm works. Understanding the
57 | fundamentals actually pays off in this language!
58 |
59 | [tea]: https://guide.elm-lang.org/architecture/
60 | [guide]: https://guide.elm-lang.org/
61 |
62 | -}
63 | sandbox :
64 | { init : model
65 | , view : model -> Html msg
66 | , update : msg -> model -> model
67 | }
68 | -> Program () model msg
69 | sandbox impl =
70 | Elm.Kernel.Browser.element
71 | { init = \() -> ( impl.init, Cmd.none )
72 | , view = impl.view
73 | , update = \msg model -> ( impl.update msg model, Cmd.none )
74 | , subscriptions = \_ -> Sub.none
75 | }
76 |
77 |
78 |
79 | -- ELEMENT
80 |
81 |
82 | {-| Create an HTML element managed by Elm. The resulting elements are easy to
83 | embed in larger JavaScript projects, and lots of companies that use Elm
84 | started with this approach! Try it out on something small. If it works, great,
85 | do more! If not, revert, no big deal.
86 |
87 | Unlike a [`sandbox`](#sandbox), an `element` can talk to the outside world in
88 | a couple ways:
89 |
90 | - `Cmd` — you can “command” the Elm runtime to do stuff, like HTTP.
91 | - `Sub` — you can “subscribe” to event sources, like clock ticks.
92 | - `flags` — JavaScript can pass in data when starting the Elm program
93 | - `ports` — set up a client-server relationship with JavaScript
94 |
95 | As you read [the guide][guide] you will run into a bunch of examples of `element`
96 | in [this section][fx]. You can learn more about flags and ports in [the interop
97 | section][interop].
98 |
99 | [guide]: https://guide.elm-lang.org/
100 | [fx]: https://guide.elm-lang.org/effects/
101 | [interop]: https://guide.elm-lang.org/interop/
102 |
103 | -}
104 | element :
105 | { init : flags -> ( model, Cmd msg )
106 | , view : model -> Html msg
107 | , update : msg -> model -> ( model, Cmd msg )
108 | , subscriptions : model -> Sub msg
109 | }
110 | -> Program flags model msg
111 | element =
112 | Elm.Kernel.Browser.element
113 |
114 |
115 |
116 | -- DOCUMENT
117 |
118 |
119 | {-| Create an HTML document managed by Elm. This expands upon what `element`
120 | can do in that `view` now gives you control over the `` and ``.
121 | -}
122 | document :
123 | { init : flags -> ( model, Cmd msg )
124 | , view : model -> Document msg
125 | , update : msg -> model -> ( model, Cmd msg )
126 | , subscriptions : model -> Sub msg
127 | }
128 | -> Program flags model msg
129 | document =
130 | Elm.Kernel.Browser.document
131 |
132 |
133 | {-| This data specifies the `` and all of the nodes that should go in
134 | the ``. This means you can update the title as your application changes.
135 | Maybe your "single-page app" navigates to a "different page", maybe a calendar
136 | app shows an accurate date in the title, etc.
137 |
138 | > **Note about CSS:** This looks similar to an `` document, but this is
139 | > not the place to manage CSS assets. If you want to work with CSS, there are
140 | > a couple ways:
141 | >
142 | > 1. Packages like [`rtfeldman/elm-css`][elm-css] give all of the features
143 | > of CSS without any CSS files. You can add all the styles you need in your
144 | > `view` function, and there is no need to worry about class names matching.
145 | >
146 | > 2. Compile your Elm code to JavaScript with `elm make --output=elm.js` and
147 | > then make your own HTML file that loads `elm.js` and the CSS file you want.
148 | > With this approach, it does not matter where the CSS comes from. Write it
149 | > by hand. Generate it. Whatever you want to do.
150 | >
151 | > 3. If you need to change `` tags dynamically, you can send messages
152 | > out a port to do it in JavaScript.
153 | >
154 | > The bigger point here is that loading assets involves touching the ``
155 | > as an implementation detail of browsers, but that does not mean it should be
156 | > the responsibility of the `view` function in Elm. So we do it differently!
157 |
158 | [elm-css]: /packages/rtfeldman/elm-css/latest/
159 |
160 | -}
161 | type alias Document msg =
162 | { title : String
163 | , body : List (Html msg)
164 | }
165 |
166 |
167 |
168 | -- APPLICATION
169 |
170 |
171 | {-| Create an application that manages [`Url`][url] changes.
172 |
173 | **When the application starts**, `init` gets the initial `Url`. You can show
174 | different things depending on the `Url`!
175 |
176 | **When someone clicks a link**, like `Home`, it always goes
177 | through `onUrlRequest`. The resulting message goes to your `update` function,
178 | giving you a chance to save scroll position or persist data before changing
179 | the URL yourself with [`pushUrl`][bnp] or [`load`][bnl]. More info on this in
180 | the [`UrlRequest`](#UrlRequest) docs!
181 |
182 | **When the URL changes**, the new `Url` goes through `onUrlChange`. The
183 | resulting message goes to `update` where you can decide what to show next.
184 |
185 | Applications always use the [`Browser.Navigation`][bn] module for precise
186 | control over `Url` changes.
187 |
188 | **More Info:** Here are some example usages of `application` programs:
189 |
190 | - [RealWorld example app](https://github.com/rtfeldman/elm-spa-example)
191 | - [Elm’s package website](https://github.com/elm/package.elm-lang.org)
192 |
193 | These are quite advanced Elm programs, so be sure to go through [the guide][g]
194 | first to get a solid conceptual foundation before diving in! If you start
195 | reading a calculus book from page 314, it might seem confusing. Same here!
196 |
197 | **Note:** Can an [`element`](#element) manage the URL too? Read [this]!
198 |
199 | [g]: https://guide.elm-lang.org/
200 | [bn]: Browser-Navigation
201 | [bnp]: Browser-Navigation#pushUrl
202 | [bnl]: Browser-Navigation#load
203 | [url]: /packages/elm/url/latest/Url#Url
204 | [this]: https://github.com/elm/browser/blob/1.0.2/notes/navigation-in-elements.md
205 |
206 | -}
207 | application :
208 | { init : flags -> Url.Url -> Navigation.Key -> ( model, Cmd msg )
209 | , view : model -> Document msg
210 | , update : msg -> model -> ( model, Cmd msg )
211 | , subscriptions : model -> Sub msg
212 | , onUrlRequest : UrlRequest -> msg
213 | , onUrlChange : Url.Url -> msg
214 | }
215 | -> Program flags model msg
216 | application =
217 | Elm.Kernel.Browser.application
218 |
219 |
220 | {-| All links in an [`application`](#application) create a `UrlRequest`. So
221 | when you click `Home`, it does not just navigate! It
222 | notifies `onUrlRequest` that the user wants to change the `Url`.
223 |
224 |
225 | ### `Internal` vs `External`
226 |
227 | Imagine we are browsing `https://example.com`. An `Internal` link would be
228 | like:
229 |
230 | - `settings#privacy`
231 | - `/home`
232 | - `https://example.com/home`
233 | - `//example.com/home`
234 |
235 | All of these links exist under the `https://example.com` domain. An `External`
236 | link would be like:
237 |
238 | - `https://elm-lang.org/examples`
239 | - `https://other.example.com/home`
240 | - `http://example.com/home`
241 |
242 | Anything that changes the domain. Notice that changing the protocol from
243 | `https` to `http` is considered a different domain! (And vice versa!)
244 |
245 |
246 | ### Purpose
247 |
248 | Having a `UrlRequest` requires a case in your `update` like this:
249 |
250 | import Browser exposing (..)
251 | import Browser.Navigation as Nav
252 | import Url
253 |
254 | type Msg
255 | = ClickedLink UrlRequest
256 |
257 | update : Msg -> Model -> ( Model, Cmd msg )
258 | update msg model =
259 | case msg of
260 | ClickedLink urlRequest ->
261 | case urlRequest of
262 | Internal url ->
263 | ( model
264 | , Nav.pushUrl model.key (Url.toString url)
265 | )
266 |
267 | External url ->
268 | ( model
269 | , Nav.load url
270 | )
271 |
272 | This is useful because it gives you a chance to customize the behavior in each
273 | case. Maybe on some `Internal` links you save the scroll position with
274 | [`Browser.Dom.getViewport`](Browser-Dom#getViewport) so you can restore it
275 | later. Maybe on `External` links you persist parts of the `Model` on your
276 | servers before leaving. Whatever you need to do!
277 |
278 | **Note:** Knowing the scroll position is not enough to restore it! What if the
279 | browser dimensions change? The scroll position will not correlate with
280 | “what was on screen” anymore. So it may be better to remember
281 | “what was on screen” and recreate the position based on that. For
282 | example, in a Wikipedia article, remember the header that they were looking at
283 | most recently. [`Browser.Dom.getElement`](Browser-Dom#getElement) is designed
284 | for figuring that out!
285 |
286 | -}
287 | type UrlRequest
288 | = Internal Url.Url
289 | | External String
290 |
--------------------------------------------------------------------------------
/src/Browser/AnimationManager.elm:
--------------------------------------------------------------------------------
1 | effect module Browser.AnimationManager where { subscription = MySub } exposing
2 | ( onAnimationFrame
3 | , onAnimationFrameDelta
4 | )
5 |
6 | import Elm.Kernel.Browser
7 | import Process
8 | import Task exposing (Task)
9 | import Time
10 |
11 |
12 |
13 | -- PUBLIC STUFF
14 |
15 |
16 | onAnimationFrame : (Time.Posix -> msg) -> Sub msg
17 | onAnimationFrame tagger =
18 | subscription (Time tagger)
19 |
20 |
21 | onAnimationFrameDelta : (Float -> msg) -> Sub msg
22 | onAnimationFrameDelta tagger =
23 | subscription (Delta tagger)
24 |
25 |
26 |
27 | -- SUBSCRIPTIONS
28 |
29 |
30 | type MySub msg
31 | = Time (Time.Posix -> msg)
32 | | Delta (Float -> msg)
33 |
34 |
35 | subMap : (a -> b) -> MySub a -> MySub b
36 | subMap func sub =
37 | case sub of
38 | Time tagger ->
39 | Time (func << tagger)
40 |
41 | Delta tagger ->
42 | Delta (func << tagger)
43 |
44 |
45 |
46 | -- EFFECT MANAGER
47 |
48 |
49 | type alias State msg =
50 | { subs : List (MySub msg)
51 | , request : Maybe Process.Id
52 | , oldTime : Int
53 | }
54 |
55 |
56 |
57 | -- NOTE: used in onEffects
58 | --
59 |
60 |
61 | init : Task Never (State msg)
62 | init =
63 | Task.succeed (State [] Nothing 0)
64 |
65 |
66 | onEffects : Platform.Router msg Int -> List (MySub msg) -> State msg -> Task Never (State msg)
67 | onEffects router subs { request, oldTime } =
68 | case ( request, subs ) of
69 | ( Nothing, [] ) ->
70 | init
71 |
72 | ( Just pid, [] ) ->
73 | Process.kill pid
74 | |> Task.andThen (\_ -> init)
75 |
76 | ( Nothing, _ ) ->
77 | Process.spawn (Task.andThen (Platform.sendToSelf router) rAF)
78 | |> Task.andThen
79 | (\pid ->
80 | now
81 | |> Task.andThen (\time -> Task.succeed (State subs (Just pid) time))
82 | )
83 |
84 | ( Just _, _ ) ->
85 | Task.succeed (State subs request oldTime)
86 |
87 |
88 | onSelfMsg : Platform.Router msg Int -> Int -> State msg -> Task Never (State msg)
89 | onSelfMsg router newTime { subs, oldTime } =
90 | let
91 | send sub =
92 | case sub of
93 | Time tagger ->
94 | Platform.sendToApp router (tagger (Time.millisToPosix newTime))
95 |
96 | Delta tagger ->
97 | Platform.sendToApp router (tagger (toFloat (newTime - oldTime)))
98 | in
99 | Process.spawn (Task.andThen (Platform.sendToSelf router) rAF)
100 | |> Task.andThen
101 | (\pid ->
102 | Task.sequence (List.map send subs)
103 | |> Task.andThen (\_ -> Task.succeed (State subs (Just pid) newTime))
104 | )
105 |
106 |
107 | rAF : Task x Int
108 | rAF =
109 | Elm.Kernel.Browser.rAF ()
110 |
111 |
112 | now : Task x Int
113 | now =
114 | Elm.Kernel.Browser.now ()
115 |
--------------------------------------------------------------------------------
/src/Browser/Dom.elm:
--------------------------------------------------------------------------------
1 | module Browser.Dom exposing
2 | ( focus, blur, Error(..)
3 | , getViewport, Viewport, getViewportOf
4 | , setViewport, setViewportOf
5 | , getElement, Element
6 | )
7 |
8 | {-| This module allows you to manipulate the DOM in various ways. It covers:
9 |
10 | - Focus and blur input elements.
11 | - Get the `width` and `height` of elements.
12 | - Get the `x` and `y` coordinates of elements.
13 | - Figure out the scroll position.
14 | - Change the scroll position!
15 |
16 | We use different terminology than JavaScript though...
17 |
18 |
19 | # Terminology
20 |
21 | Have you ever thought about how “scrolling” is a metaphor about
22 | scrolls? Like hanging scrolls of caligraphy made during the Han Dynasty
23 | in China?
24 |
25 | This metaphor falls apart almost immediately though. For example, many scrolls
26 | read horizontally! Like a [Sefer Torah][torah] or [Chinese Handscrolls][hand].
27 | The two sides move independently, sometimes kept in place with stones. What is
28 | a scroll bar in this world? And [hanging scrolls][hang] (which _are_ displayed
29 | vertically) do not “scroll” at all! They hang!
30 |
31 | So in JavaScript, we start with a badly stretched metaphor and add a bunch of
32 | DOM details like padding, borders, and margins. How do those relate to scrolls?
33 | For example, JavaScript has `clientWidth`. Client like a feudal state that pays
34 | tribute to the emperor? And `offsetHeight`. Can an offset even have height? And
35 | what has that got to do with scrolls?
36 |
37 | So instead of inheriting this metaphorical hodge-podge, we use terminology from
38 | 3D graphics. You have a **scene** containing all your elements and a **viewport**
39 | into the scene. I think it ends up being a lot clearer, but you can evaluate
40 | for yourself when you see the diagrams later!
41 |
42 | **Note:** For more scroll facts, I recommend [A Day on the Grand Canal with
43 | the Emperor of China or: Surface Is Illusion But So Is Depth][doc] where David
44 | Hockney explores the history of _perspective_ in art. Really interesting!
45 |
46 | [torah]: https://en.wikipedia.org/wiki/Sefer_Torah
47 | [hand]: https://www.metmuseum.org/toah/hd/chhs/hd_chhs.htm
48 | [hang]: https://en.wikipedia.org/wiki/Hanging_scroll
49 | [doc]: https://www.imdb.com/title/tt0164525/
50 |
51 |
52 | # Focus
53 |
54 | @docs focus, blur, Error
55 |
56 |
57 | # Get Viewport
58 |
59 | @docs getViewport, Viewport, getViewportOf
60 |
61 |
62 | # Set Viewport
63 |
64 | @docs setViewport, setViewportOf
65 |
66 |
67 | # Position
68 |
69 | @docs getElement, Element
70 |
71 | -}
72 |
73 | import Elm.Kernel.Browser
74 | import Task exposing (Task)
75 |
76 |
77 |
78 | -- FOCUS
79 |
80 |
81 | {-| Find a DOM node by `id` and focus on it. So if you wanted to focus a node
82 | like `` you could say:
83 |
84 | import Browser.Dom as Dom
85 | import Task
86 |
87 | type Msg
88 | = NoOp
89 |
90 | focusSearchBox : Cmd Msg
91 | focusSearchBox =
92 | Task.attempt (\_ -> NoOp) (Dom.focus "search-box")
93 |
94 | Notice that this code ignores the possibility that `search-box` is not used
95 | as an `id` by any node, failing silently in that case. It would be better to
96 | log the failure with whatever error reporting system you use.
97 |
98 | -}
99 | focus : String -> Task Error ()
100 | focus =
101 | Elm.Kernel.Browser.call "focus"
102 |
103 |
104 | {-| Find a DOM node by `id` and make it lose focus. So if you wanted a node
105 | like `` to lose focus you could say:
106 |
107 | import Browser.Dom as Dom
108 | import Task
109 |
110 | type Msg
111 | = NoOp
112 |
113 | unfocusSearchBox : Cmd Msg
114 | unfocusSearchBox =
115 | Task.attempt (\_ -> NoOp) (Dom.blur "search-box")
116 |
117 | Notice that this code ignores the possibility that `search-box` is not used
118 | as an `id` by any node, failing silently in that case. It would be better to
119 | log the failure with whatever error reporting system you use.
120 |
121 | -}
122 | blur : String -> Task Error ()
123 | blur =
124 | Elm.Kernel.Browser.call "blur"
125 |
126 |
127 |
128 | -- ERROR
129 |
130 |
131 | {-| Many functions in this module look up DOM nodes up by their `id`. If you
132 | ask for an `id` that is not in the DOM, you will get this error.
133 | -}
134 | type Error
135 | = NotFound String
136 |
137 |
138 |
139 | -- VIEWPORT
140 |
141 |
142 | {-| Get information on the current viewport of the browser.
143 |
144 | 
145 |
146 | If you want to move the viewport around (i.e. change the scroll position) you
147 | can use [`setViewport`](#setViewport) which change the `x` and `y` of the
148 | viewport.
149 |
150 | -}
151 | getViewport : Task x Viewport
152 | getViewport =
153 | Elm.Kernel.Browser.withWindow Elm.Kernel.Browser.getViewport
154 |
155 |
156 | {-| All the information about the current viewport.
157 |
158 | 
159 |
160 | -}
161 | type alias Viewport =
162 | { scene :
163 | { width : Float
164 | , height : Float
165 | }
166 | , viewport :
167 | { x : Float
168 | , y : Float
169 | , width : Float
170 | , height : Float
171 | }
172 | }
173 |
174 |
175 | {-| Just like `getViewport`, but for any scrollable DOM node. Say we have an
176 | application with a chat box in the bottow right corner like this:
177 |
178 | 
179 |
180 | There are probably a whole bunch of messages that are not being shown. You
181 | could scroll up to see them all. Well, we can think of that chat box is a
182 | viewport into a scene!
183 |
184 | 
185 |
186 | This can be useful with [`setViewportOf`](#setViewportOf) to make sure new
187 | messages always appear on the bottom.
188 |
189 | The viewport size _does not_ include the border or margins.
190 |
191 | **Note:** This data is collected from specific fields in JavaScript, so it
192 | may be helpful to know that:
193 |
194 | - `scene.width` = [`scrollWidth`][sw]
195 | - `scene.height` = [`scrollHeight`][sh]
196 | - `viewport.x` = [`scrollLeft`][sl]
197 | - `viewport.y` = [`scrollTop`][st]
198 | - `viewport.width` = [`clientWidth`][cw]
199 | - `viewport.height` = [`clientHeight`][ch]
200 |
201 | Neither [`offsetWidth`][ow] nor [`offsetHeight`][oh] are available. The theory
202 | is that (1) the information can always be obtained by using `getElement` on a
203 | node without margins, (2) no cases came to mind where you actually care in the
204 | first place, and (3) it is available through ports if it is really needed.
205 | If you have a case that really needs it though, please share your specific
206 | scenario in an issue! Nicely presented case studies are the raw ingredients for
207 | API improvements!
208 |
209 | [sw]: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollWidth
210 | [sh]: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight
211 | [st]: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTop
212 | [sl]: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft
213 | [cw]: https://developer.mozilla.org/en-US/docs/Web/API/Element/clientWidth
214 | [ch]: https://developer.mozilla.org/en-US/docs/Web/API/Element/clientHeight
215 | [ow]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetWidth
216 | [oh]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetHeight
217 |
218 | -}
219 | getViewportOf : String -> Task Error Viewport
220 | getViewportOf =
221 | Elm.Kernel.Browser.getViewportOf
222 |
223 |
224 |
225 | -- SET VIEWPORT
226 |
227 |
228 | {-| Change the `x` and `y` offset of the browser viewport immediately. For
229 | example, you could make a command to jump to the top of the page:
230 |
231 | import Browser.Dom as Dom
232 | import Task
233 |
234 | type Msg
235 | = NoOp
236 |
237 | resetViewport : Cmd Msg
238 | resetViewport =
239 | Task.perform (\_ -> NoOp) (Dom.setViewport 0 0)
240 |
241 | This sets the viewport offset to zero.
242 |
243 | This could be useful with `Browser.application` where you may want to reset
244 | the viewport when the URL changes. Maybe you go to a “new page”
245 | and want people to start at the top!
246 |
247 | -}
248 | setViewport : Float -> Float -> Task x ()
249 | setViewport =
250 | Elm.Kernel.Browser.setViewport
251 |
252 |
253 | {-| Change the `x` and `y` offset of a DOM node’s viewport by ID. This
254 | is common in text messaging and chat rooms, where once the messages fill the
255 | screen, you want to always be at the very bottom of the message chain. This
256 | way the latest message is always on screen! You could do this:
257 |
258 | import Browser.Dom as Dom
259 | import Task
260 |
261 | type Msg
262 | = NoOp
263 |
264 | jumpToBottom : String -> Cmd Msg
265 | jumpToBottom id =
266 | Dom.getViewportOf id
267 | |> Task.andThen (\info -> Dom.setViewportOf id 0 info.scene.height)
268 | |> Task.attempt (\_ -> NoOp)
269 |
270 | So you could call `jumpToBottom "chat-box"` whenever you add a new message.
271 |
272 | **Note 1:** What happens if the viewport is placed out of bounds? Where there
273 | is no `scene` to show? To avoid this question, the `x` and `y` offsets are
274 | clamped such that the viewport is always fully within the `scene`. So when
275 | `jumpToBottom` sets the `y` offset of the viewport to the `height` of the
276 | `scene` (i.e. too far!) it relies on this clamping behavior to put the viewport
277 | back in bounds.
278 |
279 | **Note 2:** The example ignores when the element ID is not found, but it would
280 | be great to log that information. It means there may be a bug or a dead link
281 | somewhere!
282 |
283 | -}
284 | setViewportOf : String -> Float -> Float -> Task Error ()
285 | setViewportOf =
286 | Elm.Kernel.Browser.setViewportOf
287 |
288 |
289 |
290 | {--SLIDE VIEWPORT
291 |
292 |
293 | {-| Change the `x` and `y` offset of the viewport with an animation. In JS,
294 | this corresponds to setting [`scroll-behavior`][sb] to `smooth`.
295 |
296 | This can definitely be overused, so try to use it specifically when you want
297 | the user to be spatially situated in a scene. For example, a “back to
298 | top” button might use it:
299 |
300 | import Browser.Dom as Dom
301 | import Task
302 |
303 | type Msg = NoOp
304 |
305 | backToTop : Cmd Msg
306 | backToTop =
307 | Task.perform (\_ -> NoOp) (Dom.slideViewport 0 0)
308 |
309 | Be careful when paring this with `Browser.application`. When the URL changes
310 | and a whole new scene is going to be rendered, using `setViewport` is probably
311 | best. If you are moving within a scene, you may benefit from a mix of
312 | `setViewport` and `slideViewport`. Sliding to the top is nice, but sliding
313 | around everywhere is probably annoying.
314 |
315 | [sb]: https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-behavior
316 | -}
317 | slideViewport : Float -> Float -> Task x ()
318 | slideViewport =
319 | Debug.todo "slideViewport"
320 |
321 |
322 | slideViewportOf : String -> Float -> Float -> Task Error ()
323 | slideViewportOf =
324 | Debug.todo "slideViewportOf"
325 |
326 | --}
327 | -- ELEMENT
328 |
329 |
330 | {-| Get position information about specific elements. Say we put
331 | `id "jesting-aside"` on the seventh paragraph of the text. When we call
332 | `getElement "jesting-aside"` we would get the following information:
333 |
334 | 
335 |
336 | This can be useful for:
337 |
338 | - **Scrolling** — Pair this information with `setViewport` to scroll
339 | specific elements into view. This gives you a lot of control over where exactly
340 | the element would be after the viewport moved.
341 |
342 | - **Drag and Drop** — As of this writing, `touchmove` events do not tell
343 | you which element you are currently above. To figure out if you have dragged
344 | something over the target, you could see if the `pageX` and `pageY` of the
345 | touch are inside the `x`, `y`, `width`, and `height` of the target element.
346 |
347 | **Note:** This corresponds to JavaScript’s [`getBoundingClientRect`][gbcr],
348 | so **the element’s margins are included in its `width` and `height`**.
349 | With scrolling, maybe you want to include the margins. With drag-and-drop, you
350 | probably do not, so some folks set the margins to zero and put the target
351 | element in a `
` that adds the spacing. Just something to be aware of!
352 |
353 | [gbcr]: https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
354 |
355 | -}
356 | getElement : String -> Task Error Element
357 | getElement =
358 | Elm.Kernel.Browser.getElement
359 |
360 |
361 | {-| A bunch of information about the position and size of an element relative
362 | to the overall scene.
363 |
364 | 
365 |
366 | -}
367 | type alias Element =
368 | { scene :
369 | { width : Float
370 | , height : Float
371 | }
372 | , viewport :
373 | { x : Float
374 | , y : Float
375 | , width : Float
376 | , height : Float
377 | }
378 | , element :
379 | { x : Float
380 | , y : Float
381 | , width : Float
382 | , height : Float
383 | }
384 | }
385 |
--------------------------------------------------------------------------------
/src/Browser/Events.elm:
--------------------------------------------------------------------------------
1 | effect module Browser.Events where { subscription = MySub } exposing
2 | ( onAnimationFrame, onAnimationFrameDelta
3 | , onKeyPress, onKeyDown, onKeyUp
4 | , onClick, onMouseMove, onMouseDown, onMouseUp
5 | , onResize, onVisibilityChange, Visibility(..)
6 | )
7 |
8 | {-| In JavaScript, information about the root of an HTML document is held in
9 | the `document` and `window` objects. This module lets you create event
10 | listeners on those objects for the following topics: [animation](#animation),
11 | [keyboard](#keyboard), [mouse](#mouse), and [window](#window).
12 |
13 | If there is something else you need, use [ports] to do it in JavaScript!
14 |
15 | [ports]: https://guide.elm-lang.org/interop/ports.html
16 |
17 |
18 | # Animation
19 |
20 | @docs onAnimationFrame, onAnimationFrameDelta
21 |
22 |
23 | # Keyboard
24 |
25 | @docs onKeyPress, onKeyDown, onKeyUp
26 |
27 |
28 | # Mouse
29 |
30 | @docs onClick, onMouseMove, onMouseDown, onMouseUp
31 |
32 |
33 | # Window
34 |
35 | @docs onResize, onVisibilityChange, Visibility
36 |
37 | -}
38 |
39 | import Browser.AnimationManager as AM
40 | import Dict
41 | import Elm.Kernel.Browser
42 | import Json.Decode as Decode
43 | import Process
44 | import Task exposing (Task)
45 | import Time
46 |
47 |
48 |
49 | -- ANIMATION
50 |
51 |
52 | {-| An animation frame triggers about 60 times per second. Get the POSIX time
53 | on each frame. (See [`elm/time`](/packages/elm/time/latest) for more info on
54 | POSIX times.)
55 |
56 | **Note:** Browsers have their own render loop, repainting things as fast as
57 | possible. If you want smooth animations in your application, it is helpful to
58 | sync up with the browsers natural refresh rate. This hooks into JavaScript's
59 | `requestAnimationFrame` function.
60 |
61 | -}
62 | onAnimationFrame : (Time.Posix -> msg) -> Sub msg
63 | onAnimationFrame =
64 | AM.onAnimationFrame
65 |
66 |
67 | {-| Just like `onAnimationFrame`, except message is the time in milliseconds
68 | since the previous frame. So you should get a sequence of values all around
69 | `1000 / 60` which is nice for stepping animations by a time delta.
70 | -}
71 | onAnimationFrameDelta : (Float -> msg) -> Sub msg
72 | onAnimationFrameDelta =
73 | AM.onAnimationFrameDelta
74 |
75 |
76 |
77 | -- KEYBOARD
78 |
79 |
80 | {-| Subscribe to key presses that normally produce characters. So you should
81 | not rely on this for arrow keys.
82 |
83 | **Note:** Check out [this advice][note] to learn more about decoding key codes.
84 | It is more complicated than it should be.
85 |
86 | [note]: https://github.com/elm/browser/blob/1.0.2/notes/keyboard.md
87 |
88 | -}
89 | onKeyPress : Decode.Decoder msg -> Sub msg
90 | onKeyPress =
91 | on Document "keypress"
92 |
93 |
94 | {-| Subscribe to get codes whenever a key goes down. This can be useful for
95 | creating games. Maybe you want to know if people are pressing `w`, `a`, `s`,
96 | or `d` at any given time.
97 |
98 | **Note:** Check out [this advice][note] to learn more about decoding key codes.
99 | It is more complicated than it should be.
100 |
101 | [note]: https://github.com/elm/browser/blob/1.0.2/notes/keyboard.md
102 |
103 | -}
104 | onKeyDown : Decode.Decoder msg -> Sub msg
105 | onKeyDown =
106 | on Document "keydown"
107 |
108 |
109 | {-| Subscribe to get codes whenever a key goes up. Often used in combination
110 | with [`onVisibilityChange`](#onVisibilityChange) to be sure keys do not appear
111 | to down and never come back up.
112 | -}
113 | onKeyUp : Decode.Decoder msg -> Sub msg
114 | onKeyUp =
115 | on Document "keyup"
116 |
117 |
118 |
119 | -- MOUSE
120 |
121 |
122 | {-| Subscribe to mouse clicks anywhere on screen. Maybe you need to create a
123 | custom drop down. You could listen for clicks when it is open, letting you know
124 | if someone clicked out of it:
125 |
126 | import Browser.Events as Events
127 | import Json.Decode as D
128 |
129 | type Msg
130 | = ClickOut
131 |
132 | subscriptions : Model -> Sub Msg
133 | subscriptions model =
134 | case model.dropDown of
135 | Closed _ ->
136 | Sub.none
137 |
138 | Open _ ->
139 | Events.onClick (D.succeed ClickOut)
140 |
141 | -}
142 | onClick : Decode.Decoder msg -> Sub msg
143 | onClick =
144 | on Document "click"
145 |
146 |
147 | {-| Subscribe to mouse moves anywhere on screen.
148 |
149 | You could use this to implement resizable panels like in Elm's online code
150 | editor. Check out the example imprementation [here][drag].
151 |
152 | [drag]: https://github.com/elm/browser/blob/1.0.2/examples/src/Drag.elm
153 |
154 | **Note:** Unsubscribe if you do not need these events! Running code on every
155 | single mouse movement can be very costly, and it is recommended to only
156 | subscribe when absolutely necessary.
157 |
158 | -}
159 | onMouseMove : Decode.Decoder msg -> Sub msg
160 | onMouseMove =
161 | on Document "mousemove"
162 |
163 |
164 | {-| Subscribe to get mouse information whenever the mouse button goes down.
165 | -}
166 | onMouseDown : Decode.Decoder msg -> Sub msg
167 | onMouseDown =
168 | on Document "mousedown"
169 |
170 |
171 | {-| Subscribe to get mouse information whenever the mouse button goes up.
172 | Often used in combination with [`onVisibilityChange`](#onVisibilityChange)
173 | to be sure keys do not appear to down and never come back up.
174 | -}
175 | onMouseUp : Decode.Decoder msg -> Sub msg
176 | onMouseUp =
177 | on Document "mouseup"
178 |
179 |
180 |
181 | -- WINDOW
182 |
183 |
184 | {-| Subscribe to any changes in window size.
185 |
186 | For example, you could track the current width by saying:
187 |
188 | import Browser.Events as E
189 |
190 | type Msg
191 | = GotNewWidth Int
192 |
193 | subscriptions : model -> Cmd Msg
194 | subscriptions _ =
195 | E.onResize (\w h -> GotNewWidth w)
196 |
197 | **Note:** This is equivalent to getting events from [`window.onresize`][resize].
198 |
199 | [resize]: https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onresize
200 |
201 | -}
202 | onResize : (Int -> Int -> msg) -> Sub msg
203 | onResize func =
204 | on Window "resize" <|
205 | Decode.field "target" <|
206 | Decode.map2 func
207 | (Decode.field "innerWidth" Decode.int)
208 | (Decode.field "innerHeight" Decode.int)
209 |
210 |
211 | {-| Subscribe to any visibility changes, like if the user switches to a
212 | different tab or window. When the user looks away, you may want to:
213 |
214 | - Pause a timer.
215 | - Pause an animation.
216 | - Pause video or audio.
217 | - Pause an image carousel.
218 | - Stop polling a server for new information.
219 | - Stop waiting for an [`onKeyUp`](#onKeyUp) event.
220 |
221 | -}
222 | onVisibilityChange : (Visibility -> msg) -> Sub msg
223 | onVisibilityChange func =
224 | let
225 | info = Elm.Kernel.Browser.visibilityInfo ()
226 | in
227 | on Document info.change <|
228 | Decode.map (withHidden func) <|
229 | Decode.field "target" <|
230 | Decode.field info.hidden Decode.bool
231 |
232 |
233 | withHidden : (Visibility -> msg) -> Bool -> msg
234 | withHidden func isHidden =
235 | func (if isHidden then Hidden else Visible)
236 |
237 |
238 | {-| Value describing whether the page is hidden or visible.
239 | -}
240 | type Visibility
241 | = Visible
242 | | Hidden
243 |
244 |
245 |
246 | -- SUBSCRIPTIONS
247 |
248 |
249 | type Node
250 | = Document
251 | | Window
252 |
253 |
254 | on : Node -> String -> Decode.Decoder msg -> Sub msg
255 | on node name decoder =
256 | subscription (MySub node name decoder)
257 |
258 |
259 | type MySub msg
260 | = MySub Node String (Decode.Decoder msg)
261 |
262 |
263 | subMap : (a -> b) -> MySub a -> MySub b
264 | subMap func (MySub node name decoder) =
265 | MySub node name (Decode.map func decoder)
266 |
267 |
268 |
269 | -- EFFECT MANAGER
270 |
271 |
272 | type alias State msg =
273 | { subs : List ( String, MySub msg )
274 | , pids : Dict.Dict String Process.Id
275 | }
276 |
277 |
278 | init : Task Never (State msg)
279 | init =
280 | Task.succeed (State [] Dict.empty)
281 |
282 |
283 | type alias Event =
284 | { key : String
285 | , event : Decode.Value
286 | }
287 |
288 |
289 | onSelfMsg : Platform.Router msg Event -> Event -> State msg -> Task Never (State msg)
290 | onSelfMsg router { key, event } state =
291 | let
292 | toMessage ( subKey, MySub node name decoder ) =
293 | if subKey == key then
294 | Elm.Kernel.Browser.decodeEvent decoder event
295 | else
296 | Nothing
297 |
298 | messages = List.filterMap toMessage state.subs
299 | in
300 | Task.sequence (List.map (Platform.sendToApp router) messages)
301 | |> Task.andThen (\_ -> Task.succeed state)
302 |
303 |
304 | onEffects : Platform.Router msg Event -> List (MySub msg) -> State msg -> Task Never (State msg)
305 | onEffects router subs state =
306 | let
307 | newSubs = List.map addKey subs
308 |
309 | stepLeft _ pid (deads, lives, news) =
310 | (pid :: deads, lives, news)
311 |
312 | stepBoth key pid _ (deads, lives, news) =
313 | (deads, Dict.insert key pid lives, news)
314 |
315 | stepRight key sub (deads, lives, news) =
316 | (deads, lives, spawn router key sub :: news)
317 |
318 | (deadPids, livePids, makeNewPids) =
319 | Dict.merge stepLeft stepBoth stepRight state.pids (Dict.fromList newSubs) ([], Dict.empty, [])
320 | in
321 | Task.sequence (List.map Process.kill deadPids)
322 | |> Task.andThen (\_ -> Task.sequence makeNewPids)
323 | |> Task.andThen (\pids -> Task.succeed (State newSubs (Dict.union livePids (Dict.fromList pids))))
324 |
325 |
326 |
327 | -- TO KEY
328 |
329 |
330 | addKey : MySub msg -> ( String, MySub msg )
331 | addKey ((MySub node name _) as sub) =
332 | (nodeToKey node ++ name, sub)
333 |
334 |
335 | nodeToKey : Node -> String
336 | nodeToKey node =
337 | case node of
338 | Document -> "d_"
339 | Window -> "w_"
340 |
341 |
342 |
343 | -- SPAWN
344 |
345 |
346 | spawn : Platform.Router msg Event -> String -> MySub msg -> Task Never ( String, Process.Id )
347 | spawn router key (MySub node name _) =
348 | let
349 | actualNode =
350 | case node of
351 | Document -> Elm.Kernel.Browser.doc
352 | Window -> Elm.Kernel.Browser.window
353 | in
354 | Task.map (\value -> ( key, value )) <|
355 | Elm.Kernel.Browser.on actualNode name <|
356 | \event -> Platform.sendToSelf router (Event key event)
357 |
--------------------------------------------------------------------------------
/src/Browser/Navigation.elm:
--------------------------------------------------------------------------------
1 | module Browser.Navigation exposing
2 | ( Key, pushUrl, replaceUrl, back, forward
3 | , load, reload, reloadAndSkipCache
4 | )
5 |
6 | {-| This module helps you manage the browser’s URL yourself. This is the
7 | crucial trick when using [`Browser.application`](Browser#application).
8 |
9 | The most important function is [`pushUrl`](#pushUrl) which changes the
10 | address bar _without_ starting a page load.
11 |
12 |
13 | ## What is a page load?
14 |
15 | 1. Request a new HTML document. The page goes blank.
16 | 2. As the HTML loads, request any `