From 53986f870acd32cbfee08935e5418cf0087fb430 Mon Sep 17 00:00:00 2001 From: Ann Korepanova Date: Sun, 29 Oct 2017 19:45:17 +0000 Subject: [PATCH 01/10] Range datepicker without memory --- css/elm-datepicker.css | 3 + examples/Makefile | 5 +- examples/range2/.gitignore | 1 + examples/range2/Range2.elm | 99 +++++++++ examples/range2/index.html | 41 ++++ index.html | 41 ++++ src/DatePicker.elm | 398 +++++++++++++++++++++++-------------- 7 files changed, 443 insertions(+), 145 deletions(-) create mode 100644 examples/range2/.gitignore create mode 100644 examples/range2/Range2.elm create mode 100644 examples/range2/index.html create mode 100644 index.html diff --git a/css/elm-datepicker.css b/css/elm-datepicker.css index cc80066..dcb0595 100644 --- a/css/elm-datepicker.css +++ b/css/elm-datepicker.css @@ -76,6 +76,9 @@ .elm-datepicker--day:hover { background: #F2F2F2; } +.elm-datepicker--range { + background: #F2F2F2; } + .elm-datepicker--disabled { cursor: default; color: #DDD; } diff --git a/examples/Makefile b/examples/Makefile index e05cca7..1017693 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -1,4 +1,4 @@ -all: simple-example bootstrap-example range-example +all: simple-example bootstrap-example range-example range2-example simple-example: cd .. && elm make --warn examples/simple/Simple.elm --output=examples/simple/simple.js @@ -8,3 +8,6 @@ bootstrap-example: range-example: cd .. && elm make --warn examples/range/Range.elm --output=examples/range/range.js + +range2-example: + cd .. && elm make --warn examples/range2/Range2.elm --output=examples/range2/range2.js diff --git a/examples/range2/.gitignore b/examples/range2/.gitignore new file mode 100644 index 0000000..14ade9f --- /dev/null +++ b/examples/range2/.gitignore @@ -0,0 +1 @@ +range2.js \ No newline at end of file diff --git a/examples/range2/Range2.elm b/examples/range2/Range2.elm new file mode 100644 index 0000000..eff0fb0 --- /dev/null +++ b/examples/range2/Range2.elm @@ -0,0 +1,99 @@ +module Range2 exposing (main) + +import Date exposing (Date, Day(..), day, dayOfWeek, month, year) +import DatePicker exposing (DateEvent(..), defaultSettings) +import Html exposing (Html, div, h1, text) + + +type Msg + = ToDatePicker DatePicker.Msg + + +type alias Model = + { startDate : Maybe Date + , finishDate : Maybe Date + , datePicker : DatePicker.DatePicker + } + + +settings : DatePicker.Settings +settings = + { defaultSettings | isRange = True } + + +init : ( Model, Cmd Msg ) +init = + let + ( datePicker, datePickerFx ) = + DatePicker.init + in + { startDate = Nothing + , finishDate = Nothing + , datePicker = datePicker + } + ! [ Cmd.map ToDatePicker datePickerFx ] + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg ({ startDate, finishDate, datePicker } as model) = + case msg of + ToDatePicker msg -> + let + ( newDatePicker, datePickerFx, dateEvent ) = + DatePicker.update settings msg datePicker + in + case dateEvent of + StartChanged newDate -> + { model + | startDate = newDate + , datePicker = newDatePicker + } + ! [ Cmd.map ToDatePicker datePickerFx ] + + FinishChanged newDate -> + { model + | finishDate = newDate + , datePicker = newDatePicker + } + ! [ Cmd.map ToDatePicker datePickerFx ] + + _ -> + { model + | datePicker = newDatePicker + } + ! [ Cmd.map ToDatePicker datePickerFx ] + + +view : Model -> Html Msg +view ({ startDate, finishDate, datePicker } as model) = + div [] + [ case startDate of + Nothing -> + h1 [] [ text "Pick a date" ] + + Just date -> + h1 [] [ text <| formatDate date ] + , case finishDate of + Nothing -> + h1 [] [ text "Pick a date" ] + + Just date -> + h1 [] [ text <| formatDate date ] + , DatePicker.view startDate settings datePicker + |> Html.map ToDatePicker + ] + + +formatDate : Date -> String +formatDate d = + toString (month d) ++ " " ++ toString (day d) ++ ", " ++ toString (year d) + + +main : Program Never Model Msg +main = + Html.program + { init = init + , update = update + , view = view + , subscriptions = always Sub.none + } diff --git a/examples/range2/index.html b/examples/range2/index.html new file mode 100644 index 0000000..3f5df18 --- /dev/null +++ b/examples/range2/index.html @@ -0,0 +1,41 @@ + + + + + elm-datepicker example + + + + + + + + + + +
+
+
+
+ + + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..cb9979e --- /dev/null +++ b/index.html @@ -0,0 +1,41 @@ + + + + + elm-datepicker example + + + + + + + + + + +
+
+
+
+ + + + + diff --git a/src/DatePicker.elm b/src/DatePicker.elm index abcb5fc..b91f8b1 100644 --- a/src/DatePicker.elm +++ b/src/DatePicker.elm @@ -1,23 +1,23 @@ module DatePicker exposing - ( Msg - , Settings - , DateEvent(..) + ( DateEvent(..) , DatePicker + , Msg + , Settings + , between , defaultSettings + , focusedDate + , from , init , initFromDate , initFromDates - , update - , view - , pick , isOpen - , between , moreOrLess , off - , from + , pick , to - , focusedDate + , update + , view ) {-| A customizable date picker component. @@ -38,8 +38,8 @@ module DatePicker import Date exposing (Date, Day(..), Month, day, month, year) import DatePicker.Date exposing (..) import Html exposing (..) -import Html.Attributes as Attrs exposing (href, placeholder, tabindex, type_, value, defaultValue, selected) -import Html.Events exposing (on, onBlur, onClick, onInput, onFocus, onWithOptions, targetValue) +import Html.Attributes as Attrs exposing (defaultValue, href, placeholder, selected, tabindex, type_, value) +import Html.Events exposing (on, onBlur, onClick, onFocus, onInput, onMouseOver, onWithOptions, targetValue) import Html.Keyed import Json.Decode as Json import Task @@ -57,6 +57,7 @@ type Msg | Blur | MouseDown | MouseUp + | Over (Maybe Date) {-| The type of date picker settings. @@ -77,6 +78,7 @@ type alias Settings = , cellFormatter : String -> Html Msg , firstDayOfWeek : Day , changeYear : YearRange + , isRange : Bool } @@ -85,13 +87,27 @@ type alias Model = , forceOpen : Bool , focused : Maybe Date - -- date currently center-focused by picker, but not necessarily chosen + + -- date currently center-focused by picker, but not necessarily chosen , inputText : Maybe String - -- for user input that hasn't yet been submitted + + -- for user input that hasn't yet been submitted , today : Date - -- actual, current day as far as we know + + -- actual, current day as far as we know + , rangeCondition : + RangeCondition + + -- describes picked dates condition + } + + +type alias RangeCondition = + { isStartPicked : Bool + , isFinishPicked : Bool + , hoverDate : Maybe Date } @@ -101,18 +117,15 @@ type DatePicker = DatePicker Model -{-| A record of default settings for the date picker. Extend this if +{-| A record of default settings for the date picker. Extend this if you want to customize your date picker. - import DatePicker exposing (defaultSettings) DatePicker.init { defaultSettings | placeholder = "Pick a date" } - To disable certain dates: - import Date exposing (Day(..), dayOfWeek) import DatePicker exposing (defaultSettings) @@ -138,6 +151,7 @@ defaultSettings = , cellFormatter = formatCell , firstDayOfWeek = Sun , changeYear = off + , isRange = False } @@ -148,7 +162,6 @@ yearRangeActive yearRange = {-| Select a range of date to display - DatePicker.init { defaultSettings | changeYear = between 1555 2018 } -} @@ -162,7 +175,6 @@ between start end = {-| Select a symmetric range of date to display - DatePicker.init { defaultSettings | changeYear = moreOrLess 10 } -} @@ -173,7 +185,6 @@ moreOrLess range = {-| Select a range from a given year to this year - DatePicker.init { defaultSettings | changeYear = from 1995 } -} @@ -184,7 +195,6 @@ from year = {-| Select a range from this year to a given year - DatePicker.init { defaultSettings | changeYear = to 2020 } -} @@ -195,7 +205,6 @@ to year = {-| Turn off the date range - DatePicker.init { defaultSettings | changeYear = off } -} @@ -209,16 +218,24 @@ formatCell day = text day -{-| The default initial state of the Datepicker. You must execute +initRangeCondition : RangeCondition +initRangeCondition = + { isStartPicked = False + , isFinishPicked = False + , hoverDate = Nothing + } + + +{-| The default initial state of the Datepicker. You must execute the returned command (which, for the curious, sets the current date) for the date picker to behave correctly. init = - let - (datePicker, datePickerFx) = - DatePicker.init - in - { picker = datePicker } ! [ Cmd.map ToDatePicker datePickerfx ] + let + ( datePicker, datePickerFx ) = + DatePicker.init + in + { picker = datePicker } ! [ Cmd.map ToDatePicker datePickerfx ] -} init : ( DatePicker, Cmd Msg ) @@ -229,6 +246,7 @@ init = , focused = Just initDate , inputText = Nothing , today = initDate + , rangeCondition = initRangeCondition } , Task.perform CurrentDate Date.now ) @@ -237,7 +255,7 @@ init = {-| Initialize a DatePicker with a given Date init date = - { picker = DatePicker.initFromDate date } ! [ ] + { picker = DatePicker.initFromDate date } ! [] -} initFromDate : Date -> DatePicker @@ -248,13 +266,14 @@ initFromDate date = , focused = Just date , inputText = Nothing , today = date + , rangeCondition = initRangeCondition } {-| Initialize a DatePicker with a date for today and Maybe a date picked init today date = - { picker = DatePicker.initFromDates today date } ! [] + { picker = DatePicker.initFromDates today date } ! [] -} initFromDates : Date -> Maybe Date -> DatePicker @@ -265,6 +284,7 @@ initFromDates today date = , focused = date , inputText = Nothing , today = today + , rangeCondition = initRangeCondition } @@ -277,13 +297,12 @@ prepareDates date firstDayOfWeek = end = nextMonth date |> addDays 6 in - { currentMonth = date - , currentDates = datesInRange firstDayOfWeek start end - } + { currentMonth = date + , currentDates = datesInRange firstDayOfWeek start end + } -{-| -Expose if the datepicker is open +{-| Expose if the datepicker is open -} isOpen : DatePicker -> Bool isOpen (DatePicker model) = @@ -298,18 +317,20 @@ focusedDate (DatePicker model) = {-| A sugaring of `Maybe` to explicitly tell you how to interpret `Changed Nothing`, because `Just Nothing` seems somehow wrong. - Used to represent a request, by the datepicker, to change the selected date. +Used to represent a request, by the datepicker, to change the selected date. -} type DateEvent = NoChange | Changed (Maybe Date) + | StartChanged (Maybe Date) + | FinishChanged (Maybe Date) {-| The date picker update function. The third tuple member represents a user action to change the - date. +date. -} update : Settings -> Msg -> DatePicker -> ( DatePicker, Cmd Msg, DateEvent ) -update settings msg (DatePicker ({ forceOpen, focused } as model)) = +update settings msg (DatePicker ({ forceOpen, focused, rangeCondition } as model)) = case msg of CurrentDate date -> { model | focused = Just date, today = date } ! [] @@ -317,16 +338,79 @@ update settings msg (DatePicker ({ forceOpen, focused } as model)) = ChangeFocus date -> { model | focused = Just date } ! [] + Over date -> + { model + | rangeCondition = + { rangeCondition + | hoverDate = date + } + } + ! [] + Pick date -> - ( DatePicker <| - { model - | open = False - , inputText = Nothing - , focused = Nothing - } - , Cmd.none - , Changed date - ) + case settings.isRange of + False -> + ( DatePicker <| + { model + | open = False + , inputText = Nothing + , focused = Nothing + } + , Cmd.none + , Changed date + ) + + True -> + case rangeCondition.isStartPicked of + True -> + case rangeCondition.isFinishPicked of + True -> + ( DatePicker <| + { model + | open = False + , inputText = Nothing + , focused = Nothing + , rangeCondition = + { rangeCondition + | isStartPicked = False + , isFinishPicked = False + } + } + , Cmd.none + , NoChange + ) + + False -> + ( DatePicker <| + { model + | open = False + , inputText = Nothing + , focused = Nothing + , rangeCondition = + { rangeCondition + | isStartPicked = False + , isFinishPicked = False + } + } + , Cmd.none + , FinishChanged date + ) + + False -> + ( DatePicker <| + { model + | open = True + , inputText = Nothing + , focused = Nothing + , rangeCondition = + { rangeCondition + | isStartPicked = True + , isFinishPicked = False + } + } + , Cmd.none + , StartChanged date + ) Text text -> { model | inputText = Just text } ! [] @@ -341,42 +425,42 @@ update settings msg (DatePicker ({ forceOpen, focused } as model)) = text = model.inputText ?> "" in - if isWhitespace text then - Changed Nothing - else - text - |> settings.parser - |> Result.map - (Changed - << (\date -> - if settings.isDisabled date then - Nothing - else - Just date - ) - ) - |> Result.withDefault NoChange + if isWhitespace text then + Changed Nothing + else + text + |> settings.parser + |> Result.map + (Changed + << (\date -> + if settings.isDisabled date then + Nothing + else + Just date + ) + ) + |> Result.withDefault NoChange in - ( DatePicker <| - { model - | inputText = - case dateEvent of - Changed _ -> - Nothing - - NoChange -> - model.inputText - , focused = - case dateEvent of - Changed _ -> - Nothing - - NoChange -> - model.focused - } - , Cmd.none - , dateEvent - ) + ( DatePicker <| + { model + | inputText = + case dateEvent of + NoChange -> + model.inputText + + _ -> + Nothing + , focused = + case dateEvent of + NoChange -> + model.focused + + _ -> + Nothing + } + , Cmd.none + , dateEvent + ) Focus -> { model | open = True, forceOpen = False } ! [] @@ -392,15 +476,19 @@ update settings msg (DatePicker ({ forceOpen, focused } as model)) = {-| Generate a message that will act as if the user has chosen a certain date, - so you can call `update` on the model yourself. - Note that this is different from just changing the "current chosen" date, - since the picker doesn't actually have internal state for that. - Rather, it will: - * change the calendar focus - * replace the input text with the new value - * close the picker +so you can call `update` on the model yourself. +Note that this is different from just changing the "current chosen" date, +since the picker doesn't actually have internal state for that. +Rather, it will: + + - change the calendar focus + + - replace the input text with the new value + + - close the picker update datepickerSettings (pick (Just someDate)) datepicker + -} pick : Maybe Date -> Msg pick = @@ -452,18 +540,26 @@ view pickedDate settings (DatePicker ({ open } as model)) = |> defaultValue ] in - Html.Keyed.node "div" - [ class "container" ] - [ ( "dateInput", dateInput ) - , if open then - ( "datePicker", datePicker pickedDate settings model ) - else - ( "text", text "" ) - ] + Html.Keyed.node "div" + [ class "container" ] + [ ( "dateInput", dateInput ) + , if open then + ( "datePicker", datePicker pickedDate settings model ) + else + ( "text", text "" ) + ] + + +isLater : Maybe Date -> Date -> Bool +isLater maybeDate date = + maybeDate + |> Maybe.map + (dateTuple >> (>=) (dateTuple date)) + |> Maybe.withDefault False datePicker : Maybe Date -> Settings -> Model -> Html Msg -datePicker pickedDate settings ({ focused, today } as model) = +datePicker pickedDate settings ({ focused, today, rangeCondition } as model) = let currentDate = focused ??> pickedDate ?> today @@ -492,6 +588,17 @@ datePicker pickedDate settings ({ focused, today } as model) = dow d = td [ class "dow" ] [ text <| settings.dayFormatter d ] + inRange d = + case rangeCondition.isStartPicked of + True -> + if isLater pickedDate d && not (isLater rangeCondition.hoverDate d) then + True + else + False + + False -> + False + picked d = pickedDate |> Maybe.map @@ -505,22 +612,25 @@ datePicker pickedDate settings ({ focused, today } as model) = props = if not disabled then - [ onClick (Pick (Just d)) ] + [ onClick (Pick (Just d)) + , onMouseOver (Over (Just d)) + ] else [] in - td - ([ classList - [ ( "day", True ) - , ( "disabled", disabled ) - , ( "picked", picked d ) - , ( "today", dateTuple d == dateTuple currentDate ) - , ( "other-month", month currentMonth /= month d ) - ] - ] - ++ props - ) - [ settings.cellFormatter <| toString <| Date.day d ] + td + ([ classList + [ ( "day", True ) + , ( "disabled", disabled ) + , ( "picked", picked d ) + , ( "today", dateTuple d == dateTuple currentDate ) + , ( "other-month", month currentMonth /= month d ) + , ( "range", inRange d ) + ] + ] + ++ props + ) + [ settings.cellFormatter <| toString <| Date.day d ] row days = tr [ class "row" ] (List.map day days) @@ -554,42 +664,42 @@ datePicker pickedDate settings ({ focused, today } as model) = (yearRange { focused = currentDate, currentMonth = currentMonth } settings.changeYear) ) in - div - [ class "picker" - , onPicker "mousedown" MouseDown - , onPicker "mouseup" MouseUp - ] - [ div [ class "picker-header" ] - [ div [ class "prev-container" ] - [ arrow "prev" (ChangeFocus (prevMonth currentDate)) ] - , div [ class "month-container" ] - [ span [ class "month" ] - [ text <| settings.monthFormatter <| month currentMonth ] - , span [ class "year" ] - [ if not (yearRangeActive settings.changeYear) then - text <| settings.yearFormatter <| year currentMonth - else - Html.Keyed.node "span" [] [ ( toString (year currentMonth), dropdownYear ) ] - ] + div + [ class "picker" + , onPicker "mousedown" MouseDown + , onPicker "mouseup" MouseUp + ] + [ div [ class "picker-header" ] + [ div [ class "prev-container" ] + [ arrow "prev" (ChangeFocus (prevMonth currentDate)) ] + , div [ class "month-container" ] + [ span [ class "month" ] + [ text <| settings.monthFormatter <| month currentMonth ] + , span [ class "year" ] + [ if not (yearRangeActive settings.changeYear) then + text <| settings.yearFormatter <| year currentMonth + else + Html.Keyed.node "span" [] [ ( toString (year currentMonth), dropdownYear ) ] ] - , div [ class "next-container" ] - [ arrow "next" (ChangeFocus (nextMonth currentDate)) ] ] - , table [ class "table" ] - [ thead [ class "weekdays" ] - [ tr [] - [ dow <| firstDay - , dow <| addDows 1 firstDay - , dow <| addDows 2 firstDay - , dow <| addDows 3 firstDay - , dow <| addDows 4 firstDay - , dow <| addDows 5 firstDay - , dow <| addDows 6 firstDay - ] + , div [ class "next-container" ] + [ arrow "next" (ChangeFocus (nextMonth currentDate)) ] + ] + , table [ class "table" ] + [ thead [ class "weekdays" ] + [ tr [] + [ dow <| firstDay + , dow <| addDows 1 firstDay + , dow <| addDows 2 firstDay + , dow <| addDows 3 firstDay + , dow <| addDows 4 firstDay + , dow <| addDows 5 firstDay + , dow <| addDows 6 firstDay ] - , tbody [ class "days" ] days ] + , tbody [ class "days" ] days ] + ] {-| Turn a list of dates into a list of date rows with 7 columns per @@ -609,7 +719,7 @@ groupDates dates = else go (i + 1) xs (x :: racc) acc in - go 0 dates [] [] + go 0 dates [] [] mkClass : Settings -> String -> Html.Attribute msg From e28579c0e0cdb4dc684832896f8ef618fa4141cd Mon Sep 17 00:00:00 2001 From: Ann Korepanova Date: Sun, 29 Oct 2017 20:29:30 +0000 Subject: [PATCH 02/10] datapicker with memory --- src/DatePicker.elm | 61 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/src/DatePicker.elm b/src/DatePicker.elm index b91f8b1..268e06f 100644 --- a/src/DatePicker.elm +++ b/src/DatePicker.elm @@ -39,7 +39,7 @@ import Date exposing (Date, Day(..), Month, day, month, year) import DatePicker.Date exposing (..) import Html exposing (..) import Html.Attributes as Attrs exposing (defaultValue, href, placeholder, selected, tabindex, type_, value) -import Html.Events exposing (on, onBlur, onClick, onFocus, onInput, onMouseOver, onWithOptions, targetValue) +import Html.Events exposing (on, onBlur, onClick, onFocus, onInput, onMouseLeave, onMouseOver, onWithOptions, targetValue) import Html.Keyed import Json.Decode as Json import Task @@ -58,6 +58,7 @@ type Msg | MouseDown | MouseUp | Over (Maybe Date) + | MouseLeave {-| The type of date picker settings. @@ -108,6 +109,8 @@ type alias RangeCondition = { isStartPicked : Bool , isFinishPicked : Bool , hoverDate : Maybe Date + , startDate : Maybe Date + , finishDate : Maybe Date } @@ -223,6 +226,8 @@ initRangeCondition = { isStartPicked = False , isFinishPicked = False , hoverDate = Nothing + , startDate = Nothing + , finishDate = Nothing } @@ -338,6 +343,14 @@ update settings msg (DatePicker ({ forceOpen, focused, rangeCondition } as model ChangeFocus date -> { model | focused = Just date } ! [] + MouseLeave -> + case rangeCondition.isStartPicked && rangeCondition.isFinishPicked of + True -> + { model | open = False } ! [] + + _ -> + model ! [] + Over date -> { model | rangeCondition = @@ -367,29 +380,32 @@ update settings msg (DatePicker ({ forceOpen, focused, rangeCondition } as model True -> ( DatePicker <| { model - | open = False + | open = True , inputText = Nothing , focused = Nothing , rangeCondition = { rangeCondition - | isStartPicked = False + | isStartPicked = True , isFinishPicked = False + , startDate = date + , finishDate = Nothing } } , Cmd.none - , NoChange + , StartChanged date ) False -> ( DatePicker <| { model - | open = False + | open = True , inputText = Nothing , focused = Nothing , rangeCondition = { rangeCondition - | isStartPicked = False - , isFinishPicked = False + | isStartPicked = True + , isFinishPicked = True + , finishDate = date } } , Cmd.none @@ -406,6 +422,7 @@ update settings msg (DatePicker ({ forceOpen, focused, rangeCondition } as model { rangeCondition | isStartPicked = True , isFinishPicked = False + , startDate = date } } , Cmd.none @@ -558,6 +575,14 @@ isLater maybeDate date = |> Maybe.withDefault False +isEqual : Maybe Date -> Date -> Bool +isEqual maybeDate date = + maybeDate + |> Maybe.map + (dateTuple >> (==) (dateTuple date)) + |> Maybe.withDefault False + + datePicker : Maybe Date -> Settings -> Model -> Html Msg datePicker pickedDate settings ({ focused, today, rangeCondition } as model) = let @@ -591,19 +616,24 @@ datePicker pickedDate settings ({ focused, today, rangeCondition } as model) = inRange d = case rangeCondition.isStartPicked of True -> - if isLater pickedDate d && not (isLater rangeCondition.hoverDate d) then - True - else - False + case rangeCondition.isFinishPicked of + False -> + if isLater rangeCondition.startDate d && not (isLater rangeCondition.hoverDate d) then + True + else + False + + True -> + if isLater rangeCondition.startDate d && not (isLater rangeCondition.finishDate d) then + True + else + False False -> False picked d = - pickedDate - |> Maybe.map - (dateTuple >> (==) (dateTuple d)) - |> Maybe.withDefault False + isEqual pickedDate d || isEqual rangeCondition.finishDate d || isEqual rangeCondition.startDate d day d = let @@ -668,6 +698,7 @@ datePicker pickedDate settings ({ focused, today, rangeCondition } as model) = [ class "picker" , onPicker "mousedown" MouseDown , onPicker "mouseup" MouseUp + , onMouseLeave MouseLeave ] [ div [ class "picker-header" ] [ div [ class "prev-container" ] From fa493f739989aec9692163280223ba6ffe26cf41 Mon Sep 17 00:00:00 2001 From: Ann Korepanova Date: Sun, 29 Oct 2017 21:09:04 +0000 Subject: [PATCH 03/10] no reverse data inputing --- examples/range2/Range2.elm | 12 ++------ src/DatePicker.elm | 61 ++++++++++++++++++++++++++------------ 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/examples/range2/Range2.elm b/examples/range2/Range2.elm index eff0fb0..b5584dc 100644 --- a/examples/range2/Range2.elm +++ b/examples/range2/Range2.elm @@ -43,16 +43,10 @@ update msg ({ startDate, finishDate, datePicker } as model) = DatePicker.update settings msg datePicker in case dateEvent of - StartChanged newDate -> + RangeChanged startDate finishDate -> { model - | startDate = newDate - , datePicker = newDatePicker - } - ! [ Cmd.map ToDatePicker datePickerFx ] - - FinishChanged newDate -> - { model - | finishDate = newDate + | startDate = startDate + , finishDate = finishDate , datePicker = newDatePicker } ! [ Cmd.map ToDatePicker datePickerFx ] diff --git a/src/DatePicker.elm b/src/DatePicker.elm index 268e06f..50b73a5 100644 --- a/src/DatePicker.elm +++ b/src/DatePicker.elm @@ -327,8 +327,7 @@ Used to represent a request, by the datepicker, to change the selected date. type DateEvent = NoChange | Changed (Maybe Date) - | StartChanged (Maybe Date) - | FinishChanged (Maybe Date) + | RangeChanged (Maybe Date) (Maybe Date) {-| The date picker update function. The third tuple member represents a user action to change the @@ -392,25 +391,48 @@ update settings msg (DatePicker ({ forceOpen, focused, rangeCondition } as model } } , Cmd.none - , StartChanged date + , RangeChanged date Nothing ) False -> - ( DatePicker <| - { model - | open = True - , inputText = Nothing - , focused = Nothing - , rangeCondition = - { rangeCondition - | isStartPicked = True - , isFinishPicked = True - , finishDate = date + case + Maybe.map (isLater rangeCondition.startDate) date + |> Maybe.withDefault False + of + True -> + ( DatePicker <| + { model + | open = True + , inputText = Nothing + , focused = Nothing + , rangeCondition = + { rangeCondition + | isStartPicked = True + , isFinishPicked = True + , finishDate = date + } } - } - , Cmd.none - , FinishChanged date - ) + , Cmd.none + , RangeChanged rangeCondition.startDate date + ) + + False -> + ( DatePicker <| + { model + | open = True + , inputText = Nothing + , focused = Nothing + , rangeCondition = + { rangeCondition + | isStartPicked = False + , isFinishPicked = False + , finishDate = Nothing + , startDate = Nothing + } + } + , Cmd.none + , RangeChanged Nothing Nothing + ) False -> ( DatePicker <| @@ -423,10 +445,11 @@ update settings msg (DatePicker ({ forceOpen, focused, rangeCondition } as model | isStartPicked = True , isFinishPicked = False , startDate = date + , finishDate = Nothing } } , Cmd.none - , StartChanged date + , RangeChanged date Nothing ) Text text -> @@ -571,7 +594,7 @@ isLater : Maybe Date -> Date -> Bool isLater maybeDate date = maybeDate |> Maybe.map - (dateTuple >> (>=) (dateTuple date)) + (dateTuple >> (>) (dateTuple date)) |> Maybe.withDefault False From 510f713999ae746f669c824ee0e9e9d3301d6c9f Mon Sep 17 00:00:00 2001 From: Ann Korepanova Date: Sun, 29 Oct 2017 22:11:46 +0000 Subject: [PATCH 04/10] close after mouseLeave --- src/DatePicker.elm | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/DatePicker.elm b/src/DatePicker.elm index 50b73a5..e1f1463 100644 --- a/src/DatePicker.elm +++ b/src/DatePicker.elm @@ -321,6 +321,11 @@ focusedDate (DatePicker model) = model.focused +inputText : Maybe Date -> Maybe Date -> Settings -> Maybe String +inputText startDate finishDate settings = + Maybe.map2 (++) (Maybe.map settings.dateFormatter finishDate) (Maybe.map ((++) " - ") (Maybe.map settings.dateFormatter startDate)) + + {-| A sugaring of `Maybe` to explicitly tell you how to interpret `Changed Nothing`, because `Just Nothing` seems somehow wrong. Used to represent a request, by the datepicker, to change the selected date. -} @@ -343,12 +348,7 @@ update settings msg (DatePicker ({ forceOpen, focused, rangeCondition } as model { model | focused = Just date } ! [] MouseLeave -> - case rangeCondition.isStartPicked && rangeCondition.isFinishPicked of - True -> - { model | open = False } ! [] - - _ -> - model ! [] + { model | open = False } ! [] Over date -> { model @@ -538,7 +538,7 @@ pick = {-| The date picker view. The Date passed is whatever date it should treat as selected. -} view : Maybe Date -> Settings -> DatePicker -> Html Msg -view pickedDate settings (DatePicker ({ open } as model)) = +view pickedDate settings (DatePicker ({ open, rangeCondition } as model)) = let class = mkClass settings @@ -574,8 +574,14 @@ view pickedDate settings (DatePicker ({ open } as model)) = [ placeholder settings.placeholder , model.inputText |> Maybe.withDefault - (Maybe.map settings.dateFormatter pickedDate - |> Maybe.withDefault "" + (case settings.isRange of + True -> + inputText rangeCondition.startDate rangeCondition.finishDate settings + |> Maybe.withDefault "" + + False -> + Maybe.map settings.dateFormatter pickedDate + |> Maybe.withDefault "" ) |> defaultValue ] From b14f2b0deb0d8ea1288ba8d902dca9e5d3661ce9 Mon Sep 17 00:00:00 2001 From: Ann Korepanova Date: Tue, 31 Oct 2017 16:08:17 +0000 Subject: [PATCH 05/10] rangeDatePicker as separate package --- elm-package.json | 1 + examples/range2/Range2.elm | 18 +- index.html | 41 -- src/DatePicker.elm | 233 ++---------- src/RangeDatePicker.elm | 756 +++++++++++++++++++++++++++++++++++++ 5 files changed, 798 insertions(+), 251 deletions(-) delete mode 100644 index.html create mode 100644 src/RangeDatePicker.elm diff --git a/elm-package.json b/elm-package.json index 7ed90e7..eca00f1 100644 --- a/elm-package.json +++ b/elm-package.json @@ -9,6 +9,7 @@ ], "exposed-modules": [ "DatePicker" + , "RangeDatePicker" ], "dependencies": { "elm-lang/core": "5.0.0 <= v < 6.0.0", diff --git a/examples/range2/Range2.elm b/examples/range2/Range2.elm index b5584dc..9189f47 100644 --- a/examples/range2/Range2.elm +++ b/examples/range2/Range2.elm @@ -1,8 +1,8 @@ module Range2 exposing (main) import Date exposing (Date, Day(..), day, dayOfWeek, month, year) -import DatePicker exposing (DateEvent(..), defaultSettings) import Html exposing (Html, div, h1, text) +import RangeDatePicker as DatePicker exposing (DateEvent(..), defaultSettings) type Msg @@ -18,7 +18,7 @@ type alias Model = settings : DatePicker.Settings settings = - { defaultSettings | isRange = True } + defaultSettings init : ( Model, Cmd Msg ) @@ -43,10 +43,16 @@ update msg ({ startDate, finishDate, datePicker } as model) = DatePicker.update settings msg datePicker in case dateEvent of - RangeChanged startDate finishDate -> + ChangedStart date -> { model - | startDate = startDate - , finishDate = finishDate + | startDate = date + , datePicker = newDatePicker + } + ! [ Cmd.map ToDatePicker datePickerFx ] + + ChangedFinish date -> + { model + | finishDate = date , datePicker = newDatePicker } ! [ Cmd.map ToDatePicker datePickerFx ] @@ -73,7 +79,7 @@ view ({ startDate, finishDate, datePicker } as model) = Just date -> h1 [] [ text <| formatDate date ] - , DatePicker.view startDate settings datePicker + , DatePicker.view startDate finishDate settings datePicker |> Html.map ToDatePicker ] diff --git a/index.html b/index.html deleted file mode 100644 index cb9979e..0000000 --- a/index.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - elm-datepicker example - - - - - - - - - - -
-
-
-
- - - - - diff --git a/src/DatePicker.elm b/src/DatePicker.elm index e1f1463..4e89272 100644 --- a/src/DatePicker.elm +++ b/src/DatePicker.elm @@ -39,7 +39,7 @@ import Date exposing (Date, Day(..), Month, day, month, year) import DatePicker.Date exposing (..) import Html exposing (..) import Html.Attributes as Attrs exposing (defaultValue, href, placeholder, selected, tabindex, type_, value) -import Html.Events exposing (on, onBlur, onClick, onFocus, onInput, onMouseLeave, onMouseOver, onWithOptions, targetValue) +import Html.Events exposing (on, onBlur, onClick, onFocus, onInput, onWithOptions, targetValue) import Html.Keyed import Json.Decode as Json import Task @@ -57,8 +57,6 @@ type Msg | Blur | MouseDown | MouseUp - | Over (Maybe Date) - | MouseLeave {-| The type of date picker settings. @@ -79,7 +77,6 @@ type alias Settings = , cellFormatter : String -> Html Msg , firstDayOfWeek : Day , changeYear : YearRange - , isRange : Bool } @@ -98,19 +95,6 @@ type alias Model = Date -- actual, current day as far as we know - , rangeCondition : - RangeCondition - - -- describes picked dates condition - } - - -type alias RangeCondition = - { isStartPicked : Bool - , isFinishPicked : Bool - , hoverDate : Maybe Date - , startDate : Maybe Date - , finishDate : Maybe Date } @@ -154,7 +138,6 @@ defaultSettings = , cellFormatter = formatCell , firstDayOfWeek = Sun , changeYear = off - , isRange = False } @@ -221,16 +204,6 @@ formatCell day = text day -initRangeCondition : RangeCondition -initRangeCondition = - { isStartPicked = False - , isFinishPicked = False - , hoverDate = Nothing - , startDate = Nothing - , finishDate = Nothing - } - - {-| The default initial state of the Datepicker. You must execute the returned command (which, for the curious, sets the current date) for the date picker to behave correctly. @@ -251,7 +224,6 @@ init = , focused = Just initDate , inputText = Nothing , today = initDate - , rangeCondition = initRangeCondition } , Task.perform CurrentDate Date.now ) @@ -271,7 +243,6 @@ initFromDate date = , focused = Just date , inputText = Nothing , today = date - , rangeCondition = initRangeCondition } @@ -289,7 +260,6 @@ initFromDates today date = , focused = date , inputText = Nothing , today = today - , rangeCondition = initRangeCondition } @@ -321,25 +291,19 @@ focusedDate (DatePicker model) = model.focused -inputText : Maybe Date -> Maybe Date -> Settings -> Maybe String -inputText startDate finishDate settings = - Maybe.map2 (++) (Maybe.map settings.dateFormatter finishDate) (Maybe.map ((++) " - ") (Maybe.map settings.dateFormatter startDate)) - - {-| A sugaring of `Maybe` to explicitly tell you how to interpret `Changed Nothing`, because `Just Nothing` seems somehow wrong. Used to represent a request, by the datepicker, to change the selected date. -} type DateEvent = NoChange | Changed (Maybe Date) - | RangeChanged (Maybe Date) (Maybe Date) {-| The date picker update function. The third tuple member represents a user action to change the date. -} update : Settings -> Msg -> DatePicker -> ( DatePicker, Cmd Msg, DateEvent ) -update settings msg (DatePicker ({ forceOpen, focused, rangeCondition } as model)) = +update settings msg (DatePicker ({ forceOpen, focused } as model)) = case msg of CurrentDate date -> { model | focused = Just date, today = date } ! [] @@ -347,110 +311,16 @@ update settings msg (DatePicker ({ forceOpen, focused, rangeCondition } as model ChangeFocus date -> { model | focused = Just date } ! [] - MouseLeave -> - { model | open = False } ! [] - - Over date -> - { model - | rangeCondition = - { rangeCondition - | hoverDate = date - } - } - ! [] - Pick date -> - case settings.isRange of - False -> - ( DatePicker <| - { model - | open = False - , inputText = Nothing - , focused = Nothing - } - , Cmd.none - , Changed date - ) - - True -> - case rangeCondition.isStartPicked of - True -> - case rangeCondition.isFinishPicked of - True -> - ( DatePicker <| - { model - | open = True - , inputText = Nothing - , focused = Nothing - , rangeCondition = - { rangeCondition - | isStartPicked = True - , isFinishPicked = False - , startDate = date - , finishDate = Nothing - } - } - , Cmd.none - , RangeChanged date Nothing - ) - - False -> - case - Maybe.map (isLater rangeCondition.startDate) date - |> Maybe.withDefault False - of - True -> - ( DatePicker <| - { model - | open = True - , inputText = Nothing - , focused = Nothing - , rangeCondition = - { rangeCondition - | isStartPicked = True - , isFinishPicked = True - , finishDate = date - } - } - , Cmd.none - , RangeChanged rangeCondition.startDate date - ) - - False -> - ( DatePicker <| - { model - | open = True - , inputText = Nothing - , focused = Nothing - , rangeCondition = - { rangeCondition - | isStartPicked = False - , isFinishPicked = False - , finishDate = Nothing - , startDate = Nothing - } - } - , Cmd.none - , RangeChanged Nothing Nothing - ) - - False -> - ( DatePicker <| - { model - | open = True - , inputText = Nothing - , focused = Nothing - , rangeCondition = - { rangeCondition - | isStartPicked = True - , isFinishPicked = False - , startDate = date - , finishDate = Nothing - } - } - , Cmd.none - , RangeChanged date Nothing - ) + ( DatePicker <| + { model + | open = False + , inputText = Nothing + , focused = Nothing + } + , Cmd.none + , Changed date + ) Text text -> { model | inputText = Just text } ! [] @@ -485,18 +355,18 @@ update settings msg (DatePicker ({ forceOpen, focused, rangeCondition } as model { model | inputText = case dateEvent of + Changed _ -> + Nothing + NoChange -> model.inputText - - _ -> - Nothing , focused = case dateEvent of + Changed _ -> + Nothing + NoChange -> model.focused - - _ -> - Nothing } , Cmd.none , dateEvent @@ -520,12 +390,9 @@ so you can call `update` on the model yourself. Note that this is different from just changing the "current chosen" date, since the picker doesn't actually have internal state for that. Rather, it will: - - - change the calendar focus - - - replace the input text with the new value - - - close the picker +* change the calendar focus +* replace the input text with the new value +* close the picker update datepickerSettings (pick (Just someDate)) datepicker @@ -538,7 +405,7 @@ pick = {-| The date picker view. The Date passed is whatever date it should treat as selected. -} view : Maybe Date -> Settings -> DatePicker -> Html Msg -view pickedDate settings (DatePicker ({ open, rangeCondition } as model)) = +view pickedDate settings (DatePicker ({ open } as model)) = let class = mkClass settings @@ -574,14 +441,8 @@ view pickedDate settings (DatePicker ({ open, rangeCondition } as model)) = [ placeholder settings.placeholder , model.inputText |> Maybe.withDefault - (case settings.isRange of - True -> - inputText rangeCondition.startDate rangeCondition.finishDate settings - |> Maybe.withDefault "" - - False -> - Maybe.map settings.dateFormatter pickedDate - |> Maybe.withDefault "" + (Maybe.map settings.dateFormatter pickedDate + |> Maybe.withDefault "" ) |> defaultValue ] @@ -596,24 +457,8 @@ view pickedDate settings (DatePicker ({ open, rangeCondition } as model)) = ] -isLater : Maybe Date -> Date -> Bool -isLater maybeDate date = - maybeDate - |> Maybe.map - (dateTuple >> (>) (dateTuple date)) - |> Maybe.withDefault False - - -isEqual : Maybe Date -> Date -> Bool -isEqual maybeDate date = - maybeDate - |> Maybe.map - (dateTuple >> (==) (dateTuple date)) - |> Maybe.withDefault False - - datePicker : Maybe Date -> Settings -> Model -> Html Msg -datePicker pickedDate settings ({ focused, today, rangeCondition } as model) = +datePicker pickedDate settings ({ focused, today } as model) = let currentDate = focused ??> pickedDate ?> today @@ -642,27 +487,11 @@ datePicker pickedDate settings ({ focused, today, rangeCondition } as model) = dow d = td [ class "dow" ] [ text <| settings.dayFormatter d ] - inRange d = - case rangeCondition.isStartPicked of - True -> - case rangeCondition.isFinishPicked of - False -> - if isLater rangeCondition.startDate d && not (isLater rangeCondition.hoverDate d) then - True - else - False - - True -> - if isLater rangeCondition.startDate d && not (isLater rangeCondition.finishDate d) then - True - else - False - - False -> - False - picked d = - isEqual pickedDate d || isEqual rangeCondition.finishDate d || isEqual rangeCondition.startDate d + pickedDate + |> Maybe.map + (dateTuple >> (==) (dateTuple d)) + |> Maybe.withDefault False day d = let @@ -671,9 +500,7 @@ datePicker pickedDate settings ({ focused, today, rangeCondition } as model) = props = if not disabled then - [ onClick (Pick (Just d)) - , onMouseOver (Over (Just d)) - ] + [ onClick (Pick (Just d)) ] else [] in @@ -684,7 +511,6 @@ datePicker pickedDate settings ({ focused, today, rangeCondition } as model) = , ( "picked", picked d ) , ( "today", dateTuple d == dateTuple currentDate ) , ( "other-month", month currentMonth /= month d ) - , ( "range", inRange d ) ] ] ++ props @@ -727,7 +553,6 @@ datePicker pickedDate settings ({ focused, today, rangeCondition } as model) = [ class "picker" , onPicker "mousedown" MouseDown , onPicker "mouseup" MouseUp - , onMouseLeave MouseLeave ] [ div [ class "picker-header" ] [ div [ class "prev-container" ] diff --git a/src/RangeDatePicker.elm b/src/RangeDatePicker.elm new file mode 100644 index 0000000..379aec0 --- /dev/null +++ b/src/RangeDatePicker.elm @@ -0,0 +1,756 @@ +module RangeDatePicker + exposing + ( DateEvent(..) + , DatePicker + , Msg + , Settings + , between + , defaultSettings + , focusedDate + , from + , init + , initFromDate + , initFromDates + , isOpen + , moreOrLess + , off + , pickFinish + , pickStart + , to + , update + , view + ) + +{-| A customizable date picker component. + + +# Tea ☕ + +@docs Msg, DateEvent, DatePicker +@docs init, initFromDate, initFromDates, update, view, isOpen, focusedDate + + +# Settings + +@docs Settings, defaultSettings, pickStart, pickFinish, between, moreOrLess, from, to, off + +-} + +import Date exposing (Date, Day(..), Month, day, month, year) +import DatePicker.Date exposing (..) +import Html exposing (..) +import Html.Attributes as Attrs exposing (defaultValue, href, placeholder, selected, tabindex, type_, value) +import Html.Events exposing (on, onBlur, onClick, onFocus, onInput, onMouseLeave, onMouseOver, onWithOptions, targetValue) +import Html.Keyed +import Json.Decode as Json +import Task + + +{-| An opaque type representing messages that are passed inside the DatePicker. +-} +type Msg + = CurrentDate Date + | ChangeFocus Date + | PickStart (Maybe Date) + | PickFinish (Maybe Date) + | StartText String + | FinishText String + | SubmitText (Maybe String) (Maybe Date -> DateEvent) + | Focus (Maybe Date -> Msg) + | Blur + | MouseDown + | MouseUp + | Over (Maybe Date) + | MouseLeave + | ErrorClick + + +{-| The type of date picker settings. +-} +type alias Settings = + { placeholder : String + , classNamespace : String + , inputClassList : List ( String, Bool ) + , inputName : Maybe String + , inputId : Maybe String + , inputAttributes : List (Html.Attribute Msg) + , isDisabled : Date -> Bool + , parser : String -> Result String Date + , dateFormatter : Date -> String + , dayFormatter : Day -> String + , monthFormatter : Month -> String + , yearFormatter : Int -> String + , cellFormatter : String -> Html Msg + , firstDayOfWeek : Day + , changeYear : YearRange + } + + +type alias Model = + { open : Bool + , forceOpen : Bool + , focused : + Maybe Date + + -- date currently center-focused by picker, but not necessarily chosen + , inputStartText : + Maybe String + + -- for user start input that hasn't yet been submitted + , inputFinishText : + Maybe String + + -- for user finsh input that hasn't yet been submitted + , today : + Date + + -- actual, current day as far as we know + , hoverDate : Maybe Date + + -- mouse hovered dates for drawing background color of range + , pickEvent : Maybe Date -> Msg + + -- PickStart or PickFinish depends on who's initiator of datapicker opening + } + + +{-| The DatePicker model. Opaque, hence no field docs. +-} +type DatePicker + = DatePicker Model + + +{-| A record of default settings for the date picker. Extend this if +you want to customize your date picker. + + import DatePicker exposing (defaultSettings) + + DatePicker.init { defaultSettings | placeholder = "Pick a date" } + +To disable certain dates: + + import Date exposing (Day(..), dayOfWeek) + import DatePicker exposing (defaultSettings) + + DatePicker.init { defaultSettings | isDisabled = \d -> dayOfWeek d `List.member` [ Sat, Sun ] } + +-} +defaultSettings : Settings +defaultSettings = + { placeholder = "Please pick a date..." + , classNamespace = "elm-datepicker--" + , inputClassList = [] + , inputName = Nothing + , inputId = Nothing + , inputAttributes = + [ Attrs.required False + ] + , isDisabled = always False + , parser = Date.fromString + , dateFormatter = formatDate + , dayFormatter = formatDay + , monthFormatter = formatMonth + , yearFormatter = toString + , cellFormatter = formatCell + , firstDayOfWeek = Sun + , changeYear = off + } + + +yearRangeActive : YearRange -> Bool +yearRangeActive yearRange = + yearRange /= Off + + +{-| Select a range of date to display + + DatePicker.init { defaultSettings | changeYear = between 1555 2018 } + +-} +between : Int -> Int -> YearRange +between start end = + if start > end then + Between end start + else + Between start end + + +{-| Select a symmetric range of date to display + + DatePicker.init { defaultSettings | changeYear = moreOrLess 10 } + +-} +moreOrLess : Int -> YearRange +moreOrLess range = + MoreOrLess range + + +{-| Select a range from a given year to this year + + DatePicker.init { defaultSettings | changeYear = from 1995 } + +-} +from : Int -> YearRange +from year = + From year + + +{-| Select a range from this year to a given year + + DatePicker.init { defaultSettings | changeYear = to 2020 } + +-} +to : Int -> YearRange +to year = + To year + + +{-| Turn off the date range + + DatePicker.init { defaultSettings | changeYear = off } + +-} +off : YearRange +off = + Off + + +formatCell : String -> Html Msg +formatCell day = + text day + + +{-| The default initial state of the Datepicker. You must execute +the returned command (which, for the curious, sets the current date) +for the date picker to behave correctly. + + init = + let + ( datePicker, datePickerFx ) = + DatePicker.init + in + { picker = datePicker } ! [ Cmd.map ToDatePicker datePickerfx ] + +-} +init : ( DatePicker, Cmd Msg ) +init = + ( DatePicker <| + { open = False + , forceOpen = False + , focused = Just initDate + , inputStartText = Nothing + , inputFinishText = Nothing + , today = initDate + , hoverDate = Nothing + , pickEvent = PickStart + } + , Task.perform CurrentDate Date.now + ) + + +{-| Initialize a DatePicker with a given Date + + init date = + { picker = DatePicker.initFromDate date } ! [] + +-} +initFromDate : Date -> DatePicker +initFromDate date = + DatePicker <| + { open = False + , forceOpen = False + , focused = Just date + , inputStartText = Nothing + , inputFinishText = Nothing + , today = date + , hoverDate = Nothing + , pickEvent = PickStart + } + + +{-| Initialize a DatePicker with a date for today and Maybe a date picked + + init today date = + { picker = DatePicker.initFromDates today date } ! [] + +-} +initFromDates : Date -> Maybe Date -> DatePicker +initFromDates today date = + DatePicker <| + { open = False + , forceOpen = False + , focused = date + , inputStartText = Nothing + , inputFinishText = Nothing + , today = today + , hoverDate = Nothing + , pickEvent = PickStart + } + + +prepareDates : Date -> Day -> { currentMonth : Date, currentDates : List Date } +prepareDates date firstDayOfWeek = + let + start = + firstOfMonth date |> subDays 6 + + end = + nextMonth date |> addDays 6 + in + { currentMonth = date + , currentDates = datesInRange firstDayOfWeek start end + } + + +{-| Expose if the datepicker is open +-} +isOpen : DatePicker -> Bool +isOpen (DatePicker model) = + model.open + + +{-| Expose the currently focused date +-} +focusedDate : DatePicker -> Maybe Date +focusedDate (DatePicker model) = + model.focused + + +inputText : Maybe Date -> Maybe Date -> Settings -> Maybe String +inputText startDate finishDate settings = + Maybe.map2 (++) (Maybe.map settings.dateFormatter finishDate) (Maybe.map ((++) " - ") (Maybe.map settings.dateFormatter startDate)) + + +{-| A sugaring of `Maybe` to explicitly tell you how to interpret `Changed Nothing`, because `Just Nothing` seems somehow wrong. +Used to represent a request, by the datepicker, to change the selected date. +-} +type DateEvent + = NoChange + | ChangedStart (Maybe Date) + | ChangedFinish (Maybe Date) + + +{-| The date picker update function. The third tuple member represents a user action to change the +date. +-} +update : Settings -> Msg -> DatePicker -> ( DatePicker, Cmd Msg, DateEvent ) +update settings msg (DatePicker ({ forceOpen, focused, hoverDate } as model)) = + case msg of + CurrentDate date -> + { model | focused = Just date, today = date } ! [] + + ChangeFocus date -> + { model | focused = Just date } ! [] + + MouseLeave -> + { model | open = False } ! [] + + Over date -> + { model + | hoverDate = date + } + ! [] + + PickStart date -> + ( DatePicker <| + { model + | open = True + , inputStartText = Nothing + , focused = Nothing + , pickEvent = PickFinish + } + , Cmd.none + , ChangedStart date + ) + + PickFinish date -> + ( DatePicker <| + { model + | open = True + , inputStartText = Nothing + , focused = Nothing + , pickEvent = PickStart + } + , Cmd.none + , ChangedFinish date + ) + + StartText text -> + { model | inputStartText = Just text } ! [] + + FinishText text -> + { model | inputFinishText = Just text } ! [] + + SubmitText inputText dateEventChanged -> + let + isWhitespace = + String.trim >> String.isEmpty + + dateEvent = + let + text = + inputText ?> "" + in + if isWhitespace text then + dateEventChanged Nothing + else + text + |> settings.parser + |> Result.map + (dateEventChanged + << (\date -> + if settings.isDisabled date then + Nothing + else + Just date + ) + ) + |> Result.withDefault NoChange + in + ( DatePicker <| + { model + | inputStartText = + case dateEvent of + NoChange -> + model.inputStartText + + _ -> + Nothing + , inputFinishText = + case dateEvent of + NoChange -> + model.inputFinishText + + _ -> + Nothing + , focused = + case dateEvent of + NoChange -> + model.focused + + _ -> + Nothing + } + , Cmd.none + , dateEvent + ) + + Focus pickEvent -> + { model | open = True, forceOpen = False, pickEvent = pickEvent } ! [] + + Blur -> + { model | open = forceOpen } ! [] + + MouseDown -> + { model | forceOpen = True } ! [] + + MouseUp -> + { model | forceOpen = False } ! [] + + ErrorClick -> + model ! [] + + +{-| Generate a message that will act as if the user has chosen a certain date, +so you can call `update` on the model yourself. +Note that this is different from just changing the "current chosen" date, +since the picker doesn't actually have internal state for that. +Rather, it will: + + - change the calendar focus + + - replace the input text with the new value + + - close the picker for second pick + + update datepickerSettings (pick (Just someDate)) datepicker + +-} +pickStart : Maybe Date -> Msg +pickStart = + PickStart + + +{-| Like under +-} +pickFinish : Maybe Date -> Msg +pickFinish = + PickFinish + + +{-| The date picker view. The Date passed is whatever date it should treat as selected. +-} +view : Maybe Date -> Maybe Date -> Settings -> DatePicker -> Html Msg +view startDate finishDate settings (DatePicker ({ open, pickEvent } as model)) = + let + class = + mkClass settings + + potentialInputId = + settings.inputId + |> Maybe.map Attrs.id + |> (List.singleton >> List.filterMap identity) + + inputClasses = + [ ( settings.classNamespace ++ "input", True ) ] + ++ settings.inputClassList + + inputCommon inputText textEvent dateEvent pickEvent xs = + input + ([ Attrs.classList inputClasses + , Attrs.name (settings.inputName ?> "") + , type_ "text" + , on "change" (Json.succeed (SubmitText inputText dateEvent)) + , onInput textEvent + , onBlur Blur + , onClick (Focus pickEvent) + , onFocus (Focus pickEvent) + ] + ++ settings.inputAttributes + ++ potentialInputId + ++ xs + ) + [] + + dateInput inputText textEvent dateEvent date pickEvent = + inputCommon + inputText + textEvent + dateEvent + pickEvent + [ placeholder settings.placeholder + , inputText + |> Maybe.withDefault + (Maybe.map settings.dateFormatter date + |> Maybe.withDefault "" + ) + |> defaultValue + ] + in + Html.Keyed.node "div" + [ class "container" ] + [ ( "dateInput", dateInput model.inputStartText StartText ChangedStart startDate PickStart ) + , ( "text", text " - " ) + , ( "dateInput", dateInput model.inputFinishText FinishText ChangedFinish finishDate PickFinish ) + , if open then + ( "datePicker", datePicker startDate finishDate settings pickEvent model ) + else + ( "text", text "" ) + ] + + +isLater : Maybe Date -> Date -> Bool +isLater maybeDate date = + maybeDate + |> Maybe.map + (dateTuple >> (>) (dateTuple date)) + |> Maybe.withDefault False + + +isEqual : Maybe Date -> Date -> Bool +isEqual maybeDate date = + maybeDate + |> Maybe.map + (dateTuple >> (==) (dateTuple date)) + |> Maybe.withDefault False + + +datePicker : Maybe Date -> Maybe Date -> Settings -> (Maybe Date -> Msg) -> Model -> Html Msg +datePicker startDate finishDate settings pickEvent ({ focused, today, hoverDate } as model) = + let + currentDate = + focused ??> startDate ?> today + + { currentMonth, currentDates } = + prepareDates currentDate settings.firstDayOfWeek + + class = + mkClass settings + + classList = + mkClassList settings + + firstDay = + settings.firstDayOfWeek + + arrow className message = + a + [ class className + , href "javascript:;" + , onClick message + , tabindex -1 + ] + [] + + dow d = + td [ class "dow" ] [ text <| settings.dayFormatter d ] + + inRange d = + case startDate of + Nothing -> + False + + _ -> + case finishDate of + Nothing -> + if isLater startDate d && not (isLater hoverDate d) then + True + else + False + + _ -> + if isLater startDate d && not (isLater finishDate d) then + True + else + False + + picked d = + isEqual finishDate d || isEqual startDate d + + day d = + let + disabled = + settings.isDisabled d + + props = + if not disabled then + [ onClick (pickEvent (Just d)) + , onMouseOver (Over (Just d)) + ] + else + [] + in + td + ([ classList + [ ( "day", True ) + , ( "disabled", disabled ) + , ( "picked", picked d ) + , ( "today", dateTuple d == dateTuple currentDate ) + , ( "other-month", month currentMonth /= month d ) + , ( "range", inRange d ) + ] + ] + ++ props + ) + [ settings.cellFormatter <| toString <| Date.day d ] + + row days = + tr [ class "row" ] (List.map day days) + + days = + List.map row (groupDates currentDates) + + onPicker ev = + Json.succeed + >> onWithOptions ev + { preventDefault = False + , stopPropagation = True + } + + onChange handler = + on "change" <| Json.map handler targetValue + + isCurrentYear selectedYear = + year currentMonth == selectedYear + + yearOption index selectedYear = + ( toString index + , option [ value (toString selectedYear), selected (isCurrentYear selectedYear) ] + [ text <| toString selectedYear ] + ) + + dropdownYear = + Html.Keyed.node "select" + [ onChange (newYear currentDate >> ChangeFocus), class "year-menu" ] + (List.indexedMap yearOption + (yearRange { focused = currentDate, currentMonth = currentMonth } settings.changeYear) + ) + in + div + [ class "picker" + , onPicker "mousedown" MouseDown + , onPicker "mouseup" MouseUp + , onMouseLeave MouseLeave + ] + [ div [ class "picker-header" ] + [ div [ class "prev-container" ] + [ arrow "prev" (ChangeFocus (prevMonth currentDate)) ] + , div [ class "month-container" ] + [ span [ class "month" ] + [ text <| settings.monthFormatter <| month currentMonth ] + , span [ class "year" ] + [ if not (yearRangeActive settings.changeYear) then + text <| settings.yearFormatter <| year currentMonth + else + Html.Keyed.node "span" [] [ ( toString (year currentMonth), dropdownYear ) ] + ] + ] + , div [ class "next-container" ] + [ arrow "next" (ChangeFocus (nextMonth currentDate)) ] + ] + , table [ class "table" ] + [ thead [ class "weekdays" ] + [ tr [] + [ dow <| firstDay + , dow <| addDows 1 firstDay + , dow <| addDows 2 firstDay + , dow <| addDows 3 firstDay + , dow <| addDows 4 firstDay + , dow <| addDows 5 firstDay + , dow <| addDows 6 firstDay + ] + ] + , tbody [ class "days" ] days + ] + ] + + +{-| Turn a list of dates into a list of date rows with 7 columns per +row representing each day of the week. +-} +groupDates : List Date -> List (List Date) +groupDates dates = + let + go i xs racc acc = + case xs of + [] -> + List.reverse acc + + x :: xs -> + if i == 6 then + go 0 xs [] (List.reverse (x :: racc) :: acc) + else + go (i + 1) xs (x :: racc) acc + in + go 0 dates [] [] + + +mkClass : Settings -> String -> Html.Attribute msg +mkClass { classNamespace } c = + Attrs.class (classNamespace ++ c) + + +mkClassList : Settings -> List ( String, Bool ) -> Html.Attribute msg +mkClassList { classNamespace } cs = + List.map (\( c, b ) -> ( classNamespace ++ c, b )) cs + |> Attrs.classList + + +(!) : Model -> List (Cmd Msg) -> ( DatePicker, Cmd Msg, DateEvent ) +(!) m cs = + ( DatePicker m, Cmd.batch cs, NoChange ) + + +(?>) : Maybe a -> a -> a +(?>) = + flip Maybe.withDefault + + +(??>) : Maybe a -> Maybe a -> Maybe a +(??>) first default = + case first of + Just val -> + Just val + + Nothing -> + default From c18db44c871bbab1c56c6dce1e10e8ae6b7475fe Mon Sep 17 00:00:00 2001 From: Ann Korepanova Date: Thu, 2 Nov 2017 00:19:28 +0000 Subject: [PATCH 06/10] rewrite datepicker --- examples/range2/Range2.elm | 12 +- src/RangeDatePicker.elm | 270 +++++++++++++++++-------------------- 2 files changed, 127 insertions(+), 155 deletions(-) diff --git a/examples/range2/Range2.elm b/examples/range2/Range2.elm index 9189f47..21cf48a 100644 --- a/examples/range2/Range2.elm +++ b/examples/range2/Range2.elm @@ -43,16 +43,10 @@ update msg ({ startDate, finishDate, datePicker } as model) = DatePicker.update settings msg datePicker in case dateEvent of - ChangedStart date -> + Changed startDate finishDate -> { model - | startDate = date - , datePicker = newDatePicker - } - ! [ Cmd.map ToDatePicker datePickerFx ] - - ChangedFinish date -> - { model - | finishDate = date + | startDate = startDate + , finishDate = finishDate , datePicker = newDatePicker } ! [ Cmd.map ToDatePicker datePickerFx ] diff --git a/src/RangeDatePicker.elm b/src/RangeDatePicker.elm index 379aec0..64d5920 100644 --- a/src/RangeDatePicker.elm +++ b/src/RangeDatePicker.elm @@ -14,8 +14,7 @@ module RangeDatePicker , isOpen , moreOrLess , off - , pickFinish - , pickStart + , pick , to , update , view @@ -32,7 +31,7 @@ module RangeDatePicker # Settings -@docs Settings, defaultSettings, pickStart, pickFinish, between, moreOrLess, from, to, off +@docs Settings, defaultSettings, pick, between, moreOrLess, from, to, off -} @@ -40,7 +39,7 @@ import Date exposing (Date, Day(..), Month, day, month, year) import DatePicker.Date exposing (..) import Html exposing (..) import Html.Attributes as Attrs exposing (defaultValue, href, placeholder, selected, tabindex, type_, value) -import Html.Events exposing (on, onBlur, onClick, onFocus, onInput, onMouseLeave, onMouseOver, onWithOptions, targetValue) +import Html.Events exposing (on, onBlur, onClick, onFocus, onInput, onMouseOver, onWithOptions, targetValue) import Html.Keyed import Json.Decode as Json import Task @@ -51,18 +50,15 @@ import Task type Msg = CurrentDate Date | ChangeFocus Date - | PickStart (Maybe Date) - | PickFinish (Maybe Date) - | StartText String - | FinishText String - | SubmitText (Maybe String) (Maybe Date -> DateEvent) - | Focus (Maybe Date -> Msg) + | Pick (Maybe Date) (Maybe Date) (Maybe Date) + | Text String + | SubmitText + | Focus | Blur | MouseDown | MouseUp | Over (Maybe Date) - | MouseLeave - | ErrorClick + | Close {-| The type of date picker settings. @@ -93,24 +89,17 @@ type alias Model = Maybe Date -- date currently center-focused by picker, but not necessarily chosen - , inputStartText : + , inputText : Maybe String - -- for user start input that hasn't yet been submitted - , inputFinishText : - Maybe String - - -- for user finsh input that hasn't yet been submitted + -- for user input that hasn't yet been submitted , today : Date -- actual, current day as far as we know , hoverDate : Maybe Date - -- mouse hovered dates for drawing background color of range - , pickEvent : Maybe Date -> Msg - - -- PickStart or PickFinish depends on who's initiator of datapicker opening + -- date currently hovered by mouse, but not necessarily chosen } @@ -238,11 +227,9 @@ init = { open = False , forceOpen = False , focused = Just initDate - , inputStartText = Nothing - , inputFinishText = Nothing + , inputText = Nothing , today = initDate , hoverDate = Nothing - , pickEvent = PickStart } , Task.perform CurrentDate Date.now ) @@ -260,11 +247,9 @@ initFromDate date = { open = False , forceOpen = False , focused = Just date - , inputStartText = Nothing - , inputFinishText = Nothing + , inputText = Nothing , today = date , hoverDate = Nothing - , pickEvent = PickStart } @@ -280,11 +265,9 @@ initFromDates today date = { open = False , forceOpen = False , focused = date - , inputStartText = Nothing - , inputFinishText = Nothing + , inputText = Nothing , today = today , hoverDate = Nothing - , pickEvent = PickStart } @@ -316,25 +299,30 @@ focusedDate (DatePicker model) = model.focused -inputText : Maybe Date -> Maybe Date -> Settings -> Maybe String -inputText startDate finishDate settings = - Maybe.map2 (++) (Maybe.map settings.dateFormatter finishDate) (Maybe.map ((++) " - ") (Maybe.map settings.dateFormatter startDate)) - - {-| A sugaring of `Maybe` to explicitly tell you how to interpret `Changed Nothing`, because `Just Nothing` seems somehow wrong. Used to represent a request, by the datepicker, to change the selected date. -} type DateEvent = NoChange - | ChangedStart (Maybe Date) - | ChangedFinish (Maybe Date) + | Changed (Maybe Date) (Maybe Date) + + +inDirection : Maybe Date -> Maybe Date -> ( Maybe Date, Maybe Date, Bool ) +inDirection firstDate secondDate = + if + Maybe.map2 (<) (Maybe.map dateTuple firstDate) (Maybe.map dateTuple secondDate) + |> Maybe.withDefault True + then + ( firstDate, secondDate, True ) + else + ( secondDate, firstDate, False ) {-| The date picker update function. The third tuple member represents a user action to change the date. -} update : Settings -> Msg -> DatePicker -> ( DatePicker, Cmd Msg, DateEvent ) -update settings msg (DatePicker ({ forceOpen, focused, hoverDate } as model)) = +update settings msg (DatePicker ({ forceOpen, focused } as model)) = case msg of CurrentDate date -> { model | focused = Just date, today = date } ! [] @@ -342,46 +330,38 @@ update settings msg (DatePicker ({ forceOpen, focused, hoverDate } as model)) = ChangeFocus date -> { model | focused = Just date } ! [] - MouseLeave -> - { model | open = False } ! [] - - Over date -> - { model - | hoverDate = date - } - ! [] - - PickStart date -> + Pick firstDate secondDate date -> + let + ( startDate, finishDate, _ ) = + inDirection firstDate secondDate + in ( DatePicker <| { model - | open = True - , inputStartText = Nothing + | inputText = Nothing , focused = Nothing - , pickEvent = PickFinish } , Cmd.none - , ChangedStart date - ) + , case startDate of + Nothing -> + Changed date Nothing - PickFinish date -> - ( DatePicker <| - { model - | open = True - , inputStartText = Nothing - , focused = Nothing - , pickEvent = PickStart - } - , Cmd.none - , ChangedFinish date - ) + _ -> + case finishDate of + Nothing -> + let + ( earlierDate, laterDate, _ ) = + inDirection startDate date + in + Changed earlierDate laterDate - StartText text -> - { model | inputStartText = Just text } ! [] + _ -> + Changed date Nothing + ) - FinishText text -> - { model | inputFinishText = Just text } ! [] + Text text -> + { model | inputText = Just text } ! [] - SubmitText inputText dateEventChanged -> + SubmitText -> let isWhitespace = String.trim >> String.isEmpty @@ -389,15 +369,15 @@ update settings msg (DatePicker ({ forceOpen, focused, hoverDate } as model)) = dateEvent = let text = - inputText ?> "" + model.inputText ?> "" in if isWhitespace text then - dateEventChanged Nothing + Changed Nothing Nothing else text |> settings.parser |> Result.map - (dateEventChanged + (Changed Nothing << (\date -> if settings.isDisabled date then Nothing @@ -409,34 +389,27 @@ update settings msg (DatePicker ({ forceOpen, focused, hoverDate } as model)) = in ( DatePicker <| { model - | inputStartText = + | inputText = case dateEvent of - NoChange -> - model.inputStartText - - _ -> + Changed _ _ -> Nothing - , inputFinishText = - case dateEvent of - NoChange -> - model.inputFinishText - _ -> - Nothing + NoChange -> + model.inputText , focused = case dateEvent of + Changed _ _ -> + Nothing + NoChange -> model.focused - - _ -> - Nothing } , Cmd.none , dateEvent ) - Focus pickEvent -> - { model | open = True, forceOpen = False, pickEvent = pickEvent } ! [] + Focus -> + { model | open = True, forceOpen = False } ! [] Blur -> { model | open = forceOpen } ! [] @@ -447,8 +420,11 @@ update settings msg (DatePicker ({ forceOpen, focused, hoverDate } as model)) = MouseUp -> { model | forceOpen = False } ! [] - ErrorClick -> - model ! [] + Over date -> + { model | hoverDate = date } ! [] + + Close -> + { model | open = False } ! [] {-| Generate a message that will act as if the user has chosen a certain date, @@ -461,27 +437,20 @@ Rather, it will: - replace the input text with the new value - - close the picker for second pick - - update datepickerSettings (pick (Just someDate)) datepicker - --} -pickStart : Maybe Date -> Msg -pickStart = - PickStart + - close the picker + update datepickerSettings (pick firstPickedDate secondPickedDate (Just someDate)) datepicker -{-| Like under -} -pickFinish : Maybe Date -> Msg -pickFinish = - PickFinish +pick : Maybe Date -> Maybe Date -> Maybe Date -> Msg +pick = + Pick {-| The date picker view. The Date passed is whatever date it should treat as selected. -} view : Maybe Date -> Maybe Date -> Settings -> DatePicker -> Html Msg -view startDate finishDate settings (DatePicker ({ open, pickEvent } as model)) = +view firstDate secondDate settings (DatePicker ({ open } as model)) = let class = mkClass settings @@ -495,16 +464,16 @@ view startDate finishDate settings (DatePicker ({ open, pickEvent } as model)) = [ ( settings.classNamespace ++ "input", True ) ] ++ settings.inputClassList - inputCommon inputText textEvent dateEvent pickEvent xs = + inputCommon xs = input ([ Attrs.classList inputClasses , Attrs.name (settings.inputName ?> "") , type_ "text" - , on "change" (Json.succeed (SubmitText inputText dateEvent)) - , onInput textEvent + , on "change" (Json.succeed SubmitText) + , onInput Text , onBlur Blur - , onClick (Focus pickEvent) - , onFocus (Focus pickEvent) + , onClick Focus + , onFocus Focus ] ++ settings.inputAttributes ++ potentialInputId @@ -512,54 +481,32 @@ view startDate finishDate settings (DatePicker ({ open, pickEvent } as model)) = ) [] - dateInput inputText textEvent dateEvent date pickEvent = + dateInput = inputCommon - inputText - textEvent - dateEvent - pickEvent [ placeholder settings.placeholder - , inputText + , model.inputText |> Maybe.withDefault - (Maybe.map settings.dateFormatter date + (Maybe.map settings.dateFormatter firstDate |> Maybe.withDefault "" ) - |> defaultValue + |> value ] in Html.Keyed.node "div" [ class "container" ] - [ ( "dateInput", dateInput model.inputStartText StartText ChangedStart startDate PickStart ) - , ( "text", text " - " ) - , ( "dateInput", dateInput model.inputFinishText FinishText ChangedFinish finishDate PickFinish ) + [ ( "dateInput", dateInput ) , if open then - ( "datePicker", datePicker startDate finishDate settings pickEvent model ) + ( "datePicker", datePicker firstDate secondDate settings model ) else ( "text", text "" ) ] -isLater : Maybe Date -> Date -> Bool -isLater maybeDate date = - maybeDate - |> Maybe.map - (dateTuple >> (>) (dateTuple date)) - |> Maybe.withDefault False - - -isEqual : Maybe Date -> Date -> Bool -isEqual maybeDate date = - maybeDate - |> Maybe.map - (dateTuple >> (==) (dateTuple date)) - |> Maybe.withDefault False - - -datePicker : Maybe Date -> Maybe Date -> Settings -> (Maybe Date -> Msg) -> Model -> Html Msg -datePicker startDate finishDate settings pickEvent ({ focused, today, hoverDate } as model) = +datePicker : Maybe Date -> Maybe Date -> Settings -> Model -> Html Msg +datePicker firstDate secondDate settings ({ focused, today, hoverDate } as model) = let currentDate = - focused ??> startDate ?> today + focused ??> firstDate ?> today { currentMonth, currentDates } = prepareDates currentDate settings.firstDayOfWeek @@ -586,6 +533,10 @@ datePicker startDate finishDate settings pickEvent ({ focused, today, hoverDate td [ class "dow" ] [ text <| settings.dayFormatter d ] inRange d = + let + ( startDate, finishDate, _ ) = + inDirection firstDate secondDate + in case startDate of Nothing -> False @@ -593,19 +544,45 @@ datePicker startDate finishDate settings pickEvent ({ focused, today, hoverDate _ -> case finishDate of Nothing -> - if isLater startDate d && not (isLater hoverDate d) then + let + ( _, _, isLaterThenStart ) = + inDirection startDate d + + ( _, _, isEarlierThenHover ) = + inDirection d hoverDate + in + if + (isLaterThenStart && isEarlierThenHover) + || (not isLaterThenStart && not isEarlierThenHover) + then True else False _ -> - if isLater startDate d && not (isLater finishDate d) then + let + ( _, _, isLaterThenStart ) = + inDirection startDate d + + ( _, _, isEarlierThenFinish ) = + inDirection d finishDate + in + if isLaterThenStart && isEarlierThenFinish then True else False picked d = - isEqual finishDate d || isEqual startDate d + (firstDate + |> Maybe.map + (dateTuple >> (==) (dateTuple d)) + |> Maybe.withDefault False + ) + || (secondDate + |> Maybe.map + (dateTuple >> (==) (dateTuple d)) + |> Maybe.withDefault False + ) day d = let @@ -614,7 +591,7 @@ datePicker startDate finishDate settings pickEvent ({ focused, today, hoverDate props = if not disabled then - [ onClick (pickEvent (Just d)) + [ onClick (Pick firstDate secondDate (Just d)) , onMouseOver (Over (Just d)) ] else @@ -627,7 +604,7 @@ datePicker startDate finishDate settings pickEvent ({ focused, today, hoverDate , ( "picked", picked d ) , ( "today", dateTuple d == dateTuple currentDate ) , ( "other-month", month currentMonth /= month d ) - , ( "range", inRange d ) + , ( "range", inRange (Just d) ) ] ] ++ props @@ -670,7 +647,8 @@ datePicker startDate finishDate settings pickEvent ({ focused, today, hoverDate [ class "picker" , onPicker "mousedown" MouseDown , onPicker "mouseup" MouseUp - , onMouseLeave MouseLeave + , tabindex 2 + , onBlur Close ] [ div [ class "picker-header" ] [ div [ class "prev-container" ] From fb261f9daba215988dcb0f36c5e7588a7be344c4 Mon Sep 17 00:00:00 2001 From: Ann Korepanova Date: Thu, 2 Nov 2017 18:36:38 +0000 Subject: [PATCH 07/10] improved typing --- src/RangeDatePicker.elm | 53 ++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/src/RangeDatePicker.elm b/src/RangeDatePicker.elm index 64d5920..3d32760 100644 --- a/src/RangeDatePicker.elm +++ b/src/RangeDatePicker.elm @@ -58,7 +58,6 @@ type Msg | MouseDown | MouseUp | Over (Maybe Date) - | Close {-| The type of date picker settings. @@ -374,28 +373,32 @@ update settings msg (DatePicker ({ forceOpen, focused } as model)) = if isWhitespace text then Changed Nothing Nothing else - text - |> settings.parser - |> Result.map - (Changed Nothing - << (\date -> - if settings.isDisabled date then - Nothing - else - Just date - ) - ) + let + startDate = + settings.parser (Maybe.withDefault "" (List.head (String.words text))) + + finishDate = + settings.parser (Maybe.withDefault "" (List.head (List.reverse (String.words text)))) + in + Result.map2 + (\firstDate secondDate -> + if settings.isDisabled firstDate then + Changed Nothing Nothing + else + let + ( startDate, finishDate, _ ) = + inDirection (Just firstDate) (Just secondDate) + in + Changed startDate finishDate + ) + startDate + finishDate |> Result.withDefault NoChange in ( DatePicker <| { model | inputText = - case dateEvent of - Changed _ _ -> - Nothing - - NoChange -> - model.inputText + Nothing , focused = case dateEvent of Changed _ _ -> @@ -423,9 +426,6 @@ update settings msg (DatePicker ({ forceOpen, focused } as model)) = Over date -> { model | hoverDate = date } ! [] - Close -> - { model | open = False } ! [] - {-| Generate a message that will act as if the user has chosen a certain date, so you can call `update` on the model yourself. @@ -486,8 +486,13 @@ view firstDate secondDate settings (DatePicker ({ open } as model)) = [ placeholder settings.placeholder , model.inputText |> Maybe.withDefault - (Maybe.map settings.dateFormatter firstDate - |> Maybe.withDefault "" + ((Maybe.map settings.dateFormatter firstDate + |> Maybe.withDefault "..." + ) + ++ " to " + ++ (Maybe.map settings.dateFormatter secondDate + |> Maybe.withDefault "..." + ) ) |> value ] @@ -648,7 +653,7 @@ datePicker firstDate secondDate settings ({ focused, today, hoverDate } as model , onPicker "mousedown" MouseDown , onPicker "mouseup" MouseUp , tabindex 2 - , onBlur Close + , onBlur Blur ] [ div [ class "picker-header" ] [ div [ class "prev-container" ] From f8b6b9d8c09c45f0886c8f67bcb5f3e4804b0f93 Mon Sep 17 00:00:00 2001 From: Ann Korepanova Date: Thu, 2 Nov 2017 23:03:28 +0000 Subject: [PATCH 08/10] next month added --- css/elm-datepicker.css | 8 ++- src/DatePicker/Date.elm | 86 ++++++++++++++-------------- src/RangeDatePicker.elm | 124 ++++++++++++++++++++++++++-------------- 3 files changed, 130 insertions(+), 88 deletions(-) diff --git a/css/elm-datepicker.css b/css/elm-datepicker.css index dcb0595..1ed8f1c 100644 --- a/css/elm-datepicker.css +++ b/css/elm-datepicker.css @@ -5,9 +5,15 @@ outline: 0; } .elm-datepicker--picker { - position: absolute; + position: relative; + max-width: 190px; border: 1px solid #CCC; z-index: 10; } + .elm-datepicker--picker-right { + position: relative; + max-width: 190px; + border: 1px solid #CCC; + z-index: 10; } .elm-datepicker--picker-header, .elm-datepicker--weekdays { diff --git a/src/DatePicker/Date.elm b/src/DatePicker/Date.elm index ad7e253..9b07c91 100644 --- a/src/DatePicker/Date.elm +++ b/src/DatePicker/Date.elm @@ -1,23 +1,23 @@ module DatePicker.Date exposing ( YearRange(..) - , initDate - , formatDate - , formatDay - , formatMonth , addDays , addDows - , subDays , dateTuple , datesInRange , firstOfMonth - , prevMonth - , nextMonth + , formatDate + , formatDay + , formatMonth + , initDate , newYear + , nextMonth + , prevMonth + , subDays , yearRange ) -import Date exposing (Date, Day(..), Month(..), year, month, day) +import Date exposing (Date, Day(..), Month(..), day, month, year) type alias Year = @@ -139,10 +139,10 @@ trimDates firstDay dates = else dr xs in - dl dates - |> List.reverse - |> dr - |> List.reverse + dl dates + |> List.reverse + |> dr + |> List.reverse datesInRange : Date.Day -> Date -> Date -> List Date @@ -153,13 +153,13 @@ datesInRange firstDay min max = y = subDay x in - if dateTuple y == dateTuple min then - y :: acc - else - go y (y :: acc) + if dateTuple y == dateTuple min then + y :: acc + else + go y (y :: acc) in - go max [] - |> trimDates firstDay + go max [] + |> trimDates firstDay dateTuple : Date -> ( Int, Int, Int ) @@ -176,7 +176,7 @@ repeat f = else go (n - 1) (f x) in - go + go firstOfMonth : Date -> Date @@ -196,7 +196,7 @@ nextMonth date = else year date in - mkDate nextYear nextMonth 1 + mkDate nextYear nextMonth 1 prevMonth : Date -> Date @@ -211,7 +211,7 @@ prevMonth date = else year date in - mkDate prevYear prevMonth 1 + mkDate prevYear prevMonth 1 addDays : Int -> Date -> Date @@ -243,10 +243,10 @@ addDay date = else year in - if day > dim then - mkDate succYear succ 1 - else - mkDate year month day + if day > dim then + mkDate succYear succ 1 + else + mkDate year month day subDays : Int -> Date -> Date @@ -275,10 +275,10 @@ subDay date = else year in - if day < 1 then - mkDate predYear pred (daysInMonth predYear pred) - else - mkDate year month day + if day < 1 then + mkDate predYear pred (daysInMonth predYear pred) + else + mkDate year month day addDows : Int -> Date.Day -> Date.Day @@ -306,10 +306,10 @@ predDow day = (dayToInt day - 1) |> flip rem 7 in - if prev == 0 then - Sun - else - dayFromInt prev + if prev == 0 then + Sun + else + dayFromInt prev dayToString : Int -> String @@ -379,10 +379,10 @@ monthToString month = int = monthToInt month in - if int < 10 then - "0" ++ toString int - else - toString int + if int < 10 then + "0" ++ toString int + else + toString int predMonth : Month -> Month @@ -392,10 +392,10 @@ predMonth month = (monthToInt month - 1) |> flip rem 12 in - if prev == 0 then - Dec - else - monthFromInt prev + if prev == 0 then + Dec + else + monthFromInt prev succMonth : Month -> Month @@ -564,14 +564,14 @@ newYear currentMonth newYear = mkDate year (month currentMonth) (day currentMonth) Err _ -> - Debug.crash ("Unknown Month " ++ (toString currentMonth)) + Debug.crash ("Unknown Month " ++ toString currentMonth) yearRange : { focused : Date, currentMonth : Date } -> YearRange -> List Int yearRange { focused, currentMonth } range = case range of MoreOrLess num -> - List.range ((year currentMonth) - num) ((year currentMonth) + num) + List.range (year currentMonth - num) (year currentMonth + num) Between start end -> List.range start end diff --git a/src/RangeDatePicker.elm b/src/RangeDatePicker.elm index 3d32760..e6ff81e 100644 --- a/src/RangeDatePicker.elm +++ b/src/RangeDatePicker.elm @@ -49,8 +49,9 @@ import Task -} type Msg = CurrentDate Date - | ChangeFocus Date - | Pick (Maybe Date) (Maybe Date) (Maybe Date) + | ChangeFirstFocus Date + | ChangeSecondFocus Date + | Pick (Maybe Date) (Maybe Date) (Maybe Date) WhichPicker | Text String | SubmitText | Focus @@ -84,10 +85,14 @@ type alias Settings = type alias Model = { open : Bool , forceOpen : Bool - , focused : + , firstFocused : Maybe Date - -- date currently center-focused by picker, but not necessarily chosen + -- date currently center-focused by first picker, but not necessarily chosen + , secondFocused : + Maybe Date + + -- date currently center-focused by second picker, but not necessarily chosen , inputText : Maybe String @@ -108,6 +113,11 @@ type DatePicker = DatePicker Model +type WhichPicker + = FirstPicker + | SecondPicker + + {-| A record of default settings for the date picker. Extend this if you want to customize your date picker. @@ -225,7 +235,8 @@ init = ( DatePicker <| { open = False , forceOpen = False - , focused = Just initDate + , secondFocused = Just initDate + , firstFocused = Just initDate , inputText = Nothing , today = initDate , hoverDate = Nothing @@ -245,7 +256,8 @@ initFromDate date = DatePicker <| { open = False , forceOpen = False - , focused = Just date + , secondFocused = Just date + , firstFocused = Just date , inputText = Nothing , today = date , hoverDate = Nothing @@ -263,7 +275,8 @@ initFromDates today date = DatePicker <| { open = False , forceOpen = False - , focused = date + , secondFocused = date + , firstFocused = date , inputText = Nothing , today = today , hoverDate = Nothing @@ -291,11 +304,11 @@ isOpen (DatePicker model) = model.open -{-| Expose the currently focused date +{-| Expose the currently focused dates -} -focusedDate : DatePicker -> Maybe Date +focusedDate : DatePicker -> ( Maybe Date, Maybe Date ) focusedDate (DatePicker model) = - model.focused + (,) model.firstFocused model.secondFocused {-| A sugaring of `Maybe` to explicitly tell you how to interpret `Changed Nothing`, because `Just Nothing` seems somehow wrong. @@ -321,40 +334,50 @@ inDirection firstDate secondDate = date. -} update : Settings -> Msg -> DatePicker -> ( DatePicker, Cmd Msg, DateEvent ) -update settings msg (DatePicker ({ forceOpen, focused } as model)) = +update settings msg (DatePicker ({ forceOpen, firstFocused, secondFocused } as model)) = case msg of CurrentDate date -> - { model | focused = Just date, today = date } ! [] + { model + | firstFocused = Just date + , secondFocused = Just (nextMonth date) + , today = date + } + ! [] - ChangeFocus date -> - { model | focused = Just date } ! [] + ChangeFirstFocus date -> + { model | firstFocused = Just date } ! [] - Pick firstDate secondDate date -> + ChangeSecondFocus date -> + { model | secondFocused = Just date } ! [] + + Pick firstDate secondDate date whichPicker -> let ( startDate, finishDate, _ ) = inDirection firstDate secondDate + + dateEvent = + case startDate of + Nothing -> + Changed date Nothing + + _ -> + case finishDate of + Nothing -> + let + ( earlierDate, laterDate, _ ) = + inDirection startDate date + in + Changed earlierDate laterDate + + _ -> + Changed date Nothing in ( DatePicker <| { model | inputText = Nothing - , focused = Nothing } , Cmd.none - , case startDate of - Nothing -> - Changed date Nothing - - _ -> - case finishDate of - Nothing -> - let - ( earlierDate, laterDate, _ ) = - inDirection startDate date - in - Changed earlierDate laterDate - - _ -> - Changed date Nothing + , dateEvent ) Text text -> @@ -399,13 +422,20 @@ update settings msg (DatePicker ({ forceOpen, focused } as model)) = { model | inputText = Nothing - , focused = + , firstFocused = case dateEvent of - Changed _ _ -> - Nothing + Changed a _ -> + a NoChange -> - model.focused + model.firstFocused + , secondFocused = + case dateEvent of + Changed _ b -> + b + + NoChange -> + model.secondFocused } , Cmd.none , dateEvent @@ -442,7 +472,7 @@ Rather, it will: update datepickerSettings (pick firstPickedDate secondPickedDate (Just someDate)) datepicker -} -pick : Maybe Date -> Maybe Date -> Maybe Date -> Msg +pick : Maybe Date -> Maybe Date -> Maybe Date -> WhichPicker -> Msg pick = Pick @@ -501,14 +531,20 @@ view firstDate secondDate settings (DatePicker ({ open } as model)) = [ class "container" ] [ ( "dateInput", dateInput ) , if open then - ( "datePicker", datePicker firstDate secondDate settings model ) + ( "doublePicker" + , div + [ class "pickers-container" ] + [ datePicker firstDate secondDate settings model model.firstFocused ChangeFirstFocus FirstPicker + , datePicker firstDate secondDate settings model model.secondFocused ChangeSecondFocus SecondPicker + ] + ) else ( "text", text "" ) ] -datePicker : Maybe Date -> Maybe Date -> Settings -> Model -> Html Msg -datePicker firstDate secondDate settings ({ focused, today, hoverDate } as model) = +datePicker : Maybe Date -> Maybe Date -> Settings -> Model -> Maybe Date -> (Date -> Msg) -> WhichPicker -> Html Msg +datePicker firstDate secondDate settings ({ today, hoverDate } as model) focused changeFocusMsg whichPicker = let currentDate = focused ??> firstDate ?> today @@ -595,8 +631,8 @@ datePicker firstDate secondDate settings ({ focused, today, hoverDate } as model settings.isDisabled d props = - if not disabled then - [ onClick (Pick firstDate secondDate (Just d)) + if not disabled && (month currentMonth == month d) then + [ onClick (Pick firstDate secondDate (Just d) whichPicker) , onMouseOver (Over (Just d)) ] else @@ -643,7 +679,7 @@ datePicker firstDate secondDate settings ({ focused, today, hoverDate } as model dropdownYear = Html.Keyed.node "select" - [ onChange (newYear currentDate >> ChangeFocus), class "year-menu" ] + [ onChange (newYear currentDate >> changeFocusMsg), class "year-menu" ] (List.indexedMap yearOption (yearRange { focused = currentDate, currentMonth = currentMonth } settings.changeYear) ) @@ -657,7 +693,7 @@ datePicker firstDate secondDate settings ({ focused, today, hoverDate } as model ] [ div [ class "picker-header" ] [ div [ class "prev-container" ] - [ arrow "prev" (ChangeFocus (prevMonth currentDate)) ] + [ arrow "prev" (changeFocusMsg (prevMonth currentDate)) ] , div [ class "month-container" ] [ span [ class "month" ] [ text <| settings.monthFormatter <| month currentMonth ] @@ -669,7 +705,7 @@ datePicker firstDate secondDate settings ({ focused, today, hoverDate } as model ] ] , div [ class "next-container" ] - [ arrow "next" (ChangeFocus (nextMonth currentDate)) ] + [ arrow "next" (changeFocusMsg (nextMonth currentDate)) ] ] , table [ class "table" ] [ thead [ class "weekdays" ] From f65dc21d817f5b982c9081ab9c0a4690646fbf51 Mon Sep 17 00:00:00 2001 From: Ann Korepanova Date: Fri, 3 Nov 2017 14:47:41 +0000 Subject: [PATCH 09/10] deleted current date bolding --- css/elm-range-datepicker.css | 112 +++++++++++++++++++++++++++++++++++ elm-package.json | 4 +- src/RangeDatePicker.elm | 2 +- 3 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 css/elm-range-datepicker.css diff --git a/css/elm-range-datepicker.css b/css/elm-range-datepicker.css new file mode 100644 index 0000000..70356cb --- /dev/null +++ b/css/elm-range-datepicker.css @@ -0,0 +1,112 @@ +.elm-datepicker--container { + position: relative; } + +.elm-datepicker--pickers-container { + position: absolute; + border: 1px solid #CCC; + max-width: 420px; + outline: 0; } + +.elm-datepicker--input { + display: block; } + +.elm-datepicker--input:focus { + outline: 0; } + +.elm-datepicker--picker { + position: relative; + max-width: 190px; + display: inline-block; + margin: 10px; + z-index: 10; + outline: 0;} + +.elm-datepicker--picker-header { + display: flex; + align-items: center; } + +.elm-datepicker--prev-container, +.elm-datepicker--next-container { + flex: 0 1 auto; } + +.elm-datepicker--month-container { + flex: 1 1 auto; + padding: 0.5em; + display: flex; + flex-direction: column; } + +.elm-datepicker--month, +.elm-datepicker--year { + flex: 1 1 auto; + cursor: default; + text-align: center; } + +.elm-datepicker--year { + font-size: 0.6em; + font-weight: 700; } + +.elm-datepicker--prev, +.elm-datepicker--next { + border: 6px solid transparent; + display: block; + width: 0; + height: 0; + padding: 0 0.2em; } + +.elm-datepicker--prev { + border-right-color: #AAA; } + .elm-datepicker--prev:hover { + border-right-color: #BBB; } + +.elm-datepicker--next { + border-left-color: #AAA; } + .elm-datepicker--next:hover { + border-left-color: #BBB; } + +.elm-datepicker--table { + border-spacing: 0; + border-collapse: collapse; + font-size: 0.8em; } + .elm-datepicker--table td { + width: 2em; + height: 2em; + text-align: center; } + +.elm-datepicker--row { + border-top: 1px solid #F2F2F2; } + +.elm-datepicker--dow { + border-bottom: 1px solid #CCC; + cursor: default; } + +.elm-datepicker--day { + cursor: pointer; } + .elm-datepicker--day:hover { + background: #F2F2F2; } + +.elm-datepicker--range { + background: #F2F2F2; } + +.elm-datepicker--disabled { + cursor: default; + color: #DDD; } + .elm-datepicker--disabled:hover { + background: inherit; } + +.elm-datepicker--picked { + color: white; + background: darkblue; } + .elm-datepicker--picked:hover { + background: darkblue; } + +.elm-datepicker--today { + font-weight: bold; } + +.elm-datepicker--other-month { + color: #AAA; + cursor: default; + visibility: hidden; } + .elm-datepicker--other-month.elm-datepicker--disabled { + color: #EEE; } + .elm-datepicker--other-month.elm-datepicker--picked { + color: white; } diff --git a/elm-package.json b/elm-package.json index eca00f1..4e509df 100644 --- a/elm-package.json +++ b/elm-package.json @@ -8,8 +8,8 @@ "src" ], "exposed-modules": [ - "DatePicker" - , "RangeDatePicker" + "DatePicker", + "RangeDatePicker" ], "dependencies": { "elm-lang/core": "5.0.0 <= v < 6.0.0", diff --git a/src/RangeDatePicker.elm b/src/RangeDatePicker.elm index e6ff81e..eaf0068 100644 --- a/src/RangeDatePicker.elm +++ b/src/RangeDatePicker.elm @@ -643,7 +643,7 @@ datePicker firstDate secondDate settings ({ today, hoverDate } as model) focused [ ( "day", True ) , ( "disabled", disabled ) , ( "picked", picked d ) - , ( "today", dateTuple d == dateTuple currentDate ) + , ( "today", dateTuple d == dateTuple today ) , ( "other-month", month currentMonth /= month d ) , ( "range", inRange (Just d) ) ] From 6ae393dd5d57ea5b54a55cfa6a40eb5d17ec82f7 Mon Sep 17 00:00:00 2001 From: Ann Korepanova Date: Fri, 3 Nov 2017 14:47:41 +0000 Subject: [PATCH 10/10] deleted current date bolding --- css/elm-datepicker.css | 11 +--- css/elm-range-datepicker.css | 112 +++++++++++++++++++++++++++++++++++ elm-package.json | 4 +- src/RangeDatePicker.elm | 2 +- 4 files changed, 116 insertions(+), 13 deletions(-) create mode 100644 css/elm-range-datepicker.css diff --git a/css/elm-datepicker.css b/css/elm-datepicker.css index 1ed8f1c..cc80066 100644 --- a/css/elm-datepicker.css +++ b/css/elm-datepicker.css @@ -5,15 +5,9 @@ outline: 0; } .elm-datepicker--picker { - position: relative; - max-width: 190px; + position: absolute; border: 1px solid #CCC; z-index: 10; } - .elm-datepicker--picker-right { - position: relative; - max-width: 190px; - border: 1px solid #CCC; - z-index: 10; } .elm-datepicker--picker-header, .elm-datepicker--weekdays { @@ -82,9 +76,6 @@ .elm-datepicker--day:hover { background: #F2F2F2; } -.elm-datepicker--range { - background: #F2F2F2; } - .elm-datepicker--disabled { cursor: default; color: #DDD; } diff --git a/css/elm-range-datepicker.css b/css/elm-range-datepicker.css new file mode 100644 index 0000000..70356cb --- /dev/null +++ b/css/elm-range-datepicker.css @@ -0,0 +1,112 @@ +.elm-datepicker--container { + position: relative; } + +.elm-datepicker--pickers-container { + position: absolute; + border: 1px solid #CCC; + max-width: 420px; + outline: 0; } + +.elm-datepicker--input { + display: block; } + +.elm-datepicker--input:focus { + outline: 0; } + +.elm-datepicker--picker { + position: relative; + max-width: 190px; + display: inline-block; + margin: 10px; + z-index: 10; + outline: 0;} + +.elm-datepicker--picker-header { + display: flex; + align-items: center; } + +.elm-datepicker--prev-container, +.elm-datepicker--next-container { + flex: 0 1 auto; } + +.elm-datepicker--month-container { + flex: 1 1 auto; + padding: 0.5em; + display: flex; + flex-direction: column; } + +.elm-datepicker--month, +.elm-datepicker--year { + flex: 1 1 auto; + cursor: default; + text-align: center; } + +.elm-datepicker--year { + font-size: 0.6em; + font-weight: 700; } + +.elm-datepicker--prev, +.elm-datepicker--next { + border: 6px solid transparent; + display: block; + width: 0; + height: 0; + padding: 0 0.2em; } + +.elm-datepicker--prev { + border-right-color: #AAA; } + .elm-datepicker--prev:hover { + border-right-color: #BBB; } + +.elm-datepicker--next { + border-left-color: #AAA; } + .elm-datepicker--next:hover { + border-left-color: #BBB; } + +.elm-datepicker--table { + border-spacing: 0; + border-collapse: collapse; + font-size: 0.8em; } + .elm-datepicker--table td { + width: 2em; + height: 2em; + text-align: center; } + +.elm-datepicker--row { + border-top: 1px solid #F2F2F2; } + +.elm-datepicker--dow { + border-bottom: 1px solid #CCC; + cursor: default; } + +.elm-datepicker--day { + cursor: pointer; } + .elm-datepicker--day:hover { + background: #F2F2F2; } + +.elm-datepicker--range { + background: #F2F2F2; } + +.elm-datepicker--disabled { + cursor: default; + color: #DDD; } + .elm-datepicker--disabled:hover { + background: inherit; } + +.elm-datepicker--picked { + color: white; + background: darkblue; } + .elm-datepicker--picked:hover { + background: darkblue; } + +.elm-datepicker--today { + font-weight: bold; } + +.elm-datepicker--other-month { + color: #AAA; + cursor: default; + visibility: hidden; } + .elm-datepicker--other-month.elm-datepicker--disabled { + color: #EEE; } + .elm-datepicker--other-month.elm-datepicker--picked { + color: white; } diff --git a/elm-package.json b/elm-package.json index eca00f1..4e509df 100644 --- a/elm-package.json +++ b/elm-package.json @@ -8,8 +8,8 @@ "src" ], "exposed-modules": [ - "DatePicker" - , "RangeDatePicker" + "DatePicker", + "RangeDatePicker" ], "dependencies": { "elm-lang/core": "5.0.0 <= v < 6.0.0", diff --git a/src/RangeDatePicker.elm b/src/RangeDatePicker.elm index e6ff81e..eaf0068 100644 --- a/src/RangeDatePicker.elm +++ b/src/RangeDatePicker.elm @@ -643,7 +643,7 @@ datePicker firstDate secondDate settings ({ today, hoverDate } as model) focused [ ( "day", True ) , ( "disabled", disabled ) , ( "picked", picked d ) - , ( "today", dateTuple d == dateTuple currentDate ) + , ( "today", dateTuple d == dateTuple today ) , ( "other-month", month currentMonth /= month d ) , ( "range", inRange (Just d) ) ]