├── .gitignore ├── assets ├── prod │ └── icons │ │ ├── 16.png │ │ ├── 32.png │ │ ├── 64.png │ │ ├── 128.png │ │ ├── 144.png │ │ ├── 152.png │ │ ├── 192.png │ │ ├── 256.png │ │ └── 512.png └── dev │ ├── collection.png │ ├── elm-starter.gif │ ├── lighthouse.png │ ├── slack-previews.jpg │ ├── twitter-card.jpg │ ├── elm-physics-example.gif │ ├── elm-physics-example.png │ └── elm-physics-example-animation.gif ├── netlify.toml ├── src-elm-starter ├── Starter │ ├── Version.elm │ ├── Model.elm │ ├── ConfMain.elm │ ├── SnippetCss.elm │ ├── Icon.elm │ ├── FileNames.elm │ ├── Cache.elm │ ├── ConfMeta.elm │ ├── Manifest.elm │ ├── ElmGo.elm │ ├── ServiceWorker.elm │ ├── SnippetJavascript.elm │ ├── SnippetHtml.elm │ ├── Flags.elm │ └── Conf.elm ├── String │ └── Conversions.elm ├── Worker.elm ├── Html │ ├── String │ │ ├── Keyed.elm │ │ ├── Extra.elm │ │ ├── Lazy.elm │ │ ├── Events.elm │ │ └── Attributes.elm │ ├── Types.elm │ └── String.elm ├── Application.elm └── elm-starter ├── elm.json ├── ssc-src.txt ├── LICENSE ├── package.json ├── src ├── Index.elm └── Main.elm └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | node_modules 3 | .DS_Store 4 | .idea 5 | Elmjutsu* 6 | -------------------------------------------------------------------------------- /assets/prod/icons/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucamug/elm-starter/HEAD/assets/prod/icons/16.png -------------------------------------------------------------------------------- /assets/prod/icons/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucamug/elm-starter/HEAD/assets/prod/icons/32.png -------------------------------------------------------------------------------- /assets/prod/icons/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucamug/elm-starter/HEAD/assets/prod/icons/64.png -------------------------------------------------------------------------------- /assets/dev/collection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucamug/elm-starter/HEAD/assets/dev/collection.png -------------------------------------------------------------------------------- /assets/dev/elm-starter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucamug/elm-starter/HEAD/assets/dev/elm-starter.gif -------------------------------------------------------------------------------- /assets/dev/lighthouse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucamug/elm-starter/HEAD/assets/dev/lighthouse.png -------------------------------------------------------------------------------- /assets/prod/icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucamug/elm-starter/HEAD/assets/prod/icons/128.png -------------------------------------------------------------------------------- /assets/prod/icons/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucamug/elm-starter/HEAD/assets/prod/icons/144.png -------------------------------------------------------------------------------- /assets/prod/icons/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucamug/elm-starter/HEAD/assets/prod/icons/152.png -------------------------------------------------------------------------------- /assets/prod/icons/192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucamug/elm-starter/HEAD/assets/prod/icons/192.png -------------------------------------------------------------------------------- /assets/prod/icons/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucamug/elm-starter/HEAD/assets/prod/icons/256.png -------------------------------------------------------------------------------- /assets/prod/icons/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucamug/elm-starter/HEAD/assets/prod/icons/512.png -------------------------------------------------------------------------------- /assets/dev/slack-previews.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucamug/elm-starter/HEAD/assets/dev/slack-previews.jpg -------------------------------------------------------------------------------- /assets/dev/twitter-card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucamug/elm-starter/HEAD/assets/dev/twitter-card.jpg -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "npm run build" 3 | publish = "elm-stuff/elm-starter-files/build/" 4 | -------------------------------------------------------------------------------- /assets/dev/elm-physics-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucamug/elm-starter/HEAD/assets/dev/elm-physics-example.gif -------------------------------------------------------------------------------- /assets/dev/elm-physics-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucamug/elm-starter/HEAD/assets/dev/elm-physics-example.png -------------------------------------------------------------------------------- /assets/dev/elm-physics-example-animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucamug/elm-starter/HEAD/assets/dev/elm-physics-example-animation.gif -------------------------------------------------------------------------------- /src-elm-starter/Starter/Version.elm: -------------------------------------------------------------------------------- 1 | module Starter.Version exposing (version) 2 | 3 | 4 | version : String 5 | version = 6 | "0.0.1" 7 | -------------------------------------------------------------------------------- /src-elm-starter/Starter/Model.elm: -------------------------------------------------------------------------------- 1 | module Starter.Model exposing (Model) 2 | 3 | import Starter.Flags 4 | 5 | 6 | type alias Model = 7 | Starter.Flags.Flags 8 | -------------------------------------------------------------------------------- /src-elm-starter/String/Conversions.elm: -------------------------------------------------------------------------------- 1 | module String.Conversions exposing (fromValue) 2 | 3 | import Json.Encode 4 | 5 | 6 | {-| Convert a Json.Decode.Value to a JSON String. 7 | -} 8 | fromValue : Json.Encode.Value -> String 9 | fromValue value = 10 | Json.Encode.encode 0 value 11 | -------------------------------------------------------------------------------- /src-elm-starter/Starter/ConfMain.elm: -------------------------------------------------------------------------------- 1 | module Starter.ConfMain exposing 2 | ( Conf 3 | , encoder 4 | ) 5 | 6 | import Json.Encode 7 | 8 | 9 | type alias Conf = 10 | { urls : List String 11 | , assetsToCache : List String 12 | } 13 | 14 | 15 | encoder : Conf -> Json.Encode.Value 16 | encoder mainConf_ = 17 | Json.Encode.object 18 | [ ( "urls", Json.Encode.list Json.Encode.string mainConf_.urls ) 19 | , ( "assetsToCache", Json.Encode.list Json.Encode.string mainConf_.assetsToCache ) 20 | ] 21 | -------------------------------------------------------------------------------- /src-elm-starter/Worker.elm: -------------------------------------------------------------------------------- 1 | port module Worker exposing (main) 2 | 3 | import Json.Encode 4 | import Starter.Conf 5 | import Starter.Flags 6 | import Starter.Model 7 | 8 | 9 | port dataFromElmToJavascript : Json.Encode.Value -> Cmd msg 10 | 11 | 12 | main : Program Starter.Flags.Flags Starter.Model.Model msg 13 | main = 14 | Platform.worker 15 | { init = \flags -> ( flags, dataFromElmToJavascript (Starter.Conf.conf_ flags) ) 16 | , update = \_ model -> ( model, Cmd.none ) 17 | , subscriptions = \_ -> Sub.none 18 | } 19 | -------------------------------------------------------------------------------- /src-elm-starter/Starter/SnippetCss.elm: -------------------------------------------------------------------------------- 1 | module Starter.SnippetCss exposing (noJsAndLoadingNotifications) 2 | 3 | 4 | noJsAndLoadingNotifications : String -> String 5 | noJsAndLoadingNotifications classNotification = 6 | """ 7 | .""" ++ classNotification ++ """ 8 | { padding: 20px 9 | ; background-color: rgba(255,255,255,0.7) 10 | ; pointer-events: none 11 | ; color: black 12 | ; width: 100% 13 | ; text-align: center 14 | ; position: fixed 15 | ; left: 0 16 | ; z-index: 1 17 | ; box-sizing: border-box 18 | } 19 | """ 20 | -------------------------------------------------------------------------------- /src-elm-starter/Starter/Icon.elm: -------------------------------------------------------------------------------- 1 | module Starter.Icon exposing 2 | ( iconFileName 3 | , iconsForManifest 4 | , iconsToBeCached 5 | ) 6 | 7 | 8 | iconsForManifest : List number 9 | iconsForManifest = 10 | [ 128, 144, 152, 192, 256, 512 ] 11 | 12 | 13 | iconsToBeCached : List number 14 | iconsToBeCached = 15 | [ 16, 32, 64 ] ++ iconsForManifest 16 | 17 | 18 | iconFileName : String -> Int -> String 19 | iconFileName relative size = 20 | let 21 | sizeString = 22 | String.fromInt size 23 | in 24 | relative ++ "/icons/" ++ sizeString ++ ".png" 25 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src", 5 | "src-elm-starter" 6 | ], 7 | "elm-version": "0.19.1", 8 | "dependencies": { 9 | "direct": { 10 | "elm/browser": "1.0.2", 11 | "elm/core": "1.0.5", 12 | "elm/html": "1.0.0", 13 | "elm/json": "1.1.3", 14 | "elm/svg": "1.0.1", 15 | "elm/url": "1.0.0", 16 | "mdgriffith/elm-ui": "1.1.7" 17 | }, 18 | "indirect": { 19 | "elm/time": "1.0.0", 20 | "elm/virtual-dom": "1.0.2" 21 | } 22 | }, 23 | "test-dependencies": { 24 | "direct": {}, 25 | "indirect": {} 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src-elm-starter/Starter/FileNames.elm: -------------------------------------------------------------------------------- 1 | module Starter.FileNames exposing 2 | ( FileNames 3 | , fileNames 4 | ) 5 | 6 | 7 | type alias FileNames = 8 | { outputCompiledJs : String 9 | , outputCompiledJsProd : String 10 | , indexHtml : String 11 | , manifestJson : String 12 | , redirects : String 13 | , robotsTxt : String 14 | , serviceWorker : String 15 | , sitemap : String 16 | , snapshot : String 17 | } 18 | 19 | 20 | fileNames : String -> String -> FileNames 21 | fileNames version commit = 22 | { manifestJson = "/manifest.json" 23 | , redirects = "/_redirects" 24 | , robotsTxt = "/robots.txt" 25 | , outputCompiledJs = "/elm.js" 26 | , outputCompiledJsProd = "/elm-" ++ version ++ "-" ++ commit ++ ".min.js" 27 | , indexHtml = "/index.html" 28 | , serviceWorker = "/service-worker.js" 29 | , sitemap = "/sitemap.txt" 30 | , snapshot = "/snapshot.jpg" 31 | } 32 | -------------------------------------------------------------------------------- /src-elm-starter/Starter/Cache.elm: -------------------------------------------------------------------------------- 1 | module Starter.Cache exposing (stuffToCache) 2 | 3 | import Main 4 | import Starter.FileNames 5 | 6 | 7 | stuffToCache : String -> String -> String -> List ( String, String ) -> List ( String, String ) 8 | stuffToCache relative version commit assets = 9 | let 10 | fileNames = 11 | Starter.FileNames.fileNames version commit 12 | in 13 | [] 14 | -- Production elm.js 15 | ++ [ ( relative ++ fileNames.outputCompiledJsProd, version ) ] 16 | -- manifest.json 17 | ++ [ ( relative ++ fileNames.manifestJson, version ) ] 18 | -- Static pages coming from src/Main.elm 19 | ++ List.map (\url -> ( url, version )) Main.conf.urls 20 | -- Extra stuff coming from src/Main.elm 21 | ++ List.map (\url -> ( url, version )) Main.conf.assetsToCache 22 | ++ assets 23 | |> List.map (\( url, hash ) -> ( String.replace "//" "/" url, hash )) 24 | -------------------------------------------------------------------------------- /ssc-src.txt: -------------------------------------------------------------------------------- 1 | ─────────────────────────────────────────────────────────────────────────────── 2 | Language Files Lines Blanks Comments Code Complexity 3 | ─────────────────────────────────────────────────────────────────────────────── 4 | Elm 27 5174 1094 1186 2894 92 5 | JSON 3 2264 0 0 2264 0 6 | JavaScript 2 564 54 63 447 73 7 | License 1 17 9 0 8 0 8 | Markdown 1 474 164 0 310 0 9 | Plain Text 1 0 0 0 0 0 10 | TOML 1 4 1 0 3 0 11 | gitignore 1 5 0 0 5 0 12 | ─────────────────────────────────────────────────────────────────────────────── 13 | Total 37 8502 1322 1249 5931 165 14 | ─────────────────────────────────────────────────────────────────────────────── 15 | Estimated Cost to Develop $175,137 16 | Estimated Schedule Effort 7.094117 months 17 | Estimated People Required 2.193296 18 | ─────────────────────────────────────────────────────────────────────────────── 19 | Processed 354800 bytes, 0.355 megabytes (SI) 20 | ─────────────────────────────────────────────────────────────────────────────── 21 | 22 | -------------------------------------------------------------------------------- /src-elm-starter/Html/String/Keyed.elm: -------------------------------------------------------------------------------- 1 | module Html.String.Keyed exposing 2 | ( node 3 | , ol, ul 4 | ) 5 | 6 | {-| A keyed node helps optimize cases where children are getting added, moved, 7 | removed, etc. Common examples include: 8 | 9 | - The user can delete items from a list. 10 | - The user can create new items in a list. 11 | - You can sort a list based on name or date or whatever. 12 | 13 | When you use a keyed node, every child is paired with a string identifier. This 14 | makes it possible for the underlying diffing algorithm to reuse nodes more 15 | efficiently. 16 | 17 | 18 | # Keyed Nodes 19 | 20 | @docs node 21 | 22 | 23 | # Commonly Keyed Nodes 24 | 25 | @docs ol, ul 26 | 27 | -} 28 | 29 | import Html.String exposing (Attribute, Html) 30 | import Html.Types 31 | 32 | 33 | {-| Works just like `Html.node`, but you add a unique identifier to each child 34 | node. You want this when you have a list of nodes that is changing: adding 35 | nodes, removing nodes, etc. In these cases, the unique identifiers help make 36 | the DOM modifications more efficient. 37 | -} 38 | node : String -> List (Attribute msg) -> List ( String, Html msg ) -> Html msg 39 | node tag attrs children = 40 | Html.Types.Node tag attrs (Html.Types.Keyed children) 41 | 42 | 43 | {-| -} 44 | ol : List (Attribute msg) -> List ( String, Html msg ) -> Html msg 45 | ol = 46 | node "ol" 47 | 48 | 49 | {-| -} 50 | ul : List (Attribute msg) -> List ( String, Html msg ) -> Html msg 51 | ul = 52 | node "ul" 53 | -------------------------------------------------------------------------------- /src-elm-starter/Starter/ConfMeta.elm: -------------------------------------------------------------------------------- 1 | module Starter.ConfMeta exposing 2 | ( ConfMeta 3 | , confMeta 4 | ) 5 | 6 | import Starter.Version 7 | 8 | 9 | type alias ConfMeta = 10 | -- ports 11 | { portBuild : Int 12 | , portStatic : Int 13 | , portDev : Int 14 | 15 | -- 16 | , versionElmStarter : String 17 | 18 | -- 19 | , indentation : Int 20 | 21 | -- notifications 22 | , messageDoNotEditDisclaimer : String 23 | , messageEnableJavascriptForBetterExperience : String 24 | , messageLoading : String 25 | , messageYouNeedToEnableJavascript : String 26 | 27 | -- tags 28 | , tagLoader : String 29 | , tagNotification : String 30 | } 31 | 32 | 33 | confMeta : ConfMeta 34 | confMeta = 35 | -- ports 36 | { portStatic = 7000 37 | , portDev = 8000 38 | , portBuild = 9000 39 | 40 | -- 41 | , versionElmStarter = Starter.Version.version 42 | 43 | -- 44 | , indentation = 0 45 | 46 | -- messages 47 | , messageYouNeedToEnableJavascript = "You need to enable JavaScript to run this app." 48 | , messageEnableJavascriptForBetterExperience = "Enable Javascript for a better experience." 49 | , messageLoading = "L O A D I N G . . ." 50 | , messageDoNotEditDisclaimer = "Generated file ** DO NOT EDIT DIRECTLY ** Edit Elm files instead" 51 | 52 | -- tags 53 | , tagLoader = prefix ++ "notification" 54 | , tagNotification = prefix ++ "loader" 55 | } 56 | 57 | 58 | prefix : String 59 | prefix = 60 | "elm-starter-" 61 | -------------------------------------------------------------------------------- /src-elm-starter/Starter/Manifest.elm: -------------------------------------------------------------------------------- 1 | module Starter.Manifest exposing 2 | ( Manifest 3 | , manifest 4 | ) 5 | 6 | import Json.Encode 7 | import Starter.Icon 8 | 9 | 10 | manifestIcon : String -> Int -> Json.Encode.Value 11 | manifestIcon relative size = 12 | let 13 | sizeString = 14 | String.fromInt size 15 | in 16 | Json.Encode.object 17 | [ ( "src", Json.Encode.string (Starter.Icon.iconFileName relative size) ) 18 | , ( "sizes", Json.Encode.string (sizeString ++ "x" ++ sizeString) ) 19 | , ( "type", Json.Encode.string "image/png" ) 20 | , ( "purpose", Json.Encode.string "any maskable" ) 21 | ] 22 | 23 | 24 | type alias Manifest = 25 | { iconSizes : List Int 26 | , themeColor : String 27 | , name : String 28 | , nameLong : String 29 | } 30 | 31 | 32 | manifest : String -> Manifest -> Json.Encode.Value 33 | manifest relative args = 34 | -- https://developer.mozilla.org/en-US/docs/Web/Manifest 35 | Json.Encode.object 36 | [ ( "short_name", Json.Encode.string args.name ) 37 | , ( "name", Json.Encode.string args.nameLong ) 38 | , ( "start_url", Json.Encode.string (relative ++ "/") ) 39 | , ( "display", Json.Encode.string "standalone" ) 40 | , ( "background_color", Json.Encode.string args.themeColor ) 41 | , ( "theme_color", Json.Encode.string args.themeColor ) 42 | , ( "icons", Json.Encode.list (manifestIcon relative) args.iconSizes ) 43 | 44 | -- TODO - add https://developer.mozilla.org/en-US/docs/Web/Manifest/screenshots 45 | ] 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | elm-html-string is Copyright (c) 2017, Ilias Van Peer 2 | 3 | https://github.com/zwilias/elm-html-string/blob/2.0.2/LICENSE 4 | 5 | 6 | 7 | elm-starter is Copyright 2020-present, Luca Mugnaini 8 | 9 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 10 | 11 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 12 | 13 | 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. 14 | 15 | 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. 16 | 17 | 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. 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.14", 3 | "name": "elm-starter", 4 | "nameLong": "elm-starter - An Elm-based bootstrapper for Elm applications", 5 | "description": "An Elm-based light way to setup a fully fledged Elm PWA.", 6 | "author": "Luca Mugnaini", 7 | "twitterSite": "lucamug", 8 | "twitterCreator": "lucamug", 9 | "homepage": "https://elm-starter.guupa.com/", 10 | "license": "BSD-3-Clause", 11 | "snapshotWidth": 700, 12 | "snapshotHeight": 350, 13 | "themeColor": { 14 | "red": 15, 15 | "green": 85, 16 | "blue": 123 17 | }, 18 | "main": "index.js", 19 | "repository": { 20 | "type": "git", 21 | "url": "git@github.com:lucamug/elm-starter" 22 | }, 23 | "scripts": { 24 | "start": "node ./src-elm-starter/elm-starter start", 25 | "build": "node ./src-elm-starter/elm-starter build", 26 | "serverBuild": "node ./src-elm-starter/elm-starter serverBuild", 27 | "elm-starter:boot": "node ./src-elm-starter/elm-starter boot", 28 | "elm-starter:start": "node ./src-elm-starter/elm-starter start", 29 | "elm-starter:generateDevFiles": "node ./src-elm-starter/elm-starter generateDevFiles", 30 | "elm-starter:build": "node ./src-elm-starter/elm-starter build", 31 | "elm-starter:buildExpectingTheServerRunning": "node ./src-elm-starter/elm-starter buildExpectingTheServerRunning", 32 | "elm-starter:serverBuild": "node ./src-elm-starter/elm-starter serverBuild", 33 | "elm-starter:serverDev": "node ./src-elm-starter/elm-starter serverDev", 34 | "elm-starter:serverStatic": "node ./src-elm-starter/elm-starter serverStatic", 35 | "elm-starter:watchStartElm": "node ./src-elm-starter/elm-starter watchStartElm" 36 | }, 37 | "devDependencies": { 38 | "chokidar-cli": "^3.0.0", 39 | "concurrently": "^6.4.0", 40 | "elm": "^0.19.1-3", 41 | "elm-go": "^5.0.13", 42 | "html-minifier": "^4.0.0", 43 | "puppeteer": "^11.0.0", 44 | "puppeteer-core": "^11.0.0", 45 | "terser": "^5.9.0" 46 | }, 47 | "dependencies": { 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src-elm-starter/Html/String/Extra.elm: -------------------------------------------------------------------------------- 1 | module Html.String.Extra exposing 2 | ( body 3 | , charset 4 | , content 5 | , crossorigin 6 | , doctype 7 | , head 8 | , html 9 | , httpEquiv 10 | , link 11 | , meta 12 | , noscript 13 | , property_ 14 | , script 15 | , style_ 16 | , title_ 17 | ) 18 | 19 | import Html.String exposing (..) 20 | import Html.String.Attributes exposing (..) 21 | 22 | 23 | 24 | -- HTML EXTENSIONS 25 | -- 26 | -- Adding nodes and attributes useful to generate an entire HTML page 27 | 28 | 29 | doctype : String 30 | doctype = 31 | "" 32 | 33 | 34 | html : List (Attribute msg) -> List (Html msg) -> Html msg 35 | html = 36 | node "html" 37 | 38 | 39 | head : List (Attribute msg) -> List (Html msg) -> Html msg 40 | head = 41 | node "head" 42 | 43 | 44 | body : List (Attribute msg) -> List (Html msg) -> Html msg 45 | body = 46 | node "body" 47 | 48 | 49 | script : List (Attribute msg) -> List (Html msg) -> Html msg 50 | script = 51 | node "script" 52 | 53 | 54 | noscript : List (Attribute msg) -> List (Html msg) -> Html msg 55 | noscript = 56 | node "noscript" 57 | 58 | 59 | meta : List (Attribute msg) -> List (Html msg) -> Html msg 60 | meta = 61 | node "meta" 62 | 63 | 64 | link : List (Attribute msg) -> List (Html msg) -> Html msg 65 | link = 66 | node "link" 67 | 68 | 69 | style_ : List (Attribute msg) -> List (Html msg) -> Html msg 70 | style_ = 71 | node "style" 72 | 73 | 74 | title_ : List (Attribute msg) -> List (Html msg) -> Html msg 75 | title_ = 76 | node "title" 77 | 78 | 79 | charset : String -> Attribute msg 80 | charset = 81 | attribute "charset" 82 | 83 | 84 | crossorigin : String -> Attribute msg 85 | crossorigin = 86 | attribute "crossorigin" 87 | 88 | 89 | property_ : String -> Attribute msg 90 | property_ = 91 | attribute "property" 92 | 93 | 94 | content : String -> Attribute msg 95 | content = 96 | attribute "content" 97 | 98 | 99 | httpEquiv : String -> Attribute msg 100 | httpEquiv = 101 | attribute "http-equiv" 102 | -------------------------------------------------------------------------------- /src-elm-starter/Html/String/Lazy.elm: -------------------------------------------------------------------------------- 1 | module Html.String.Lazy exposing (lazy, lazy2, lazy3, lazy4, lazy5, lazy6, lazy7, lazy8) 2 | 3 | {-| 4 | 5 | 6 | # 🔥 This isn't actually lazy in this library.. 7 | 8 | .. because we can't keep track of the model without existential types. It just 9 | eagerly evaluates. This set of function is here to serve as a drop-in 10 | replacement. 11 | 12 | @docs lazy, lazy2, lazy3, lazy4, lazy5, lazy6, lazy7, lazy8 13 | 14 | -} 15 | 16 | import Html.String exposing (Html) 17 | 18 | 19 | {-| A performance optimization that delays the building of virtual DOM nodes. 20 | 21 | Calling `(view model)` will definitely build some virtual DOM, perhaps a lot of 22 | it. Calling `(lazy view model)` delays the call until later. During diffing, we 23 | can check to see if `model` is referentially equal to the previous value used, 24 | and if so, we just stop. No need to build up the tree structure and diff it, 25 | we know if the input to `view` is the same, the output must be the same! 26 | 27 | -} 28 | lazy : (a -> Html msg) -> a -> Html msg 29 | lazy f x = 30 | f x 31 | 32 | 33 | {-| Same as `lazy` but checks on two arguments. 34 | -} 35 | lazy2 : (a -> b -> Html msg) -> a -> b -> Html msg 36 | lazy2 f x y = 37 | f x y 38 | 39 | 40 | {-| Same as `lazy` but checks on three arguments. 41 | -} 42 | lazy3 : (a -> b -> c -> Html msg) -> a -> b -> c -> Html msg 43 | lazy3 f x y z = 44 | f x y z 45 | 46 | 47 | {-| Same as `lazy` but checks on four arguments. 48 | -} 49 | lazy4 : (a -> b -> c -> Html msg) -> a -> b -> c -> Html msg 50 | lazy4 f x y z = 51 | f x y z 52 | 53 | 54 | {-| Same as `lazy` but checks on five arguments. 55 | -} 56 | lazy5 : (a -> b -> c -> Html msg) -> a -> b -> c -> Html msg 57 | lazy5 f x y z = 58 | f x y z 59 | 60 | 61 | {-| Same as `lazy` but checks on six arguments. 62 | -} 63 | lazy6 : (a -> b -> c -> Html msg) -> a -> b -> c -> Html msg 64 | lazy6 f x y z = 65 | f x y z 66 | 67 | 68 | {-| Same as `lazy` but checks on seven arguments. 69 | -} 70 | lazy7 : (a -> b -> c -> Html msg) -> a -> b -> c -> Html msg 71 | lazy7 f x y z = 72 | f x y z 73 | 74 | 75 | {-| Same as `lazy` but checks on eight arguments. 76 | -} 77 | lazy8 : (a -> b -> c -> Html msg) -> a -> b -> c -> Html msg 78 | lazy8 f x y z = 79 | f x y z 80 | -------------------------------------------------------------------------------- /src-elm-starter/Application.elm: -------------------------------------------------------------------------------- 1 | module Application exposing (main) 2 | 3 | import Browser 4 | import Html 5 | import Html.Attributes 6 | import Json.Encode 7 | import Starter.Conf 8 | 9 | 10 | type Msg 11 | = OnUrlRequest 12 | | OnUrlChange 13 | 14 | 15 | type alias Flags = 16 | () 17 | 18 | 19 | type alias Model = 20 | () 21 | 22 | 23 | view : Model -> { body : List (Html.Html msg), title : String } 24 | view _ = 25 | { title = "title" 26 | , body = 27 | [ Html.div [ Html.Attributes.style "margin" "40px" ] 28 | [ Html.h1 [] [ Html.text "elm-starter configuration" ] 29 | , Html.pre 30 | [] 31 | [ Html.text <| 32 | Json.Encode.encode 4 <| 33 | Starter.Conf.conf 34 | -- From package.jspn 35 | { name = "NOT-AVAILABLE [name]" 36 | , nameLong = "NOT-AVAILABLE [nameLong]" 37 | , description = "NOT-AVAILABLE [description]" 38 | , author = "NOT-AVAILABLE [author]" 39 | , version = "NOT-AVAILABLE [version]" 40 | , homepage = "http://example.com/xxx/yyy" 41 | , license = "NOT-AVAILABLE [license]" 42 | , twitterSite = Just "NOT-AVAILABLE [twitterSite]" 43 | , twitterAuthor = Just "NOT-AVAILABLE [twitterAuthor]" 44 | , snapshotWidth = Just "NOT-AVAILABLE [snapshotWidth]" 45 | , snapshotHeight = Just "NOT-AVAILABLE [snapshotHeight]" 46 | , themeColor = Nothing 47 | 48 | -- From Git 49 | , commit = "NOT-AVAILABLE [commit]" 50 | , branch = "NOT-AVAILABLE [branch]" 51 | 52 | -- From starter.js 53 | , env = "NOT-AVAILABLE [env]" 54 | , dirPw = "NOT-AVAILABLE [dirPw]" 55 | , dirBin = "NOT-AVAILABLE [dirBin]" 56 | , dirIgnoredByGit = "NOT-AVAILABLE [dirIgnoredByGit]" 57 | , dirTemp = "NOT-AVAILABLE [dirTemp]" 58 | , fileElmWorker = "NOT-AVAILABLE [fileElmWorker]" 59 | } 60 | ] 61 | ] 62 | ] 63 | } 64 | 65 | 66 | main : Program Flags Model Msg 67 | main = 68 | Browser.application 69 | { init = \_ _ _ -> ( (), Cmd.none ) 70 | , view = view 71 | , update = \_ model -> ( model, Cmd.none ) 72 | , subscriptions = \_ -> Sub.none 73 | , onUrlRequest = \_ -> OnUrlRequest 74 | , onUrlChange = \_ -> OnUrlChange 75 | } 76 | -------------------------------------------------------------------------------- /src-elm-starter/Starter/ElmGo.elm: -------------------------------------------------------------------------------- 1 | module Starter.ElmGo exposing 2 | ( Compilation(..) 3 | , ElmLiveArgs 4 | , HotReload(..) 5 | , Pushstate(..) 6 | , Reload(..) 7 | , Ssl(..) 8 | , Verbose(..) 9 | , elmGo 10 | , encoder 11 | ) 12 | 13 | import Json.Encode 14 | 15 | 16 | type alias ElmLiveArgs = 17 | { dir : String 18 | , outputCompiledJs : String 19 | , indexHtml : String 20 | , port_ : Int 21 | , compilation : Compilation 22 | , verbose : Verbose 23 | , pushstate : Pushstate 24 | , reload : Reload 25 | , hotReload : HotReload 26 | , ssl : Ssl 27 | , elmFileToCompile : String 28 | , dirBin : String 29 | , relative : String 30 | , certificatesFolder : String 31 | } 32 | 33 | 34 | type Compilation 35 | = Optimize 36 | | Normal 37 | | Debug 38 | 39 | 40 | type Reload 41 | = ReloadYes 42 | | ReloadNo 43 | 44 | 45 | type HotReload 46 | = HotReloadYes 47 | | HotReloadNo 48 | 49 | 50 | type Verbose 51 | = VerboseYes 52 | | VerboseNo 53 | 54 | 55 | type Pushstate 56 | = PushstateYes 57 | | PushstateNo 58 | 59 | 60 | type Ssl 61 | = SslYes 62 | | SslNo 63 | 64 | 65 | elmGo : ElmLiveArgs -> { command : String, parameters : List String } 66 | elmGo args = 67 | -- { command = args.dirBin ++ "/elm-go" 68 | { command = "node" 69 | , parameters = 70 | [ "node_modules/.bin/elm-go" 71 | 72 | -- [ "elm-go/bin/elm-go.js" 73 | , args.elmFileToCompile 74 | , "--path-to-elm=" ++ args.dirBin ++ "/elm" 75 | , "--dir=" ++ args.dir 76 | , "--start-page=" ++ args.indexHtml 77 | , "--port=" ++ String.fromInt args.port_ 78 | ] 79 | ++ (case args.ssl of 80 | SslYes -> 81 | [ "--ssl" 82 | 83 | -- , "--ssl-cert=" ++ args.certificatesFolder ++ "/localhost.crt" 84 | -- , "--ssl-key=" ++ args.certificatesFolder ++ "/localhost.key" 85 | ] 86 | 87 | SslNo -> 88 | [] 89 | ) 90 | ++ (case args.pushstate of 91 | PushstateYes -> 92 | [ "--pushstate" ] 93 | 94 | PushstateNo -> 95 | [] 96 | ) 97 | ++ (case args.verbose of 98 | VerboseYes -> 99 | [ "--verbose" ] 100 | 101 | VerboseNo -> 102 | [] 103 | ) 104 | ++ (case args.hotReload of 105 | HotReloadYes -> 106 | [ "--hot" ] 107 | 108 | HotReloadNo -> 109 | [] 110 | ) 111 | ++ (case args.reload of 112 | ReloadYes -> 113 | [] 114 | 115 | ReloadNo -> 116 | [ "--no-reload" ] 117 | ) 118 | ++ [ "--" 119 | , "--output=" ++ args.outputCompiledJs 120 | ] 121 | ++ (case args.compilation of 122 | Optimize -> 123 | [ "--optimize" ] 124 | 125 | Normal -> 126 | [] 127 | 128 | Debug -> 129 | [ "--debug" ] 130 | ) 131 | } 132 | 133 | 134 | encoder : { command : String, parameters : List String } -> Json.Encode.Value 135 | encoder args = 136 | Json.Encode.object 137 | [ ( "command", Json.Encode.string args.command ) 138 | , ( "parameters", Json.Encode.list Json.Encode.string args.parameters ) 139 | ] 140 | -------------------------------------------------------------------------------- /src-elm-starter/Starter/ServiceWorker.elm: -------------------------------------------------------------------------------- 1 | module Starter.ServiceWorker exposing 2 | ( encoderCacheableUrls 3 | , precacheFiles 4 | , serviceWorker 5 | ) 6 | 7 | import Json.Encode 8 | import Starter.Cache 9 | import Starter.ConfMeta 10 | 11 | 12 | encoderCacheableUrls : { a | revision : String, url : String } -> Json.Encode.Value 13 | encoderCacheableUrls obj = 14 | Json.Encode.object 15 | [ ( "url", Json.Encode.string obj.url ) 16 | , ( "revision", Json.Encode.string obj.revision ) 17 | ] 18 | 19 | 20 | precacheFiles : 21 | { assets : List ( String, String ) 22 | , commit : String 23 | , relative : String 24 | , version : String 25 | } 26 | -> String 27 | precacheFiles { relative, version, commit, assets } = 28 | Starter.Cache.stuffToCache relative version commit assets 29 | |> List.map (\( url, revision ) -> { url = url, revision = revision ++ "." ++ commit }) 30 | |> Json.Encode.list encoderCacheableUrls 31 | |> Json.Encode.encode 4 32 | 33 | 34 | serviceWorker : 35 | { assets : List ( String, String ) 36 | , commit : String 37 | , relative : String 38 | , version : String 39 | } 40 | -> String 41 | serviceWorker { relative, version, commit, assets } = 42 | "// " 43 | ++ Starter.ConfMeta.confMeta.messageDoNotEditDisclaimer 44 | -- 45 | -- 2021.06.18 - We added "skipWaiting" to solve a problem about 46 | -- the page not refreshing after an update. 47 | -- 48 | -- We also added location.reload() in SnippetJavascript.elm, for the 49 | -- same issue. 50 | -- 51 | -- Useful links 52 | -- 53 | -- https://dev-test.jp.account.rakuten.com/device-management/faq/en/general/ 54 | -- 55 | -- https://developers.google.com/web/tools/workbox/modules/workbox-strategies 56 | -- 57 | -- https://stackoverflow.com/questions/60912127/workbox-update-cache-on-new-version 58 | -- 59 | -- https://stackoverflow.com/questions/49897182/workbox-sw-runtime-caching-not-working-until-second-reload 60 | -- 61 | -- https://blog.sessionstack.com/how-javascript-works-service-workers-their-life-cycle-and-use-cases-52b19ad98b58 62 | -- 63 | -- https://developers.google.com/web/tools/workbox/modules/workbox-strategies 64 | -- 65 | -- https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/skipWaiting 66 | -- 67 | -- https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle 68 | -- 69 | -- https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle 70 | -- 71 | ++ """ 72 | 73 | self.addEventListener('install', event => { 74 | self.skipWaiting(); 75 | }); 76 | 77 | // 78 | // This is implemented using Workbox 79 | // https://developers.google.com/web/tools/workbox 80 | // 81 | 82 | importScripts('https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js'); 83 | 84 | const registerRoute = workbox.routing.registerRoute; 85 | const NetworkFirst = workbox.strategies.NetworkFirst; 86 | const CacheFirst = workbox.strategies.CacheFirst; 87 | const StaleWhileRevalidate = workbox.strategies.StaleWhileRevalidate; 88 | const ExpirationPlugin = workbox.expiration.ExpirationPlugin; 89 | const precacheAndRoute = workbox.precaching.precacheAndRoute; 90 | 91 | // https://developers.google.com/web/tools/workbox/guides/precache-files 92 | precacheAndRoute( 93 | """ 94 | ++ precacheFiles 95 | { assets = assets 96 | , commit = commit 97 | , relative = relative 98 | , version = version 99 | } 100 | ++ """ 101 | ); 102 | 103 | 104 | registerRoute( 105 | ({request}) => { 106 | return request.destination === 'document' 107 | }, 108 | new NetworkFirst() 109 | ); 110 | 111 | registerRoute( 112 | ({request}) => { 113 | return request.destination === 'script' 114 | }, 115 | new NetworkFirst() 116 | ); 117 | 118 | registerRoute( 119 | // Cache style assets, i.e. CSS files. 120 | ({request}) => request.destination === 'style', 121 | // Use cache but update in the background. 122 | new StaleWhileRevalidate({ 123 | // Use a custom cache name. 124 | cacheName: 'css-cache', 125 | }) 126 | ); 127 | 128 | // From https://developers.google.com/web/tools/workbox/guides/common-recipes 129 | registerRoute( 130 | ({request}) => request.destination === 'image', 131 | new CacheFirst({ 132 | cacheName: 'images', 133 | plugins: [ 134 | new ExpirationPlugin({ 135 | maxEntries: 60, 136 | maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days 137 | }), 138 | ], 139 | }) 140 | ); 141 | """ 142 | -------------------------------------------------------------------------------- /src/Index.elm: -------------------------------------------------------------------------------- 1 | module Index exposing 2 | ( htmlToReinjectInBody 3 | , htmlToReinjectInHead 4 | , index 5 | ) 6 | 7 | import Html.String exposing (..) 8 | import Html.String.Attributes exposing (..) 9 | import Html.String.Extra exposing (..) 10 | import Main 11 | import Starter.FileNames 12 | import Starter.Flags 13 | import Starter.Icon 14 | import Starter.SnippetHtml 15 | import Starter.SnippetJavascript 16 | 17 | 18 | index : Starter.Flags.Flags -> Html msg 19 | index flags = 20 | let 21 | relative = 22 | Starter.Flags.toRelative flags 23 | 24 | fileNames = 25 | Starter.FileNames.fileNames flags.version flags.commit 26 | in 27 | html 28 | [ lang "en" ] 29 | [ head [] 30 | ([] 31 | ++ [ meta [ charset "utf-8" ] [] 32 | , title_ [] [ text flags.nameLong ] 33 | , meta [ name "author", content flags.author ] [] 34 | , meta [ name "description", content flags.description ] [] 35 | , meta [ name "viewport", content "width=device-width, initial-scale=1, shrink-to-fit=no" ] [] 36 | , meta [ httpEquiv "x-ua-compatible", content "ie=edge" ] [] 37 | , link [ rel "canonical", href flags.homepage ] [] 38 | , link [ rel "icon", href (Starter.Icon.iconFileName relative 64) ] [] 39 | , link [ rel "apple-touch-icon", href (Starter.Icon.iconFileName relative 152) ] [] 40 | , style_ [] 41 | [ text <| """ 42 | body 43 | { background-color: """ ++ Starter.Flags.toThemeColor flags ++ """ 44 | ; font-family: 'IBM Plex Sans', helvetica, sans-serif 45 | ; margin: 0px; 46 | }""" ] 47 | ] 48 | ++ Starter.SnippetHtml.messagesStyle 49 | ++ Starter.SnippetHtml.pwa 50 | { commit = flags.commit 51 | , relative = relative 52 | , themeColor = Starter.Flags.toThemeColor flags 53 | , version = flags.version 54 | } 55 | ++ Starter.SnippetHtml.previewCards 56 | { commit = flags.commit 57 | , flags = flags 58 | , mainConf = Main.conf 59 | , version = flags.version 60 | } 61 | ) 62 | , body [] 63 | ([] 64 | -- Friendly message in case Javascript is disabled 65 | ++ (if flags.env == "dev" then 66 | Starter.SnippetHtml.messageYouNeedToEnableJavascript 67 | 68 | else 69 | Starter.SnippetHtml.messageEnableJavascriptForBetterExperience 70 | ) 71 | -- "Loading..." message 72 | ++ Starter.SnippetHtml.messageLoading 73 | -- The DOM node that Elm will take over 74 | ++ [ div [ id "elm" ] [] ] 75 | -- Activating the "Loading..." message 76 | ++ Starter.SnippetHtml.messageLoadingOn 77 | -- Loading Elm code 78 | ++ [ script [ src (relative ++ fileNames.outputCompiledJsProd) ] [] ] 79 | -- Elm finished to load, de-activating the "Loading..." message 80 | ++ Starter.SnippetHtml.messageLoadingOff 81 | -- Loading utility for pretty console formatting 82 | ++ Starter.SnippetHtml.prettyConsoleFormatting relative flags.env 83 | -- Signature "Made with ❤ and Elm" 84 | ++ [ script [] [ textUnescaped Starter.SnippetJavascript.signature ] ] 85 | -- Initializing "window.ElmStarter" 86 | ++ [ script [] [ textUnescaped <| Starter.SnippetJavascript.metaInfo flags ] ] 87 | -- Let's start Elm! 88 | ++ [ Html.String.Extra.script [] 89 | [ Html.String.textUnescaped 90 | (""" 91 | var node = document.getElementById('elm'); 92 | window.ElmApp = Elm.Main.init( 93 | { node: node 94 | , flags: 95 | { starter : """ 96 | ++ Starter.SnippetJavascript.metaInfoData flags 97 | ++ """ 98 | , width: window.innerWidth 99 | , height: window.innerHeight 100 | , languages: window.navigator.userLanguages || window.navigator.languages || [] 101 | , locationHref: location.href 102 | } 103 | } 104 | );""" 105 | ++ Starter.SnippetJavascript.portOnUrlChange 106 | ++ Starter.SnippetJavascript.portPushUrl 107 | ++ Starter.SnippetJavascript.portChangeMeta 108 | ) 109 | ] 110 | ] 111 | -- Register the Service Worker, we are a PWA! 112 | ++ [ script [] [ textUnescaped (Starter.SnippetJavascript.registerServiceWorker relative) ] ] 113 | ) 114 | ] 115 | 116 | 117 | htmlToReinjectInHead : a -> List b 118 | htmlToReinjectInHead _ = 119 | [] 120 | 121 | 122 | htmlToReinjectInBody : a -> List b 123 | htmlToReinjectInBody _ = 124 | [] 125 | -------------------------------------------------------------------------------- /src-elm-starter/Starter/SnippetJavascript.elm: -------------------------------------------------------------------------------- 1 | module Starter.SnippetJavascript exposing 2 | ( appWorkAlsoWithoutJS 3 | , metaInfo 4 | , metaInfoData 5 | , portChangeMeta 6 | , portOnUrlChange 7 | , portPushUrl 8 | , registerServiceWorker 9 | , selfInvoking 10 | , signature 11 | ) 12 | 13 | import Json.Encode 14 | import Starter.Flags 15 | 16 | 17 | selfInvoking : String -> String 18 | selfInvoking code = 19 | "( function () {\"use strict\";\n" ++ code ++ "\n})();" 20 | 21 | 22 | metaInfo : Starter.Flags.Flags -> String 23 | metaInfo flags = 24 | "window.ElmStarter = " ++ metaInfoData flags ++ ";" 25 | 26 | 27 | metaInfoData : Starter.Flags.Flags -> String 28 | metaInfoData flags = 29 | Json.Encode.encode 4 <| 30 | Starter.Flags.encoder flags 31 | 32 | 33 | signature : String 34 | signature = 35 | selfInvoking <| """ 36 | var color = 37 | { default: "background: #eee; color: gray; font-family: monospace" 38 | , love: "background: red; color: #eee" 39 | , elm: "background: #77d7ef; color: #00479a" 40 | }; 41 | var emptyLine = " ".repeat(49); 42 | var message = 43 | [ "" 44 | , "%c" 45 | , emptyLine 46 | , " m a d e w i t h %c ❤ %c a n d %c e l m %c " 47 | , emptyLine 48 | , "" 49 | , "" 50 | ].join("\\n"); 51 | console.info 52 | ( message 53 | , color.default 54 | , color.love 55 | , color.default 56 | , color.elm 57 | , color.default 58 | );""" 59 | 60 | 61 | registerServiceWorker : String -> String 62 | registerServiceWorker relative = 63 | -- 64 | -- 2021.06.18 We added location.reload() in SnippetJavascript.elm to 65 | -- solve a problem about the page not refreshing after an update. 66 | -- 67 | -- We also added "skipWaiting" in ServiceWorker.elm for the 68 | -- same issue. 69 | -- 70 | -- https://stackoverflow.com/questions/41891031/refresh-page-on-controllerchange-in-service-worker 71 | -- 72 | -- https://developers.google.com/web/tools/workbox/guides/get-started 73 | selfInvoking <| """ 74 | if (location.hostname === "localhost") { 75 | console.log("NOT loading the service worker in development"); 76 | } else { 77 | if ('serviceWorker' in navigator) { 78 | // Use the window load event to keep the page load performant 79 | window.addEventListener('load', function() { 80 | navigator.serviceWorker.register('""" ++ relative ++ """/service-worker.js').then(function(registration) { 81 | // Registration was successful 82 | 83 | if (!navigator.serviceWorker.controller) { 84 | return 85 | } 86 | 87 | registration.addEventListener('updatefound', function () { 88 | const newWorker = registration.installing 89 | newWorker.state 90 | 91 | var refreshing 92 | 93 | newWorker.addEventListener('statechange', () => { 94 | if (newWorker.state == 'activated') { 95 | if (refreshing) return 96 | window.location.reload() 97 | refreshing = true 98 | } 99 | }) 100 | }) 101 | 102 | }, function(err) { 103 | // registration failed :( 104 | }); 105 | }); 106 | } 107 | }""" 108 | 109 | 110 | {-| Changing "You need js..." to "Better to use js..." because 111 | the app is working also wihtout js in production when 112 | these pages are generated with Puppeteer 113 | -} 114 | appWorkAlsoWithoutJS : 115 | { a 116 | | messageEnableJavascriptForBetterExperience : String 117 | , messageYouNeedToEnableJavascript : String 118 | } 119 | -> String 120 | appWorkAlsoWithoutJS args = 121 | """ 122 | var noscriptElement = document.querySelector('noscript'); 123 | if (noscriptElement) { 124 | noscriptElement.innerHTML = noscriptElement.innerHTML.replace 125 | ( \"""" ++ args.messageYouNeedToEnableJavascript ++ """" 126 | , \"""" ++ args.messageEnableJavascriptForBetterExperience ++ """" 127 | ); 128 | } """ 129 | 130 | 131 | portOnUrlChange : String 132 | portOnUrlChange = 133 | """ 134 | // From https://github.com/elm/browser/blob/1.0.2/notes/navigation-in-elements.md 135 | // Inform app of browser navigation (the BACK and FORWARD buttons) 136 | if (ElmApp && ElmApp.ports && ElmApp.ports.onUrlChange) { 137 | window.addEventListener('popstate', function () { 138 | ElmApp.ports.onUrlChange.send(location.href); 139 | }); 140 | } """ 141 | 142 | 143 | portPushUrl : String 144 | portPushUrl = 145 | """ 146 | // From https://github.com/elm/browser/blob/1.0.2/notes/navigation-in-elements.md 147 | // Change the URL upon request, inform app of the change. 148 | if (ElmApp && ElmApp.ports && ElmApp.ports.pushUrl) { 149 | ElmApp.ports.pushUrl.subscribe(function(url) { 150 | history.pushState({}, '', url); 151 | // Comment the next line if you don't want pages to scroll to the 152 | // top automatically, everytime they switch 153 | window.scrollTo(0, 0); 154 | if (ElmApp && ElmApp.ports && ElmApp.ports.onUrlChange) { 155 | ElmApp.ports.onUrlChange.send(location.href); 156 | } 157 | }); 158 | } """ 159 | 160 | 161 | portChangeMeta : String 162 | portChangeMeta = 163 | """ 164 | if (ElmApp && ElmApp.ports && ElmApp.ports.changeMeta) { 165 | ElmApp.ports.changeMeta.subscribe(function(args) { 166 | if (args.querySelector !== "") { 167 | var element = document.querySelector(args.querySelector); 168 | if (element) { 169 | if (args.type_ == "attribute") { 170 | element.setAttribute(args.fieldName, args.content); 171 | } else if (args.type_ == "property" && element[args.fieldName]) { 172 | element[args.fieldName] = args.content; 173 | } 174 | } 175 | } 176 | }); 177 | } """ 178 | -------------------------------------------------------------------------------- /src-elm-starter/Starter/SnippetHtml.elm: -------------------------------------------------------------------------------- 1 | module Starter.SnippetHtml exposing 2 | ( messageEnableJavascriptForBetterExperience 3 | , messageLoading 4 | , messageLoadingOff 5 | , messageLoadingOn 6 | , messageYouNeedToEnableJavascript 7 | , messagesStyle 8 | , prettyConsoleFormatting 9 | , previewCards 10 | , pwa 11 | ) 12 | 13 | import Html.String exposing (..) 14 | import Html.String.Attributes exposing (..) 15 | import Html.String.Extra exposing (..) 16 | import Starter.ConfMeta 17 | import Starter.FileNames 18 | import Starter.Flags 19 | import Starter.SnippetCss 20 | 21 | 22 | {-| PWA stuff 23 | -} 24 | pwa : 25 | { commit : String 26 | , relative : String 27 | , themeColor : String 28 | , version : String 29 | } 30 | -> List (Html msg) 31 | pwa { relative, version, commit, themeColor } = 32 | -- DNS preconnect and prefetch for 33 | -- https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js 34 | [ link [ rel "preconnect", href "https://storage.googleapis.com", crossorigin "true" ] [] 35 | , link [ rel "dns-prefetch", href "https://storage.googleapis.com" ] [] 36 | 37 | -- PWA 38 | , meta [ name "theme-color", content themeColor ] [] 39 | , meta [ name "mobile-web-app-capable", content "yes" ] [] 40 | , link [ rel "manifest", href (relative ++ .manifestJson (Starter.FileNames.fileNames version commit)) ] [] 41 | 42 | -- iOS 43 | , meta [ name "apple-mobile-web-app-capable", content "yes" ] [] 44 | , meta [ name "apple-mobile-web-app-status-bar-style", content "black" ] [] 45 | , meta [ name "apple-mobile-web-app-title", content "Test PWA" ] [] 46 | ] 47 | 48 | 49 | {-| Mix of Twitter and Open Graph tags to define a summary card 50 | 51 | 52 | 53 | -} 54 | previewCards : 55 | { commit : String 56 | , flags : Starter.Flags.Flags 57 | , mainConf : b 58 | , version : String 59 | } 60 | -> List (Html msg) 61 | previewCards args = 62 | -- 63 | -- From https://medium.com/slack-developer-blog/everything-you-ever-wanted-to-know-about-unfurling-but-were-afraid-to-ask-or-how-to-make-your-e64b4bb9254 64 | -- 65 | -- facebook open graph tags 66 | let 67 | relative = 68 | Starter.Flags.toRelative args.flags 69 | in 70 | [] 71 | ++ [ meta [ property_ "og:type", content "website" ] [] 72 | , meta [ property_ "og:url", content args.flags.homepage ] [] 73 | , meta [ property_ "og:title", content args.flags.nameLong ] [] 74 | , meta [ property_ "og:description", content args.flags.description ] [] 75 | , meta [ property_ "og:image", content (relative ++ .snapshot (Starter.FileNames.fileNames args.version args.commit)) ] [] 76 | 77 | -- twitter card tags additive with the og: tags 78 | , meta [ name "twitter:card", content "summary_large_image" ] [] 79 | ] 80 | ++ (case args.flags.twitterSite of 81 | Just twitterSite -> 82 | [ meta [ name "twitter:site", content ("@" ++ twitterSite) ] [] ] 83 | 84 | Nothing -> 85 | [] 86 | ) 87 | ++ (case args.flags.twitterAuthor of 88 | Just twitterAuthor -> 89 | [ meta [ name "twitter:site", content ("@" ++ twitterAuthor) ] [] ] 90 | 91 | Nothing -> 92 | [] 93 | ) 94 | ++ [ meta [ name "twitter:domain", value args.flags.homepage ] [] 95 | , meta [ name "twitter:title", value args.flags.nameLong ] [] 96 | , meta [ name "twitter:description", value args.flags.description ] [] 97 | , meta [ name "twitter:image", content (relative ++ .snapshot (Starter.FileNames.fileNames args.version args.commit)) ] [] 98 | , meta [ name "twitter:url", value args.flags.homepage ] [] 99 | 100 | -- , meta [ name "twitter:label1", value "Opens in Theaters" ] [] 101 | -- , meta [ name "twitter:data1", value "December 1, 2015" ] [] 102 | -- , meta [ name "twitter:label2", value "Or on demand" ] [] 103 | -- , meta [ name "twitter:data2", value "at Hulu.com" ] [] 104 | ] 105 | 106 | 107 | prettyConsoleFormatting : String -> String -> List (Html msg) 108 | prettyConsoleFormatting relative env = 109 | if env == "dev" then 110 | -- TODO - Add the right path here 111 | [ script [ src (relative ++ "/assets-dev/elm-console-debug.js") ] [] 112 | , script [] [ textUnescaped "ElmConsoleDebug.register()" ] 113 | ] 114 | 115 | else 116 | [] 117 | 118 | 119 | messageYouNeedToEnableJavascript : List (Html msg) 120 | messageYouNeedToEnableJavascript = 121 | [ noscript [] 122 | [ div 123 | [ class Starter.ConfMeta.confMeta.tagNotification 124 | , style "top" "0" 125 | , style "height" "100vh" 126 | ] 127 | [ text Starter.ConfMeta.confMeta.messageYouNeedToEnableJavascript ] 128 | ] 129 | ] 130 | 131 | 132 | messageEnableJavascriptForBetterExperience : List (Html msg) 133 | messageEnableJavascriptForBetterExperience = 134 | [ noscript [] 135 | [ div 136 | [ class Starter.ConfMeta.confMeta.tagNotification 137 | , style "bottom" "0" 138 | ] 139 | [ text Starter.ConfMeta.confMeta.messageEnableJavascriptForBetterExperience ] 140 | ] 141 | ] 142 | 143 | 144 | messageLoading : List (Html msg) 145 | messageLoading = 146 | [ div 147 | [ id Starter.ConfMeta.confMeta.tagLoader 148 | , class Starter.ConfMeta.confMeta.tagNotification 149 | , style "height" "100vh" 150 | , style "display" "none" 151 | ] 152 | [ text Starter.ConfMeta.confMeta.messageLoading ] 153 | ] 154 | 155 | 156 | messageLoadingOn : List (Html msg) 157 | messageLoadingOn = 158 | [ script [] 159 | [ textUnescaped <| 160 | "document.getElementById('" 161 | ++ Starter.ConfMeta.confMeta.tagLoader 162 | ++ "').style.display = 'block';" 163 | ] 164 | ] 165 | 166 | 167 | messageLoadingOff : List (Html msg) 168 | messageLoadingOff = 169 | [ script [] 170 | [ textUnescaped <| 171 | "document.getElementById('" 172 | ++ Starter.ConfMeta.confMeta.tagLoader 173 | ++ "').style.display = 'none';" 174 | ] 175 | ] 176 | 177 | 178 | messagesStyle : List (Html msg) 179 | messagesStyle = 180 | [ style_ [] 181 | [ text <| 182 | Starter.SnippetCss.noJsAndLoadingNotifications 183 | Starter.ConfMeta.confMeta.tagNotification 184 | ] 185 | ] 186 | -------------------------------------------------------------------------------- /src-elm-starter/Starter/Flags.elm: -------------------------------------------------------------------------------- 1 | module Starter.Flags exposing 2 | ( Dir 3 | , Env(..) 4 | , File 5 | , Flags 6 | , dir 7 | , dirEncoder 8 | , encoder 9 | , file 10 | , fileEncoder 11 | , toRelative 12 | , toThemeColor 13 | , toThemeColorRgb 14 | ) 15 | 16 | import Json.Encode 17 | import Url 18 | 19 | 20 | type Env 21 | = Dev 22 | | Prod 23 | 24 | 25 | type alias Color = 26 | { red : String, green : String, blue : String } 27 | 28 | 29 | 30 | -- FLAGS 31 | 32 | 33 | type alias Flags = 34 | -- From package.jspn 35 | { name : String 36 | , nameLong : String 37 | , description : String 38 | , author : String 39 | , version : String 40 | , homepage : String 41 | , license : String 42 | , twitterSite : Maybe String 43 | , twitterAuthor : Maybe String 44 | , snapshotWidth : Maybe String 45 | , snapshotHeight : Maybe String 46 | , themeColor : Maybe Color 47 | 48 | -- From Git 49 | , commit : String 50 | , branch : String 51 | 52 | -- From starter.js 53 | , env : String 54 | , dirPw : String 55 | , dirBin : String 56 | , dirIgnoredByGit : String 57 | , dirTemp : String 58 | , dirAssets : String 59 | , fileElmWorker : String 60 | , assets : List ( String, String ) 61 | } 62 | 63 | 64 | maybe : (a -> Json.Encode.Value) -> Maybe a -> Json.Encode.Value 65 | maybe encoder_ = 66 | Maybe.map encoder_ >> Maybe.withDefault Json.Encode.null 67 | 68 | 69 | colorEncoder : Color -> Json.Encode.Value 70 | colorEncoder color = 71 | Json.Encode.object 72 | [ ( "red", Json.Encode.string color.red ) 73 | , ( "green", Json.Encode.string color.green ) 74 | , ( "blue", Json.Encode.string color.blue ) 75 | ] 76 | 77 | 78 | encoder : Flags -> Json.Encode.Value 79 | encoder flags = 80 | Json.Encode.object 81 | -- From package.json 82 | [ ( "name", Json.Encode.string flags.name ) 83 | , ( "nameLong", Json.Encode.string flags.nameLong ) 84 | , ( "description", Json.Encode.string flags.description ) 85 | , ( "author", Json.Encode.string flags.author ) 86 | , ( "version", Json.Encode.string flags.version ) 87 | , ( "homepage", Json.Encode.string flags.homepage ) 88 | , ( "license", Json.Encode.string flags.license ) 89 | , ( "twitterSite", maybe Json.Encode.string flags.twitterSite ) 90 | , ( "twitterAuthor", maybe Json.Encode.string flags.twitterAuthor ) 91 | , ( "snapshotWidth", maybe Json.Encode.string flags.snapshotWidth ) 92 | , ( "snapshotHeight", maybe Json.Encode.string flags.snapshotHeight ) 93 | , ( "themeColor", maybe colorEncoder flags.themeColor ) 94 | 95 | -- Git 96 | , ( "commit", Json.Encode.string flags.commit ) 97 | , ( "branch", Json.Encode.string flags.branch ) 98 | 99 | -- From starter.js 100 | , ( "env", Json.Encode.string flags.env ) 101 | , ( "dirPw", Json.Encode.string flags.dirPw ) 102 | , ( "dirBin", Json.Encode.string flags.dirBin ) 103 | , ( "dirIgnoredByGit", Json.Encode.string flags.dirIgnoredByGit ) 104 | , ( "dirTemp", Json.Encode.string flags.dirTemp ) 105 | , ( "dirAssets", Json.Encode.string flags.dirAssets ) 106 | , ( "fileElmWorker", Json.Encode.string flags.fileElmWorker ) 107 | , ( "assets", Json.Encode.list (Json.Encode.list Json.Encode.string) (List.map (\( a, b ) -> [ a, b ]) flags.assets) ) 108 | ] 109 | 110 | 111 | toThemeColorRgb : Flags -> { blue : Int, green : Int, red : Int } 112 | toThemeColorRgb flags = 113 | case flags.themeColor of 114 | Just color -> 115 | { red = Maybe.withDefault 255 <| String.toInt color.red 116 | , green = Maybe.withDefault 255 <| String.toInt color.green 117 | , blue = Maybe.withDefault 255 <| String.toInt color.blue 118 | } 119 | 120 | Nothing -> 121 | { red = 255, green = 255, blue = 255 } 122 | 123 | 124 | toThemeColor : Flags -> String 125 | toThemeColor flags = 126 | let 127 | color = 128 | toThemeColorRgb flags 129 | in 130 | "rgb(" ++ String.join "," (List.map String.fromInt [ color.red, color.green, color.blue ]) ++ ")" 131 | 132 | 133 | 134 | -- DIR 135 | 136 | 137 | type alias Dir = 138 | { bin : String 139 | , build : String 140 | , buildRoot : String 141 | , dev : String 142 | , devRoot : String 143 | , assetsDevTarget : String 144 | , ignoredByGit : String 145 | , pw : String 146 | , src : String 147 | , temp : String 148 | , assets : String 149 | , assetsDevSource : String 150 | , elmStartSrc : String 151 | , relative : String 152 | } 153 | 154 | 155 | toRelative : { a | homepage : String } -> String 156 | toRelative flags = 157 | let 158 | relative = 159 | flags.homepage 160 | |> Url.fromString 161 | |> Maybe.map .path 162 | |> Maybe.withDefault "" 163 | in 164 | if relative == "/" then 165 | "" 166 | 167 | else 168 | relative 169 | 170 | 171 | dir : Flags -> Dir 172 | dir flags = 173 | let 174 | relative = 175 | toRelative flags 176 | 177 | devRoot = 178 | flags.dirIgnoredByGit ++ "/dev" 179 | 180 | buildRoot = 181 | flags.dirIgnoredByGit ++ "/build" 182 | in 183 | { pw = flags.dirPw 184 | , bin = flags.dirBin 185 | , temp = flags.dirTemp 186 | , src = flags.dirPw ++ "/src" 187 | , elmStartSrc = flags.dirPw ++ "/src-elm-starter" 188 | , relative = relative 189 | 190 | -- Assets 191 | , assets = flags.dirAssets 192 | , assetsDevSource = flags.dirPw ++ "/assets/dev" 193 | 194 | -- Working dir 195 | , ignoredByGit = flags.dirIgnoredByGit 196 | , devRoot = devRoot 197 | , dev = devRoot ++ relative 198 | , assetsDevTarget = devRoot ++ relative ++ "/assets-dev" 199 | , buildRoot = buildRoot 200 | , build = buildRoot ++ relative 201 | } 202 | 203 | 204 | dirEncoder : Dir -> Json.Encode.Value 205 | dirEncoder dir_ = 206 | Json.Encode.object 207 | [ ( "pw", Json.Encode.string dir_.pw ) 208 | , ( "bin", Json.Encode.string dir_.bin ) 209 | , ( "temp", Json.Encode.string dir_.temp ) 210 | , ( "src", Json.Encode.string dir_.src ) 211 | , ( "elmStartSrc", Json.Encode.string dir_.elmStartSrc ) 212 | , ( "relative", Json.Encode.string dir_.relative ) 213 | 214 | -- Assets 215 | , ( "assets", Json.Encode.string dir_.assets ) 216 | , ( "assetsDevSource", Json.Encode.string dir_.assetsDevSource ) 217 | 218 | -- Working dir 219 | , ( "ignoredByGit", Json.Encode.string dir_.ignoredByGit ) 220 | , ( "dev", Json.Encode.string dir_.dev ) 221 | , ( "devRoot", Json.Encode.string dir_.devRoot ) 222 | , ( "assetsDevTarget", Json.Encode.string dir_.assetsDevTarget ) 223 | , ( "build", Json.Encode.string dir_.build ) 224 | , ( "buildRoot", Json.Encode.string dir_.buildRoot ) 225 | ] 226 | 227 | 228 | 229 | -- FILE 230 | 231 | 232 | type alias File = 233 | { elmWorker : String 234 | , jsStarter : String 235 | , indexElm : String 236 | , mainElm : String 237 | } 238 | 239 | 240 | file : Flags -> File 241 | file flags = 242 | { elmWorker = flags.fileElmWorker 243 | , jsStarter = .elmStartSrc (dir flags) ++ "/elm-starter" 244 | , indexElm = .src (dir flags) ++ "/Index.elm" 245 | , mainElm = .src (dir flags) ++ "/Main.elm" 246 | } 247 | 248 | 249 | fileEncoder : File -> Json.Encode.Value 250 | fileEncoder file_ = 251 | Json.Encode.object 252 | [ ( "elmWorker", Json.Encode.string file_.elmWorker ) 253 | , ( "jsStarter", Json.Encode.string file_.jsStarter ) 254 | , ( "indexElm", Json.Encode.string file_.indexElm ) 255 | , ( "mainElm", Json.Encode.string file_.mainElm ) 256 | ] 257 | -------------------------------------------------------------------------------- /src-elm-starter/Html/String/Events.elm: -------------------------------------------------------------------------------- 1 | module Html.String.Events exposing 2 | ( onClick, onDoubleClick 3 | , onMouseDown, onMouseUp 4 | , onMouseEnter, onMouseLeave 5 | , onMouseOver, onMouseOut 6 | , onInput, onCheck, onSubmit 7 | , onBlur, onFocus 8 | , on, stopPropagationOn, preventDefaultOn, custom 9 | , targetValue, targetChecked, keyCode 10 | ) 11 | 12 | {-| It is often helpful to create an [Union Type] so you can have many different kinds 13 | of events as seen in the [TodoMVC] example. 14 | 15 | [Union Type]: https://elm-lang.org/learn/Union-Types.elm 16 | [TodoMVC]: https://github.com/evancz/elm-todomvc/blob/master/Todo.elm 17 | 18 | 19 | # Mouse 20 | 21 | @docs onClick, onDoubleClick 22 | @docs onMouseDown, onMouseUp 23 | @docs onMouseEnter, onMouseLeave 24 | @docs onMouseOver, onMouseOut 25 | 26 | 27 | # Forms 28 | 29 | @docs onInput, onCheck, onSubmit 30 | 31 | 32 | # Focus 33 | 34 | @docs onBlur, onFocus 35 | 36 | 37 | # Custom 38 | 39 | @docs on, stopPropagationOn, preventDefaultOn, custom 40 | 41 | 42 | ## Custom Decoders 43 | 44 | @docs targetValue, targetChecked, keyCode 45 | 46 | -} 47 | 48 | import Html.String exposing (Attribute) 49 | import Html.Types 50 | import Json.Decode as Json 51 | 52 | 53 | 54 | -- MOUSE EVENTS 55 | 56 | 57 | {-| -} 58 | onClick : msg -> Attribute msg 59 | onClick msg = 60 | on "click" (Json.succeed msg) 61 | 62 | 63 | {-| -} 64 | onDoubleClick : msg -> Attribute msg 65 | onDoubleClick msg = 66 | on "dblclick" (Json.succeed msg) 67 | 68 | 69 | {-| -} 70 | onMouseDown : msg -> Attribute msg 71 | onMouseDown msg = 72 | on "mousedown" (Json.succeed msg) 73 | 74 | 75 | {-| -} 76 | onMouseUp : msg -> Attribute msg 77 | onMouseUp msg = 78 | on "mouseup" (Json.succeed msg) 79 | 80 | 81 | {-| -} 82 | onMouseEnter : msg -> Attribute msg 83 | onMouseEnter msg = 84 | on "mouseenter" (Json.succeed msg) 85 | 86 | 87 | {-| -} 88 | onMouseLeave : msg -> Attribute msg 89 | onMouseLeave msg = 90 | on "mouseleave" (Json.succeed msg) 91 | 92 | 93 | {-| -} 94 | onMouseOver : msg -> Attribute msg 95 | onMouseOver msg = 96 | on "mouseover" (Json.succeed msg) 97 | 98 | 99 | {-| -} 100 | onMouseOut : msg -> Attribute msg 101 | onMouseOut msg = 102 | on "mouseout" (Json.succeed msg) 103 | 104 | 105 | 106 | -- FORM EVENTS 107 | 108 | 109 | {-| Detect [input](https://developer.mozilla.org/en-US/docs/Web/Events/input) 110 | events for things like text fields or text areas. 111 | 112 | For more details on how `onInput` works, check out [`targetValue`](#targetValue). 113 | 114 | **Note 1:** It grabs the **string** value at `event.target.value`, so it will 115 | not work if you need some other information. For example, if you want to track 116 | inputs on a range slider, make a custom handler with [`on`](#on). 117 | 118 | **Note 2:** It uses `stopPropagationOn` internally to always stop propagation 119 | of the event. This is important for complicated reasons explained [here][1] and 120 | [here][2]. 121 | 122 | [1]: /packages/elm/virtual-dom/latest/VirtualDom#Handler 123 | [2]: https://github.com/elm/virtual-dom/issues/125 124 | 125 | -} 126 | onInput : (String -> msg) -> Attribute msg 127 | onInput tagger = 128 | stopPropagationOn "input" (Json.map alwaysStop (Json.map tagger targetValue)) 129 | 130 | 131 | alwaysStop : a -> ( a, Bool ) 132 | alwaysStop x = 133 | ( x, True ) 134 | 135 | 136 | {-| Detect [change](https://developer.mozilla.org/en-US/docs/Web/Events/change) 137 | events on checkboxes. It will grab the boolean value from `event.target.checked` 138 | on any input event. 139 | 140 | Check out [`targetChecked`](#targetChecked) for more details on how this works. 141 | 142 | -} 143 | onCheck : (Bool -> msg) -> Attribute msg 144 | onCheck tagger = 145 | on "change" (Json.map tagger targetChecked) 146 | 147 | 148 | {-| Detect a [submit](https://developer.mozilla.org/en-US/docs/Web/Events/submit) 149 | event with [`preventDefault`](https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault) 150 | in order to prevent the form from changing the page’s location. If you need 151 | different behavior, create a custom event handler. 152 | -} 153 | onSubmit : msg -> Attribute msg 154 | onSubmit msg = 155 | preventDefaultOn "submit" (Json.map alwaysPreventDefault (Json.succeed msg)) 156 | 157 | 158 | alwaysPreventDefault : msg -> ( msg, Bool ) 159 | alwaysPreventDefault msg = 160 | ( msg, True ) 161 | 162 | 163 | 164 | -- FOCUS EVENTS 165 | 166 | 167 | {-| -} 168 | onBlur : msg -> Attribute msg 169 | onBlur msg = 170 | on "blur" (Json.succeed msg) 171 | 172 | 173 | {-| -} 174 | onFocus : msg -> Attribute msg 175 | onFocus msg = 176 | on "focus" (Json.succeed msg) 177 | 178 | 179 | 180 | -- CUSTOM EVENTS 181 | 182 | 183 | {-| Create a custom event listener. Normally this will not be necessary, but 184 | you have the power! Here is how `onClick` is defined for example: 185 | 186 | import Json.Decode as Decode 187 | 188 | onClick : msg -> Attribute msg 189 | onClick message = 190 | on "click" (Decode.succeed message) 191 | 192 | The first argument is the event name in the same format as with JavaScript's 193 | [`addEventListener`][aEL] function. 194 | 195 | The second argument is a JSON decoder. Read more about these [here][decoder]. 196 | When an event occurs, the decoder tries to turn the event object into an Elm 197 | value. If successful, the value is routed to your `update` function. In the 198 | case of `onClick` we always just succeed with the given `message`. 199 | 200 | If this is confusing, work through the [Elm Architecture Tutorial][tutorial]. 201 | It really helps! 202 | 203 | [aEL]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener 204 | [decoder]: /packages/elm/json/latest/Json-Decode 205 | [tutorial]: https://github.com/evancz/elm-architecture-tutorial/ 206 | 207 | **Note:** This creates a [passive] event listener, enabling optimizations for 208 | touch, scroll, and wheel events in some browsers. 209 | 210 | [passive]: https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md 211 | 212 | -} 213 | on : String -> Json.Decoder msg -> Attribute msg 214 | on event decoder = 215 | Html.Types.Event event (Html.Types.Normal decoder) 216 | 217 | 218 | {-| Create an event listener that may [`stopPropagation`][stop]. Your decoder 219 | must produce a message and a `Bool` that decides if `stopPropagation` should 220 | be called. 221 | 222 | [stop]: https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation 223 | 224 | **Note:** This creates a [passive] event listener, enabling optimizations for 225 | touch, scroll, and wheel events in some browsers. 226 | 227 | [passive]: https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md 228 | 229 | -} 230 | stopPropagationOn : String -> Json.Decoder ( msg, Bool ) -> Attribute msg 231 | stopPropagationOn event decoder = 232 | Html.Types.Event event (Html.Types.MayStopPropagation decoder) 233 | 234 | 235 | {-| Create an event listener that may [`preventDefault`][prevent]. Your decoder 236 | must produce a message and a `Bool` that decides if `preventDefault` should 237 | be called. 238 | 239 | For example, the `onSubmit` function in this library _always_ prevents the 240 | default behavior: 241 | 242 | [prevent]: https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault 243 | 244 | onSubmit : msg -> Attribute msg 245 | onSubmit msg = 246 | preventDefaultOn "submit" (Json.map alwaysPreventDefault (Json.succeed msg)) 247 | 248 | alwaysPreventDefault : msg -> ( msg, Bool ) 249 | alwaysPreventDefault msg = 250 | ( msg, True ) 251 | 252 | -} 253 | preventDefaultOn : String -> Json.Decoder ( msg, Bool ) -> Attribute msg 254 | preventDefaultOn event decoder = 255 | Html.Types.Event event (Html.Types.MayPreventDefault decoder) 256 | 257 | 258 | {-| Create an event listener that may [`stopPropagation`][stop] or 259 | [`preventDefault`][prevent]. 260 | 261 | [stop]: https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation 262 | [prevent]: https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault 263 | 264 | **Note:** If you need something even more custom (like capture phase) check 265 | out the lower-level event API in `elm/virtual-dom`. 266 | 267 | -} 268 | custom : String -> Json.Decoder { message : msg, stopPropagation : Bool, preventDefault : Bool } -> Attribute msg 269 | custom event decoder = 270 | Html.Types.Event event (Html.Types.Custom decoder) 271 | 272 | 273 | 274 | -- COMMON DECODERS 275 | 276 | 277 | {-| A `Json.Decoder` for grabbing `event.target.value`. We use this to define 278 | `onInput` as follows: 279 | 280 | import Json.Decode as Json 281 | 282 | onInput : (String -> msg) -> Attribute msg 283 | onInput tagger = 284 | stopPropagationOn "input" <| 285 | Json.map alwaysStop (Json.map tagger targetValue) 286 | 287 | alwaysStop : a -> ( a, Bool ) 288 | alwaysStop x = 289 | ( x, True ) 290 | 291 | You probably will never need this, but hopefully it gives some insights into 292 | how to make custom event handlers. 293 | 294 | -} 295 | targetValue : Json.Decoder String 296 | targetValue = 297 | Json.at [ "target", "value" ] Json.string 298 | 299 | 300 | {-| A `Json.Decoder` for grabbing `event.target.checked`. We use this to define 301 | `onCheck` as follows: 302 | 303 | import Json.Decode as Json 304 | 305 | onCheck : (Bool -> msg) -> Attribute msg 306 | onCheck tagger = 307 | on "input" (Json.map tagger targetChecked) 308 | 309 | -} 310 | targetChecked : Json.Decoder Bool 311 | targetChecked = 312 | Json.at [ "target", "checked" ] Json.bool 313 | 314 | 315 | {-| A `Json.Decoder` for grabbing `event.keyCode`. This helps you define 316 | keyboard listeners like this: 317 | 318 | import Json.Decode as Json 319 | 320 | onKeyUp : (Int -> msg) -> Attribute msg 321 | onKeyUp tagger = 322 | on "keyup" (Json.map tagger keyCode) 323 | 324 | **Note:** It looks like the spec is moving away from `event.keyCode` and 325 | towards `event.key`. Once this is supported in more browsers, we may add 326 | helpers here for `onKeyUp`, `onKeyDown`, `onKeyPress`, etc. 327 | 328 | -} 329 | keyCode : Json.Decoder Int 330 | keyCode = 331 | Json.field "keyCode" Json.int 332 | -------------------------------------------------------------------------------- /src-elm-starter/Starter/Conf.elm: -------------------------------------------------------------------------------- 1 | module Starter.Conf exposing 2 | ( Conf 3 | , conf_ 4 | ) 5 | 6 | import Html.String 7 | import Html.String.Extra 8 | import Index 9 | import Json.Encode 10 | import Main 11 | import Starter.ConfMain 12 | import Starter.ConfMeta 13 | import Starter.ElmGo 14 | import Starter.FileNames 15 | import Starter.Flags 16 | import Starter.Icon 17 | import Starter.Manifest 18 | import Starter.ServiceWorker 19 | 20 | 21 | file : ( String, String ) -> Json.Encode.Value 22 | file ( name, content_ ) = 23 | Json.Encode.object 24 | [ ( "name", Json.Encode.string name ) 25 | , ( "content", Json.Encode.string content_ ) 26 | ] 27 | 28 | 29 | fileIndexHtml : 30 | String 31 | -> Int 32 | -> Html.String.Html msg 33 | -> String 34 | fileIndexHtml messageDoNotEditDisclaimer indentation startingPage = 35 | Html.String.Extra.doctype 36 | ++ "\n" 37 | ++ "\n" 40 | ++ Html.String.toString indentation 41 | startingPage 42 | 43 | 44 | type alias Conf msg = 45 | { dir : Starter.Flags.Dir 46 | , file : Starter.Flags.File 47 | , fileNames : Starter.FileNames.FileNames 48 | , fileIndexHtml : Html.String.Html msg 49 | , htmlToReinjectInBody : List (Html.String.Html msg) 50 | , htmlToReinjectInHead : List (Html.String.Html msg) 51 | , iconsForManifest : List Int 52 | , portBuild : Int 53 | , portDev : Int 54 | , portStatic : Int 55 | , messageDoNotEditDisclaimer : String 56 | , flags : Starter.Flags.Flags 57 | } 58 | 59 | 60 | conf_ : Starter.Flags.Flags -> Json.Encode.Value 61 | conf_ flags = 62 | encoder 63 | { dir = Starter.Flags.dir flags 64 | , file = Starter.Flags.file flags 65 | , fileNames = Starter.FileNames.fileNames flags.version flags.commit 66 | , fileIndexHtml = Index.index flags 67 | , htmlToReinjectInBody = Index.htmlToReinjectInBody flags 68 | , htmlToReinjectInHead = Index.htmlToReinjectInHead flags 69 | , iconsForManifest = Starter.Icon.iconsForManifest 70 | , portBuild = Starter.ConfMeta.confMeta.portBuild 71 | , portDev = Starter.ConfMeta.confMeta.portDev 72 | , portStatic = Starter.ConfMeta.confMeta.portStatic 73 | , messageDoNotEditDisclaimer = Starter.ConfMeta.confMeta.messageDoNotEditDisclaimer 74 | , flags = flags 75 | } 76 | 77 | 78 | encoder : Conf msg -> Json.Encode.Value 79 | encoder conf = 80 | let 81 | relative = 82 | .relative (Starter.Flags.dir conf.flags) 83 | 84 | fileNames = 85 | Starter.FileNames.fileNames conf.flags.version conf.flags.commit 86 | in 87 | Json.Encode.object 88 | [ ( "outputCompiledJs", Json.Encode.string fileNames.outputCompiledJs ) 89 | , ( "outputCompiledJsProd", Json.Encode.string fileNames.outputCompiledJsProd ) 90 | , ( "dir" 91 | , conf.dir 92 | |> Starter.Flags.dirEncoder 93 | ) 94 | , ( "file" 95 | , conf.file 96 | |> Starter.Flags.fileEncoder 97 | ) 98 | , ( "serverDev" 99 | , { elmFileToCompile = .mainElm (Starter.Flags.file conf.flags) 100 | , dir = .devRoot (Starter.Flags.dir conf.flags) 101 | , outputCompiledJs = .dev (Starter.Flags.dir conf.flags) ++ conf.fileNames.outputCompiledJsProd 102 | , indexHtml = relative ++ conf.fileNames.indexHtml 103 | , relative = relative 104 | , port_ = conf.portDev 105 | , compilation = Starter.ElmGo.Debug 106 | , verbose = Starter.ElmGo.VerboseNo 107 | , pushstate = Starter.ElmGo.PushstateYes 108 | , reload = Starter.ElmGo.ReloadYes 109 | , hotReload = Starter.ElmGo.HotReloadYes 110 | , ssl = Starter.ElmGo.SslNo 111 | , dirBin = .bin (Starter.Flags.dir conf.flags) 112 | , certificatesFolder = conf.dir.elmStartSrc 113 | } 114 | |> Starter.ElmGo.elmGo 115 | |> Starter.ElmGo.encoder 116 | ) 117 | , ( "serverStatic" 118 | , { elmFileToCompile = .mainElm (Starter.Flags.file conf.flags) 119 | , dir = .devRoot (Starter.Flags.dir conf.flags) 120 | , outputCompiledJs = .dev (Starter.Flags.dir conf.flags) ++ conf.fileNames.outputCompiledJsProd 121 | , indexHtml = relative ++ conf.fileNames.indexHtml 122 | , relative = relative 123 | , port_ = conf.portStatic 124 | , compilation = Starter.ElmGo.Optimize 125 | , verbose = Starter.ElmGo.VerboseNo 126 | , pushstate = Starter.ElmGo.PushstateYes 127 | , reload = Starter.ElmGo.ReloadNo 128 | , hotReload = Starter.ElmGo.HotReloadNo 129 | , ssl = Starter.ElmGo.SslNo 130 | , dirBin = .bin (Starter.Flags.dir conf.flags) 131 | , certificatesFolder = conf.dir.elmStartSrc 132 | } 133 | |> Starter.ElmGo.elmGo 134 | |> Starter.ElmGo.encoder 135 | ) 136 | , ( "serverBuild" 137 | , { elmFileToCompile = .mainElm (Starter.Flags.file conf.flags) 138 | , dir = .buildRoot (Starter.Flags.dir conf.flags) 139 | , outputCompiledJs = .dev (Starter.Flags.dir conf.flags) ++ conf.fileNames.outputCompiledJsProd 140 | , indexHtml = conf.fileNames.indexHtml 141 | , relative = relative 142 | , port_ = conf.portBuild 143 | , compilation = Starter.ElmGo.Normal 144 | , verbose = Starter.ElmGo.VerboseNo 145 | , pushstate = Starter.ElmGo.PushstateNo 146 | , reload = Starter.ElmGo.ReloadNo 147 | , hotReload = Starter.ElmGo.HotReloadNo 148 | , ssl = Starter.ElmGo.SslNo 149 | , dirBin = .bin (Starter.Flags.dir conf.flags) 150 | , certificatesFolder = conf.dir.elmStartSrc 151 | } 152 | |> Starter.ElmGo.elmGo 153 | |> Starter.ElmGo.encoder 154 | ) 155 | , ( "headless", Json.Encode.bool True ) 156 | , ( "startingDomain" 157 | , Json.Encode.string 158 | ("http://localhost:" ++ String.fromInt conf.portStatic) 159 | ) 160 | , ( "batchesSize", Json.Encode.int 4 ) 161 | , ( "pagesName", Json.Encode.string "index.html" ) 162 | , ( "snapshots", Json.Encode.bool True ) 163 | , ( "snapshotsQuality", Json.Encode.int 80 ) 164 | , ( "snapshotWidth", Json.Encode.int <| Maybe.withDefault 700 <| String.toInt <| Maybe.withDefault "" <| conf.flags.snapshotWidth ) 165 | , ( "snapshotHeight", Json.Encode.int <| Maybe.withDefault 350 <| String.toInt <| Maybe.withDefault "" <| conf.flags.snapshotHeight ) 166 | , ( "snapshotFileName", Json.Encode.string fileNames.snapshot ) 167 | , ( "mainConf", Starter.ConfMain.encoder Main.conf ) 168 | , ( "htmlToReinjectInBody" 169 | , conf.htmlToReinjectInBody 170 | |> List.map (\html -> Html.String.toString Starter.ConfMeta.confMeta.indentation html) 171 | |> String.join "" 172 | |> Json.Encode.string 173 | ) 174 | , ( "htmlToReinjectInHead" 175 | , conf.htmlToReinjectInHead 176 | |> List.map (\html -> Html.String.toString Starter.ConfMeta.confMeta.indentation html) 177 | |> String.join "" 178 | |> Json.Encode.string 179 | ) 180 | , ( "flags", Starter.Flags.encoder conf.flags ) 181 | , ( "files" 182 | , (Json.Encode.list <| file) 183 | -- 184 | -- "/manifest.json" 185 | -- 186 | [ ( conf.fileNames.manifestJson 187 | , { iconSizes = conf.iconsForManifest 188 | , themeColor = Starter.Flags.toThemeColor conf.flags 189 | , name = conf.flags.name 190 | , nameLong = conf.flags.nameLong 191 | } 192 | |> Starter.Manifest.manifest relative 193 | |> Json.Encode.encode Starter.ConfMeta.confMeta.indentation 194 | ) 195 | 196 | -- "/_redirects" 197 | -- 198 | -- Netlify Configuration File 199 | -- 200 | -- , ( conf.fileNames.redirects 201 | -- , "/* /index.html 200" 202 | -- ) 203 | -- "/service-worker.js" 204 | -- 205 | , ( fileNames.serviceWorker 206 | , let 207 | -- Assets need to have different path 208 | assets = 209 | List.map 210 | (\( path, hash ) -> 211 | ( String.replace conf.flags.dirAssets relative path, hash ) 212 | ) 213 | conf.flags.assets 214 | in 215 | Starter.ServiceWorker.serviceWorker 216 | { assets = assets 217 | , commit = conf.flags.commit 218 | , relative = relative 219 | , version = conf.flags.version 220 | } 221 | ) 222 | 223 | -- "/index.html" 224 | -- 225 | , ( fileNames.indexHtml 226 | , fileIndexHtml conf.messageDoNotEditDisclaimer Starter.ConfMeta.confMeta.indentation conf.fileIndexHtml 227 | ) 228 | 229 | -- "/robots.txt" 230 | -- 231 | -- https://www.robotstxt.org/robotstxt.html 232 | -- 233 | , ( conf.fileNames.robotsTxt 234 | , [ "User-agent: *" 235 | , "Disallow:" 236 | , "Sitemap: " ++ conf.flags.homepage ++ conf.fileNames.sitemap 237 | ] 238 | |> String.join "\n" 239 | ) 240 | 241 | -- "/sitemap.txt" 242 | -- 243 | , ( conf.fileNames.sitemap 244 | , String.join "\n" <| List.map (\url -> String.replace relative "" conf.flags.homepage ++ url) Main.conf.urls 245 | ) 246 | ] 247 | ) 248 | ] 249 | -------------------------------------------------------------------------------- /src-elm-starter/Html/Types.elm: -------------------------------------------------------------------------------- 1 | module Html.Types exposing 2 | ( Attribute(..) 3 | , Children(..) 4 | , EventDecoder(..) 5 | , Html(..) 6 | , map 7 | , mapAttribute 8 | , toHtml 9 | , toString 10 | ) 11 | 12 | import Char 13 | import Html 14 | import Html.Attributes 15 | import Html.Events 16 | import Html.Keyed 17 | import Json.Decode as Decode exposing (Decoder, Value) 18 | import Json.Encode as Encode 19 | import String.Conversions 20 | 21 | 22 | type Children msg 23 | = NoChildren 24 | | Regular (List (Html msg)) 25 | | Keyed (List ( String, Html msg )) 26 | 27 | 28 | mapChildren : (a -> b) -> Children a -> Children b 29 | mapChildren f children = 30 | case children of 31 | NoChildren -> 32 | NoChildren 33 | 34 | Regular nodes -> 35 | Regular (List.map (map f) nodes) 36 | 37 | Keyed keyedNodes -> 38 | Keyed (List.map (Tuple.mapSecond <| map f) keyedNodes) 39 | 40 | 41 | type Html msg 42 | = Node String (List (Attribute msg)) (Children msg) 43 | | TextNode String 44 | | TextNodeUnescaped String 45 | 46 | 47 | map : (a -> b) -> Html a -> Html b 48 | map f node = 49 | case node of 50 | Node tagName attrs children -> 51 | Node tagName (List.map (mapAttribute f) attrs) (mapChildren f children) 52 | 53 | TextNode content -> 54 | TextNode content 55 | 56 | TextNodeUnescaped content -> 57 | TextNodeUnescaped content 58 | 59 | 60 | type Attribute msg 61 | = Attribute String String 62 | | StringProperty String String 63 | | BoolProperty String Bool 64 | | ValueProperty String Value 65 | | Style String String 66 | | Event String (EventDecoder msg) 67 | 68 | 69 | type EventDecoder msg 70 | = Normal (Decoder msg) 71 | | MayStopPropagation (Decoder ( msg, Bool )) 72 | | MayPreventDefault (Decoder ( msg, Bool )) 73 | | Custom (Decoder { message : msg, stopPropagation : Bool, preventDefault : Bool }) 74 | 75 | 76 | mapAttribute : (a -> b) -> Attribute a -> Attribute b 77 | mapAttribute f attribute = 78 | case attribute of 79 | Attribute key value -> 80 | Attribute key value 81 | 82 | StringProperty key value -> 83 | StringProperty key value 84 | 85 | BoolProperty key value -> 86 | BoolProperty key value 87 | 88 | ValueProperty key value -> 89 | ValueProperty key value 90 | 91 | Style key value -> 92 | Style key value 93 | 94 | Event name eventDecoder -> 95 | Event name (mapEventDecoder f eventDecoder) 96 | 97 | 98 | mapEventDecoder : (a -> b) -> EventDecoder a -> EventDecoder b 99 | mapEventDecoder f eventDecoder = 100 | case eventDecoder of 101 | Normal d -> 102 | Normal (Decode.map f d) 103 | 104 | MayStopPropagation d -> 105 | MayStopPropagation (Decode.map (Tuple.mapFirst f) d) 106 | 107 | MayPreventDefault d -> 108 | MayPreventDefault (Decode.map (Tuple.mapFirst f) d) 109 | 110 | Custom d -> 111 | Custom 112 | (Decode.map 113 | (\v -> 114 | { message = f v.message 115 | , stopPropagation = v.stopPropagation 116 | , preventDefault = v.preventDefault 117 | } 118 | ) 119 | d 120 | ) 121 | 122 | 123 | toHtml : Html msg -> Html.Html msg 124 | toHtml node = 125 | case node of 126 | Node tagName attributes children -> 127 | case children of 128 | NoChildren -> 129 | Html.node tagName (List.map attributeToHtml attributes) [] 130 | 131 | Regular nodes -> 132 | Html.node tagName (List.map attributeToHtml attributes) (List.map toHtml nodes) 133 | 134 | Keyed keyedNodes -> 135 | Html.Keyed.node tagName (List.map attributeToHtml attributes) (List.map (Tuple.mapSecond toHtml) keyedNodes) 136 | 137 | TextNode content -> 138 | Html.text content 139 | 140 | TextNodeUnescaped content -> 141 | Html.text content 142 | 143 | 144 | attributeToHtml : Attribute msg -> Html.Attribute msg 145 | attributeToHtml attribute = 146 | case attribute of 147 | Attribute key value -> 148 | Html.Attributes.attribute key value 149 | 150 | StringProperty key value -> 151 | Html.Attributes.property key (Encode.string value) 152 | 153 | BoolProperty key value -> 154 | Html.Attributes.property key (Encode.bool value) 155 | 156 | ValueProperty key value -> 157 | Html.Attributes.property key value 158 | 159 | Style key value -> 160 | Html.Attributes.style key value 161 | 162 | Event name (Normal d) -> 163 | Html.Events.on name d 164 | 165 | Event name (MayStopPropagation d) -> 166 | Html.Events.stopPropagationOn name d 167 | 168 | Event name (MayPreventDefault d) -> 169 | Html.Events.preventDefaultOn name d 170 | 171 | Event name (Custom d) -> 172 | Html.Events.custom name d 173 | 174 | 175 | toString : Int -> Html msg -> String 176 | toString depth html = 177 | let 178 | indenter : Indenter 179 | indenter = 180 | case depth of 181 | 0 -> 182 | always identity 183 | 184 | _ -> 185 | indent depth 186 | 187 | joinString : String 188 | joinString = 189 | case depth of 190 | 0 -> 191 | "" 192 | 193 | _ -> 194 | "\n" 195 | 196 | initialAcc : Acc msg 197 | initialAcc = 198 | { depth = 0 199 | , stack = [] 200 | , result = [] 201 | } 202 | in 203 | toStringHelper indenter [ html ] initialAcc 204 | |> .result 205 | |> join joinString 206 | 207 | 208 | join : String -> List String -> String 209 | join between list = 210 | case list of 211 | [] -> 212 | "" 213 | 214 | [ x ] -> 215 | x 216 | 217 | x :: xs -> 218 | List.foldl (\y acc -> y ++ between ++ acc) x xs 219 | 220 | 221 | type alias Indenter = 222 | Int -> String -> String 223 | 224 | 225 | type alias Acc msg = 226 | { depth : Int 227 | , stack : List (TagInfo msg) 228 | , result : List String 229 | } 230 | 231 | 232 | type alias TagInfo msg = 233 | ( String, List (Html msg) ) 234 | 235 | 236 | toStringHelper : Indenter -> List (Html msg) -> Acc msg -> Acc msg 237 | toStringHelper indenter tags acc = 238 | case tags of 239 | [] -> 240 | case acc.stack of 241 | [] -> 242 | acc 243 | 244 | ( tagName, cont ) :: rest -> 245 | toStringHelper indenter 246 | cont 247 | { acc 248 | | result = indenter (acc.depth - 1) (closingTag tagName) :: acc.result 249 | , depth = acc.depth - 1 250 | , stack = rest 251 | } 252 | 253 | (Node tagName attributes children) :: rest -> 254 | case children of 255 | NoChildren -> 256 | toStringHelper indenter 257 | rest 258 | { acc | result = indenter acc.depth (tag tagName attributes) :: acc.result } 259 | 260 | Regular childNodes -> 261 | toStringHelper indenter 262 | childNodes 263 | { acc 264 | | result = indenter acc.depth (tag tagName attributes) :: acc.result 265 | , depth = acc.depth + 1 266 | , stack = ( tagName, rest ) :: acc.stack 267 | } 268 | 269 | Keyed childNodes -> 270 | toStringHelper indenter 271 | (List.map Tuple.second childNodes) 272 | { acc 273 | | result = indenter acc.depth (tag tagName attributes) :: acc.result 274 | , depth = acc.depth + 1 275 | , stack = ( tagName, rest ) :: acc.stack 276 | } 277 | 278 | (TextNode string) :: rest -> 279 | toStringHelper indenter 280 | rest 281 | { acc | result = indenter acc.depth (escapeHtmlText string) :: acc.result } 282 | 283 | (TextNodeUnescaped string) :: rest -> 284 | toStringHelper indenter 285 | rest 286 | { acc | result = indenter acc.depth string :: acc.result } 287 | 288 | 289 | tag : String -> List (Attribute msg) -> String 290 | tag tagName attributes = 291 | "<" ++ String.join " " (tagName :: attributesToString attributes) ++ ">" 292 | 293 | 294 | escapeHtmlText : String -> String 295 | escapeHtmlText = 296 | String.replace "&" "&" 297 | >> String.replace "<" "<" 298 | >> String.replace ">" ">" 299 | 300 | 301 | attributesToString : List (Attribute msg) -> List String 302 | attributesToString attrs = 303 | let 304 | ( classes, styles, regular ) = 305 | List.foldl addAttribute ( [], [], [] ) attrs 306 | in 307 | regular 308 | |> withClasses classes 309 | |> withStyles styles 310 | 311 | 312 | withClasses : List String -> List String -> List String 313 | withClasses classes attrs = 314 | case classes of 315 | [] -> 316 | attrs 317 | 318 | _ -> 319 | buildProp "class" (join " " classes) :: attrs 320 | 321 | 322 | withStyles : List String -> List String -> List String 323 | withStyles styles attrs = 324 | case styles of 325 | [] -> 326 | attrs 327 | 328 | _ -> 329 | buildProp "style" (join "; " styles) :: attrs 330 | 331 | 332 | type alias AttrAcc = 333 | ( List String, List String, List String ) 334 | 335 | 336 | propName : String -> String 337 | propName prop = 338 | case prop of 339 | "className" -> 340 | "class" 341 | 342 | "defaultValue" -> 343 | "value" 344 | 345 | "htmlFor" -> 346 | "for" 347 | 348 | _ -> 349 | prop 350 | 351 | 352 | buildProp : String -> String -> String 353 | buildProp key value = 354 | hyphenate key ++ "=\"" ++ escape value ++ "\"" 355 | 356 | 357 | addAttribute : Attribute msg -> AttrAcc -> AttrAcc 358 | addAttribute attribute (( classes, styles, attrs ) as acc) = 359 | case attribute of 360 | Attribute key value -> 361 | ( classes, styles, buildProp key value :: attrs ) 362 | 363 | StringProperty "className" value -> 364 | ( value :: classes 365 | , styles 366 | , attrs 367 | ) 368 | 369 | StringProperty string value -> 370 | ( classes, styles, buildProp (propName string) value :: attrs ) 371 | 372 | BoolProperty string enabled -> 373 | if enabled then 374 | ( classes, styles, hyphenate (propName string) :: attrs ) 375 | 376 | else 377 | acc 378 | 379 | ValueProperty string value -> 380 | ( classes 381 | , styles 382 | , buildProp (propName string) (String.Conversions.fromValue value) :: attrs 383 | ) 384 | 385 | Style key value -> 386 | ( classes 387 | , (escape key ++ ": " ++ escape value) :: styles 388 | , attrs 389 | ) 390 | 391 | Event _ _ -> 392 | acc 393 | 394 | 395 | escape : String -> String 396 | escape = 397 | String.foldl 398 | (\char acc -> 399 | if char == '"' then 400 | acc ++ "\\\"" 401 | 402 | else 403 | acc ++ String.fromChar char 404 | ) 405 | "" 406 | 407 | 408 | hyphenate : String -> String 409 | hyphenate = 410 | String.foldl 411 | (\char acc -> 412 | if Char.isUpper char then 413 | acc ++ "-" ++ String.fromChar (Char.toLower char) 414 | 415 | else 416 | acc ++ String.fromChar char 417 | ) 418 | "" 419 | 420 | 421 | closingTag : String -> String 422 | closingTag tagName = 423 | "" 424 | 425 | 426 | indent : Int -> Int -> String -> String 427 | indent perLevel level x = 428 | String.repeat (perLevel * level) " " ++ x 429 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elm-starter 2 | 3 | `elm-starter` is an experimental Elm-based Elm bootstrapper that can also be plugged into already existing Elm applications. 4 | 5 | Post ["elm-starter", a tool for the Modern Web](https://dev.to/lucamug/elm-starter-a-tool-for-the-modern-web-53b1). 6 | 7 | Example of the installed version, with and without Javascript enabled: 8 | 9 | ![elm-starter](assets/dev/elm-starter.gif) 10 | 11 | ## Demos 12 | 13 | These are three simple examples of websites built with `elm-starter`: 14 | 15 | * [https://elm-starter.guupa.com/](https://elm-starter.guupa.com/) ([Code](https://github.com/lucamug/elm-starter)) 16 | * [https://elm-todomvc.guupa.com/](https://elm-todomvc.guupa.com/) ([Code](https://github.com/lucamug/elm-todomvc)) 17 | * [https://elm-spa-example.guupa.com/](https://elm-spa-example.guupa.com/) ([Code](https://github.com/lucamug/elm-spa-example)) 18 | * [https://elm-physics-example.guupa.com/](https://elm-physics-example.guupa.com/) ([Code](https://github.com/lucamug/elm-physics-example)) 19 | 20 | ![Collection of examples](assets/dev/collection.png) 21 | ![elm-physics-example](assets/dev/elm-physics-example.gif) 22 | 23 | ## Characteristics 24 | 25 | * Generate a PWA (Progressive Web Application) 26 | * Mostly written in Elm 27 | * Pages are pre-rendered at build time 28 | * Works offline 29 | * Works without Javascript(\*) 30 | * SEO 31 | * Preview cards (Facebook, Twitter, etc.) work as expected 32 | * Installable both on desktop and on mobile 33 | * High score with Lighthouse 34 | * Friendly notifications: "Loading...", "Must enable Javascript...", "Better enable Javascript..." 35 | * Potentially compatible with all Elm libraries (elm-ui, elm-spa, etc.) 36 | * Hopefully relatively simple to use and maintain 37 | * Non invasive (you can easily add/remove it) 38 | * Works with Netlify, Surge, etc. 39 | * Supports websites living in subfolder 40 | 41 | Lighthouse report: 42 | 43 | ![Lighthouse report](assets/dev/lighthouse.png) 44 | 45 | Slack's previews (note how different urls have different snapshot and meta-data): 46 | 47 | ![Slack's previews](assets/dev/slack-previews.jpg) 48 | 49 | ## How to bootstrap a new project 50 | 51 | `elm-starter` is not published in npm yet and it doesn't have a specific command to bootstrap a project, so the way it works now is cloning this repo. 52 | 53 | The fastest way is to [click here](https://app.netlify.com/start/deploy?repository=https://github.com/lucamug/elm-starter). This will automatically clone the repo and publish in Netlify. 54 | 55 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/lucamug/elm-starter) 56 | 57 | Otherwise the steps are: 58 | 59 | ```sh 60 | git clone https://github.com/lucamug/elm-starter 61 | mv elm-starter my-new-project 62 | cd my-new-project 63 | rm -rf .git 64 | npm install 65 | ``` 66 | 67 | Done! `elm-starter` is installed. 68 | 69 | To start using it you should create your own git repository, add files and make a commit (example: `git init && git add . && git commit -m "initial commit"`). 70 | 71 | These are the available commands: 72 | 73 | ### `$ npm start` 74 | 75 | Runs the app in the development mode. 76 | Open [http://localhost:8000](http://localhost:8000) to view it in the browser. 77 | 78 | Edit `src/Main.elm` and save to reload the browser. 79 | 80 | Also edit `src/Index.elm` and `package.json` for further customization. 81 | 82 | ### `$ npm run build` 83 | 84 | Builds the app for production to the `elm-stuff/elm-starter-files/build` folder. 85 | 86 | ### `$ npm run serverBuild` 87 | 88 | Launches a server in the `build` folder. 89 | 90 | Open [http://localhost:9000](http://localhost:9000) to view it in the browser. 91 | 92 | ## How to use `elm-starter` in existing Elm application 93 | 94 | Let's suppose your existing project is in `my-elm-app` 95 | 96 | * Clone `elm-starter` with 97 | `$ git clone https://github.com/lucamug/elm-starter.git` 98 | * Copy the folder [`elm-starter/src-elm-starter/`](https://github.com/lucamug/elm-starter/tree/master/src-elm-starter) to `my-elm-app/src-elm-starter/` 99 | * Copy the file [`elm-starter/src/Index.elm`](https://github.com/lucamug/elm-starter/blob/master/src/Index.elm) to `my-elm-app/src/Index.elm` 100 | * Copy the function `conf` from [`elm-starter/src/Main.elm`](https://github.com/lucamug/elm-starter/blob/master/src/Main.elm#L33) to `my-elm-app/src/Main.elm` (remember also to expose it) 101 | * If you don't have `package.json` in your project, add one with `$ npm init` 102 | * Be sure that you have these values in `package.json` as they will be used all over the places: 103 | 104 | * "name" - An npm-compatible name (cannot contain spaces) 105 | * "nameLong" - The regular name used all over the places, like in the `` of the page, for example 106 | * "description" 107 | * "author" 108 | * "twitterSite" - A reference to a Twitter handle (Just the id without "@") 109 | * "twitterCreator" - Another Twitter handle, refer to [Twitter cards markups]([https://developer.twitter.com/en/docs/](https://developer.twitter.com/en/docs/) tweets/optimize-with-cards/overview/markup) 110 | * "version" 111 | * "homepage" 112 | * "license" 113 | * "snapshotWidth" - default: 700 px 114 | * "snapshotHeight" - default: 350 px 115 | * "themeColor" - default: { "red": 15, "green": 85, "blue": 123 } 116 | 117 | * Add `node` dependencies with these commands 118 | 119 | ```sh 120 | npm install --save-dev chokidar-cli 121 | npm install --save-dev concurrently 122 | npm install --save-dev elm 123 | npm install --save-dev elm-go 124 | npm install --save-dev html-minifier 125 | npm install --save-dev puppeteer 126 | npm install --save-dev terser 127 | ``` 128 | 129 | * Add `src-elm-starter` as an extra `source-directory` in `elm.json`, the same as in `elm-starter/elm.json` 130 | * Add these commands to `package.json` (or run them directly) 131 | 132 | ```json 133 | "scripts": { 134 | "start": "node ./src-elm-starter/starter.js start", 135 | "build": "node ./src-elm-starter/starter.js build", 136 | "serverBuild": "node ./src-elm-starter/starter.js serverBuild" 137 | }, 138 | ``` 139 | 140 | Done! 141 | 142 | ## `sandbox` vs. `element` vs. `document` vs. `application` 143 | 144 | In Elm there are several ways to start an application: 145 | 146 | * [Browser.sandbox](https://package.elm-lang.org/packages/elm/browser/latest/Browser#sandbox) 147 | * [Browser.element](https://package.elm-lang.org/packages/elm/browser/latest/Browser#element) 148 | * [Browser.document](https://package.elm-lang.org/packages/elm/browser/latest/Browser#document) 149 | * [Browser.application](https://package.elm-lang.org/packages/elm/browser/latest/Browser#application) 150 | 151 | Among these, the first two take over only one specific node of the DOM, the other two instead take over the entire body of the page. So we need to follow two different strategies. 152 | 153 | ### Case for `sandbox` & `element` 154 | 155 | In the `Index.elm` you need to create a `<div>` that will be used to attach the Elm application. 156 | 157 | In the `elm-starter` example we use a `<div>` with id `elm`. In `Index.elm`, see the line: 158 | 159 | ``` 160 | ++ [ div [ id "elm" ] [] ] 161 | ``` 162 | 163 | Then we use this node to initialize Elm: 164 | 165 | ``` 166 | var node = document.getElementById('elm'); 167 | window.ElmApp = Elm.Main.init( 168 | { node: node 169 | , flags: ...you flags here... 170 | } 171 | ``` 172 | 173 | Then, to be sure that the node created by the static page generator is replaced later on, we need to add such `<div>` in the `view` of `Main.elm` like this: 174 | 175 | ``` 176 | view : Model -> Html.Html Msg 177 | view model = 178 | Html.div 179 | [ Html.Attributes.id "elm" ] 180 | [ ...you content here... ] 181 | ``` 182 | 183 | Note: You can change the id from `elm` to anything you like. 184 | 185 | ### Case for `document` & `application` 186 | 187 | This case require a different approach. You can see an example in the [elm-spa-example](https://github.com/lucamug/elm-spa-example). 188 | 189 | The main differences compared to the above approach are: 190 | 191 | * You **don't** need to create a specific `<div>` with id `elm`. 192 | * You need to move all Javascript section of the page into the `htmlToReinjectInBody` section (see [Index.elm](https://github.com/lucamug/elm-spa-example/blob/master/src/Index.elm) as example). 193 | 194 | `htmlToReinjectInBody` will be injected after the page is generated during the build process assuring that the system will work also in this case. 195 | 196 | ## Netlify 197 | 198 | When setting up the app with Netlify, input these in the deploy configuration: 199 | 200 | * Build command: `npm run build` (or `node ./src-elm-starter/starter.js start`) 201 | * Publish directory: `elm-stuff/elm-starter-files/build` 202 | 203 | ## Other CI/CD platforms 204 | 205 | There are cases where some CI process is not able to install and use Puppeteer properly. 206 | 207 | One of these cases was solved installing chromium manually running 208 | 209 | `RUN apk update && apk add --no-cache bash chromium` 210 | 211 | Then setting up some environment variables: 212 | 213 | 214 | ``` 215 | ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true 216 | ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser 217 | ``` 218 | 219 | Then extra paramater were added to `elm-starter` 220 | 221 | ``` 222 | --no-sandbox 223 | --disable-setuid-sandbox 224 | --disable-dev-shm-usage 225 | ``` 226 | 227 | More info at https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#running-on-alpine 228 | 229 | ## (\*) Applications working without Javascript 230 | 231 | Working without Javascript depends on the application. The `elm-starter` example works completely fine also without Javascript, the only missing thing is the smooth transition between pages. 232 | 233 | The `elm-todomvc` example requires Javascript. Note that in this example, compared to Evan's original TodoMVC, I slightly changed the CSS to improve the a11y (mainly lack of contrast and fonts too small). 234 | 235 | The `elm-spa-example` partially works without Javascript. You can browse across pages but the counters are not working. 236 | 237 | `elm-starter` and `elm-todomvc` use `Browser.element`, while `elm-spa-example` use `Browser.application`. 238 | 239 | The setup for these two cases is a bit different. `Browser.application` requires to use `htmlToReinjectInBody` (see `Index.elm`) because Elm is wiping out all the content in the body. Also the node where Elm attach itself needs to be removed (see `node.remove()` ). 240 | 241 | The working folder of `elm-starter` is `elm-stuff/elm-starter-files`. These files are automatically generated and should not be edited directly, unless during some debugging process. 242 | 243 | ## Advanced stuff 244 | 245 | ## `htmlToReinjectInHead` and `htmlToReinjectInBody` 246 | 247 | These two functions are used to re-inject some HTML after Puppeteer generated the page. This can be useful in several cases: 248 | 249 | * Using `Browser.document` or `Browser.application` (see above). 250 | * Using Google Tags, Google Analytics or other advanced JavaScript dependencies that should run only once. In these cases better not to add these JavaScript snipped during the development, but inject them using `htmlToReinjectInHead` only after the generation of HTML is done. 251 | 252 | ## File generation 253 | 254 | * Most of the logic is written in Elm, including the code to generate all necessary files: 255 | * `index.html` (generated from [`Index.elm`](https://github.com/lucamug/elm-start-private/blob/master/src/Index.elm) using [`zwilias/elm-html-string`](https://package.elm-lang.org/packages/zwilias/elm-html-string/latest/)) 256 | * [`sitemap.txt`](https://elm-starter.guupa.com/sitemap.txt) 257 | * [`manifest.json`](https://elm-starter.guupa.com/manifest.json) 258 | * [`service-worker.js`](https://elm-starter.guupa.com/service-worker.js) 259 | * [`robots.txt`](https://elm-starter.guupa.com/robots.txt) 260 | 261 | ## Disabling pre-rendering 262 | 263 | Is possible to disable pre-rendering just passing an empty list to `Main.conf.urls`. In this case the app will work as "Full CSR" (Full Client-side Rendering) 264 | 265 | This is an example of application with pre-rendering disabled. In this case also the WebGL animation caused some time out issue with puppeteer and it would not be necessary anyway to have this application render without Javascript as it based entirely on Javascript. 266 | 267 | Not the message when Javascript is disabled: 268 | 269 | ![elm-physics-example](assets/dev/elm-physics-example-animation.gif) 270 | 271 | 272 | ## How to customize your project 273 | 274 | The main two places where you can change stuff are: 275 | 276 | * `src/Index.elm` 277 | * `src/Main.elm` (`conf` function) 278 | 279 | `elm-starter` is opinionated about many things. If you want more freedom, you can change stuff in 280 | 281 | * `src-elm-starter/**/*.elm` 282 | 283 | The reason `Main.conf` is inside `Main.elm` is so that it can exchange data. For example: 284 | 285 | * `title`: `Main.conf` -> `Main` 286 | * `urls`: `Main.conf` <- `Main` 287 | 288 | Moreover `Main.conf` is used by `src-elm-starter` to generate all the static files. 289 | 290 | ## elm-console-debug.js for nice console output 291 | 292 | Support [https://github.com/kraklin/elm-debug-transformer](https://github.com/kraklin/elm-debug-transformer) out of the box for nice `Debug.log` messages in the console. 293 | 294 | ## Changing meta-tags 295 | 296 | For better SEO, you should update meta-tags using the predefined port `changeMeta` that you can use this way: 297 | 298 | ```elm 299 | Cmd.batch 300 | [ changeMeta { querySelector = "title", fieldName = "innerHTML", content = title } 301 | , changeMeta { querySelector = "meta[property='og:title']", fieldName = "content", content = title } 302 | , changeMeta { querySelector = "meta[name='twitter:title']", fieldName = "value", content = title } 303 | , changeMeta { querySelector = "meta[property='og:image']", fieldName = "content", content = image } 304 | , changeMeta { querySelector = "meta[name='twitter:image']", fieldName = "content", content = image } 305 | , changeMeta { querySelector = "meta[property='og:url']", fieldName = "content", content = url } 306 | , changeMeta { querySelector = "meta[name='twitter:url']", fieldName = "value", content = url } 307 | ] 308 | ``` 309 | 310 | You can validate Twitter preview cards at [https://cards-dev.twitter.com/validator](https://cards-dev.twitter.com/validator) 311 | 312 | ![elm-starter](assets/dev/twitter-card.jpg) 313 | 314 | ## Configuration 315 | 316 | You can verify the configuration in real-time using elm reactor: 317 | 318 | ```sh 319 | node_modules/.bin/elm reactor 320 | ``` 321 | 322 | and check the page 323 | 324 | [http://localhost:8000/src-elm-starter/Application.elm](http://localhost:8000/src-elm-starter/Application.elm) 325 | 326 | ## Globally available objects 327 | 328 | There are three global objects available 329 | 330 | ### `ElmStarter` 331 | 332 | `ElmStarter` contain metadata about the app: 333 | 334 | ```javascript 335 | { commit: "abf04f3" // coming from git 336 | , branch: "master" // coming from git 337 | , env: "dev" // can be "dev" or "prod" 338 | , version: "0.0.5" // coming from package.json 339 | } 340 | ``` 341 | 342 | This data is also available in Elm through Flags. 343 | 344 | ### `ElmApp` 345 | 346 | `ElmApp` is another global object that contains the handle of the Elm app. 347 | 348 | ### `Elm` 349 | 350 | This is the object exposed by the compiler used to initialize the application. 351 | 352 | ## Website living in subfolder 353 | 354 | If your website need to live in a subfolder, including all the assets, simply specify the subfolder in `homepage` of `package.json`. 355 | 356 | ``` 357 | "homepage": "https://example.com/your/sub/folders", 358 | ``` 359 | 360 | This will adjust all the links for you automatically. So for example, `elm.js` instead being loaded as 361 | 362 | ``` 363 | <script src="/elm.js"></script> 364 | ``` 365 | 366 | It will be loaded as 367 | 368 | ``` 369 | <script src="/your/sub/folders/elm.js"></script> 370 | ``` 371 | 372 | Remember to account for these extra folders in your Elm route parser. 373 | 374 | ## Extra commands 375 | 376 | To avoid possible conflicts with other commands in `package.json`, al commands are also available with a prefix: 377 | 378 | ``` 379 | $ npm run elm-starter:boot 380 | $ npm run elm-starter:start 381 | $ npm run elm-starter:generateDevFiles 382 | $ npm run elm-starter:build 383 | $ npm run elm-starter:buildExpectingTheServerRunning 384 | $ npm run elm-starter:serverBuild 385 | $ npm run elm-starter:serverDev 386 | $ npm run elm-starter:serverStatic 387 | $ npm run elm-starter:watchStartElm 388 | ``` 389 | 390 | This list also include extra commands that can be useful for debugging 391 | 392 | ## Limitations 393 | 394 | * Javascript and CSS to generate the initial `index.html` are actually strings :-( 395 | * `src-elm-starter/starter.js`, the core of `elm-starter`, is ~330 lines of Javascript. I wish it could be smaller 396 | * If your Elm code relies on data only available at runtime, such as window size or dark mode, prerendering is probably not the right approach. In this case you may consider [disabling pre-rendering](#disabling-pre-rendering) and use other alternatives, such as [Netlify prerendering](https://docs.netlify.com/site-deploys/post-processing/prerendering/#set-up-prerendering) 397 | 398 | Note 399 | 400 | * The smooth rotational transition in the demo only works in Chrome. I realized it too late, but you get the picture 401 | 402 | ## How does it work internally 403 | 404 | 405 | ### Command `start` 406 | 407 | * [function bootstrap] 408 | * Set initial values for some folder and file names 409 | * Compile `Worker.elm` 410 | * Execute `Worker.elm` using data set initially and other data coming from `package.json` and command line options 411 | * `Worker.elm` generates a configuration file that will be used for the next tasks. A copy of it is accessible at http://localhost:8000/conf.json 412 | * [function command_start] 413 | * [function command_generateDevFiles] 414 | * Clean the `developmennt` working folder 415 | * Generate files as per `conf.json` and also copy over assets 416 | * Touch `src/Main.elm` so that `elm-go` recompile and the browser refresh 417 | * Starts two commands: `serverDev` and `watchStartElm` (see below) 418 | 419 | 420 | 421 | ### Command `serverDev` 422 | 423 | * [function bootstrap] (see above) 424 | * [function command_serverDev] 425 | * start `elm-go` at http://localhost:8000 426 | 427 | 428 | 429 | ### Command `watchStartElm` 430 | 431 | * [function bootstrap] (see above) 432 | * Watch all Elm files in `elm-starter` folder and `src/Index.elm` for modifications 433 | * If there is a change, it run the command `generateDevFiles` (see below) 434 | 435 | 436 | 437 | ### Command `generateDevFiles` 438 | * [function bootstrap] (see above) 439 | * [function command_generateDevFiles] (see above) 440 | 441 | 442 | ..TO BE CONTINUED 443 | 444 | ## Non-goals 445 | 446 | Things that `elm-starter` is not expected to do 447 | 448 | * Doesn't generate Elm code automatically, like Route-parser, for example 449 | * Doesn't touch/wrap the code of your Elm Application 450 | * Doesn't do live SSR (Server Side Render) but just pre-render during the build 451 | * Doesn't change the Javascript coming out from the Elm compiler 452 | * Doesn't create a web site based on static files containing Markdown 453 | * There is no "[hydration](https://developers.google.com/web/updates/2019/02/rendering-on-the-web)", unless Elm does some magic that I am not aware of. 454 | 455 | You can find several of these characteristics in some of the [similar projects](#similar-projects). 456 | 457 | Using as reference the table at the bottom of the article [Rendering on the Web](https://developers.google.com/web/updates/2019/02/rendering-on-the-web), `elm-starter` can support you in these rendering approach 458 | 459 | * Static SSR 460 | * CSR with Prerendering 461 | * Full CSR 462 | 463 | It cannot help you with 464 | 465 | * Server Rendering 466 | * SSR with (re)hydration 467 | 468 | ## Similar projects 469 | 470 | These are other projects that can be used to bootstrap an Elm application or to generate a static site: 471 | 472 | * [elm-pages](https://package.elm-lang.org/packages/dillonkearns/elm-pages/latest/) 473 | * [elmstatic](https://github.com/alexkorban/elmstatic) 474 | * [elm-spa](https://package.elm-lang.org/packages/ryannhg/elm-spa/latest/) 475 | * [create-elm-app](https://github.com/halfzebra/create-elm-app) 476 | * [spades](https://github.com/rogeriochaves/spades) 477 | * [gulp-elm-starter-project](https://github.com/Chadtech/gulp-elm-starter-project) 478 | 479 | Here are similar projects for other Languages/Frameworks: 480 | 481 | * [staticgen.com](https://www.staticgen.com/) 482 | -------------------------------------------------------------------------------- /src-elm-starter/elm-starter: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/node 2 | const debugMode = false; 3 | const fs = require("fs"); 4 | const path = require('path'); 5 | const crypto = require('crypto'); 6 | const terser = require("terser"); 7 | const puppeteer = require('puppeteer'); 8 | const concurrently = require('concurrently'); 9 | const child_process = require("child_process"); 10 | const html_minifier = require('html-minifier'); 11 | const package = require(`${process.cwd()}/package.json`); 12 | const fgGreen = "\x1b[32m"; 13 | const fgYellow = "\x1b[33m"; 14 | const fgBlue = "\x1b[34m" 15 | const fgMagenta = "\x1b[35m"; 16 | const fgRed = "\x1b[31m"; 17 | const dim = "\x1b[2m"; 18 | const reset = "\x1b[0m"; 19 | const totsu = "凸"; 20 | const styleTitle = ` ${fgYellow}${totsu} %s${reset}\n`; 21 | const styleSubtitle = ` ${dim}${fgYellow}%s${reset}`; 22 | const styleDebug = ` ${fgMagenta}%s${reset}`; 23 | const styleWarning = ` ${fgRed}Warning:${reset} %s`; 24 | const arg = process.argv[2] ? process.argv[2] : ""; 25 | const DEV = "dev"; 26 | const PROD = "prod"; 27 | const NOT_AVAILABLE = "na"; 28 | let commit; 29 | let branch; 30 | 31 | var argv = require('yargs') 32 | .usage('Usage: $0 <command> [options]') 33 | .command('start', 'Start the development environment, open http://localhost:8000 and edit src/Main.elm') 34 | .command('build', 'Build all the files for production in elm-stuff/elm-starter-files/build') 35 | .command('serverBuild', 'A simple way to test the build. Open http://localhost:9000') 36 | .command('generateDevFiles', 'INTERNAL - Prepare all the file for development') 37 | .command('buildUsingServer', 'INTERNAL - Build all the file for production. It expects serverStatic running') 38 | .command('serverDev', 'INTERNAL - Start the server used for development') 39 | .command('serverStatic', 'INTERNAL - Start the server used to generate the build at http://localhost:7000') 40 | .command('watchStartElm', 'INTERNAL - Watch some extra file during the development') 41 | .command('debug', 'INTERNAL - Display the configuration') 42 | .command('listResources', 'INTERNAL') 43 | .demandCommand(1) 44 | .strict() 45 | .option('a', { 46 | alias: 'assets', 47 | default: 'assets/prod', 48 | describe: 'assets folder', 49 | type: 'string' 50 | }) .example('$0 start', 'Start') 51 | .alias('b', 'branch') 52 | .alias('c', 'commit') 53 | .alias('v', 'version') 54 | .nargs('b', 1) 55 | .nargs('c', 1) 56 | .describe('b', 'GIT branch name. If not given, the script try to extract it automatically') 57 | .describe('c', 'GIT commit hash. If not given, the script try to extract it automatically') 58 | .help('h') 59 | .alias('h', 'help') 60 | .epilog('copyright 2020') 61 | .argv; 62 | 63 | // console.log(argv); 64 | // process.exit(1); 65 | 66 | const command = argv['_'][0]; 67 | 68 | if (argv.commit) { 69 | commit = argv.commit; 70 | } else { 71 | try { 72 | commit = child_process.execSync('git rev-parse --short HEAD').toString().replace(/^\s+|\s+$/g, ''); 73 | } catch(err) { 74 | console.log(styleWarning, `Next time pass the commit as option or make this a git repository so we can add the commit info.`); 75 | commit = `commit-${NOT_AVAILABLE}`; 76 | } 77 | } 78 | 79 | if (argv.branch) { 80 | branch = argv.branch; 81 | } else { 82 | try { 83 | branch = child_process.execSync('git rev-parse --abbrev-ref HEAD').toString().replace(/^\s+|\s+$/g, ''); 84 | } catch(err) { 85 | console.log(styleWarning, `Next time pass the branch as option or make make this a git repository so we can add the branch info.`); 86 | branch = `branch-${NOT_AVAILABLE}`; 87 | } 88 | } 89 | 90 | [ "name" 91 | , "nameLong" 92 | , "description" 93 | , "author" 94 | , "version" 95 | , "homepage" 96 | , "license" 97 | ].map(checkRequired) 98 | 99 | 100 | function checkRequired(key) { 101 | if (! package[key] || String(package[key]) === "") { 102 | package[key] = NOT_AVAILABLE; 103 | console.log(styleWarning, `"${key}" is required in package.json`); 104 | } 105 | } 106 | 107 | // 108 | // 109 | // PARSING ARGUMENTS 110 | // 111 | // 112 | 113 | if (command === "boot") { 114 | console.log(styleTitle, `Bootstrapping...`); 115 | bootstrap(DEV); 116 | 117 | } else if (command === "debug") { 118 | console.log(styleTitle, `Starting (${commit}, ${branch})...`); 119 | bootstrap(DEV, function(conf) {console.log(conf)}); 120 | 121 | } else if (command === "listResources") { 122 | console.log(styleTitle, `Starting (${commit}, ${branch})...`); 123 | bootstrap(DEV, function(conf) { 124 | console.log( listFiles(conf.dir.pw + "/assets/prod", []) ); 125 | }); 126 | 127 | } else if (command === "start") { 128 | console.log(styleTitle, `Starting (${commit}, ${branch})...`); 129 | bootstrap(DEV, command_start); 130 | 131 | } else if (command === "generateDevFiles" ) { 132 | console.log(styleTitle, `Generating dev files...`); 133 | bootstrap(DEV, command_generateDevFiles); 134 | 135 | } else if (command === "build" ) { 136 | console.log(styleTitle, `Building... (${commit} ${branch})`); 137 | bootstrap(PROD, command_build); 138 | 139 | } else if (command === "buildUsingServer" ) { 140 | console.log(styleTitle, `Building (I expect a server running on port 7000)...`); 141 | bootstrap(PROD, command_buildUsingServer); 142 | 143 | } else if (command === "serverBuild" ) { 144 | console.log(styleTitle, `Starting server "build"...`); 145 | bootstrap(PROD, command_serverBuild); 146 | 147 | } else if (command === "serverDev" ) { 148 | console.log(styleTitle, `Starting server DEV...`); 149 | bootstrap(DEV, command_serverDev); 150 | 151 | } else if (command === "serverStatic" ) { 152 | console.log(styleTitle, `Starting server "static" for generation of static pages...`); 153 | bootstrap(PROD, command_serverStatic); 154 | 155 | } else if (command === "watchStartElm" ) { 156 | console.log(styleTitle, `Watching elm-starter Elm files...`); 157 | bootstrap(DEV, command_watchStartElm); 158 | 159 | } else { 160 | console.log(styleTitle, `Invalid parameter: ${arg}`); 161 | } 162 | 163 | // 164 | // 165 | // BOOTSTRAP 166 | // 167 | // 168 | 169 | function bootstrap (env, callback) { 170 | callback = callback || function(){}; 171 | env = env === DEV ? DEV : PROD; 172 | const dirPw = process.cwd(); 173 | const relDirIgnoredByGit = `elm-stuff/elm-starter-files`; 174 | const dir = 175 | { pw: `${dirPw}` 176 | , bin: `${dirPw}/node_modules/.bin` 177 | , ignoredByGit: `${dirPw}/${relDirIgnoredByGit}` 178 | , temp: `${dirPw}/${relDirIgnoredByGit}/temp` 179 | , assets: `${dirPw}/${argv.assets}` 180 | } 181 | const file = 182 | { elmWorker: `${dirPw}/src-elm-starter/Worker.elm` 183 | } 184 | const fileNameOutput = `${dir.temp}/worker.js`; 185 | const command = `${dir.bin}/elm make ${file.elmWorker} --output=${fileNameOutput}`; 186 | child_process.exec(command, (error, out) => { 187 | if (error) throw error; 188 | // Temporary silencing Elm warnings 189 | const consoleWarn = console.warn; 190 | console.warn = function() {}; 191 | const Elm = require(fileNameOutput); 192 | // Restoring warnings 193 | console.warn = consoleWarn; 194 | var app = Elm.Elm.Worker.init( 195 | { flags : 196 | 197 | // From package.jspn 198 | { name : String(package.name) 199 | , nameLong : String(package.nameLong) 200 | , description : String(package.description) 201 | , author : String(package.author) 202 | , version : String(package.version) 203 | , homepage : String(package.homepage).replace(/[/]*$/g, "") // Remove trayling "/" 204 | , license : String(package.license) 205 | , twitterSite : typeof package.twitterSite === "undefined"? null : String(package.twitterSite) 206 | , twitterAuthor : typeof package.twitterAuthor === "undefined"? null : String(package.twitterAuthor) 207 | , snapshotWidth: typeof package.snapshotWidth === "undefined"? null : String(package.snapshotWidth) 208 | , snapshotHeight: typeof package.snapshotHeight === "undefined"? null : String(package.snapshotHeight) 209 | , themeColor: typeof package.snapshotHeight === "undefined"? null : 210 | { red: String(package.themeColor.red) 211 | , green: String(package.themeColor.green) 212 | , blue: String(package.themeColor.blue) 213 | } 214 | 215 | // From Git 216 | , commit: commit.toString() 217 | , branch: branch.toString() 218 | 219 | // From starter.js 220 | , env: env 221 | , dirPw : dir.pw 222 | , dirBin : dir.bin 223 | , dirIgnoredByGit : dir.ignoredByGit 224 | , dirTemp : dir.temp 225 | , dirAssets : dir.assets 226 | , fileElmWorker : file.elmWorker 227 | , assets : listFiles(dir.pw + "/assets/prod", []) 228 | } 229 | } 230 | ); 231 | app.ports.dataFromElmToJavascript.subscribe(function(conf) { 232 | // Got the file back from Elm! 233 | fs.writeFile(dir.ignoredByGit + "/dev" + "/conf.json", JSON.stringify(conf,null,4), function(){}); 234 | callback(conf); 235 | 236 | }); 237 | }); 238 | } 239 | 240 | // 241 | // 242 | // COMMANDS 243 | // 244 | // 245 | 246 | function command_start (conf) { 247 | command_generateDevFiles(conf); 248 | startCommand(`${conf.dir.bin}/concurrently`, 249 | [ `node ${conf.file.jsStarter} serverDev --branch=${branch} --commit=${commit}` 250 | , `node ${conf.file.jsStarter} watchStartElm --branch=${branch} --commit=${commit}` 251 | , `--kill-others` 252 | ] 253 | ); 254 | } 255 | 256 | function command_generateDevFiles (conf) { 257 | removeDir(conf.dir.devRoot, false); 258 | mkdir(conf.dir.dev); 259 | mkdir(conf.dir.assetsDevTarget); 260 | generateFiles(conf, conf.dir.dev); 261 | // We symlink all assets from `assets` folder to `dev` folder 262 | // so that in development, changes to the assets are immediately 263 | // reflected. During the build instead we phisically copy files. 264 | symlinkDir(conf.dir.assets, conf.dir.dev); 265 | symlinkDir(conf.dir.assetsDevSource, conf.dir.assetsDevTarget); 266 | // Touching Main.elm so that, in case there is a server running, 267 | // it will re-generate elm.js 268 | child_process.exec(`touch ${conf.file.mainElm}`, (error, out) => {}); 269 | } 270 | 271 | function command_build (conf) { 272 | startCommand(`${conf.dir.bin}/concurrently`, 273 | [ `node ${conf.file.jsStarter} serverStatic --branch=${branch} --commit=${commit}` 274 | // Here we wait two seconds so that the server has time to 275 | // compile Elm code and start itself 276 | , `sleep 2 && node ${conf.file.jsStarter} buildUsingServer --branch=${branch} --commit=${commit}` 277 | , `--kill-others` 278 | , `--success=first` 279 | ] 280 | ); 281 | } 282 | 283 | function command_buildUsingServer (conf) { 284 | removeDir(conf.dir.buildRoot, false); 285 | mkdir(conf.dir.build); 286 | generateFiles(conf, conf.dir.build); 287 | console.log(styleSubtitle, `Compiling Elm`); 288 | const command = `${conf.dir.bin}/elm make ${conf.file.mainElm} --output=${conf.dir.build}/${conf.outputCompiledJsProd} --optimize`; 289 | child_process.exec(command, (error, out) => { 290 | if (error) throw error; 291 | // Going back to the original directory 292 | minifyJs(conf, `${conf.outputCompiledJsProd}`); 293 | console.log(styleSubtitle, `Copying assets`); 294 | copyDir(conf.dir.assets, conf.dir.build); 295 | generateStaticPages(conf); 296 | }); 297 | } 298 | 299 | function command_serverBuild (conf) { 300 | startCommand 301 | ( conf.serverBuild.command 302 | , conf.serverBuild.parameters 303 | ); 304 | } 305 | 306 | function command_serverDev (conf) { 307 | startCommand 308 | ( conf.serverDev.command 309 | , conf.serverDev.parameters 310 | ); 311 | } 312 | 313 | function command_serverStatic (conf) { 314 | command_generateDevFiles(conf); 315 | startCommand 316 | ( conf.serverStatic.command 317 | , conf.serverStatic.parameters 318 | ); 319 | } 320 | 321 | function command_watchStartElm (conf) { 322 | // Watching the src file to check in eny of the Elm file is changing 323 | startCommand(`${conf.dir.bin}/chokidar`, 324 | [ conf.dir.elmStartSrc 325 | , conf.file.indexElm 326 | , `-c` 327 | , `node ${conf.file.jsStarter} generateDevFiles --branch=${branch} --commit=${commit}` 328 | ] 329 | ); 330 | } 331 | 332 | // 333 | // 334 | // HELPERS 335 | // 336 | // 337 | 338 | async function generateStaticPages (conf) { 339 | try { 340 | console.log(styleSubtitle, `Building ${conf.mainConf.urls.length} static pages for ${conf.mainConf.domain}`); 341 | // the `args` property is only necessary during docker build. if its not fine to keep this here, please make configurable. 342 | const browser = await puppeteer.launch({ 343 | args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'], 344 | headless: conf.headless 345 | }); 346 | const urlsInBatches = chunkArray(conf.mainConf.urls, conf.batchesSize); 347 | await urlsInBatches.reduce(async (previousBatch, currentBatch, index) => { 348 | await previousBatch; 349 | console.log(styleSubtitle, `Processing batch ${index + 1} of ${urlsInBatches.length}...`); 350 | const currentBatchPromises = currentBatch.map(url => processUrl(url, browser, conf)) 351 | const result = await Promise.all(currentBatchPromises); 352 | }, Promise.resolve()); 353 | await browser.close(); 354 | console.log(styleTitle, `Done!`); 355 | console.log(styleSubtitle, `The build is ready in "/${conf.dir.build}". Run "npm run serverBuild" to test it.`); 356 | } catch (error) { 357 | console.error(error); 358 | } 359 | }; 360 | 361 | async function processUrl (url, browser, conf) { 362 | const page = await browser.newPage(); 363 | await page.setViewport({width: conf.snapshotWidth, height: conf.snapshotHeight}); 364 | await page.goto(`${conf.startingDomain}${url}`, {waitUntil: 'networkidle0'}); 365 | if ( !fs.existsSync( `${conf.dir.buildRoot}${url}` ) ) { 366 | mkdir( `${conf.dir.buildRoot}${url}` ); 367 | } 368 | let html = await page.content(); 369 | html = html.replace('</head>',`${conf.htmlToReinjectInHead}</head>`); 370 | html = html.replace('</body>',`${conf.htmlToReinjectInBody}</body>`); 371 | console.log(styleSubtitle, ` * ${conf.startingDomain}${url}`); 372 | const minHtml = html_minifier.minify(html, 373 | { minifyCSS: true 374 | , minifyJS: true 375 | , removeComments: true 376 | } 377 | ); 378 | 379 | fs.writeFileSync(`${conf.dir.buildRoot}${url}/${conf.pagesName}`, minHtml); 380 | if (conf.snapshots) { 381 | await page.screenshot( 382 | { path: `${conf.dir.buildRoot}${url}/${conf.snapshotFileName}` 383 | , quality: conf.snapshotsQuality 384 | } 385 | ); 386 | } 387 | await page.close(); 388 | } 389 | 390 | function minifyJs (conf, fileName) { 391 | runTerser(`${conf.dir.build}/${fileName}`); 392 | } 393 | 394 | function generateFiles(conf, dest) { 395 | conf.files.map (function(file) { 396 | fs.writeFileSync(`${dest}/${file.name}`, file.content); 397 | }); 398 | } 399 | 400 | // 401 | // 402 | // UTILITIES 403 | // 404 | // 405 | 406 | function consoleDebug (string) { 407 | if (debugMode) { 408 | console.log (styleDebug, string); 409 | } 410 | } 411 | 412 | function runHtmlMinifier (fileName) { 413 | const code = fs.readFileSync(fileName, 'utf8'); 414 | const minCode = html_minifier.minify(code, 415 | { collapseWhitespace: true 416 | , minifyCSS: true 417 | , minifyJS: true 418 | , removeComments: true 419 | , removeEmptyAttributes : true 420 | , removeEmptyElements : true 421 | , removeAttributeQuotes : true 422 | , removeOptionalTags : true 423 | , removeRedundantAttributes : true 424 | , removeScriptTypeAttributes : true 425 | , collapseBooleanAttributes : true 426 | , useShortDoctype : true 427 | } 428 | ); 429 | fs.writeFileSync(fileName, minCode); 430 | } 431 | 432 | function runTerser (fileName) { 433 | const code = fs.readFileSync(fileName, 'utf8'); 434 | // TODO - Add special arguments to terser, to optimize pure functions 435 | const minCode = terser.minify(code); 436 | if (minCode.error) throw minCode.error; 437 | fs.writeFileSync(fileName, minCode.code); 438 | } 439 | 440 | function startCommand (cmd, parameters, callback) { 441 | callback = callback || function(){}; 442 | const command = child_process.spawn(cmd, parameters, {stdio: "inherit"}); 443 | command.on('close', function(code) { 444 | return callback(code); 445 | }); 446 | } 447 | 448 | function chunkArray(myArray, chunk_size){ 449 | var results = []; 450 | while (myArray.length) { 451 | results.push(myArray.splice(0, chunk_size)); 452 | } 453 | return results; 454 | } 455 | 456 | // 457 | // 458 | // DIRECTORY UTILITIES 459 | // 460 | // 461 | 462 | function mkdir (path) { 463 | if (fs.existsSync(path)) { 464 | // path already exsists 465 | } else { 466 | try { 467 | fs.mkdirSync(path, { recursive: true }) 468 | } catch(e) { 469 | // error creating dir 470 | } 471 | } 472 | } 473 | 474 | function symlinkDir (srcDir, dstDir) { 475 | if (!fs.existsSync(srcDir)) { 476 | // source directory doesn't exists 477 | return; 478 | } 479 | const list = fs.readdirSync(srcDir); 480 | var src, dst; 481 | list.forEach(function(file) { 482 | src = `${srcDir}/${file}`; 483 | dst = `${dstDir}/${file}`; 484 | var stat = fs.lstatSync(src); 485 | if ( stat && ( stat.isDirectory() || stat.isFile() ) && (file !== ".DS_Store") ) { 486 | fs.symlinkSync(src, dst); 487 | } 488 | }); 489 | } 490 | 491 | function copyDir (srcDir, dstDir) { 492 | if (!fs.existsSync(srcDir)) { 493 | // source directory doesn't exists 494 | return; 495 | } 496 | const files = fs.readdirSync(srcDir); 497 | files.map( function (file) { 498 | const src = `${srcDir}/${file}`; 499 | const dst = `${dstDir}/${file}`; 500 | const stat = fs.lstatSync(src); 501 | if (stat && stat.isDirectory()) { 502 | mkdir(dst); 503 | copyDir(src, dst); 504 | } else if ( file !== ".DS_Store" ) { 505 | try { 506 | fs.writeFileSync(dst, fs.readFileSync(src)); 507 | } catch(e) { 508 | console.log(e); 509 | } 510 | } 511 | }); 512 | } 513 | 514 | function removeDir (srcDir, removeSelf) { 515 | if (!fs.existsSync(srcDir)) { 516 | // source directory doesn't exists 517 | return; 518 | } 519 | const files = fs.readdirSync(srcDir); 520 | files.map(function (file) { 521 | const src = `${srcDir}/${file}`; 522 | const stat = fs.lstatSync(src); 523 | if (stat && stat.isDirectory()) { 524 | // Calling recursively removeDir 525 | removeDir(src, true); 526 | } else { 527 | fs.unlinkSync(src); 528 | } 529 | } 530 | ) 531 | if (removeSelf) { 532 | fs.rmdirSync(srcDir); 533 | } 534 | }; 535 | 536 | function listFiles (srcDir, acc) { 537 | if (!fs.existsSync(srcDir)) { 538 | // source directory doesn't exists 539 | return(acc); 540 | } else { 541 | const files = fs.readdirSync(srcDir); 542 | files.map( function (file) { 543 | const src = `${srcDir}/${file}`; 544 | const stat = fs.lstatSync(src); 545 | if (stat && stat.isDirectory()) { 546 | acc = listFiles(src, acc); 547 | } else if ( file !== ".DS_Store" ) { 548 | const filePath = srcDir + "/" + file 549 | const fileData = fs.readFileSync(filePath, 'utf8'); 550 | const hash = crypto.createHash('sha1').update(fileData, 'utf8').digest('hex'); 551 | acc.push([filePath, hash]); 552 | } 553 | }); 554 | return(acc); 555 | } 556 | } 557 | -------------------------------------------------------------------------------- /src/Main.elm: -------------------------------------------------------------------------------- 1 | port module Main exposing (conf, main) 2 | 3 | import Browser 4 | import Browser.Events 5 | import Element exposing (..) 6 | import Element.Background as Background 7 | import Element.Font as Font 8 | import Html 9 | import Html.Attributes 10 | import Html.Events 11 | import Json.Decode 12 | import Starter.ConfMain 13 | import Starter.Flags 14 | import Svg 15 | import Svg.Attributes 16 | import Url 17 | import Url.Parser exposing ((</>)) 18 | 19 | 20 | conf : Starter.ConfMain.Conf 21 | conf = 22 | { urls = urls 23 | , assetsToCache = [] 24 | } 25 | 26 | 27 | 28 | -- MAIN 29 | 30 | 31 | main : Program Flags Model Msg 32 | main = 33 | Browser.element 34 | { init = init 35 | , view = view 36 | , update = update 37 | , subscriptions = subscriptions 38 | } 39 | 40 | 41 | internalConf : { urlLabel : String } 42 | internalConf = 43 | { urlLabel = "tangram" } 44 | 45 | 46 | 47 | -- MODEL 48 | 49 | 50 | type alias Model = 51 | { route : Route, flags : Flags } 52 | 53 | 54 | 55 | -- FLAGS 56 | 57 | 58 | type alias Flags = 59 | { starter : Starter.Flags.Flags 60 | , width : Int 61 | , height : Int 62 | , languages : List String 63 | , locationHref : String 64 | } 65 | 66 | 67 | 68 | -- INIT 69 | 70 | 71 | init : Flags -> ( Model, Cmd msg ) 72 | init flags = 73 | let 74 | route = 75 | locationHrefToRoute flags.locationHref 76 | in 77 | ( { route = route, flags = flags } 78 | , updateHtmlMeta flags.starter route 79 | ) 80 | 81 | 82 | 83 | -- SUBSCRIPTIONS 84 | 85 | 86 | subscriptions : Model -> Sub Msg 87 | subscriptions _ = 88 | Sub.batch 89 | [ onUrlChange (locationHrefToRoute >> UrlChanged) 90 | , Browser.Events.onKeyDown (Json.Decode.map KeyDown Html.Events.keyCode) 91 | ] 92 | 93 | 94 | 95 | -- ROUTE 96 | 97 | 98 | type Route 99 | = RouteTop 100 | | RouteTangram Tangram 101 | 102 | 103 | routeList : List Route 104 | routeList = 105 | RouteTop :: List.map (\tangram -> RouteTangram tangram) tangramList 106 | 107 | 108 | urls : List String 109 | urls = 110 | List.map routeToAbsolutePath routeList 111 | 112 | 113 | routeToTangram : Route -> Tangram 114 | routeToTangram route = 115 | case route of 116 | RouteTop -> 117 | Heart 118 | 119 | RouteTangram tangram -> 120 | tangram 121 | 122 | 123 | tangramParser : Url.Parser.Parser (Tangram -> a) a 124 | tangramParser = 125 | Url.Parser.custom "TANGRAM" stringToTangram 126 | 127 | 128 | routeToAbsolutePath : Route -> String 129 | routeToAbsolutePath route = 130 | "/" 131 | ++ (String.join "/" <| 132 | case route of 133 | RouteTop -> 134 | [] 135 | 136 | RouteTangram tangram -> 137 | [ internalConf.urlLabel, String.toLower <| tangramToString tangram ] 138 | ) 139 | 140 | 141 | urlToRoute : Url.Url -> Maybe Route 142 | urlToRoute url = 143 | Url.Parser.parse routeParser url 144 | 145 | 146 | locationHrefToRoute : String -> Route 147 | locationHrefToRoute locationHref = 148 | locationHref 149 | |> Url.fromString 150 | |> Maybe.andThen urlToRoute 151 | |> Maybe.withDefault RouteTop 152 | 153 | 154 | routeParser : Url.Parser.Parser (Route -> b) b 155 | routeParser = 156 | Url.Parser.oneOf 157 | [ Url.Parser.map RouteTangram (Url.Parser.s internalConf.urlLabel </> tangramParser) 158 | , Url.Parser.map RouteTop Url.Parser.top 159 | ] 160 | 161 | 162 | 163 | -- MESSAGES 164 | 165 | 166 | type Msg 167 | = LinkClicked String 168 | | UrlChanged Route 169 | | KeyDown Int 170 | 171 | 172 | 173 | -- UPDATE 174 | 175 | 176 | updateHtmlMeta : Starter.Flags.Flags -> Route -> Cmd msg 177 | updateHtmlMeta starterFlags route = 178 | let 179 | title = 180 | "a " ++ String.toUpper (tangramToString (routeToTangram route)) ++ " from " ++ starterFlags.nameLong 181 | 182 | url = 183 | starterFlags.homepage ++ routeToAbsolutePath route 184 | 185 | image = 186 | url ++ "/snapshot.jpg" 187 | in 188 | Cmd.batch 189 | [ changeMeta { type_ = "property", querySelector = "title", fieldName = "innerHTML", content = title } 190 | , changeMeta { type_ = "attribute", querySelector = "link[rel='canonical']", fieldName = "href", content = url } 191 | , changeMeta { type_ = "attribute", querySelector = "meta[name='twitter:title']", fieldName = "value", content = title } 192 | , changeMeta { type_ = "attribute", querySelector = "meta[property='og:image']", fieldName = "content", content = image } 193 | , changeMeta { type_ = "attribute", querySelector = "meta[name='twitter:image']", fieldName = "content", content = image } 194 | , changeMeta { type_ = "attribute", querySelector = "meta[property='og:url']", fieldName = "content", content = url } 195 | , changeMeta { type_ = "attribute", querySelector = "meta[name='twitter:url']", fieldName = "value", content = url } 196 | ] 197 | 198 | 199 | update : Msg -> Model -> ( Model, Cmd Msg ) 200 | update msg model = 201 | case msg of 202 | LinkClicked path -> 203 | ( model, pushUrl path ) 204 | 205 | UrlChanged route -> 206 | ( { model | route = route } 207 | , updateHtmlMeta model.flags.starter route 208 | ) 209 | 210 | KeyDown key -> 211 | if key == 37 || key == 38 then 212 | ( model 213 | , model.route 214 | |> routeToTangram 215 | |> previousTangram 216 | |> RouteTangram 217 | |> routeToAbsolutePath 218 | |> pushUrl 219 | ) 220 | 221 | else if key == 39 || key == 40 then 222 | ( model 223 | , model.route 224 | |> routeToTangram 225 | |> nextTangram 226 | |> RouteTangram 227 | |> routeToAbsolutePath 228 | |> pushUrl 229 | ) 230 | 231 | else 232 | ( model, Cmd.none ) 233 | 234 | 235 | 236 | -- PORTS 237 | 238 | 239 | port onUrlChange : (String -> msg) -> Sub msg 240 | 241 | 242 | port pushUrl : String -> Cmd msg 243 | 244 | 245 | port changeMeta : 246 | { querySelector : String 247 | , fieldName : String 248 | , content : String 249 | , type_ : String 250 | } 251 | -> Cmd msg 252 | 253 | 254 | 255 | -- VIEW HELPERS 256 | 257 | 258 | linkInternal : 259 | (String -> msg) 260 | -> List (Attribute msg) 261 | -> { label : Element msg, url : String } 262 | -> Element msg 263 | linkInternal internalLinkClicked attrs args = 264 | let 265 | -- From https://github.com/elm/browser/blob/1.0.2/notes/navigation-in-elements.md 266 | preventDefault : msg -> Html.Attribute msg 267 | preventDefault msg = 268 | Html.Events.preventDefaultOn "click" (Json.Decode.succeed ( msg, True )) 269 | in 270 | link 271 | ((htmlAttribute <| preventDefault (internalLinkClicked args.url)) :: attrs) 272 | args 273 | 274 | 275 | mouseOverEffect : List (Attr () msg) 276 | mouseOverEffect = 277 | [ alpha 0.8 278 | , mouseOver [ alpha 1 ] 279 | , htmlAttribute <| Html.Attributes.style "transition" "0.3s" 280 | ] 281 | 282 | 283 | linkAttrs : List (Attr () msg) 284 | linkAttrs = 285 | mouseOverEffect 286 | ++ [ htmlAttribute <| Html.Attributes.style "text-decoration" "underline" ] 287 | 288 | 289 | 290 | -- VIEW 291 | 292 | 293 | view : Model -> Html.Html Msg 294 | view model = 295 | let 296 | themeColor = 297 | Starter.Flags.toThemeColorRgb model.flags.starter 298 | in 299 | Html.div 300 | [ Html.Attributes.id "elm" ] 301 | [ Html.node "style" [] [ Html.text css ] 302 | , Html.a [ Html.Attributes.class "skip-link", Html.Attributes.href "#main" ] 303 | [ Html.text "Skip to main" ] 304 | , layout 305 | [ Background.color <| 306 | rgb255 307 | themeColor.red 308 | themeColor.green 309 | themeColor.blue 310 | , Font.color <| rgb 0.95 0.95 0.95 311 | , Font.family [] 312 | , Font.size 20 313 | , centerY 314 | , htmlAttribute <| Html.Attributes.style "height" "100vh" 315 | ] 316 | <| 317 | column 318 | [ centerX 319 | , centerY 320 | , spacing 40 321 | , width (fill |> maximum 400) 322 | ] 323 | [ viewTangram model.route 324 | , viewMessage 325 | ] 326 | ] 327 | 328 | 329 | viewTangram : Route -> Element Msg 330 | viewTangram route = 331 | row 332 | [ centerX 333 | , spacing 0 334 | , width fill 335 | ] 336 | [ linkInternal LinkClicked 337 | ([ height fill 338 | , width <| px 48 339 | ] 340 | ++ mouseOverEffect 341 | ) 342 | { label = 343 | el [ centerX ] <| 344 | html <| 345 | left 346 | { id = "previous" 347 | , title = "Previous" 348 | , desc = "Go to the previous Tangram" 349 | , width = 32 350 | } 351 | , url = routeToAbsolutePath <| RouteTangram <| previousTangram <| routeToTangram route 352 | } 353 | , linkInternal LinkClicked 354 | [ htmlAttribute <| Html.Attributes.style "animation" "elmLogoSpin infinite 2.5s ease-in-out" 355 | , width fill 356 | ] 357 | { label = 358 | html <| 359 | logo (tangramToData <| routeToTangram route) 360 | { id = "tangram" 361 | , title = "A " ++ tangramToString (routeToTangram route) ++ " made of Tangram pieces" 362 | , desc = "Rotating Tangram" 363 | , width = 250 364 | } 365 | , url = routeToAbsolutePath <| RouteTangram <| nextTangram <| routeToTangram route 366 | } 367 | , linkInternal LinkClicked 368 | ([ width (fillPortion 1) 369 | , height fill 370 | , width <| px 48 371 | ] 372 | ++ mouseOverEffect 373 | ) 374 | { label = 375 | el [ centerX ] <| 376 | html <| 377 | right 378 | { id = "next" 379 | , title = "Next" 380 | , desc = "Go to the next Tangram" 381 | , width = 32 382 | } 383 | , url = routeToAbsolutePath <| RouteTangram <| nextTangram <| routeToTangram route 384 | } 385 | ] 386 | 387 | 388 | viewMessage : Element msg 389 | viewMessage = 390 | column 391 | [ spacing 14 392 | , centerX 393 | , paddingXY 20 0 394 | , htmlAttribute <| Html.Attributes.style "word-spacing" "5px" 395 | , htmlAttribute <| Html.Attributes.style "letter-spacing" "1px" 396 | ] 397 | [ paragraph [ Font.center ] 398 | [ text "Bootstrapped with " 399 | , newTabLink [ centerX ] 400 | { label = el linkAttrs <| text "elm-starter" 401 | , url = "https://github.com/lucamug/elm-starter" 402 | } 403 | , text "." 404 | ] 405 | , paragraph [ Font.center ] 406 | [ text "Edit " 407 | , el (Font.family [ Font.monospace ] :: mouseOverEffect) <| text "src/Main.elm" 408 | , text " and save to reload." 409 | ] 410 | , paragraph [ Font.center ] 411 | [ newTabLink [ centerX ] 412 | { label = el linkAttrs <| text "Learn Elm" 413 | , url = "https://elm-lang.org/" 414 | } 415 | , text "." 416 | ] 417 | ] 418 | 419 | 420 | 421 | -- TANGRAM 422 | 423 | 424 | type Tangram 425 | = ElmLogo 426 | | Heart 427 | | Camel 428 | | Cat 429 | | Bird 430 | | House 431 | | Person 432 | 433 | 434 | tangramList : List Tangram 435 | tangramList = 436 | [ Heart 437 | , Camel 438 | , Cat 439 | , Bird 440 | , House 441 | , Person 442 | , ElmLogo 443 | ] 444 | 445 | 446 | stringToTangram : String -> Maybe Tangram 447 | stringToTangram string = 448 | if String.toLower string == String.toLower (tangramToString ElmLogo) then 449 | Just ElmLogo 450 | 451 | else if String.toLower string == String.toLower (tangramToString Heart) then 452 | Just Heart 453 | 454 | else if String.toLower string == String.toLower (tangramToString Camel) then 455 | Just Camel 456 | 457 | else if String.toLower string == String.toLower (tangramToString Cat) then 458 | Just Cat 459 | 460 | else if String.toLower string == String.toLower (tangramToString Bird) then 461 | Just Bird 462 | 463 | else if String.toLower string == String.toLower (tangramToString House) then 464 | Just House 465 | 466 | else if String.toLower string == String.toLower (tangramToString Person) then 467 | Just Person 468 | 469 | else 470 | Nothing 471 | 472 | 473 | tangramToData : Tangram -> TangramData 474 | tangramToData tangram = 475 | case tangram of 476 | ElmLogo -> 477 | { p1 = ( 0, -210, 0 ) 478 | , p2 = ( -210, 0, -90 ) 479 | , p3 = ( 207, 207, -45 ) 480 | , p4 = ( 150, 0, 0 ) 481 | , p5 = ( -89, 239, 0 ) 482 | , p6 = ( 0, 106, -180 ) 483 | , p7 = ( 256, -150, -270 ) 484 | } 485 | 486 | Heart -> 487 | { p1 = ( -160, 120, 0 ) 488 | , p2 = ( 150, -90, -180 ) 489 | , p3 = ( -270, -93, -45 ) 490 | , p4 = ( -5, -305, 0 ) 491 | , p5 = ( 231, 91, 0 ) 492 | , p6 = ( 150, 224, 0 ) 493 | , p7 = ( -106, -150, -90 ) 494 | } 495 | 496 | Camel -> 497 | { p1 = ( -250, -256, -315 ) 498 | , p2 = ( 100, -260, -270 ) 499 | , p3 = ( -190, -30, 0 ) 500 | , p4 = ( 40, 40, 0 ) 501 | , p5 = ( 278, 40, -90 ) 502 | , p6 = ( 262, 276, -90 ) 503 | , p7 = ( 366, 380, -180 ) 504 | } 505 | 506 | Cat -> 507 | { p1 = ( -40, -120, -90 ) 508 | , p2 = ( 20, -420, -135 ) 509 | , p3 = ( -226, -38, -270 ) 510 | , p4 = ( -220, 276, 0 ) 511 | , p5 = ( 350, -462, -315 ) 512 | , p6 = ( -320, 428, -90 ) 513 | , p7 = ( -120, 428, -270 ) 514 | } 515 | 516 | Bird -> 517 | { p1 = ( -296, 166, -45 ) 518 | , p2 = ( 0, 40, -225 ) 519 | , p3 = ( 200, 136, -270 ) 520 | , p4 = ( -42, -212, -45 ) 521 | , p5 = ( -138, -424, -135 ) 522 | , p6 = ( 139, -181, -315 ) 523 | , p7 = ( 352, 214, -225 ) 524 | } 525 | 526 | House -> 527 | { p1 = ( 0, -250, 0 ) 528 | , p2 = ( 96, 54, 0 ) 529 | , p3 = ( -218, -152, -315 ) 530 | , p4 = ( -106, 266, -45 ) 531 | , p5 = ( -212, 56, -315 ) 532 | , p6 = ( 162, -104, -180 ) 533 | , p7 = ( 264, -206, -270 ) 534 | } 535 | 536 | Person -> 537 | { p1 = ( -88, -46, -135 ) 538 | , p2 = ( 208, 86, -315 ) 539 | , p3 = ( 120, -300, 0 ) 540 | , p4 = ( 104, 352, -36 ) 541 | , p5 = ( -140, -300, -315 ) 542 | , p6 = ( -404, -380, -315 ) 543 | , p7 = ( 328, -434, -180 ) 544 | } 545 | 546 | 547 | type alias TangramData = 548 | { p1 : ( Int, Int, Int ) 549 | , p2 : ( Int, Int, Int ) 550 | , p3 : ( Int, Int, Int ) 551 | , p4 : ( Int, Int, Int ) 552 | , p5 : ( Int, Int, Int ) 553 | , p6 : ( Int, Int, Int ) 554 | , p7 : ( Int, Int, Int ) 555 | } 556 | 557 | 558 | nextTangram : Tangram -> Tangram 559 | nextTangram tangram = 560 | case tangram of 561 | ElmLogo -> 562 | Heart 563 | 564 | Heart -> 565 | Camel 566 | 567 | Camel -> 568 | Cat 569 | 570 | Cat -> 571 | Bird 572 | 573 | Bird -> 574 | House 575 | 576 | House -> 577 | Person 578 | 579 | Person -> 580 | ElmLogo 581 | 582 | 583 | previousTangram : Tangram -> Tangram 584 | previousTangram tangram = 585 | case tangram of 586 | ElmLogo -> 587 | Person 588 | 589 | Heart -> 590 | ElmLogo 591 | 592 | Camel -> 593 | Heart 594 | 595 | Cat -> 596 | Camel 597 | 598 | Bird -> 599 | Cat 600 | 601 | House -> 602 | Bird 603 | 604 | Person -> 605 | House 606 | 607 | 608 | tangramToString : Tangram -> String 609 | tangramToString tangram = 610 | case tangram of 611 | ElmLogo -> 612 | "ElmLogo" 613 | 614 | Heart -> 615 | "Heart" 616 | 617 | Camel -> 618 | "Camel" 619 | 620 | Cat -> 621 | "Cat" 622 | 623 | Bird -> 624 | "Bird" 625 | 626 | House -> 627 | "House" 628 | 629 | Person -> 630 | "Person" 631 | 632 | 633 | 634 | -- SVG 635 | 636 | 637 | wrapperWithViewbox : 638 | String 639 | -> { desc : String, id : String, title : String, width : Int } 640 | -> List (Svg.Svg msg) 641 | -> Html.Html msg 642 | wrapperWithViewbox viewbox { id, title, desc, width } listSvg = 643 | Svg.svg 644 | [ Svg.Attributes.xmlSpace "http://www.w3.org/2000/svg" 645 | , Svg.Attributes.preserveAspectRatio "xMinYMin slice" 646 | , Svg.Attributes.viewBox viewbox 647 | , Svg.Attributes.width <| String.fromInt width ++ "px" 648 | , Html.Attributes.attribute "role" "img" 649 | , Html.Attributes.attribute "aria-labelledby" (id ++ "Title " ++ id ++ "Desc") 650 | ] 651 | ([ Svg.title [ Svg.Attributes.id (id ++ "Title") ] [ Svg.text title ] 652 | , Svg.desc [ Svg.Attributes.id (id ++ "Desc") ] [ Svg.text desc ] 653 | ] 654 | ++ listSvg 655 | ) 656 | 657 | 658 | left : { desc : String, id : String, title : String, width : Int } -> Html.Html msg 659 | left args = 660 | wrapperWithViewbox 661 | "0 0 31.49 31.49" 662 | args 663 | [ Svg.path [ Svg.Attributes.fill "white", Svg.Attributes.d "M10.27 5a1.11 1.11 0 011.59 0c.43.44.43 1.15 0 1.58l-8.05 8.05h26.56a1.12 1.12 0 110 2.24H3.8l8.05 8.03c.43.44.43 1.16 0 1.58-.44.45-1.14.45-1.59 0L.32 16.53a1.12 1.12 0 010-1.57l9.95-9.95z" ] [] ] 664 | 665 | 666 | right : { desc : String, id : String, title : String, width : Int } -> Html.Html msg 667 | right args = 668 | wrapperWithViewbox 669 | "0 0 31.49 31.49" 670 | args 671 | [ Svg.path [ Svg.Attributes.fill "white", Svg.Attributes.d "M21.2 5a1.11 1.11 0 00-1.58 0 1.12 1.12 0 000 1.58l8.04 8.04H1.11c-.62 0-1.11.5-1.11 1.12 0 .62.5 1.12 1.11 1.12h26.55l-8.04 8.04a1.14 1.14 0 000 1.58c.44.45 1.16.45 1.58 0l9.96-9.95a1.1 1.1 0 000-1.57L21.2 5.01z" ] [] ] 672 | 673 | 674 | logo : 675 | TangramData 676 | -> { desc : String, id : String, title : String, width : Int } 677 | -> Html.Html msg 678 | logo data args = 679 | wrapperWithViewbox 680 | "-600 -600 1200 1200" 681 | args 682 | [ Svg.g 683 | [ Svg.Attributes.transform "scale(1 -1)" 684 | ] 685 | [ poly "-280,-90 0,190 280,-90" data.p1 686 | , poly "-280,-90 0,190 280,-90" data.p2 687 | , poly "-198,-66 0,132 198,-66" data.p3 688 | , poly "-130,0 0,-130 130,0 0,130" data.p4 689 | , poly "-191,61 69,61 191,-61 -69,-61" data.p5 690 | , poly "-130,-44 0,86 130,-44" data.p6 691 | , poly "-130,-44 0,86 130,-44" data.p7 692 | ] 693 | ] 694 | 695 | 696 | poly : String -> ( Int, Int, Int ) -> Svg.Svg msg 697 | poly points ( translateX, translateY, rotation ) = 698 | Svg.polygon 699 | [ Svg.Attributes.fill "currentColor" 700 | , Svg.Attributes.points points 701 | , Html.Attributes.style "transition" "1s" 702 | , Svg.Attributes.transform 703 | ("translate(" 704 | ++ String.fromInt translateX 705 | ++ " " 706 | ++ String.fromInt translateY 707 | ++ ") rotate(" 708 | ++ String.fromInt rotation 709 | ++ ")" 710 | ) 711 | ] 712 | [] 713 | 714 | 715 | 716 | -- CSS 717 | 718 | 719 | css : String 720 | css = 721 | """.skip-link { 722 | position: absolute; 723 | top: -40px; 724 | left: 0; 725 | background: #000000; 726 | color: white; 727 | padding: 8px; 728 | z-index: 100; 729 | } 730 | 731 | .skip-link:focus { 732 | top: 0; 733 | } 734 | 735 | @keyframes elmLogoSpin { 736 | 0%, 100% { 737 | transform: rotate(15deg); 738 | } 739 | 50% { 740 | transform: rotate(-15deg); 741 | } 742 | } 743 | 744 | """ 745 | -------------------------------------------------------------------------------- /src-elm-starter/Html/String/Attributes.elm: -------------------------------------------------------------------------------- 1 | module Html.String.Attributes exposing 2 | ( style, property, attribute, map 3 | , class, classList, id, title, hidden 4 | , type_, value, checked, placeholder, selected 5 | , accept, acceptCharset, action, autocomplete, autofocus 6 | , disabled, enctype, list, maxlength, minlength, method, multiple 7 | , name, novalidate, pattern, readonly, required, size, for, form 8 | , max, min, step 9 | , cols, rows, wrap 10 | , href, target, download, hreflang, media, ping, rel 11 | , ismap, usemap, shape, coords 12 | , src, height, width, alt 13 | , autoplay, controls, loop, preload, poster, default, kind, srclang 14 | , sandbox, srcdoc 15 | , reversed, start 16 | , align, colspan, rowspan, headers, scope 17 | , accesskey, contenteditable, contextmenu, dir, draggable, dropzone 18 | , itemprop, lang, spellcheck, tabindex 19 | , cite, datetime, pubdate, manifest 20 | ) 21 | 22 | {-| Helper functions for HTML attributes. They are organized roughly by 23 | category. Each attribute is labeled with the HTML tags it can be used with, so 24 | just search the page for `video` if you want video stuff. 25 | 26 | 27 | # Primitives 28 | 29 | @docs style, property, attribute, map 30 | 31 | 32 | # Super Common Attributes 33 | 34 | @docs class, classList, id, title, hidden 35 | 36 | 37 | # Inputs 38 | 39 | @docs type_, value, checked, placeholder, selected 40 | 41 | 42 | ## Input Helpers 43 | 44 | @docs accept, acceptCharset, action, autocomplete, autofocus 45 | @docs disabled, enctype, list, maxlength, minlength, method, multiple 46 | @docs name, novalidate, pattern, readonly, required, size, for, form 47 | 48 | 49 | ## Input Ranges 50 | 51 | @docs max, min, step 52 | 53 | 54 | ## Input Text Areas 55 | 56 | @docs cols, rows, wrap 57 | 58 | 59 | # Links and Areas 60 | 61 | @docs href, target, download, hreflang, media, ping, rel 62 | 63 | 64 | ## Maps 65 | 66 | @docs ismap, usemap, shape, coords 67 | 68 | 69 | # Embedded Content 70 | 71 | @docs src, height, width, alt 72 | 73 | 74 | ## Audio and Video 75 | 76 | @docs autoplay, controls, loop, preload, poster, default, kind, srclang 77 | 78 | 79 | ## iframes 80 | 81 | @docs sandbox, srcdoc 82 | 83 | 84 | # Ordered Lists 85 | 86 | @docs reversed, start 87 | 88 | 89 | # Tables 90 | 91 | @docs align, colspan, rowspan, headers, scope 92 | 93 | 94 | # Less Common Global Attributes 95 | 96 | Attributes that can be attached to any HTML tag but are less commonly used. 97 | 98 | @docs accesskey, contenteditable, contextmenu, dir, draggable, dropzone 99 | @docs itemprop, lang, spellcheck, tabindex 100 | 101 | 102 | # Miscellaneous 103 | 104 | @docs cite, datetime, pubdate, manifest 105 | 106 | -} 107 | 108 | import Html.String exposing (Attribute) 109 | import Html.Types 110 | import Json.Encode as Json 111 | 112 | 113 | {-| Specify a single CSS rule. 114 | 115 | greeting : Html msg 116 | greeting = 117 | div 118 | [ style "backgroundColor" "red" 119 | , style "height" "90px" 120 | , style "width" "100%" 121 | ] 122 | [ text "Hello!" ] 123 | 124 | There is no `Html.Styles` module because best practices for working with HTML 125 | suggest that this should primarily be specified in CSS files. So the general 126 | recommendation is to use this function lightly. 127 | 128 | -} 129 | style : String -> String -> Attribute msg 130 | style = 131 | Html.Types.Style 132 | 133 | 134 | {-| This function makes it easier to build a space-separated class attribute. 135 | Each class can easily be added and removed depending on the boolean val it 136 | is paired with. For example, maybe we want a way to view notices: 137 | 138 | viewNotice : Notice -> Html msg 139 | viewNotice notice = 140 | div 141 | [ classList 142 | [ ( "notice", True ) 143 | , ( "notice-important", notice.isImportant ) 144 | , ( "notice-seen", notice.isSeen ) 145 | ] 146 | ] 147 | [ text notice.content ] 148 | 149 | -} 150 | classList : List ( String, Bool ) -> Attribute msg 151 | classList conditionalClasses = 152 | conditionalClasses 153 | |> List.filter Tuple.second 154 | |> List.map Tuple.first 155 | |> String.join " " 156 | |> class 157 | 158 | 159 | {-| Create _properties_, like saying `domNode.className = 'greeting'` in 160 | JavaScript. 161 | 162 | import Json.Encode as Encode 163 | 164 | class : String -> Attribute msg 165 | class name = 166 | property "className" (Encode.string name) 167 | 168 | Read more about the difference between properties and attributes [here]. 169 | 170 | [here]: https://github.com/elm-lang/html/blob/master/properties-vs-attributes.md 171 | 172 | -} 173 | property : String -> Json.Value -> Attribute msg 174 | property = 175 | Html.Types.ValueProperty 176 | 177 | 178 | stringProperty : String -> String -> Attribute msg 179 | stringProperty = 180 | Html.Types.StringProperty 181 | 182 | 183 | boolProperty : String -> Bool -> Attribute msg 184 | boolProperty = 185 | Html.Types.BoolProperty 186 | 187 | 188 | {-| Create _attributes_, like saying `domNode.setAttribute('class', 'greeting')` 189 | in JavaScript. 190 | 191 | class : String -> Attribute msg 192 | class name = 193 | attribute "class" name 194 | 195 | Read more about the difference between properties and attributes [here]. 196 | 197 | [here]: https://github.com/elm-lang/html/blob/master/properties-vs-attributes.md 198 | 199 | -} 200 | attribute : String -> String -> Attribute msg 201 | attribute = 202 | Html.Types.Attribute 203 | 204 | 205 | {-| Transform the messages produced by an `Attribute`. 206 | -} 207 | map : (a -> msg) -> Attribute a -> Attribute msg 208 | map = 209 | Html.Types.mapAttribute 210 | 211 | 212 | 213 | -- GLOBAL ATTRIBUTES 214 | 215 | 216 | {-| Often used with CSS to style elements with common properties. 217 | -} 218 | class : String -> Attribute msg 219 | class className = 220 | stringProperty "className" className 221 | 222 | 223 | {-| Indicates the relevance of an element. 224 | -} 225 | hidden : Bool -> Attribute msg 226 | hidden bool = 227 | boolProperty "hidden" bool 228 | 229 | 230 | {-| Often used with CSS to style a specific element. The val of this 231 | attribute must be unique. 232 | -} 233 | id : String -> Attribute msg 234 | id val = 235 | stringProperty "id" val 236 | 237 | 238 | {-| Text to be displayed in a tooltip when hovering over the element. 239 | -} 240 | title : String -> Attribute msg 241 | title val = 242 | stringProperty "title" val 243 | 244 | 245 | 246 | -- LESS COMMON GLOBAL ATTRIBUTES 247 | 248 | 249 | {-| Defines a keyboard shortcut to activate or add focus to the element. 250 | -} 251 | accesskey : Char -> Attribute msg 252 | accesskey char = 253 | stringProperty "accessKey" (String.fromChar char) 254 | 255 | 256 | {-| Indicates whether the element's content is editable. 257 | -} 258 | contenteditable : Bool -> Attribute msg 259 | contenteditable bool = 260 | boolProperty "contentEditable" bool 261 | 262 | 263 | {-| Defines the ID of a `menu` element which will serve as the element's 264 | context menu. 265 | -} 266 | contextmenu : String -> Attribute msg 267 | contextmenu val = 268 | attribute "contextmenu" val 269 | 270 | 271 | {-| Defines the text direction. Allowed vals are ltr (Left-To-Right) or rtl 272 | (Right-To-Left). 273 | -} 274 | dir : String -> Attribute msg 275 | dir val = 276 | stringProperty "dir" val 277 | 278 | 279 | {-| Defines whether the element can be dragged. 280 | -} 281 | draggable : String -> Attribute msg 282 | draggable val = 283 | attribute "draggable" val 284 | 285 | 286 | {-| Indicates that the element accept the dropping of content on it. 287 | -} 288 | dropzone : String -> Attribute msg 289 | dropzone val = 290 | stringProperty "dropzone" val 291 | 292 | 293 | {-| -} 294 | itemprop : String -> Attribute msg 295 | itemprop val = 296 | attribute "itemprop" val 297 | 298 | 299 | {-| Defines the language used in the element. 300 | -} 301 | lang : String -> Attribute msg 302 | lang val = 303 | stringProperty "lang" val 304 | 305 | 306 | {-| Indicates whether spell checking is allowed for the element. 307 | -} 308 | spellcheck : Bool -> Attribute msg 309 | spellcheck bool = 310 | boolProperty "spellcheck" bool 311 | 312 | 313 | {-| Overrides the browser's default tab order and follows the one specified 314 | instead. 315 | -} 316 | tabindex : Int -> Attribute msg 317 | tabindex n = 318 | attribute "tabIndex" (String.fromInt n) 319 | 320 | 321 | 322 | -- EMBEDDED CONTENT 323 | 324 | 325 | {-| The URL of the embeddable content. For `audio`, `embed`, `iframe`, `img`, 326 | `input`, `script`, `source`, `track`, and `video`. 327 | -} 328 | src : String -> Attribute msg 329 | src val = 330 | stringProperty "src" val 331 | 332 | 333 | {-| Declare the height of a `canvas`, `embed`, `iframe`, `img`, `input`, 334 | `object`, or `video`. 335 | -} 336 | height : Int -> Attribute msg 337 | height val = 338 | attribute "height" (String.fromInt val) 339 | 340 | 341 | {-| Declare the width of a `canvas`, `embed`, `iframe`, `img`, `input`, 342 | `object`, or `video`. 343 | -} 344 | width : Int -> Attribute msg 345 | width val = 346 | attribute "width" (String.fromInt val) 347 | 348 | 349 | {-| Alternative text in case an image can't be displayed. Works with `img`, 350 | `area`, and `input`. 351 | -} 352 | alt : String -> Attribute msg 353 | alt val = 354 | stringProperty "alt" val 355 | 356 | 357 | 358 | -- AUDIO and VIDEO 359 | 360 | 361 | {-| The `audio` or `video` should play as soon as possible. 362 | -} 363 | autoplay : Bool -> Attribute msg 364 | autoplay bool = 365 | boolProperty "autoplay" bool 366 | 367 | 368 | {-| Indicates whether the browser should show playback controls for the `audio` 369 | or `video`. 370 | -} 371 | controls : Bool -> Attribute msg 372 | controls bool = 373 | boolProperty "controls" bool 374 | 375 | 376 | {-| Indicates whether the `audio` or `video` should start playing from the 377 | start when it's finished. 378 | -} 379 | loop : Bool -> Attribute msg 380 | loop bool = 381 | boolProperty "loop" bool 382 | 383 | 384 | {-| Control how much of an `audio` or `video` resource should be preloaded. 385 | -} 386 | preload : String -> Attribute msg 387 | preload val = 388 | stringProperty "preload" val 389 | 390 | 391 | {-| A URL indicating a poster frame to show until the user plays or seeks the 392 | `video`. 393 | -} 394 | poster : String -> Attribute msg 395 | poster val = 396 | stringProperty "poster" val 397 | 398 | 399 | {-| Indicates that the `track` should be enabled unless the user's preferences 400 | indicate something different. 401 | -} 402 | default : Bool -> Attribute msg 403 | default bool = 404 | boolProperty "default" bool 405 | 406 | 407 | {-| Specifies the kind of text `track`. 408 | -} 409 | kind : String -> Attribute msg 410 | kind val = 411 | stringProperty "kind" val 412 | 413 | 414 | 415 | {--TODO: maybe reintroduce once there's a better way to disambiguate imports 416 | {-| Specifies a user-readable title of the text `track`. -} 417 | label : String -> Attribute msg 418 | label val = 419 | stringProperty "label" val 420 | --} 421 | 422 | 423 | {-| A two letter language code indicating the language of the `track` text data. 424 | -} 425 | srclang : String -> Attribute msg 426 | srclang val = 427 | stringProperty "srclang" val 428 | 429 | 430 | 431 | -- IFRAMES 432 | 433 | 434 | {-| A space separated list of security restrictions you'd like to lift for an 435 | `iframe`. 436 | -} 437 | sandbox : String -> Attribute msg 438 | sandbox val = 439 | stringProperty "sandbox" val 440 | 441 | 442 | {-| An HTML document that will be displayed as the body of an `iframe`. It will 443 | override the content of the `src` attribute if it has been specified. 444 | -} 445 | srcdoc : String -> Attribute msg 446 | srcdoc val = 447 | stringProperty "srcdoc" val 448 | 449 | 450 | 451 | -- INPUT 452 | 453 | 454 | {-| Defines the type of a `button`, `input`, `embed`, `object`, `script`, 455 | `source`, `style`, or `menu`. 456 | -} 457 | type_ : String -> Attribute msg 458 | type_ val = 459 | stringProperty "type" val 460 | 461 | 462 | {-| Defines a default val which will be displayed in a `button`, `option`, 463 | `input`, `li`, `meter`, `progress`, or `param`. 464 | -} 465 | value : String -> Attribute msg 466 | value val = 467 | stringProperty "value" val 468 | 469 | 470 | {-| Indicates whether an `input` of type checkbox is checked. 471 | -} 472 | checked : Bool -> Attribute msg 473 | checked bool = 474 | boolProperty "checked" bool 475 | 476 | 477 | {-| Provides a hint to the user of what can be entered into an `input` or 478 | `textarea`. 479 | -} 480 | placeholder : String -> Attribute msg 481 | placeholder val = 482 | stringProperty "placeholder" val 483 | 484 | 485 | {-| Defines which `option` will be selected on page load. 486 | -} 487 | selected : Bool -> Attribute msg 488 | selected bool = 489 | boolProperty "selected" bool 490 | 491 | 492 | 493 | -- INPUT HELPERS 494 | 495 | 496 | {-| List of types the server accepts, typically a file type. 497 | For `form` and `input`. 498 | -} 499 | accept : String -> Attribute msg 500 | accept val = 501 | stringProperty "accept" val 502 | 503 | 504 | {-| List of supported charsets in a `form`. 505 | -} 506 | acceptCharset : String -> Attribute msg 507 | acceptCharset val = 508 | stringProperty "acceptCharset" val 509 | 510 | 511 | {-| The URI of a program that processes the information submitted via a `form`. 512 | -} 513 | action : String -> Attribute msg 514 | action val = 515 | stringProperty "action" val 516 | 517 | 518 | {-| Indicates whether a `form` or an `input` can have their vals automatically 519 | completed by the browser. 520 | -} 521 | autocomplete : Bool -> Attribute msg 522 | autocomplete bool = 523 | stringProperty "autocomplete" 524 | (if bool then 525 | "on" 526 | 527 | else 528 | "off" 529 | ) 530 | 531 | 532 | {-| The element should be automatically focused after the page loaded. 533 | For `button`, `input`, `keygen`, `select`, and `textarea`. 534 | -} 535 | autofocus : Bool -> Attribute msg 536 | autofocus bool = 537 | boolProperty "autofocus" bool 538 | 539 | 540 | {-| Indicates whether the user can interact with a `button`, `fieldset`, 541 | `input`, `keygen`, `optgroup`, `option`, `select` or `textarea`. 542 | -} 543 | disabled : Bool -> Attribute msg 544 | disabled bool = 545 | boolProperty "disabled" bool 546 | 547 | 548 | {-| How `form` data should be encoded when submitted with the POST method. 549 | Options include: application/x-www-form-urlencoded, multipart/form-data, and 550 | text/plain. 551 | -} 552 | enctype : String -> Attribute msg 553 | enctype val = 554 | stringProperty "enctype" val 555 | 556 | 557 | {-| Associates an `input` with a `datalist` tag. The datalist gives some 558 | pre-defined options to suggest to the user as they interact with an input. 559 | The val of the list attribute must match the id of a `datalist` node. 560 | For `input`. 561 | -} 562 | list : String -> Attribute msg 563 | list val = 564 | attribute "list" val 565 | 566 | 567 | {-| Defines the minimum number of characters allowed in an `input` or 568 | `textarea`. 569 | -} 570 | minlength : Int -> Attribute msg 571 | minlength n = 572 | attribute "minLength" (String.fromInt n) 573 | 574 | 575 | {-| Defines the maximum number of characters allowed in an `input` or 576 | `textarea`. 577 | -} 578 | maxlength : Int -> Attribute msg 579 | maxlength n = 580 | attribute "maxlength" (String.fromInt n) 581 | 582 | 583 | {-| Defines which HTTP method to use when submitting a `form`. Can be GET 584 | (default) or POST. 585 | -} 586 | method : String -> Attribute msg 587 | method val = 588 | stringProperty "method" val 589 | 590 | 591 | {-| Indicates whether multiple vals can be entered in an `input` of type 592 | email or file. Can also indicate that you can `select` many options. 593 | -} 594 | multiple : Bool -> Attribute msg 595 | multiple bool = 596 | boolProperty "multiple" bool 597 | 598 | 599 | {-| Name of the element. For example used by the server to identify the fields 600 | in form submits. For `button`, `form`, `fieldset`, `iframe`, `input`, `keygen`, 601 | `object`, `output`, `select`, `textarea`, `map`, `meta`, and `param`. 602 | -} 603 | name : String -> Attribute msg 604 | name val = 605 | stringProperty "name" val 606 | 607 | 608 | {-| This attribute indicates that a `form` shouldn't be validated when 609 | submitted. 610 | -} 611 | novalidate : Bool -> Attribute msg 612 | novalidate bool = 613 | boolProperty "noValidate" bool 614 | 615 | 616 | {-| Defines a regular expression which an `input`'s val will be validated 617 | against. 618 | -} 619 | pattern : String -> Attribute msg 620 | pattern val = 621 | stringProperty "pattern" val 622 | 623 | 624 | {-| Indicates whether an `input` or `textarea` can be edited. 625 | -} 626 | readonly : Bool -> Attribute msg 627 | readonly bool = 628 | boolProperty "readOnly" bool 629 | 630 | 631 | {-| Indicates whether this element is required to fill out or not. 632 | For `input`, `select`, and `textarea`. 633 | -} 634 | required : Bool -> Attribute msg 635 | required bool = 636 | boolProperty "required" bool 637 | 638 | 639 | {-| For `input` specifies the width of an input in characters. 640 | 641 | For `select` specifies the number of visible options in a drop-down list. 642 | 643 | -} 644 | size : Int -> Attribute msg 645 | size n = 646 | attribute "size" (String.fromInt n) 647 | 648 | 649 | {-| The element ID described by this `label` or the element IDs that are used 650 | for an `output`. 651 | -} 652 | for : String -> Attribute msg 653 | for val = 654 | stringProperty "htmlFor" val 655 | 656 | 657 | {-| Indicates the element ID of the `form` that owns this particular `button`, 658 | `fieldset`, `input`, `keygen`, `label`, `meter`, `object`, `output`, 659 | `progress`, `select`, or `textarea`. 660 | -} 661 | form : String -> Attribute msg 662 | form val = 663 | attribute "form" val 664 | 665 | 666 | 667 | -- RANGES 668 | 669 | 670 | {-| Indicates the maximum val allowed. When using an input of type number or 671 | date, the max val must be a number or date. For `input`, `meter`, and `progress`. 672 | -} 673 | max : String -> Attribute msg 674 | max val = 675 | stringProperty "max" val 676 | 677 | 678 | {-| Indicates the minimum val allowed. When using an input of type number or 679 | date, the min val must be a number or date. For `input` and `meter`. 680 | -} 681 | min : String -> Attribute msg 682 | min val = 683 | stringProperty "min" val 684 | 685 | 686 | {-| Add a step size to an `input`. Use `step "any"` to allow any floating-point 687 | number to be used in the input. 688 | -} 689 | step : String -> Attribute msg 690 | step n = 691 | stringProperty "step" n 692 | 693 | 694 | 695 | -------------------------- 696 | 697 | 698 | {-| Defines the number of columns in a `textarea`. 699 | -} 700 | cols : Int -> Attribute msg 701 | cols n = 702 | attribute "cols" (String.fromInt n) 703 | 704 | 705 | {-| Defines the number of rows in a `textarea`. 706 | -} 707 | rows : Int -> Attribute msg 708 | rows n = 709 | attribute "rows" (String.fromInt n) 710 | 711 | 712 | {-| Indicates whether the text should be wrapped in a `textarea`. Possible 713 | vals are "hard" and "soft". 714 | -} 715 | wrap : String -> Attribute msg 716 | wrap val = 717 | stringProperty "wrap" val 718 | 719 | 720 | 721 | -- MAPS 722 | 723 | 724 | {-| When an `img` is a descendent of an `a` tag, the `ismap` attribute 725 | indicates that the click location should be added to the parent `a`'s href as 726 | a query string. 727 | -} 728 | ismap : Bool -> Attribute msg 729 | ismap val = 730 | boolProperty "isMap" val 731 | 732 | 733 | {-| Specify the hash name reference of a `map` that should be used for an `img` 734 | or `object`. A hash name reference is a hash symbol followed by the element's name or id. 735 | E.g. `"#planet-map"`. 736 | -} 737 | usemap : String -> Attribute msg 738 | usemap val = 739 | stringProperty "useMap" val 740 | 741 | 742 | {-| Declare the shape of the clickable area in an `a` or `area`. Valid vals 743 | include: default, rect, circle, poly. This attribute can be paired with 744 | `coords` to create more particular shapes. 745 | -} 746 | shape : String -> Attribute msg 747 | shape val = 748 | stringProperty "shape" val 749 | 750 | 751 | {-| A set of vals specifying the coordinates of the hot-spot region in an 752 | `area`. Needs to be paired with a `shape` attribute to be meaningful. 753 | -} 754 | coords : String -> Attribute msg 755 | coords val = 756 | stringProperty "coords" val 757 | 758 | 759 | 760 | -- REAL STUFF 761 | 762 | 763 | {-| Specifies the horizontal alignment of a `caption`, `col`, `colgroup`, 764 | `hr`, `iframe`, `img`, `table`, `tbody`, `td`, `tfoot`, `th`, `thead`, or 765 | `tr`. 766 | -} 767 | align : String -> Attribute msg 768 | align val = 769 | stringProperty "align" val 770 | 771 | 772 | {-| Contains a URI which points to the source of the quote or change in a 773 | `blockquote`, `del`, `ins`, or `q`. 774 | -} 775 | cite : String -> Attribute msg 776 | cite val = 777 | stringProperty "cite" val 778 | 779 | 780 | 781 | -- LINKS AND AREAS 782 | 783 | 784 | {-| The URL of a linked resource, such as `a`, `area`, `base`, or `link`. 785 | -} 786 | href : String -> Attribute msg 787 | href val = 788 | stringProperty "href" val 789 | 790 | 791 | {-| Specify where the results of clicking an `a`, `area`, `base`, or `form` 792 | should appear. Possible special vals include: 793 | 794 | - \_blank — a new window or tab 795 | - \_self — the same frame (this is default) 796 | - \_parent — the parent frame 797 | - \_top — the full body of the window 798 | 799 | You can also give the name of any `frame` you have created. 800 | 801 | -} 802 | target : String -> Attribute msg 803 | target val = 804 | stringProperty "target" val 805 | 806 | 807 | {-| Indicates that clicking an `a` and `area` will download the resource 808 | directly. 809 | -} 810 | download : String -> Attribute msg 811 | download val = 812 | stringProperty "download" val 813 | 814 | 815 | {-| Two-letter language code of the linked resource of an `a`, `area`, or `link`. 816 | -} 817 | hreflang : String -> Attribute msg 818 | hreflang val = 819 | stringProperty "hreflang" val 820 | 821 | 822 | {-| Specifies a hint of the target media of a `a`, `area`, `link`, `source`, 823 | or `style`. 824 | -} 825 | media : String -> Attribute msg 826 | media val = 827 | attribute "media" val 828 | 829 | 830 | {-| Specify a URL to send a short POST request to when the user clicks on an 831 | `a` or `area`. Useful for monitoring and tracking. 832 | -} 833 | ping : String -> Attribute msg 834 | ping val = 835 | stringProperty "ping" val 836 | 837 | 838 | {-| Specifies the relationship of the target object to the link object. 839 | For `a`, `area`, `link`. 840 | -} 841 | rel : String -> Attribute msg 842 | rel val = 843 | attribute "rel" val 844 | 845 | 846 | 847 | -- CRAZY STUFF 848 | 849 | 850 | {-| Indicates the date and time associated with the element. 851 | For `del`, `ins`, `time`. 852 | -} 853 | datetime : String -> Attribute msg 854 | datetime val = 855 | attribute "datetime" val 856 | 857 | 858 | {-| Indicates whether this date and time is the date of the nearest `article` 859 | ancestor element. For `time`. 860 | -} 861 | pubdate : String -> Attribute msg 862 | pubdate val = 863 | attribute "pubdate" val 864 | 865 | 866 | 867 | -- ORDERED LISTS 868 | 869 | 870 | {-| Indicates whether an ordered list `ol` should be displayed in a descending 871 | order instead of a ascending. 872 | -} 873 | reversed : Bool -> Attribute msg 874 | reversed bool = 875 | boolProperty "reversed" bool 876 | 877 | 878 | {-| Defines the first number of an ordered list if you want it to be something 879 | besides 1. 880 | -} 881 | start : Int -> Attribute msg 882 | start n = 883 | stringProperty "start" (String.fromInt n) 884 | 885 | 886 | 887 | -- TABLES 888 | 889 | 890 | {-| The colspan attribute defines the number of columns a cell should span. 891 | For `td` and `th`. 892 | -} 893 | colspan : Int -> Attribute msg 894 | colspan n = 895 | attribute "colspan" (String.fromInt n) 896 | 897 | 898 | {-| A space separated list of element IDs indicating which `th` elements are 899 | headers for this cell. For `td` and `th`. 900 | -} 901 | headers : String -> Attribute msg 902 | headers val = 903 | stringProperty "headers" val 904 | 905 | 906 | {-| Defines the number of rows a table cell should span over. 907 | For `td` and `th`. 908 | -} 909 | rowspan : Int -> Attribute msg 910 | rowspan n = 911 | attribute "rowspan" (String.fromInt n) 912 | 913 | 914 | {-| Specifies the scope of a header cell `th`. Possible vals are: col, row, 915 | colgroup, rowgroup. 916 | -} 917 | scope : String -> Attribute msg 918 | scope val = 919 | stringProperty "scope" val 920 | 921 | 922 | {-| Specifies the URL of the cache manifest for an `html` tag. 923 | -} 924 | manifest : String -> Attribute msg 925 | manifest val = 926 | attribute "manifest" val 927 | -------------------------------------------------------------------------------- /src-elm-starter/Html/String.elm: -------------------------------------------------------------------------------- 1 | module Html.String exposing 2 | ( toHtml, toString 3 | , Html, Attribute, text, textUnescaped, node, map 4 | , h1, h2, h3, h4, h5, h6 5 | , div, p, hr, pre, blockquote 6 | , span, a, code, em, strong, i, b, u, sub, sup, br 7 | , ol, ul, li, dl, dt, dd 8 | , img, iframe, canvas, math 9 | , form, input, textarea, button, select, option 10 | , section, nav, article, aside, header, footer, address, main_ 11 | , figure, figcaption 12 | , table, caption, colgroup, col, tbody, thead, tfoot, tr, td, th 13 | , fieldset, legend, label, datalist, optgroup, output, progress, meter 14 | , audio, video, source, track 15 | , embed, object, param 16 | , ins, del 17 | , small, cite, dfn, abbr, time, var, samp, kbd, s, q 18 | , mark, ruby, rt, rp, bdi, bdo, wbr 19 | , details, summary, menuitem, menu 20 | ) 21 | 22 | {-| This file is organized roughly in order of popularity. The tags which you'd 23 | expect to use frequently will be closer to the top. 24 | 25 | 26 | # Serialization 27 | 28 | @docs toHtml, toString 29 | 30 | 31 | # Primitives 32 | 33 | @docs Html, Attribute, text, textUnescaped, node, map 34 | 35 | 36 | # Tags 37 | 38 | 39 | ## Headers 40 | 41 | @docs h1, h2, h3, h4, h5, h6 42 | 43 | 44 | ## Grouping Content 45 | 46 | @docs div, p, hr, pre, blockquote 47 | 48 | 49 | ## Text 50 | 51 | @docs span, a, code, em, strong, i, b, u, sub, sup, br 52 | 53 | 54 | ## Lists 55 | 56 | @docs ol, ul, li, dl, dt, dd 57 | 58 | 59 | ## Embedded Content 60 | 61 | @docs img, iframe, canvas, math 62 | 63 | 64 | ## Inputs 65 | 66 | @docs form, input, textarea, button, select, option 67 | 68 | 69 | ## Sections 70 | 71 | @docs section, nav, article, aside, header, footer, address, main_ 72 | 73 | 74 | ## Figures 75 | 76 | @docs figure, figcaption 77 | 78 | 79 | ## Tables 80 | 81 | @docs table, caption, colgroup, col, tbody, thead, tfoot, tr, td, th 82 | 83 | 84 | ## Less Common Elements 85 | 86 | 87 | ### Less Common Inputs 88 | 89 | @docs fieldset, legend, label, datalist, optgroup, output, progress, meter 90 | 91 | 92 | ### Audio and Video 93 | 94 | @docs audio, video, source, track 95 | 96 | 97 | ### Embedded Objects 98 | 99 | @docs embed, object, param 100 | 101 | 102 | ### Text Edits 103 | 104 | @docs ins, del 105 | 106 | 107 | ### Semantic Text 108 | 109 | @docs small, cite, dfn, abbr, time, var, samp, kbd, s, q 110 | 111 | 112 | ### Less Common Text Tags 113 | 114 | @docs mark, ruby, rt, rp, bdi, bdo, wbr 115 | 116 | 117 | ## Interactive Elements 118 | 119 | @docs details, summary, menuitem, menu 120 | 121 | -} 122 | 123 | import Html 124 | import Html.Types as Types exposing (..) 125 | 126 | 127 | {-| The core building block used to build up HTML. Here we create an `Html` 128 | value with no attributes and one child: 129 | 130 | hello : Html msg 131 | hello = 132 | div [] [ text "Hello!" ] 133 | 134 | -} 135 | type alias Html msg = 136 | Types.Html msg 137 | 138 | 139 | {-| Set attributes on your `Html`. Learn more in the 140 | [`Html.String.Attributes`](Html.String.Attributes) module. 141 | -} 142 | type alias Attribute msg = 143 | Types.Attribute msg 144 | 145 | 146 | {-| General way to create HTML nodes. It is used to define all of the helper 147 | functions in this library. 148 | 149 | div : List (Attribute msg) -> List (Html msg) -> Html msg 150 | div attributes children = 151 | node "div" attributes children 152 | 153 | You can use this to create custom nodes if you need to create something that 154 | is not covered by the helper functions in this library. 155 | 156 | -} 157 | node : String -> List (Attribute msg) -> List (Html msg) -> Html msg 158 | node tag attributes children = 159 | Node tag attributes (Regular children) 160 | 161 | 162 | nodeWithoutChildren : String -> List (Attribute msg) -> List a -> Html msg 163 | nodeWithoutChildren tag attrs _ = 164 | Node tag attrs NoChildren 165 | 166 | 167 | {-| Transform the messages produced by some `Html`. In the following example, 168 | we have `viewButton` that produces `()` messages, and we transform those values 169 | into `Msg` values in `view`. 170 | 171 | type Msg 172 | = Left 173 | | Right 174 | 175 | view : model -> Html Msg 176 | view model = 177 | div [] 178 | [ map (\_ -> Left) (viewButton "Left") 179 | , map (\_ -> Right) (viewButton "Right") 180 | ] 181 | 182 | viewButton : String -> Html () 183 | viewButton name = 184 | button [ onClick () ] [ text name ] 185 | 186 | This should not come in handy too often. Definitely read [this][reuse] before 187 | deciding if this is what you want. 188 | 189 | [reuse]: https://guide.elm-lang.org/reuse/ 190 | 191 | -} 192 | map : (a -> b) -> Html a -> Html b 193 | map = 194 | Types.map 195 | 196 | 197 | {-| Convert to regular `elm-lang/html` Html. 198 | -} 199 | toHtml : Html msg -> Html.Html msg 200 | toHtml = 201 | Types.toHtml 202 | 203 | 204 | {-| Convert to a string with indentation. 205 | 206 | Setting indentation to 0 will additionally remove newlines between tags, sort of 207 | like `Json.Encode.encode 0`. 208 | 209 | import Html.String.Attributes exposing (href) 210 | 211 | 212 | someHtml : Html msg 213 | someHtml = 214 | a [ href "http://google.com" ] [ text "Google!" ] 215 | 216 | 217 | Html.String.toString 2 someHtml 218 | --> "<a href=\"http://google.com\">\n Google!\n</a>" 219 | 220 | 221 | Html.String.toString 0 someHtml 222 | --> "<a href=\"http://google.com\">Google!</a>" 223 | 224 | -} 225 | toString : Int -> Html msg -> String 226 | toString indent = 227 | Types.toString indent 228 | 229 | 230 | {-| Just put plain text in the DOM. It will escape the string so that it appears 231 | exactly as you specify. 232 | 233 | text "Hello World!" 234 | 235 | -} 236 | text : String -> Html msg 237 | text = 238 | TextNode 239 | 240 | 241 | {-| Just put plain text in the DOM. It will NOT escape the string so that you can 242 | embedd Javascript without converting & to & 243 | 244 | text "if (a && b) {}" 245 | 246 | -} 247 | textUnescaped : String -> Html msg 248 | textUnescaped = 249 | TextNodeUnescaped 250 | 251 | 252 | 253 | -- SECTIONS 254 | 255 | 256 | {-| Defines a section in a document. 257 | -} 258 | section : List (Attribute msg) -> List (Html msg) -> Html msg 259 | section = 260 | node "section" 261 | 262 | 263 | {-| Defines a section that contains only navigation links. 264 | -} 265 | nav : List (Attribute msg) -> List (Html msg) -> Html msg 266 | nav = 267 | node "nav" 268 | 269 | 270 | {-| Defines self-contained content that could exist independently of the rest 271 | of the content. 272 | -} 273 | article : List (Attribute msg) -> List (Html msg) -> Html msg 274 | article = 275 | node "article" 276 | 277 | 278 | {-| Defines some content loosely related to the page content. If it is removed, 279 | the remaining content still makes sense. 280 | -} 281 | aside : List (Attribute msg) -> List (Html msg) -> Html msg 282 | aside = 283 | node "aside" 284 | 285 | 286 | {-| -} 287 | h1 : List (Attribute msg) -> List (Html msg) -> Html msg 288 | h1 = 289 | node "h1" 290 | 291 | 292 | {-| -} 293 | h2 : List (Attribute msg) -> List (Html msg) -> Html msg 294 | h2 = 295 | node "h2" 296 | 297 | 298 | {-| -} 299 | h3 : List (Attribute msg) -> List (Html msg) -> Html msg 300 | h3 = 301 | node "h3" 302 | 303 | 304 | {-| -} 305 | h4 : List (Attribute msg) -> List (Html msg) -> Html msg 306 | h4 = 307 | node "h4" 308 | 309 | 310 | {-| -} 311 | h5 : List (Attribute msg) -> List (Html msg) -> Html msg 312 | h5 = 313 | node "h5" 314 | 315 | 316 | {-| -} 317 | h6 : List (Attribute msg) -> List (Html msg) -> Html msg 318 | h6 = 319 | node "h6" 320 | 321 | 322 | {-| Defines the header of a page or section. It often contains a logo, the 323 | title of the web site, and a navigational table of content. 324 | -} 325 | header : List (Attribute msg) -> List (Html msg) -> Html msg 326 | header = 327 | node "header" 328 | 329 | 330 | {-| Defines the footer for a page or section. It often contains a copyright 331 | notice, some links to legal information, or addresses to give feedback. 332 | -} 333 | footer : List (Attribute msg) -> List (Html msg) -> Html msg 334 | footer = 335 | node "footer" 336 | 337 | 338 | {-| Defines a section containing contact information. 339 | -} 340 | address : List (Attribute msg) -> List (Html msg) -> Html msg 341 | address = 342 | node "address" 343 | 344 | 345 | {-| Defines the main or important content in the document. There is only one 346 | `main` element in the document. 347 | -} 348 | main_ : List (Attribute msg) -> List (Html msg) -> Html msg 349 | main_ = 350 | node "main" 351 | 352 | 353 | 354 | -- GROUPING CONTENT 355 | 356 | 357 | {-| Defines a portion that should be displayed as a paragraph. 358 | -} 359 | p : List (Attribute msg) -> List (Html msg) -> Html msg 360 | p = 361 | node "p" 362 | 363 | 364 | {-| Represents a thematic break between paragraphs of a section or article or 365 | any longer content. 366 | -} 367 | hr : List (Attribute msg) -> List (Html msg) -> Html msg 368 | hr = 369 | nodeWithoutChildren "hr" 370 | 371 | 372 | {-| Indicates that its content is preformatted and that this format must be 373 | preserved. 374 | -} 375 | pre : List (Attribute msg) -> List (Html msg) -> Html msg 376 | pre = 377 | node "pre" 378 | 379 | 380 | {-| Represents a content that is quoted from another source. 381 | -} 382 | blockquote : List (Attribute msg) -> List (Html msg) -> Html msg 383 | blockquote = 384 | node "blockquote" 385 | 386 | 387 | {-| Defines an ordered list of items. 388 | -} 389 | ol : List (Attribute msg) -> List (Html msg) -> Html msg 390 | ol = 391 | node "ol" 392 | 393 | 394 | {-| Defines an unordered list of items. 395 | -} 396 | ul : List (Attribute msg) -> List (Html msg) -> Html msg 397 | ul = 398 | node "ul" 399 | 400 | 401 | {-| Defines a item of an enumeration list. 402 | -} 403 | li : List (Attribute msg) -> List (Html msg) -> Html msg 404 | li = 405 | node "li" 406 | 407 | 408 | {-| Defines a definition list, that is, a list of terms and their associated 409 | definitions. 410 | -} 411 | dl : List (Attribute msg) -> List (Html msg) -> Html msg 412 | dl = 413 | node "dl" 414 | 415 | 416 | {-| Represents a term defined by the next `dd`. 417 | -} 418 | dt : List (Attribute msg) -> List (Html msg) -> Html msg 419 | dt = 420 | node "dt" 421 | 422 | 423 | {-| Represents the definition of the terms immediately listed before it. 424 | -} 425 | dd : List (Attribute msg) -> List (Html msg) -> Html msg 426 | dd = 427 | node "dd" 428 | 429 | 430 | {-| Represents a figure illustrated as part of the document. 431 | -} 432 | figure : List (Attribute msg) -> List (Html msg) -> Html msg 433 | figure = 434 | node "figure" 435 | 436 | 437 | {-| Represents the legend of a figure. 438 | -} 439 | figcaption : List (Attribute msg) -> List (Html msg) -> Html msg 440 | figcaption = 441 | node "figcaption" 442 | 443 | 444 | {-| Represents a generic container with no special meaning. 445 | -} 446 | div : List (Attribute msg) -> List (Html msg) -> Html msg 447 | div = 448 | node "div" 449 | 450 | 451 | 452 | -- TEXT LEVEL SEMANTIC 453 | 454 | 455 | {-| Represents a hyperlink, linking to another resource. 456 | -} 457 | a : List (Attribute msg) -> List (Html msg) -> Html msg 458 | a = 459 | node "a" 460 | 461 | 462 | {-| Represents emphasized text, like a stress accent. 463 | -} 464 | em : List (Attribute msg) -> List (Html msg) -> Html msg 465 | em = 466 | node "em" 467 | 468 | 469 | {-| Represents especially important text. 470 | -} 471 | strong : List (Attribute msg) -> List (Html msg) -> Html msg 472 | strong = 473 | node "strong" 474 | 475 | 476 | {-| Represents a side comment, that is, text like a disclaimer or a 477 | copyright, which is not essential to the comprehension of the document. 478 | -} 479 | small : List (Attribute msg) -> List (Html msg) -> Html msg 480 | small = 481 | node "small" 482 | 483 | 484 | {-| Represents content that is no longer accurate or relevant. 485 | -} 486 | s : List (Attribute msg) -> List (Html msg) -> Html msg 487 | s = 488 | node "s" 489 | 490 | 491 | {-| Represents the title of a work. 492 | -} 493 | cite : List (Attribute msg) -> List (Html msg) -> Html msg 494 | cite = 495 | node "cite" 496 | 497 | 498 | {-| Represents an inline quotation. 499 | -} 500 | q : List (Attribute msg) -> List (Html msg) -> Html msg 501 | q = 502 | node "q" 503 | 504 | 505 | {-| Represents a term whose definition is contained in its nearest ancestor 506 | content. 507 | -} 508 | dfn : List (Attribute msg) -> List (Html msg) -> Html msg 509 | dfn = 510 | node "dfn" 511 | 512 | 513 | {-| Represents an abbreviation or an acronym; the expansion of the 514 | abbreviation can be represented in the title attribute. 515 | -} 516 | abbr : List (Attribute msg) -> List (Html msg) -> Html msg 517 | abbr = 518 | node "abbr" 519 | 520 | 521 | {-| Represents a date and time value; the machine-readable equivalent can be 522 | represented in the datetime attribute. 523 | -} 524 | time : List (Attribute msg) -> List (Html msg) -> Html msg 525 | time = 526 | node "time" 527 | 528 | 529 | {-| Represents computer code. 530 | -} 531 | code : List (Attribute msg) -> List (Html msg) -> Html msg 532 | code = 533 | node "code" 534 | 535 | 536 | {-| Represents a variable. Specific cases where it should be used include an 537 | actual mathematical expression or programming context, an identifier 538 | representing a constant, a symbol identifying a physical quantity, a function 539 | parameter, or a mere placeholder in prose. 540 | -} 541 | var : List (Attribute msg) -> List (Html msg) -> Html msg 542 | var = 543 | node "var" 544 | 545 | 546 | {-| Represents the output of a program or a computer. 547 | -} 548 | samp : List (Attribute msg) -> List (Html msg) -> Html msg 549 | samp = 550 | node "samp" 551 | 552 | 553 | {-| Represents user input, often from the keyboard, but not necessarily; it 554 | may represent other input, like transcribed voice commands. 555 | -} 556 | kbd : List (Attribute msg) -> List (Html msg) -> Html msg 557 | kbd = 558 | node "kbd" 559 | 560 | 561 | {-| Represent a subscript. 562 | -} 563 | sub : List (Attribute msg) -> List (Html msg) -> Html msg 564 | sub = 565 | node "sub" 566 | 567 | 568 | {-| Represent a superscript. 569 | -} 570 | sup : List (Attribute msg) -> List (Html msg) -> Html msg 571 | sup = 572 | node "sup" 573 | 574 | 575 | {-| Represents some text in an alternate voice or mood, or at least of 576 | different quality, such as a taxonomic designation, a technical term, an 577 | idiomatic phrase, a thought, or a ship name. 578 | -} 579 | i : List (Attribute msg) -> List (Html msg) -> Html msg 580 | i = 581 | node "i" 582 | 583 | 584 | {-| Represents a text which to which attention is drawn for utilitarian 585 | purposes. It doesn't convey extra importance and doesn't imply an alternate 586 | voice. 587 | -} 588 | b : List (Attribute msg) -> List (Html msg) -> Html msg 589 | b = 590 | node "b" 591 | 592 | 593 | {-| Represents a non-textual annoatation for which the conventional 594 | presentation is underlining, such labeling the text as being misspelt or 595 | labeling a proper name in Chinese text. 596 | -} 597 | u : List (Attribute msg) -> List (Html msg) -> Html msg 598 | u = 599 | node "u" 600 | 601 | 602 | {-| Represents text highlighted for reference purposes, that is for its 603 | relevance in another context. 604 | -} 605 | mark : List (Attribute msg) -> List (Html msg) -> Html msg 606 | mark = 607 | node "mark" 608 | 609 | 610 | {-| Represents content to be marked with ruby annotations, short runs of text 611 | presented alongside the text. This is often used in conjunction with East Asian 612 | language where the annotations act as a guide for pronunciation, like the 613 | Japanese furigana. 614 | -} 615 | ruby : List (Attribute msg) -> List (Html msg) -> Html msg 616 | ruby = 617 | node "ruby" 618 | 619 | 620 | {-| Represents the text of a ruby annotation. 621 | -} 622 | rt : List (Attribute msg) -> List (Html msg) -> Html msg 623 | rt = 624 | node "rt" 625 | 626 | 627 | {-| Represents parenthesis around a ruby annotation, used to display the 628 | annotation in an alternate way by browsers not supporting the standard display 629 | for annotations. 630 | -} 631 | rp : List (Attribute msg) -> List (Html msg) -> Html msg 632 | rp = 633 | node "rp" 634 | 635 | 636 | {-| Represents text that must be isolated from its surrounding for 637 | bidirectional text formatting. It allows embedding a span of text with a 638 | different, or unknown, directionality. 639 | -} 640 | bdi : List (Attribute msg) -> List (Html msg) -> Html msg 641 | bdi = 642 | node "bdi" 643 | 644 | 645 | {-| Represents the directionality of its children, in order to explicitly 646 | override the Unicode bidirectional algorithm. 647 | -} 648 | bdo : List (Attribute msg) -> List (Html msg) -> Html msg 649 | bdo = 650 | node "bdo" 651 | 652 | 653 | {-| Represents text with no specific meaning. This has to be used when no other 654 | text-semantic element conveys an adequate meaning, which, in this case, is 655 | often brought by global attributes like `class`, `lang`, or `dir`. 656 | -} 657 | span : List (Attribute msg) -> List (Html msg) -> Html msg 658 | span = 659 | node "span" 660 | 661 | 662 | {-| Represents a line break. 663 | -} 664 | br : List (Attribute msg) -> List (Html msg) -> Html msg 665 | br = 666 | nodeWithoutChildren "br" 667 | 668 | 669 | {-| Represents a line break opportunity, that is a suggested point for 670 | wrapping text in order to improve readability of text split on several lines. 671 | -} 672 | wbr : List (Attribute msg) -> List (Html msg) -> Html msg 673 | wbr = 674 | nodeWithoutChildren "wbr" 675 | 676 | 677 | 678 | -- EDITS 679 | 680 | 681 | {-| Defines an addition to the document. 682 | -} 683 | ins : List (Attribute msg) -> List (Html msg) -> Html msg 684 | ins = 685 | node "ins" 686 | 687 | 688 | {-| Defines a removal from the document. 689 | -} 690 | del : List (Attribute msg) -> List (Html msg) -> Html msg 691 | del = 692 | node "del" 693 | 694 | 695 | 696 | -- EMBEDDED CONTENT 697 | 698 | 699 | {-| Represents an image. 700 | -} 701 | img : List (Attribute msg) -> List (Html msg) -> Html msg 702 | img = 703 | nodeWithoutChildren "img" 704 | 705 | 706 | {-| Embedded an HTML document. 707 | -} 708 | iframe : List (Attribute msg) -> List (Html msg) -> Html msg 709 | iframe = 710 | node "iframe" 711 | 712 | 713 | {-| Represents a integration point for an external, often non-HTML, 714 | application or interactive content. 715 | -} 716 | embed : List (Attribute msg) -> List (Html msg) -> Html msg 717 | embed = 718 | nodeWithoutChildren "embed" 719 | 720 | 721 | {-| Represents an external resource, which is treated as an image, an HTML 722 | sub-document, or an external resource to be processed by a plug-in. 723 | -} 724 | object : List (Attribute msg) -> List (Html msg) -> Html msg 725 | object = 726 | node "object" 727 | 728 | 729 | {-| Defines parameters for use by plug-ins invoked by `object` elements. 730 | -} 731 | param : List (Attribute msg) -> List (Html msg) -> Html msg 732 | param = 733 | nodeWithoutChildren "param" 734 | 735 | 736 | {-| Represents a video, the associated audio and captions, and controls. 737 | -} 738 | video : List (Attribute msg) -> List (Html msg) -> Html msg 739 | video = 740 | node "video" 741 | 742 | 743 | {-| Represents a sound or audio stream. 744 | -} 745 | audio : List (Attribute msg) -> List (Html msg) -> Html msg 746 | audio = 747 | node "audio" 748 | 749 | 750 | {-| Allows authors to specify alternative media resources for media elements 751 | like `video` or `audio`. 752 | -} 753 | source : List (Attribute msg) -> List (Html msg) -> Html msg 754 | source = 755 | nodeWithoutChildren "source" 756 | 757 | 758 | {-| Allows authors to specify timed text track for media elements like `video` 759 | or `audio`. 760 | -} 761 | track : List (Attribute msg) -> List (Html msg) -> Html msg 762 | track = 763 | nodeWithoutChildren "track" 764 | 765 | 766 | {-| Represents a bitmap area for graphics rendering. 767 | -} 768 | canvas : List (Attribute msg) -> List (Html msg) -> Html msg 769 | canvas = 770 | node "canvas" 771 | 772 | 773 | {-| Defines a mathematical formula. 774 | -} 775 | math : List (Attribute msg) -> List (Html msg) -> Html msg 776 | math = 777 | node "math" 778 | 779 | 780 | 781 | -- TABULAR DATA 782 | 783 | 784 | {-| Represents data with more than one dimension. 785 | -} 786 | table : List (Attribute msg) -> List (Html msg) -> Html msg 787 | table = 788 | node "table" 789 | 790 | 791 | {-| Represents the title of a table. 792 | -} 793 | caption : List (Attribute msg) -> List (Html msg) -> Html msg 794 | caption = 795 | node "caption" 796 | 797 | 798 | {-| Represents a set of one or more columns of a table. 799 | -} 800 | colgroup : List (Attribute msg) -> List (Html msg) -> Html msg 801 | colgroup = 802 | node "colgroup" 803 | 804 | 805 | {-| Represents a column of a table. 806 | -} 807 | col : List (Attribute msg) -> List (Html msg) -> Html msg 808 | col = 809 | nodeWithoutChildren "col" 810 | 811 | 812 | {-| Represents the block of rows that describes the concrete data of a table. 813 | -} 814 | tbody : List (Attribute msg) -> List (Html msg) -> Html msg 815 | tbody = 816 | node "tbody" 817 | 818 | 819 | {-| Represents the block of rows that describes the column labels of a table. 820 | -} 821 | thead : List (Attribute msg) -> List (Html msg) -> Html msg 822 | thead = 823 | node "thead" 824 | 825 | 826 | {-| Represents the block of rows that describes the column summaries of a table. 827 | -} 828 | tfoot : List (Attribute msg) -> List (Html msg) -> Html msg 829 | tfoot = 830 | node "tfoot" 831 | 832 | 833 | {-| Represents a row of cells in a table. 834 | -} 835 | tr : List (Attribute msg) -> List (Html msg) -> Html msg 836 | tr = 837 | node "tr" 838 | 839 | 840 | {-| Represents a data cell in a table. 841 | -} 842 | td : List (Attribute msg) -> List (Html msg) -> Html msg 843 | td = 844 | node "td" 845 | 846 | 847 | {-| Represents a header cell in a table. 848 | -} 849 | th : List (Attribute msg) -> List (Html msg) -> Html msg 850 | th = 851 | node "th" 852 | 853 | 854 | 855 | -- FORMS 856 | 857 | 858 | {-| Represents a form, consisting of controls, that can be submitted to a 859 | server for processing. 860 | -} 861 | form : List (Attribute msg) -> List (Html msg) -> Html msg 862 | form = 863 | node "form" 864 | 865 | 866 | {-| Represents a set of controls. 867 | -} 868 | fieldset : List (Attribute msg) -> List (Html msg) -> Html msg 869 | fieldset = 870 | node "fieldset" 871 | 872 | 873 | {-| Represents the caption for a `fieldset`. 874 | -} 875 | legend : List (Attribute msg) -> List (Html msg) -> Html msg 876 | legend = 877 | node "legend" 878 | 879 | 880 | {-| Represents the caption of a form control. 881 | -} 882 | label : List (Attribute msg) -> List (Html msg) -> Html msg 883 | label = 884 | node "label" 885 | 886 | 887 | {-| Represents a typed data field allowing the user to edit the data. 888 | -} 889 | input : List (Attribute msg) -> List (Html msg) -> Html msg 890 | input = 891 | nodeWithoutChildren "input" 892 | 893 | 894 | {-| Represents a button. 895 | -} 896 | button : List (Attribute msg) -> List (Html msg) -> Html msg 897 | button = 898 | node "button" 899 | 900 | 901 | {-| Represents a control allowing selection among a set of options. 902 | -} 903 | select : List (Attribute msg) -> List (Html msg) -> Html msg 904 | select = 905 | node "select" 906 | 907 | 908 | {-| Represents a set of predefined options for other controls. 909 | -} 910 | datalist : List (Attribute msg) -> List (Html msg) -> Html msg 911 | datalist = 912 | node "datalist" 913 | 914 | 915 | {-| Represents a set of options, logically grouped. 916 | -} 917 | optgroup : List (Attribute msg) -> List (Html msg) -> Html msg 918 | optgroup = 919 | node "optgroup" 920 | 921 | 922 | {-| Represents an option in a `select` element or a suggestion of a `datalist` 923 | element. 924 | -} 925 | option : List (Attribute msg) -> List (Html msg) -> Html msg 926 | option = 927 | node "option" 928 | 929 | 930 | {-| Represents a multiline text edit control. 931 | -} 932 | textarea : List (Attribute msg) -> List (Html msg) -> Html msg 933 | textarea = 934 | node "textarea" 935 | 936 | 937 | {-| Represents the result of a calculation. 938 | -} 939 | output : List (Attribute msg) -> List (Html msg) -> Html msg 940 | output = 941 | node "output" 942 | 943 | 944 | {-| Represents the completion progress of a task. 945 | -} 946 | progress : List (Attribute msg) -> List (Html msg) -> Html msg 947 | progress = 948 | node "progress" 949 | 950 | 951 | {-| Represents a scalar measurement (or a fractional value), within a known 952 | range. 953 | -} 954 | meter : List (Attribute msg) -> List (Html msg) -> Html msg 955 | meter = 956 | node "meter" 957 | 958 | 959 | 960 | -- INTERACTIVE ELEMENTS 961 | 962 | 963 | {-| Represents a widget from which the user can obtain additional information 964 | or controls. 965 | -} 966 | details : List (Attribute msg) -> List (Html msg) -> Html msg 967 | details = 968 | node "details" 969 | 970 | 971 | {-| Represents a summary, caption, or legend for a given `details`. 972 | -} 973 | summary : List (Attribute msg) -> List (Html msg) -> Html msg 974 | summary = 975 | node "summary" 976 | 977 | 978 | {-| Represents a command that the user can invoke. 979 | -} 980 | menuitem : List (Attribute msg) -> List (Html msg) -> Html msg 981 | menuitem = 982 | nodeWithoutChildren "menuitem" 983 | 984 | 985 | {-| Represents a list of commands. 986 | -} 987 | menu : List (Attribute msg) -> List (Html msg) -> Html msg 988 | menu = 989 | node "menu" 990 | --------------------------------------------------------------------------------