├── examples
├── range
│ ├── .gitignore
│ ├── index.html
│ └── Range.elm
├── simple
│ ├── .gitignore
│ ├── index.html
│ └── Simple.elm
├── bootstrap
│ ├── .gitignore
│ ├── index.html
│ └── Bootstrap.elm
├── simple-nightwatch
│ ├── .gitignore
│ ├── index.html
│ └── SimpleNightwatch.elm
└── Makefile
├── .gitignore
├── elm-package.json
├── run-acceptance-tests
├── package.json
├── nightwatch.json
├── LICENSE
├── css
├── elm-datepicker.css
└── elm-datepicker.scss
├── nightwatch-tests
└── simple.js
├── README.md
└── src
├── DatePicker
└── Date.elm
└── DatePicker.elm
/examples/range/.gitignore:
--------------------------------------------------------------------------------
1 | range.js
2 |
--------------------------------------------------------------------------------
/examples/simple/.gitignore:
--------------------------------------------------------------------------------
1 | simple.js
--------------------------------------------------------------------------------
/examples/bootstrap/.gitignore:
--------------------------------------------------------------------------------
1 | bootstrap.js
--------------------------------------------------------------------------------
/examples/simple-nightwatch/.gitignore:
--------------------------------------------------------------------------------
1 | simple-nightwatch.js
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /elm-stuff
2 | /elm.js
3 | /.idea
4 | /node_modules
5 |
6 | #nightwatch reports
7 | /reports
8 |
--------------------------------------------------------------------------------
/elm-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "7.2.6",
3 | "summary": "A reusable date picker component",
4 | "repository": "https://github.com/elm-community/elm-datepicker.git",
5 | "license": "BSD3",
6 | "source-directories": [
7 | "examples/simple",
8 | "src"
9 | ],
10 | "exposed-modules": [
11 | "DatePicker"
12 | ],
13 | "dependencies": {
14 | "elm-lang/core": "5.0.0 <= v < 6.0.0",
15 | "elm-lang/html": "2.0.0 <= v < 3.0.0"
16 | },
17 | "elm-version": "0.18.0 <= v < 0.19.0"
18 | }
19 |
--------------------------------------------------------------------------------
/examples/Makefile:
--------------------------------------------------------------------------------
1 | all: simple-example bootstrap-example range-example simple-nightwatch-example
2 |
3 | simple-example:
4 | cd .. && elm make --warn examples/simple/Simple.elm --output=examples/simple/simple.js
5 |
6 | bootstrap-example:
7 | cd .. && elm make --warn examples/bootstrap/Bootstrap.elm --output=examples/bootstrap/bootstrap.js
8 |
9 | range-example:
10 | cd .. && elm make --warn examples/range/Range.elm --output=examples/range/range.js
11 |
12 | simple-nightwatch-example:
13 | cd .. && elm make --warn examples/simple-nightwatch/SimpleNightwatch.elm --output=examples/simple-nightwatch/simple-nightwatch.js
14 |
--------------------------------------------------------------------------------
/run-acceptance-tests:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 |
4 | # make sure latest build is being run
5 |
6 | cd examples && make simple-nightwatch-example && cd ..
7 |
8 |
9 | # start some required background processes
10 |
11 | elm-reactor &
12 | ELM_REACTOR_PID=$!
13 |
14 | chromedriver &
15 | CHROMEDRIVER_PID=$!
16 |
17 |
18 | # run nightwatch in the foreground
19 |
20 | ./node_modules/.bin/nightwatch
21 |
22 |
23 | # after nightwatch exits, kill background processes
24 |
25 | echo "shutting down Elm Reactor with pkill elm-reactor"
26 | pkill elm-reactor
27 | echo "shutting down Chromedriver with PID $CHROMEDRIVER_PID"
28 | kill "$CHROMEDRIVER_PID"
29 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "elm-datepicker",
3 | "version": "1.0.0",
4 | "description": "``` shell elm package install elm-community/elm-datepicker ```",
5 | "main": "index.js",
6 | "directories": {},
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/elm-community/elm-datepicker.git"
13 | },
14 | "author": "",
15 | "license": "ISC",
16 | "bugs": {
17 | "url": "https://github.com/elm-community/elm-datepicker/issues"
18 | },
19 | "homepage": "https://github.com/elm-community/elm-datepicker#readme",
20 | "devDependencies": {
21 | "nightwatch": "^0.9.16"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/examples/bootstrap/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | elm-datepicker example
6 |
7 |
11 |
12 |
15 |
16 |
17 |
21 |
22 |
23 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/examples/simple/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | elm-datepicker example
6 |
7 |
10 |
11 |
14 |
15 |
18 |
19 |
29 |
30 |
31 |
35 |
36 |
37 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/examples/simple-nightwatch/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | elm-datepicker example
6 |
7 |
10 |
11 |
14 |
15 |
18 |
19 |
29 |
30 |
31 |
35 |
36 |
37 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/nightwatch.json:
--------------------------------------------------------------------------------
1 | {
2 | "src_folders" : ["nightwatch-tests"],
3 | "output_folder" : "reports",
4 | "custom_commands_path" : "",
5 | "custom_assertions_path" : "",
6 | "page_objects_path" : "",
7 | "globals_path" : "",
8 |
9 | "selenium" : {
10 | "start_process" : false,
11 | "server_path" : "",
12 | "log_path" : "",
13 | "port" : 4444,
14 | "cli_args" : {
15 | "webdriver.chrome.driver" : "",
16 | "webdriver.gecko.driver" : "",
17 | "webdriver.edge.driver" : ""
18 | }
19 | },
20 |
21 | "test_settings" : {
22 | "default" : {
23 | "launch_url" : "http://localhost",
24 | "selenium_port" : 9515,
25 | "selenium_host" : "localhost",
26 | "default_path_prefix" : "",
27 | "silent": true,
28 | "screenshots" : {
29 | "enabled" : false,
30 | "path" : ""
31 | },
32 | "desiredCapabilities": {
33 | "browserName": "firefox",
34 | "marionette": true
35 | }
36 | },
37 |
38 | "chrome" : {
39 | "desiredCapabilities": {
40 | "browserName": "chrome"
41 | }
42 | },
43 |
44 | "edge" : {
45 | "desiredCapabilities": {
46 | "browserName": "MicrosoftEdge"
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/examples/range/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | elm-datepicker example
6 |
7 |
10 |
11 |
14 |
15 |
18 |
19 |
43 |
44 |
45 |
49 |
50 |
51 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016, Bogdan Paul Popa
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 | * Redistributions of source code must retain the above copyright
7 | notice, this list of conditions and the following disclaimer.
8 | * Redistributions in binary form must reproduce the above copyright
9 | notice, this list of conditions and the following disclaimer in the
10 | documentation and/or other materials provided with the distribution.
11 | * Neither the name of the nor the
12 | names of its contributors may be used to endorse or promote products
13 | derived from this software without specific prior written permission.
14 |
15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY
19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/examples/simple/Simple.elm:
--------------------------------------------------------------------------------
1 | module Simple exposing (main)
2 |
3 | import Date exposing (Date, Day(..), day, dayOfWeek, month, year)
4 | import DatePicker exposing (defaultSettings, DateEvent(..))
5 | import Html exposing (Html, div, h1, text)
6 |
7 |
8 | type Msg
9 | = ToDatePicker DatePicker.Msg
10 |
11 |
12 | type alias Model =
13 | { date : Maybe Date
14 | , datePicker : DatePicker.DatePicker
15 | }
16 |
17 |
18 | settings : DatePicker.Settings
19 | settings =
20 | let
21 | isDisabled date =
22 | dayOfWeek date
23 | |> flip List.member [ Sat, Sun ]
24 | in
25 | { defaultSettings | isDisabled = isDisabled }
26 |
27 |
28 | init : ( Model, Cmd Msg )
29 | init =
30 | let
31 | ( datePicker, datePickerFx ) =
32 | DatePicker.init
33 | in
34 | { date = Nothing
35 | , datePicker = datePicker
36 | }
37 | ! [ Cmd.map ToDatePicker datePickerFx ]
38 |
39 |
40 | update : Msg -> Model -> ( Model, Cmd Msg )
41 | update msg ({ date, datePicker } as model) =
42 | case msg of
43 | ToDatePicker msg ->
44 | let
45 | ( newDatePicker, datePickerFx, dateEvent ) =
46 | DatePicker.update settings msg datePicker
47 |
48 | newDate =
49 | case dateEvent of
50 | Changed newDate ->
51 | newDate
52 |
53 | _ ->
54 | date
55 | in
56 | { model
57 | | date = newDate
58 | , datePicker = newDatePicker
59 | }
60 | ! [ Cmd.map ToDatePicker datePickerFx ]
61 |
62 |
63 | view : Model -> Html Msg
64 | view ({ date, datePicker } as model) =
65 | div []
66 | [ case date of
67 | Nothing ->
68 | h1 [] [ text "Pick a date" ]
69 |
70 | Just date ->
71 | h1 [] [ text <| formatDate date ]
72 | , DatePicker.view date settings datePicker
73 | |> Html.map ToDatePicker
74 | ]
75 |
76 |
77 | formatDate : Date -> String
78 | formatDate d =
79 | toString (month d) ++ " " ++ toString (day d) ++ ", " ++ toString (year d)
80 |
81 |
82 | main : Program Never Model Msg
83 | main =
84 | Html.program
85 | { init = init
86 | , update = update
87 | , view = view
88 | , subscriptions = always Sub.none
89 | }
90 |
--------------------------------------------------------------------------------
/css/elm-datepicker.css:
--------------------------------------------------------------------------------
1 | .elm-datepicker--container {
2 | position: relative; }
3 |
4 | .elm-datepicker--input:focus {
5 | outline: 0; }
6 |
7 | .elm-datepicker--picker {
8 | position: absolute;
9 | border: 1px solid #CCC;
10 | z-index: 10;
11 | background: #FFFFFF; }
12 |
13 | .elm-datepicker--picker-header,
14 | .elm-datepicker--weekdays {
15 | background: #F2F2F2; }
16 |
17 | .elm-datepicker--picker-header {
18 | display: flex;
19 | align-items: center; }
20 |
21 | .elm-datepicker--prev-container,
22 | .elm-datepicker--next-container {
23 | flex: 0 1 auto; }
24 |
25 | .elm-datepicker--month-container {
26 | flex: 1 1 auto;
27 | padding: 0.5em;
28 | display: flex;
29 | flex-direction: column; }
30 |
31 | .elm-datepicker--month,
32 | .elm-datepicker--year {
33 | flex: 1 1 auto;
34 | cursor: default;
35 | text-align: center; }
36 |
37 | .elm-datepicker--year {
38 | font-size: 0.6em;
39 | font-weight: 700; }
40 |
41 | .elm-datepicker--prev,
42 | .elm-datepicker--next {
43 | border: 6px solid transparent;
44 | display: block;
45 | width: 0;
46 | height: 0;
47 | padding: 0 0.2em; }
48 |
49 | .elm-datepicker--prev {
50 | border-right-color: #AAA; }
51 | .elm-datepicker--prev:hover {
52 | border-right-color: #BBB; }
53 |
54 | .elm-datepicker--next {
55 | border-left-color: #AAA; }
56 | .elm-datepicker--next:hover {
57 | border-left-color: #BBB; }
58 |
59 | .elm-datepicker--table {
60 | border-spacing: 0;
61 | border-collapse: collapse;
62 | font-size: 0.8em; }
63 | .elm-datepicker--table td {
64 | width: 2em;
65 | height: 2em;
66 | text-align: center; }
67 |
68 | .elm-datepicker--row {
69 | border-top: 1px solid #F2F2F2; }
70 |
71 | .elm-datepicker--dow {
72 | border-bottom: 1px solid #CCC;
73 | cursor: default; }
74 |
75 | .elm-datepicker--day {
76 | cursor: pointer; }
77 | .elm-datepicker--day:hover {
78 | background: #F2F2F2; }
79 |
80 | .elm-datepicker--disabled {
81 | cursor: default;
82 | color: #DDD; }
83 | .elm-datepicker--disabled:hover {
84 | background: inherit; }
85 |
86 | .elm-datepicker--picked {
87 | color: white;
88 | background: darkblue; }
89 | .elm-datepicker--picked:hover {
90 | background: darkblue; }
91 |
92 | .elm-datepicker--today {
93 | font-weight: bold; }
94 |
95 | .elm-datepicker--other-month {
96 | color: #AAA; }
97 | .elm-datepicker--other-month.elm-datepicker--disabled {
98 | color: #EEE; }
99 | .elm-datepicker--other-month.elm-datepicker--picked {
100 | color: white; }
101 |
--------------------------------------------------------------------------------
/examples/bootstrap/Bootstrap.elm:
--------------------------------------------------------------------------------
1 | module Bootstrap exposing (main)
2 |
3 | import Date exposing (Date, Day(..), day, dayOfWeek, month, year)
4 | import DatePicker exposing (defaultSettings, DateEvent(..))
5 | import Html exposing (Html, div, form, h1, input, label, text)
6 | import Html.Attributes exposing (class, type_, value)
7 |
8 |
9 | type Msg
10 | = ToDatePicker DatePicker.Msg
11 |
12 |
13 | type alias Model =
14 | { date : Maybe Date
15 | , datePicker : DatePicker.DatePicker
16 | }
17 |
18 |
19 | settings : DatePicker.Settings
20 | settings =
21 | let
22 | isDisabled date =
23 | dayOfWeek date
24 | |> flip List.member [ Sat, Sun ]
25 | in
26 | { defaultSettings
27 | | isDisabled = isDisabled
28 | , inputClassList = [ ( "form-control", True ) ]
29 | , inputName = Just "date"
30 | , inputId = Just "date-field"
31 | }
32 |
33 |
34 | init : ( Model, Cmd Msg )
35 | init =
36 | let
37 | ( datePicker, datePickerFx ) =
38 | DatePicker.init
39 | in
40 | { date = Nothing
41 | , datePicker = datePicker
42 | }
43 | ! [ Cmd.map ToDatePicker datePickerFx ]
44 |
45 |
46 | update : Msg -> Model -> ( Model, Cmd Msg )
47 | update msg ({ datePicker } as model) =
48 | case msg of
49 | ToDatePicker msg ->
50 | let
51 | ( newDatePicker, datePickerFx, event ) =
52 | DatePicker.update settings msg datePicker
53 | in
54 | { model
55 | | date =
56 | case event of
57 | Changed date ->
58 | date
59 |
60 | NoChange ->
61 | model.date
62 | , datePicker = newDatePicker
63 | }
64 | ! [ Cmd.map ToDatePicker datePickerFx ]
65 |
66 |
67 | view : Model -> Html Msg
68 | view ({ date, datePicker } as model) =
69 | div [ class "col-md-3" ]
70 | [ form []
71 | [ div [ class "form-group" ]
72 | [ label [] [ text "Pick a date" ]
73 | , DatePicker.view date settings datePicker
74 | |> Html.map ToDatePicker
75 | ]
76 | , input
77 | [ type_ "submit"
78 | , class "btn btn-primary"
79 | , value "Submit"
80 | ]
81 | []
82 | ]
83 | ]
84 |
85 |
86 | formatDate : Date -> String
87 | formatDate d =
88 | toString (month d) ++ " " ++ toString (day d) ++ ", " ++ toString (year d)
89 |
90 |
91 | main : Program Never Model Msg
92 | main =
93 | Html.program
94 | { init = init
95 | , update = update
96 | , view = view
97 | , subscriptions = always Sub.none
98 | }
99 |
--------------------------------------------------------------------------------
/css/elm-datepicker.scss:
--------------------------------------------------------------------------------
1 | $datepicker-ns: elm-datepicker-- !default;
2 | $datepicker-hl: #F2F2F2;
3 | $datepicker-arrow: #AAA;
4 | $datepicker-arrow-hover: #BBB;
5 | $datepicker-picked-fg: white;
6 | $datepicker-picked-bg: darkblue;
7 | $datepicker-border-color: #CCC;
8 | $datepicker-row-border-color: #F2F2F2;
9 |
10 | .#{$datepicker-ns}container {
11 | position: relative;
12 | }
13 |
14 | .#{$datepicker-ns}input:focus {
15 | outline: 0;
16 | }
17 |
18 | .#{$datepicker-ns}picker {
19 | position: absolute;
20 | border: 1px solid $datepicker-border-color;
21 | z-index: 10;
22 | background-color: white;
23 | }
24 |
25 | .#{$datepicker-ns}picker-header,
26 | .#{$datepicker-ns}weekdays {
27 | background: $datepicker-hl;
28 | }
29 |
30 | .#{$datepicker-ns}picker-header {
31 | display: flex;
32 | align-items: center;
33 | }
34 |
35 | .#{$datepicker-ns}prev-container,
36 | .#{$datepicker-ns}next-container {
37 | flex: 0 1 auto;
38 | }
39 |
40 | .#{$datepicker-ns}month-container {
41 | flex: 1 1 auto;
42 |
43 | padding: 0.5em;
44 |
45 | display: flex;
46 | flex-direction: column;
47 | }
48 |
49 | .#{$datepicker-ns}month,
50 | .#{$datepicker-ns}year {
51 | flex: 1 1 auto;
52 |
53 | cursor: default;
54 | text-align: center;
55 | }
56 |
57 | .#{$datepicker-ns}year {
58 | font-size: 0.6em;
59 | font-weight: 700;
60 | }
61 |
62 | .#{$datepicker-ns}prev,
63 | .#{$datepicker-ns}next {
64 | border: 6px solid transparent;
65 |
66 | display: block;
67 | width: 0;
68 | height: 0;
69 |
70 | padding: 0 0.2em;
71 | }
72 |
73 | .#{$datepicker-ns}prev {
74 | border-right-color: $datepicker-arrow;
75 |
76 | &:hover {
77 | border-right-color: $datepicker-arrow-hover;
78 | }
79 | }
80 |
81 | .#{$datepicker-ns}next {
82 | border-left-color: $datepicker-arrow;
83 |
84 | &:hover {
85 | border-left-color: $datepicker-arrow-hover;
86 | }
87 | }
88 |
89 | .#{$datepicker-ns}table {
90 | border-spacing: 0;
91 | border-collapse: collapse;
92 |
93 | font-size: 0.8em;
94 |
95 | td {
96 | width: 2em;
97 | height: 2em;
98 |
99 | text-align: center;
100 | }
101 | }
102 |
103 | .#{$datepicker-ns}row {
104 | border-top: 1px solid $datepicker-row-border-color;
105 | }
106 |
107 | .#{$datepicker-ns}dow {
108 | border-bottom: 1px solid $datepicker-border-color;
109 |
110 | cursor: default;
111 | }
112 |
113 | .#{$datepicker-ns}day {
114 | cursor: pointer;
115 |
116 | &:hover {
117 | background: $datepicker-hl;
118 | }
119 | }
120 |
121 | .#{$datepicker-ns}disabled {
122 | cursor: default;
123 | color: #DDD;
124 |
125 | &:hover {
126 | background: inherit;
127 | }
128 | }
129 |
130 | .#{$datepicker-ns}picked {
131 | color: $datepicker-picked-fg;
132 | background: $datepicker-picked-bg;
133 |
134 | &:hover {
135 | background: $datepicker-picked-bg;
136 | }
137 | }
138 |
139 | .#{$datepicker-ns}today {
140 | font-weight: bold;
141 | }
142 |
143 | .#{$datepicker-ns}other-month {
144 | color: #AAA;
145 |
146 | &.#{$datepicker-ns}disabled {
147 | color: #EEE;
148 | }
149 |
150 | &.#{$datepicker-ns}picked {
151 | color: $datepicker-picked-fg;
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/examples/simple-nightwatch/SimpleNightwatch.elm:
--------------------------------------------------------------------------------
1 | module SimpleNightwatch exposing (main)
2 |
3 | {-| This is a simple test suitable for automated browser testing (such as with nightwatch.js)
4 | -}
5 |
6 | import Date exposing (Date, Day(..), day, dayOfWeek, month, year)
7 | import DatePicker exposing (defaultSettings, DateEvent(..))
8 | import Html exposing (Html, div, h1, text, button)
9 | import Process
10 | import Task
11 | import Time
12 |
13 | type Msg
14 | = ToDatePicker DatePicker.Msg
15 | | NoOp
16 |
17 |
18 | type alias Model =
19 | { date : Maybe Date
20 | , datePicker : DatePicker.DatePicker
21 | }
22 |
23 |
24 | settings : DatePicker.Settings
25 | settings =
26 | defaultSettings
27 |
28 |
29 | init : ( Model, Cmd Msg )
30 | init =
31 | let
32 | moonLandingDate =
33 | Date.fromString "1969-07-20"
34 | |> Result.toMaybe
35 | |> Maybe.withDefault (Date.fromTime 0)
36 |
37 | -- the fromTime 0 is just to keep the compiler happy - it will never be called
38 | in
39 | ( { date = Nothing
40 | , datePicker = DatePicker.initFromDate moonLandingDate
41 | }
42 | -- trigger a NoOp command after two seconds. This is used to test
43 | -- that re-renders of the app do not cause things to dissapear.
44 | , delayedNoOpCmd { seconds = 2 }
45 | )
46 |
47 |
48 | update : Msg -> Model -> ( Model, Cmd Msg )
49 | update msg ({ date, datePicker } as model) =
50 | case msg of
51 | ToDatePicker msg ->
52 | let
53 | ( newDatePicker, datePickerFx, dateEvent ) =
54 | DatePicker.update settings msg datePicker
55 |
56 | newDate =
57 | case dateEvent of
58 | Changed newDate ->
59 | newDate
60 |
61 | _ ->
62 | date
63 | in
64 | { model
65 | | date = newDate
66 | , datePicker = newDatePicker
67 | }
68 | ! [ Cmd.map ToDatePicker datePickerFx ]
69 |
70 | NoOp ->
71 | model ! []
72 |
73 |
74 | view : Model -> Html Msg
75 | view ({ date, datePicker } as model) =
76 | div []
77 | [ case date of
78 | Nothing ->
79 | h1 [] [ text "Pick a date" ]
80 |
81 | Just date ->
82 | h1 [] [ text <| formatDate date ]
83 | , DatePicker.view date settings datePicker
84 | |> Html.map ToDatePicker
85 | ]
86 |
87 |
88 | formatDate : Date -> String
89 | formatDate d =
90 | toString (month d) ++ " " ++ toString (day d) ++ ", " ++ toString (year d)
91 |
92 |
93 | delayedNoOpCmd : { seconds : Float } -> Cmd Msg
94 | delayedNoOpCmd { seconds } =
95 | Process.sleep (seconds * Time.second)
96 | |> Task.perform (\_ -> NoOp)
97 |
98 |
99 | main : Program Never Model Msg
100 | main =
101 | Html.program
102 | { init = init
103 | , update = update
104 | , view = view
105 | , subscriptions = always Sub.none
106 | }
107 |
--------------------------------------------------------------------------------
/nightwatch-tests/simple.js:
--------------------------------------------------------------------------------
1 | const url = "http://localhost:8000/examples/simple-nightwatch/index.html";
2 | const textInputSelector = ".elm-datepicker--input";
3 | const topLeftDaySelector = ".elm-datepicker--row:first-child .elm-datepicker--day:first-child";
4 |
5 |
6 | const defaultWait = 1000;
7 |
8 | module.exports = {
9 |
10 | 'When selecting a date with the mouse, it should appear in the text input' : function (client) {
11 | client.url(url);
12 | client.expect.element(textInputSelector).to.be.present.before(defaultWait);
13 | client.click(textInputSelector);
14 | client.expect.element(topLeftDaySelector).to.be.present.before(defaultWait);
15 | client.click(topLeftDaySelector);
16 | client.expect.element(textInputSelector).value.to.equal("1969/06/29").before(defaultWait);
17 | client.end();
18 | },
19 |
20 | 'When entering text, and then selecting a date with the mouse, the selected date should appear in the text input' : function (client) {
21 | client.url(url);
22 | client.expect.element(textInputSelector).to.be.present.before(defaultWait);
23 | client.click(textInputSelector);
24 | slowlySendKeys(client, textInputSelector, "1 Jan 1980");
25 | client.expect.element(topLeftDaySelector).to.be.present.before(defaultWait);
26 | client.click(topLeftDaySelector);
27 | client.expect.element(textInputSelector).value.to.equal("1969/06/29").before(defaultWait);
28 | client.end();
29 | },
30 |
31 | 'When entering the text of a valid date, then pressing the ENTER key, the entered date should appear in the date picker' : function (client) {
32 | client.url(url);
33 | client.expect.element(textInputSelector).to.be.present.before(defaultWait);
34 | client.click(textInputSelector);
35 | slowlySendKeys(client, textInputSelector, "1 Jan 1980");
36 | client.sendKeys(textInputSelector, client.Keys.ENTER);
37 | client.expect.element(topLeftDaySelector).to.be.present.before(defaultWait);
38 | client.expect.element(".elm-datepicker--row:first-child .elm-datepicker--day:nth-child(3)")
39 | .to.have.attribute('class').which.contains('elm-datepicker--picked');
40 | client.expect.element("h1").text.to.equal("Jan 1, 1980");
41 |
42 | // now we click on another value, to make sure the input is updated
43 | client.click(topLeftDaySelector);
44 | client.expect.element(textInputSelector).value.to.equal("1979/12/30").before(defaultWait);
45 | client.expect.element("h1").text.to.equal("Dec 30, 1979");
46 | client.end();
47 | },
48 |
49 | // This test has been commented out, we are currently unable to find a
50 | // satisfactory solution for https://github.com/elm-community/elm-datepicker/issues/63.
51 | //
52 | // 'Characters should not be dropped when entering text quickly' : function (client) {
53 | //
54 | // const longTextExample = "The quick brown fox jumped over the lazy dog";
55 | //
56 | // client.url(url);
57 | // client.expect.element(textInputSelector).to.be.present.before(defaultWait);
58 | // client.click(textInputSelector);
59 | // client.sendKeys(textInputSelector, longTextExample);
60 | // client.expect.element(textInputSelector).value.to.equal(longTextExample).before(defaultWait);
61 | // client.end();
62 | // },
63 |
64 | 'Manually entered text should not be cleared when the view re-renders' : function (client) {
65 |
66 | client.url(url);
67 | client.expect.element(textInputSelector).to.be.present.before(defaultWait);
68 | client.click(textInputSelector);
69 | slowlySendKeys(client, textInputSelector, "testing");
70 |
71 | // the SimpleNightwatch.elm app has a NoOp message that is triggered after 2
72 | // seconds. The NoOp causes a re-render of the app. This should not
73 | // clear the manually entered text.
74 | client.pause(3000);
75 | client.expect.element(textInputSelector).value.to.equal("testing").before(defaultWait);
76 | client.end();
77 | },
78 |
79 |
80 | };
81 |
82 |
83 | function slowlySendKeys(client, selector, text) {
84 | for (i in text) {
85 | client.sendKeys(selector, text[i]);
86 | client.pause(50);
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # elm-datepicker
2 |
3 | ``` shell
4 | elm package install elm-community/elm-datepicker
5 | ```
6 |
7 | A reusable date picker component in Elm.
8 |
9 | ## 0.19 Compatibility
10 |
11 | As of 0.19, the Elm community is not hosting a central/default datepicker. It didn't seem like the kind of thing that would be in a standard library. Please use the [CurrySoftware](https://github.com/CurrySoftware/elm-datepicker) fork for 0.19+. Many thanks to them for taking it on!
12 |
13 |
14 | ## Usage
15 |
16 | The `DatePicker.init` function initialises the DatePicker. It returns the initialised DatePicker and associated `Cmds` so it must be done in your program's `init` or `update` functions:
17 |
18 | **Note** Make sure you don't throw away the initial `Cmd`!
19 |
20 | ```elm
21 |
22 | init : (Model, Cmd Msg)
23 | ...
24 | let
25 | ( datePicker, datePickerCmd ) =
26 | DatePicker.init
27 | in
28 | (
29 | { model | datePicker = datePicker },
30 | Cmd.map SetDatePicker datePickerCmd
31 | )
32 | ```
33 |
34 | The `DatePicker` can be displayed in a view using the `DatePicker.view` function. It returns its own
35 | message type so you should wrap it in one of your own messages using `Html.map`:
36 |
37 |
38 | ```elm
39 | type Msg
40 | = ...
41 | | SetDatePicker DatePicker.Msg
42 | | ...
43 |
44 |
45 | view : Model -> Html Msg
46 | view model =
47 | ...
48 | div [] [
49 | DatePicker.view
50 | model.date
51 | someSettings
52 | model.startDatePicker
53 | |> Html.map SetDatePicker
54 | ]
55 |
56 | ```
57 |
58 | To handle `Msg` in your update function, you should unwrap the `DatePicker.Msg` and pass it down to the `DatePicker.update` function. The `DatePicker.update` function returns:
59 |
60 | * the new model
61 | * any command
62 | * the new date as a `DateEvent (Maybe Date)`, where `DateEvent` is really just `Maybe` with different semantics, to avoid a potentially confusing `Maybe Maybe`.
63 |
64 | To create the settings to pass to `update`, DatePicker.defaultSettings` is provided to make it easier to use. You only have to override the settings that you are interested in.
65 |
66 | **Note** The datepicker does _not_ retain an internal idea of a picked date in its model. That is, it depends completely on you for an idea of what date is chosen, so that third tuple member is important! Evan Czaplicki has a compelling argument for why components should not necessarily have an their own state for the primary data they manage [here](https://github.com/evancz/elm-sortable-table#single-source-of-truth).
67 |
68 | ```elm
69 | someSettings : DatePicker.Settings
70 | someSettings =
71 | { defaultSettings
72 | | inputClassList = [ ( "form-control", True ) ]
73 | , inputId = Just "datepicker"
74 | }
75 |
76 | update : Msg -> Model -> ( Model, Cmd Msg )
77 | update msg model =
78 | case msg of
79 | ...
80 |
81 | SetDatePicker msg ->
82 | let
83 | ( newDatePicker, datePickerCmd, dateEvent ) =
84 | DatePicker.update someSettings msg model.startDatePicker
85 |
86 | date =
87 | case dateEvent of
88 | NoChange ->
89 | model.date
90 |
91 | Changed newDate ->
92 | newDate |> processDate
93 | in
94 | { model
95 | | date = date
96 | , datePicker = newDatePicker
97 | }
98 | ! [ Cmd.map SetDatePicker datePickerCmd ]
99 |
100 | ```
101 |
102 | ## Examples
103 |
104 | See the [examples][examples] folder or try it on ellie-app: [simple] example and [bootstrap] example.
105 |
106 | [examples]: https://github.com/elm-community/elm-datepicker/tree/master/examples
107 | [simple]: https://ellie-app.com/5QFsDgQVva1/0
108 | [bootstrap]: https://ellie-app.com/pwGJj5T6TBa1/0
109 |
110 |
111 | ## CSS
112 |
113 | The CSS for the date picker is distributed separately. You can grab
114 | the compiled CSS from [here][compiled] or you can grab the SCSS source
115 | from [here][scss].
116 |
117 | [compiled]: https://github.com/elm-community/elm-datepicker/blob/master/css/elm-datepicker.css
118 | [scss]: https://github.com/elm-community/elm-datepicker/blob/master/css/elm-datepicker.scss
119 |
120 |
121 | ## Running the acceptance tests
122 | ### Prerequisites
123 |
124 | - elm reactor - this is most likely already installed if you're using Elm!
125 | - chromedriver (https://sites.google.com/a/chromium.org/chromedriver/).
126 | Try `brew install chromedriver` if you're on OSX.
127 |
128 |
129 | ### Install the testing tools
130 | run `npm install`
131 |
132 | ### build the examples
133 | cd examples && make && cd ..
134 |
135 | ### Run the tests
136 | `./run-acceptance-tests`
137 |
138 | Please file an issue if you have any difficulty running the tests.
139 |
--------------------------------------------------------------------------------
/examples/range/Range.elm:
--------------------------------------------------------------------------------
1 | module Range exposing (main)
2 |
3 | import Date exposing (Date, Day(..), day, dayOfWeek, month, year)
4 | import DatePicker exposing (defaultSettings, DateEvent(..))
5 | import Html exposing (Html, div, h1, text)
6 |
7 |
8 | type Msg
9 | = ToStartDatePicker DatePicker.Msg
10 | | ToEndDatePicker DatePicker.Msg
11 |
12 |
13 | type alias Model =
14 | { startDate : Maybe Date
15 | , endDate : Maybe Date
16 | , startDatePicker : DatePicker.DatePicker
17 | , endDatePicker : DatePicker.DatePicker
18 | }
19 |
20 |
21 |
22 | -- Could be used to customize common settings for both date pickers. Like for
23 | -- example disabling weekends from them.
24 |
25 |
26 | commonSettings : DatePicker.Settings
27 | commonSettings =
28 | defaultSettings
29 |
30 |
31 |
32 | -- Extend commonSettings with isDisabled function which would disable dates
33 | -- after already selected end date because range start should come before end.
34 |
35 |
36 | startSettings : Maybe Date -> DatePicker.Settings
37 | startSettings endDate =
38 | let
39 | isDisabled =
40 | case endDate of
41 | Nothing ->
42 | commonSettings.isDisabled
43 |
44 | Just endDate ->
45 | \d ->
46 | Date.toTime d
47 | > Date.toTime endDate
48 | || (commonSettings.isDisabled d)
49 | in
50 | { commonSettings
51 | | placeholder = "Pick a start date"
52 | , isDisabled = isDisabled
53 | }
54 |
55 |
56 |
57 | -- Extend commonSettings with isDisabled function which would disable dates
58 | -- before already selected start date because range end should come after start.
59 |
60 |
61 | endSettings : Maybe Date -> DatePicker.Settings
62 | endSettings startDate =
63 | let
64 | isDisabled =
65 | case startDate of
66 | Nothing ->
67 | commonSettings.isDisabled
68 |
69 | Just startDate ->
70 | \d ->
71 | Date.toTime d
72 | < Date.toTime startDate
73 | || (commonSettings.isDisabled d)
74 | in
75 | { commonSettings
76 | | placeholder = "Pick an end date"
77 | , isDisabled = isDisabled
78 | }
79 |
80 |
81 | init : ( Model, Cmd Msg )
82 | init =
83 | let
84 | ( startDatePicker, startDatePickerFx ) =
85 | DatePicker.init
86 |
87 | ( endDatePicker, endDatePickerFx ) =
88 | DatePicker.init
89 | in
90 | { startDate = Nothing
91 | , startDatePicker = startDatePicker
92 | , endDate = Nothing
93 | , endDatePicker = endDatePicker
94 | }
95 | ! ([ Cmd.map ToStartDatePicker startDatePickerFx ]
96 | ++ [ Cmd.map
97 | ToEndDatePicker
98 | endDatePickerFx
99 | ]
100 | )
101 |
102 |
103 | update : Msg -> Model -> ( Model, Cmd Msg )
104 | update msg model =
105 | case msg of
106 | ToStartDatePicker msg ->
107 | let
108 | ( newDatePicker, datePickerFx, dateEvent ) =
109 | DatePicker.update (startSettings model.endDate) msg model.startDatePicker
110 |
111 | newDate =
112 | case dateEvent of
113 | Changed newDate ->
114 | newDate
115 |
116 | _ ->
117 | model.startDate
118 | in
119 | { model
120 | | startDate = newDate
121 | , startDatePicker = newDatePicker
122 | }
123 | ! [ Cmd.map ToStartDatePicker datePickerFx ]
124 |
125 | ToEndDatePicker msg ->
126 | let
127 | ( newDatePicker, datePickerFx, dateEvent ) =
128 | DatePicker.update (endSettings model.startDate) msg model.endDatePicker
129 |
130 | newDate =
131 | case dateEvent of
132 | Changed newDate ->
133 | newDate
134 |
135 | _ ->
136 | model.endDate
137 | in
138 | { model
139 | | endDate = newDate
140 | , endDatePicker = newDatePicker
141 | }
142 | ! [ Cmd.map ToEndDatePicker datePickerFx ]
143 |
144 |
145 | view : Model -> Html Msg
146 | view model =
147 | div []
148 | [ viewRange model.startDate model.endDate
149 | , DatePicker.view model.startDate (startSettings model.endDate) model.startDatePicker
150 | |> Html.map ToStartDatePicker
151 | , DatePicker.view model.endDate (endSettings model.startDate) model.endDatePicker
152 | |> Html.map ToEndDatePicker
153 | ]
154 |
155 |
156 | viewRange : Maybe Date -> Maybe Date -> Html Msg
157 | viewRange start end =
158 | case ( start, end ) of
159 | ( Nothing, Nothing ) ->
160 | h1 [] [ text "Pick dates" ]
161 |
162 | ( Just start, Nothing ) ->
163 | h1 [] [ text <| formatDate start ++ " – Pick end date" ]
164 |
165 | ( Nothing, Just end ) ->
166 | h1 [] [ text <| "Pick start date – " ++ formatDate end ]
167 |
168 | ( Just start, Just end ) ->
169 | h1 [] [ text <| formatDate start ++ " – " ++ formatDate end ]
170 |
171 |
172 | formatDate : Date -> String
173 | formatDate d =
174 | toString (month d) ++ " " ++ toString (day d) ++ ", " ++ toString (year d)
175 |
176 |
177 | main : Program Never Model Msg
178 | main =
179 | Html.program
180 | { init = init
181 | , update = update
182 | , view = view
183 | , subscriptions = always Sub.none
184 | }
185 |
--------------------------------------------------------------------------------
/src/DatePicker/Date.elm:
--------------------------------------------------------------------------------
1 | module DatePicker.Date
2 | exposing
3 | ( YearRange(..)
4 | , initDate
5 | , formatDate
6 | , formatDay
7 | , formatMonth
8 | , addDays
9 | , addDows
10 | , subDays
11 | , dateTuple
12 | , datesInRange
13 | , firstOfMonth
14 | , prevMonth
15 | , nextMonth
16 | , newYear
17 | , yearRange
18 | )
19 |
20 | import Date exposing (Date, Day(..), Month(..), year, month, day)
21 |
22 |
23 | type alias Year =
24 | Int
25 |
26 |
27 | type alias Day =
28 | Int
29 |
30 |
31 | type YearRange
32 | = Off
33 | | MoreOrLess Int
34 | | Between Year Year
35 | | From Year
36 | | To Year
37 |
38 |
39 | initDate : Date
40 | initDate =
41 | mkDate 1992 May 29
42 |
43 |
44 | formatDate : Date -> String
45 | formatDate date =
46 | toString (year date) ++ "/" ++ monthToString (month date) ++ "/" ++ dayToString (day date)
47 |
48 |
49 | formatDay : Date.Day -> String
50 | formatDay day =
51 | case day of
52 | Mon ->
53 | "Mo"
54 |
55 | Tue ->
56 | "Tu"
57 |
58 | Wed ->
59 | "We"
60 |
61 | Thu ->
62 | "Th"
63 |
64 | Fri ->
65 | "Fr"
66 |
67 | Sat ->
68 | "Sa"
69 |
70 | Sun ->
71 | "Su"
72 |
73 |
74 | formatMonth : Month -> String
75 | formatMonth month =
76 | case month of
77 | Jan ->
78 | "January"
79 |
80 | Feb ->
81 | "February"
82 |
83 | Mar ->
84 | "March"
85 |
86 | Apr ->
87 | "April"
88 |
89 | May ->
90 | "May"
91 |
92 | Jun ->
93 | "June"
94 |
95 | Jul ->
96 | "July"
97 |
98 | Aug ->
99 | "August"
100 |
101 | Sep ->
102 | "September"
103 |
104 | Oct ->
105 | "October"
106 |
107 | Nov ->
108 | "November"
109 |
110 | Dec ->
111 | "December"
112 |
113 |
114 | trimDates : Date.Day -> List Date -> List Date
115 | trimDates firstDay dates =
116 | let
117 | lastDay =
118 | predDow firstDay
119 |
120 | dl dates =
121 | case dates of
122 | [] ->
123 | []
124 |
125 | x :: xs ->
126 | if Date.dayOfWeek x == firstDay then
127 | dates
128 | else
129 | dl xs
130 |
131 | dr dates =
132 | case dates of
133 | [] ->
134 | []
135 |
136 | x :: xs ->
137 | if Date.dayOfWeek x == lastDay then
138 | dates
139 | else
140 | dr xs
141 | in
142 | dl dates
143 | |> List.reverse
144 | |> dr
145 | |> List.reverse
146 |
147 |
148 | datesInRange : Date.Day -> Date -> Date -> List Date
149 | datesInRange firstDay min max =
150 | let
151 | go x acc =
152 | let
153 | y =
154 | subDay x
155 | in
156 | if dateTuple y == dateTuple min then
157 | y :: acc
158 | else
159 | go y (y :: acc)
160 | in
161 | go max []
162 | |> trimDates firstDay
163 |
164 |
165 | dateTuple : Date -> ( Int, Int, Int )
166 | dateTuple date =
167 | ( year date, monthToInt <| month date, day date )
168 |
169 |
170 | repeat : (a -> a) -> Int -> a -> a
171 | repeat f =
172 | let
173 | go n x =
174 | if n == 0 then
175 | x
176 | else
177 | go (n - 1) (f x)
178 | in
179 | go
180 |
181 |
182 | firstOfMonth : Date -> Date
183 | firstOfMonth date =
184 | mkDate (year date) (month date) 1
185 |
186 |
187 | nextMonth : Date -> Date
188 | nextMonth date =
189 | let
190 | nextMonth =
191 | succMonth (month date)
192 |
193 | nextYear =
194 | if nextMonth == Jan then
195 | year date + 1
196 | else
197 | year date
198 | in
199 | mkDate nextYear nextMonth 1
200 |
201 |
202 | prevMonth : Date -> Date
203 | prevMonth date =
204 | let
205 | prevMonth =
206 | predMonth (month date)
207 |
208 | prevYear =
209 | if prevMonth == Dec then
210 | year date - 1
211 | else
212 | year date
213 | in
214 | mkDate prevYear prevMonth 1
215 |
216 |
217 | addDays : Int -> Date -> Date
218 | addDays =
219 | repeat addDay
220 |
221 |
222 | addDay : Date -> Date
223 | addDay date =
224 | let
225 | month =
226 | Date.month date
227 |
228 | year =
229 | Date.year date
230 |
231 | dim =
232 | daysInMonth year month
233 |
234 | day =
235 | Date.day date + 1
236 |
237 | succ =
238 | succMonth month
239 |
240 | succYear =
241 | if succ == Jan then
242 | year + 1
243 | else
244 | year
245 | in
246 | if day > dim then
247 | mkDate succYear succ 1
248 | else
249 | mkDate year month day
250 |
251 |
252 | subDays : Int -> Date -> Date
253 | subDays =
254 | repeat subDay
255 |
256 |
257 | subDay : Date -> Date
258 | subDay date =
259 | let
260 | month =
261 | Date.month date
262 |
263 | year =
264 | Date.year date
265 |
266 | day =
267 | Date.day date - 1
268 |
269 | pred =
270 | predMonth month
271 |
272 | predYear =
273 | if pred == Dec then
274 | year - 1
275 | else
276 | year
277 | in
278 | if day < 1 then
279 | mkDate predYear pred (daysInMonth predYear pred)
280 | else
281 | mkDate year month day
282 |
283 |
284 | addDows : Int -> Date.Day -> Date.Day
285 | addDows =
286 | repeat succDow
287 |
288 |
289 | succDow : Date.Day -> Date.Day
290 | succDow day =
291 | dayToInt day
292 | |> flip rem 7
293 | |> (+) 1
294 | |> dayFromInt
295 |
296 |
297 | subDows : Int -> Date.Day -> Date.Day
298 | subDows =
299 | repeat succDow
300 |
301 |
302 | predDow : Date.Day -> Date.Day
303 | predDow day =
304 | let
305 | prev =
306 | (dayToInt day - 1)
307 | |> flip rem 7
308 | in
309 | if prev == 0 then
310 | Sun
311 | else
312 | dayFromInt prev
313 |
314 |
315 | dayToString : Int -> String
316 | dayToString day =
317 | if day < 10 then
318 | "0" ++ toString day
319 | else
320 | toString day
321 |
322 |
323 | dayToInt : Date.Day -> Int
324 | dayToInt day =
325 | case day of
326 | Mon ->
327 | 1
328 |
329 | Tue ->
330 | 2
331 |
332 | Wed ->
333 | 3
334 |
335 | Thu ->
336 | 4
337 |
338 | Fri ->
339 | 5
340 |
341 | Sat ->
342 | 6
343 |
344 | Sun ->
345 | 7
346 |
347 |
348 | dayFromInt : Int -> Date.Day
349 | dayFromInt day =
350 | case day of
351 | 1 ->
352 | Mon
353 |
354 | 2 ->
355 | Tue
356 |
357 | 3 ->
358 | Wed
359 |
360 | 4 ->
361 | Thu
362 |
363 | 5 ->
364 | Fri
365 |
366 | 6 ->
367 | Sat
368 |
369 | 7 ->
370 | Sun
371 |
372 | _ ->
373 | Debug.crash ("dayFromInt: invalid day: " ++ toString day)
374 |
375 |
376 | monthToString : Month -> String
377 | monthToString month =
378 | let
379 | int =
380 | monthToInt month
381 | in
382 | if int < 10 then
383 | "0" ++ toString int
384 | else
385 | toString int
386 |
387 |
388 | predMonth : Month -> Month
389 | predMonth month =
390 | let
391 | prev =
392 | (monthToInt month - 1)
393 | |> flip rem 12
394 | in
395 | if prev == 0 then
396 | Dec
397 | else
398 | monthFromInt prev
399 |
400 |
401 | succMonth : Month -> Month
402 | succMonth month =
403 | monthToInt month
404 | |> flip rem 12
405 | |> (+) 1
406 | |> monthFromInt
407 |
408 |
409 | monthToInt : Month -> Int
410 | monthToInt month =
411 | case month of
412 | Jan ->
413 | 1
414 |
415 | Feb ->
416 | 2
417 |
418 | Mar ->
419 | 3
420 |
421 | Apr ->
422 | 4
423 |
424 | May ->
425 | 5
426 |
427 | Jun ->
428 | 6
429 |
430 | Jul ->
431 | 7
432 |
433 | Aug ->
434 | 8
435 |
436 | Sep ->
437 | 9
438 |
439 | Oct ->
440 | 10
441 |
442 | Nov ->
443 | 11
444 |
445 | Dec ->
446 | 12
447 |
448 |
449 | monthFromInt : Int -> Month
450 | monthFromInt month =
451 | case month of
452 | 1 ->
453 | Jan
454 |
455 | 2 ->
456 | Feb
457 |
458 | 3 ->
459 | Mar
460 |
461 | 4 ->
462 | Apr
463 |
464 | 5 ->
465 | May
466 |
467 | 6 ->
468 | Jun
469 |
470 | 7 ->
471 | Jul
472 |
473 | 8 ->
474 | Aug
475 |
476 | 9 ->
477 | Sep
478 |
479 | 10 ->
480 | Oct
481 |
482 | 11 ->
483 | Nov
484 |
485 | 12 ->
486 | Dec
487 |
488 | x ->
489 | Debug.crash ("monthFromInt: invalid month: " ++ toString x)
490 |
491 |
492 | daysInMonth : Year -> Month -> Int
493 | daysInMonth year month =
494 | case month of
495 | Jan ->
496 | 31
497 |
498 | Feb ->
499 | if isLeapYear year then
500 | 29
501 | else
502 | 28
503 |
504 | Mar ->
505 | 31
506 |
507 | Apr ->
508 | 30
509 |
510 | May ->
511 | 31
512 |
513 | Jun ->
514 | 30
515 |
516 | Jul ->
517 | 31
518 |
519 | Aug ->
520 | 31
521 |
522 | Sep ->
523 | 30
524 |
525 | Oct ->
526 | 31
527 |
528 | Nov ->
529 | 30
530 |
531 | Dec ->
532 | 31
533 |
534 |
535 | isLeapYear : Year -> Bool
536 | isLeapYear y =
537 | y % 400 == 0 || y % 100 /= 0 && y % 4 == 0
538 |
539 |
540 | mkDate : Year -> Month -> Day -> Date
541 | mkDate year month day =
542 | toString year
543 | ++ "/"
544 | ++ monthToString month
545 | ++ "/"
546 | ++ dayToString day
547 | |> unsafeDate
548 |
549 |
550 | unsafeDate : String -> Date
551 | unsafeDate date =
552 | case Date.fromString date of
553 | Err e ->
554 | Debug.crash ("unsafeDate: failed to parse date:" ++ e)
555 |
556 | Ok date ->
557 | date
558 |
559 |
560 | newYear : Date -> String -> Date
561 | newYear currentMonth newYear =
562 | case String.toInt newYear of
563 | Ok year ->
564 | mkDate year (month currentMonth) (day currentMonth)
565 |
566 | Err _ ->
567 | Debug.crash ("Unknown Month " ++ (toString currentMonth))
568 |
569 |
570 | yearRange : { currentMonth : Date, today : Date } -> YearRange -> List Int
571 | yearRange { currentMonth, today } range =
572 | case range of
573 | MoreOrLess num ->
574 | List.range ((year currentMonth) - num) ((year currentMonth) + num)
575 |
576 | Between start end ->
577 | List.range start end
578 |
579 | From year_ ->
580 | List.range year_ (year today)
581 |
582 | To year_ ->
583 | List.range (year today) year_
584 |
585 | Off ->
586 | []
587 |
--------------------------------------------------------------------------------
/src/DatePicker.elm:
--------------------------------------------------------------------------------
1 | module DatePicker
2 | exposing
3 | ( Msg
4 | , Settings
5 | , DateEvent(..)
6 | , DatePicker
7 | , defaultSettings
8 | , init
9 | , initFromDate
10 | , initFromDates
11 | , update
12 | , view
13 | , pick
14 | , isOpen
15 | , between
16 | , moreOrLess
17 | , off
18 | , from
19 | , to
20 | , focusedDate
21 | )
22 |
23 | {-| A customizable date picker component.
24 |
25 |
26 | # Tea ☕
27 |
28 | @docs Msg, DateEvent, DatePicker
29 | @docs init, initFromDate, initFromDates, update, view, isOpen, focusedDate
30 |
31 |
32 | # Settings
33 |
34 | @docs Settings, defaultSettings, pick, between, moreOrLess, from, to, off
35 |
36 | -}
37 |
38 | import Date exposing (Date, Day(..), Month, day, month, year)
39 | import DatePicker.Date exposing (..)
40 | import Html exposing (..)
41 | import Html.Attributes as Attrs exposing (href, placeholder, tabindex, type_, value, selected)
42 | import Html.Events exposing (on, onBlur, onClick, onInput, onFocus, onWithOptions, targetValue)
43 | import Html.Keyed
44 | import Json.Decode as Json
45 | import Task
46 |
47 |
48 | {-| An opaque type representing messages that are passed inside the DatePicker.
49 | -}
50 | type Msg
51 | = CurrentDate Date
52 | | ChangeFocus Date
53 | | Pick (Maybe Date)
54 | | Text String
55 | | SubmitText
56 | | Focus
57 | | Blur
58 | | MouseDown
59 | | MouseUp
60 |
61 |
62 | {-| The type of date picker settings.
63 | -}
64 | type alias Settings =
65 | { placeholder : String
66 | , classNamespace : String
67 | , containerClassList : List ( String, Bool )
68 | , inputClassList : List ( String, Bool )
69 | , inputName : Maybe String
70 | , inputId : Maybe String
71 | , inputAttributes : List (Html.Attribute Msg)
72 | , isDisabled : Date -> Bool
73 | , parser : String -> Result String Date
74 | , dateFormatter : Date -> String
75 | , dayFormatter : Day -> String
76 | , monthFormatter : Month -> String
77 | , yearFormatter : Int -> String
78 | , cellFormatter : String -> Html Msg
79 | , firstDayOfWeek : Day
80 | , changeYear : YearRange
81 | }
82 |
83 |
84 | type alias Model =
85 | { open : Bool
86 | , forceOpen : Bool
87 | , focused :
88 | Maybe Date
89 | -- date currently center-focused by picker, but not necessarily chosen
90 | , inputText :
91 | Maybe String
92 | , today :
93 | Date
94 | -- actual, current day as far as we know
95 | }
96 |
97 |
98 | {-| The DatePicker model. Opaque, hence no field docs.
99 | -}
100 | type DatePicker
101 | = DatePicker Model
102 |
103 |
104 | {-| A record of default settings for the date picker. Extend this if
105 | you want to customize your date picker.
106 |
107 |
108 | import DatePicker exposing (defaultSettings)
109 |
110 | DatePicker.init { defaultSettings | placeholder = "Pick a date" }
111 |
112 |
113 | To disable certain dates:
114 |
115 |
116 | import Date exposing (Day(..), dayOfWeek)
117 | import DatePicker exposing (defaultSettings)
118 |
119 | DatePicker.init { defaultSettings | isDisabled = \d -> dayOfWeek d `List.member` [ Sat, Sun ] }
120 |
121 | -}
122 | defaultSettings : Settings
123 | defaultSettings =
124 | { placeholder = "Please pick a date..."
125 | , classNamespace = "elm-datepicker--"
126 | , containerClassList = []
127 | , inputClassList = []
128 | , inputName = Nothing
129 | , inputId = Nothing
130 | , inputAttributes =
131 | [ Attrs.required False
132 | ]
133 | , isDisabled = always False
134 | , parser = Date.fromString
135 | , dateFormatter = formatDate
136 | , dayFormatter = formatDay
137 | , monthFormatter = formatMonth
138 | , yearFormatter = toString
139 | , cellFormatter = formatCell
140 | , firstDayOfWeek = Sun
141 | , changeYear = off
142 | }
143 |
144 |
145 | yearRangeActive : YearRange -> Bool
146 | yearRangeActive yearRange =
147 | yearRange /= Off
148 |
149 |
150 | {-| Select a range of date to display
151 |
152 |
153 | DatePicker.init { defaultSettings | changeYear = between 1555 2018 }
154 |
155 | -}
156 | between : Int -> Int -> YearRange
157 | between start end =
158 | if start > end then
159 | Between end start
160 | else
161 | Between start end
162 |
163 |
164 | {-| Select a symmetric range of date to display
165 |
166 |
167 | DatePicker.init { defaultSettings | changeYear = moreOrLess 10 }
168 |
169 | -}
170 | moreOrLess : Int -> YearRange
171 | moreOrLess range =
172 | MoreOrLess range
173 |
174 |
175 | {-| Select a range from a given year to this year
176 |
177 |
178 | DatePicker.init { defaultSettings | changeYear = from 1995 }
179 |
180 | -}
181 | from : Int -> YearRange
182 | from year =
183 | From year
184 |
185 |
186 | {-| Select a range from this year to a given year
187 |
188 |
189 | DatePicker.init { defaultSettings | changeYear = to 2020 }
190 |
191 | -}
192 | to : Int -> YearRange
193 | to year =
194 | To year
195 |
196 |
197 | {-| Turn off the date range
198 |
199 |
200 | DatePicker.init { defaultSettings | changeYear = off }
201 |
202 | -}
203 | off : YearRange
204 | off =
205 | Off
206 |
207 |
208 | formatCell : String -> Html Msg
209 | formatCell day =
210 | text day
211 |
212 |
213 | {-| The default initial state of the Datepicker. You must execute
214 | the returned command (which, for the curious, sets the current date)
215 | for the date picker to behave correctly.
216 |
217 | init =
218 | let
219 | (datePicker, datePickerFx) =
220 | DatePicker.init
221 | in
222 | { picker = datePicker } ! [ Cmd.map ToDatePicker datePickerfx ]
223 |
224 | -}
225 | init : ( DatePicker, Cmd Msg )
226 | init =
227 | ( DatePicker <|
228 | { open = False
229 | , forceOpen = False
230 | , focused = Just initDate
231 | , inputText = Nothing
232 | , today = initDate
233 | }
234 | , Task.perform CurrentDate Date.now
235 | )
236 |
237 |
238 | {-| Initialize a DatePicker with a given Date
239 |
240 | init date =
241 | { picker = DatePicker.initFromDate date } ! [ ]
242 |
243 | -}
244 | initFromDate : Date -> DatePicker
245 | initFromDate date =
246 | DatePicker <|
247 | { open = False
248 | , forceOpen = False
249 | , focused = Just date
250 | , inputText = Nothing
251 | , today = date
252 | }
253 |
254 |
255 | {-| Initialize a DatePicker with a date for today and Maybe a date picked
256 |
257 | init today date =
258 | { picker = DatePicker.initFromDates today date } ! []
259 |
260 | -}
261 | initFromDates : Date -> Maybe Date -> DatePicker
262 | initFromDates today date =
263 | DatePicker <|
264 | { open = False
265 | , forceOpen = False
266 | , focused = date
267 | , inputText = Nothing
268 | , today = today
269 | }
270 |
271 |
272 | prepareDates : Date -> Day -> { currentMonth : Date, currentDates : List Date }
273 | prepareDates date firstDayOfWeek =
274 | let
275 | start =
276 | firstOfMonth date |> subDays 6
277 |
278 | end =
279 | nextMonth date |> addDays 6
280 | in
281 | { currentMonth = date
282 | , currentDates = datesInRange firstDayOfWeek start end
283 | }
284 |
285 |
286 | {-|
287 | Expose if the datepicker is open
288 | -}
289 | isOpen : DatePicker -> Bool
290 | isOpen (DatePicker model) =
291 | model.open
292 |
293 |
294 | {-| Expose the currently focused date
295 | -}
296 | focusedDate : DatePicker -> Maybe Date
297 | focusedDate (DatePicker model) =
298 | model.focused
299 |
300 |
301 | {-| A sugaring of `Maybe` to explicitly tell you how to interpret `Changed Nothing`, because `Just Nothing` seems somehow wrong.
302 | Used to represent a request, by the datepicker, to change the selected date.
303 | -}
304 | type DateEvent
305 | = NoChange
306 | | Changed (Maybe Date)
307 |
308 |
309 | {-| The date picker update function. The third tuple member represents a user action to change the
310 | date.
311 | -}
312 | update : Settings -> Msg -> DatePicker -> ( DatePicker, Cmd Msg, DateEvent )
313 | update settings msg (DatePicker ({ forceOpen, focused } as model)) =
314 | case msg of
315 | CurrentDate date ->
316 | { model | focused = Just date, today = date } ! []
317 |
318 | ChangeFocus date ->
319 | { model | focused = Just date } ! []
320 |
321 | Pick date ->
322 | ( DatePicker <|
323 | { model
324 | | open = False
325 | , inputText = Nothing
326 | , focused = Nothing
327 | }
328 | , Cmd.none
329 | , Changed date
330 | )
331 |
332 | Text text ->
333 | { model | inputText = Just text } ! []
334 |
335 | SubmitText ->
336 | let
337 | isWhitespace =
338 | String.trim >> String.isEmpty
339 |
340 | dateEvent =
341 | let
342 | text =
343 | model.inputText ?> ""
344 | in
345 | if isWhitespace text then
346 | Changed Nothing
347 | else
348 | text
349 | |> settings.parser
350 | |> Result.map
351 | (Changed
352 | << (\date ->
353 | if settings.isDisabled date then
354 | Nothing
355 | else
356 | Just date
357 | )
358 | )
359 | |> Result.withDefault NoChange
360 | in
361 | ( DatePicker <|
362 | { model
363 | | inputText =
364 | case dateEvent of
365 | Changed change ->
366 | Nothing
367 |
368 | NoChange ->
369 | model.inputText
370 | , focused =
371 | case dateEvent of
372 | Changed change ->
373 | case change of
374 | Just date ->
375 | Just date
376 |
377 | Nothing ->
378 | Nothing
379 |
380 | NoChange ->
381 | model.focused
382 | }
383 | , Cmd.none
384 | , dateEvent
385 | )
386 |
387 | Focus ->
388 | { model | open = True, forceOpen = False } ! []
389 |
390 | Blur ->
391 | { model | open = forceOpen } ! []
392 |
393 | MouseDown ->
394 | { model | forceOpen = True } ! []
395 |
396 | MouseUp ->
397 | { model | forceOpen = False } ! []
398 |
399 |
400 | {-| Generate a message that will act as if the user has chosen a certain date,
401 | so you can call `update` on the model yourself.
402 | Note that this is different from just changing the "current chosen" date,
403 | since the picker doesn't actually have internal state for that.
404 | Rather, it will:
405 | * change the calendar focus
406 | * replace the input text with the new value
407 | * close the picker
408 |
409 | update datepickerSettings (pick (Just someDate)) datepicker
410 | -}
411 | pick : Maybe Date -> Msg
412 | pick =
413 | Pick
414 |
415 |
416 | {-| The date picker view. The Date passed is whatever date it should treat as selected.
417 | -}
418 | view : Maybe Date -> Settings -> DatePicker -> Html Msg
419 | view pickedDate settings (DatePicker ({ open } as model)) =
420 | let
421 | class =
422 | mkClass settings
423 |
424 | potentialInputId =
425 | settings.inputId
426 | |> Maybe.map Attrs.id
427 | |> (List.singleton >> List.filterMap identity)
428 |
429 | inputClasses =
430 | [ ( settings.classNamespace ++ "input", True ) ]
431 | ++ settings.inputClassList
432 |
433 | inputCommon xs =
434 | input
435 | ([ Attrs.classList inputClasses
436 | , Attrs.name (settings.inputName ?> "")
437 | , type_ "text"
438 | , on "change" (Json.succeed SubmitText)
439 | , onInput Text
440 | , onBlur Blur
441 | , onClick Focus
442 | , onFocus Focus
443 | ]
444 | ++ settings.inputAttributes
445 | ++ potentialInputId
446 | ++ xs
447 | )
448 | []
449 |
450 | dateInput =
451 | inputCommon
452 | [ placeholder settings.placeholder
453 | , model.inputText
454 | |> Maybe.withDefault
455 | (Maybe.map settings.dateFormatter pickedDate
456 | |> Maybe.withDefault ""
457 | )
458 | |> value
459 | ]
460 |
461 | containerClassList =
462 | ( "container", True ) :: settings.containerClassList
463 | in
464 | div
465 | [ Attrs.classList containerClassList ]
466 | [ dateInput
467 | , if open then
468 | datePicker pickedDate settings model
469 | else
470 | text ""
471 | ]
472 |
473 |
474 | datePicker : Maybe Date -> Settings -> Model -> Html Msg
475 | datePicker pickedDate settings ({ focused, today } as model) =
476 | let
477 | currentDate =
478 | focused ??> pickedDate ?> today
479 |
480 | { currentMonth, currentDates } =
481 | prepareDates currentDate settings.firstDayOfWeek
482 |
483 | class =
484 | mkClass settings
485 |
486 | classList =
487 | mkClassList settings
488 |
489 | firstDay =
490 | settings.firstDayOfWeek
491 |
492 | arrow className message =
493 | a
494 | [ class className
495 | , href "javascript:;"
496 | , onClick message
497 | , tabindex -1
498 | ]
499 | []
500 |
501 | dow d =
502 | td [ class "dow" ] [ text <| settings.dayFormatter d ]
503 |
504 | picked d =
505 | pickedDate
506 | |> Maybe.map
507 | (dateTuple >> (==) (dateTuple d))
508 | |> Maybe.withDefault False
509 |
510 | day d =
511 | let
512 | disabled =
513 | settings.isDisabled d
514 |
515 | props =
516 | if not disabled then
517 | [ onClick (Pick (Just d)) ]
518 | else
519 | []
520 | in
521 | td
522 | ([ classList
523 | [ ( "day", True )
524 | , ( "disabled", disabled )
525 | , ( "picked", picked d )
526 | , ( "today", dateTuple d == dateTuple currentDate )
527 | , ( "other-month", month currentMonth /= month d )
528 | ]
529 | ]
530 | ++ props
531 | )
532 | [ settings.cellFormatter <| toString <| Date.day d ]
533 |
534 | row days =
535 | tr [ class "row" ] (List.map day days)
536 |
537 | days =
538 | List.map row (groupDates currentDates)
539 |
540 | onPicker ev =
541 | Json.succeed
542 | >> onWithOptions ev
543 | { preventDefault = False
544 | , stopPropagation = True
545 | }
546 |
547 | onChange handler =
548 | on "change" <| Json.map handler targetValue
549 |
550 | isCurrentYear selectedYear =
551 | year currentMonth == selectedYear
552 |
553 | yearOption index selectedYear =
554 | ( toString index
555 | , option [ value (toString selectedYear), selected (isCurrentYear selectedYear) ]
556 | [ text <| toString selectedYear ]
557 | )
558 |
559 | dropdownYear =
560 | Html.Keyed.node "select"
561 | [ onChange (newYear currentDate >> ChangeFocus), class "year-menu" ]
562 | (List.indexedMap yearOption
563 | (yearRange { currentMonth = currentMonth, today = today } settings.changeYear)
564 | )
565 | in
566 | div
567 | [ class "picker"
568 | , onPicker "mousedown" MouseDown
569 | , onPicker "mouseup" MouseUp
570 | ]
571 | [ div [ class "picker-header" ]
572 | [ div [ class "prev-container" ]
573 | [ arrow "prev" (ChangeFocus (prevMonth currentDate)) ]
574 | , div [ class "month-container" ]
575 | [ span [ class "month" ]
576 | [ text <| settings.monthFormatter <| month currentMonth ]
577 | , span [ class "year" ]
578 | [ if not (yearRangeActive settings.changeYear) then
579 | text <| settings.yearFormatter <| year currentMonth
580 | else
581 | Html.Keyed.node "span" [] [ ( toString (year currentMonth), dropdownYear ) ]
582 | ]
583 | ]
584 | , div [ class "next-container" ]
585 | [ arrow "next" (ChangeFocus (nextMonth currentDate)) ]
586 | ]
587 | , table [ class "table" ]
588 | [ thead [ class "weekdays" ]
589 | [ tr []
590 | [ dow <| firstDay
591 | , dow <| addDows 1 firstDay
592 | , dow <| addDows 2 firstDay
593 | , dow <| addDows 3 firstDay
594 | , dow <| addDows 4 firstDay
595 | , dow <| addDows 5 firstDay
596 | , dow <| addDows 6 firstDay
597 | ]
598 | ]
599 | , tbody [ class "days" ] days
600 | ]
601 | ]
602 |
603 |
604 | {-| Turn a list of dates into a list of date rows with 7 columns per
605 | row representing each day of the week.
606 | -}
607 | groupDates : List Date -> List (List Date)
608 | groupDates dates =
609 | let
610 | go i xs racc acc =
611 | case xs of
612 | [] ->
613 | List.reverse acc
614 |
615 | x :: xs ->
616 | if i == 6 then
617 | go 0 xs [] (List.reverse (x :: racc) :: acc)
618 | else
619 | go (i + 1) xs (x :: racc) acc
620 | in
621 | go 0 dates [] []
622 |
623 |
624 | mkClass : Settings -> String -> Html.Attribute msg
625 | mkClass { classNamespace } c =
626 | Attrs.class (classNamespace ++ c)
627 |
628 |
629 | mkClassList : Settings -> List ( String, Bool ) -> Html.Attribute msg
630 | mkClassList { classNamespace } cs =
631 | List.map (\( c, b ) -> ( classNamespace ++ c, b )) cs
632 | |> Attrs.classList
633 |
634 |
635 | (!) : Model -> List (Cmd Msg) -> ( DatePicker, Cmd Msg, DateEvent )
636 | (!) m cs =
637 | ( DatePicker m, Cmd.batch cs, NoChange )
638 |
639 |
640 | (?>) : Maybe a -> a -> a
641 | (?>) =
642 | flip Maybe.withDefault
643 |
644 |
645 | (??>) : Maybe a -> Maybe a -> Maybe a
646 | (??>) first default =
647 | case first of
648 | Just val ->
649 | Just val
650 |
651 | Nothing ->
652 | default
653 |
--------------------------------------------------------------------------------