Skip to content

Commit 70d0208

Browse files
committed
Merge Date and Time picker modules with their Utils modules
`DatePicker.Utils` and `TimePicker.Utils` didn't really hold their own. All they really housed was logic internal to their respective components—they weren't used anywhere else—so we move them into the modules they really belong in. While we're at it, we update the Date and Time componests to explicit exports, so the distinction between internal and external types and values becomes clearer.
1 parent e917cd9 commit 70d0208

File tree

5 files changed

+266
-237
lines changed

5 files changed

+266
-237
lines changed

src/DatePicker.purs

Lines changed: 163 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,50 @@
1-
module Ocelot.DatePicker where
1+
module Ocelot.DatePicker
2+
( Action
3+
, CalendarItem
4+
, ChildSlots
5+
, Component
6+
, ComponentHTML
7+
, ComponentM
8+
, ComponentRender
9+
, CompositeAction
10+
, CompositeComponent
11+
, CompositeComponentHTML
12+
, CompositeComponentM
13+
, CompositeComponentRender
14+
, CompositeInput
15+
, CompositeQuery
16+
, CompositeState
17+
, Direction
18+
, EmbeddedAction(..)
19+
, EmbeddedChildSlots
20+
, Input
21+
, Output(..)
22+
, Query(..)
23+
, Slot
24+
, State
25+
, StateRow
26+
, component
27+
) where
228

329
import Prelude
4-
import Data.Array ((!!), mapWithIndex)
30+
import Data.Array ((!!), drop, find, mapWithIndex, reverse, sort, take)
531
import Data.Array as Array
6-
import Data.Date (Date, Month, Year, canonicalDate, month, year)
32+
import Data.Date (Date, Month, Weekday(..), Year, canonicalDate, day, month, weekday, year)
733
import Data.DateTime.Instant (fromDate, toDateTime)
834
import Data.Either (either)
35+
import Data.Enum (fromEnum)
936
import Data.Formatter.DateTime (formatDateTime)
37+
import Data.Fuzzy (Fuzzy(..))
38+
import Data.Fuzzy as Data.Fuzzy
1039
import Data.Maybe (Maybe(..), fromMaybe, maybe)
40+
import Data.Rational ((%))
1141
import Data.String (trim)
42+
import Data.String.Regex (parseFlags, regex, replace)
1243
import Data.Tuple (Tuple(..), fst, snd)
1344
import Data.Tuple.Nested (type (/\), (/\))
1445
import Effect.Aff.Class (class MonadAff)
1546
import Effect.Now (nowDate)
47+
import Foreign.Object (Object, fromFoldable)
1648
import Halogen as H
1749
import Halogen.HTML as HH
1850
import Halogen.HTML.Events as HE
@@ -22,7 +54,7 @@ import Ocelot.Block.Format as Format
2254
import Ocelot.Block.Icon as Icon
2355
import Ocelot.Block.Input as Input
2456
import Ocelot.Block.Layout as Layout
25-
import Ocelot.DatePicker.Utils as Utils
57+
import Ocelot.Data.DateTime (adjustDaysBy, dateRange, firstDateOfMonth, lastDateOfMonth, nextDay, nextYear, prevDay, yearsForward)
2658
import Ocelot.Data.DateTime as ODT
2759
import Ocelot.HTML.Properties (css)
2860
import Select as S
@@ -40,6 +72,15 @@ import Web.UIEvent.KeyboardEvent as KE
4072
data Action
4173
= PassingOutput Output
4274

75+
-- A type to help assist making grid-based calendar layouts. Calendars
76+
-- can use dates directly or use array lengths as offsets.
77+
type Aligned =
78+
{ pre :: Array Date -- Dates before the first of the month
79+
, body :: Array Date -- Dates within the month
80+
, post :: Array Date -- Dates after the last of the month
81+
, all :: Array Date -- The full 35-day range
82+
}
83+
4384
data BoundaryStatus
4485
= OutOfBounds
4586
| InBounds
@@ -65,10 +106,10 @@ type CompositeComponent m = H.Component HH.HTML CompositeQuery CompositeInput Ou
65106

66107
type CompositeComponentHTML m = H.ComponentHTML CompositeAction EmbeddedChildSlots m
67108

68-
type CompositeComponentRender m = CompositeState -> CompositeComponentHTML m
69-
70109
type CompositeComponentM m a = H.HalogenM CompositeState CompositeAction EmbeddedChildSlots Output m a
71110

111+
type CompositeComponentRender m = CompositeState -> CompositeComponentHTML m
112+
72113
type CompositeInput = S.Input StateRow
73114

74115
type CompositeQuery = S.Query Query EmbeddedChildSlots
@@ -94,6 +135,11 @@ type Input =
94135
, disabled :: Boolean
95136
}
96137

138+
-- Generates a date range to search through for search term, if it doesn't
139+
-- match on first past it will generate more dates to search through until
140+
-- a specified range limit is reached, then return Nothing
141+
newtype MaxYears = MaxYears Int
142+
97143
data Output
98144
= SelectionChanged (Maybe Date)
99145
| VisibilityChanged S.Visibility
@@ -113,10 +159,10 @@ data SelectedStatus
113159
= NotSelected
114160
| Selected
115161

116-
type Spec m = S.Spec StateRow Query EmbeddedAction EmbeddedChildSlots CompositeInput Output m
117-
118162
type Slot = H.Slot Query Output
119163

164+
type Spec m = S.Spec StateRow Query EmbeddedAction EmbeddedChildSlots CompositeInput Output m
165+
120166
type State = Record StateRow
121167

122168
type StateRow =
@@ -144,6 +190,26 @@ component = H.mkComponent
144190

145191
_select = SProxy :: SProxy "select"
146192

193+
-- Summary helper function that creates a full grid calendar layout
194+
-- from a year and a month.
195+
align :: Year -> Month -> Array (Array Date)
196+
align y m = rowsFromAligned (alignByWeek y m)
197+
198+
-- A special case for when you need to match days of the month to a grid
199+
-- that's bound to weekdays Sun - Sat.
200+
alignByWeek :: Year -> Month -> Aligned
201+
alignByWeek y m = { pre: pre, body: body, post: post, all: pre <> body <> post }
202+
where
203+
start = firstDateOfMonth y m
204+
end = lastDateOfMonth y m
205+
body = dateRange start end
206+
pre =
207+
let pad = padPrev $ weekday start
208+
in if pad == 0.0 then [] else dateRange (adjustDaysBy pad start) (prevDay start)
209+
post =
210+
let pad = padNext $ weekday end
211+
in if pad == 0.0 then [] else dateRange (nextDay end) (adjustDaysBy pad end)
212+
147213
calendarHeader :: forall m. CompositeComponentHTML m
148214
calendarHeader =
149215
HH.div
@@ -304,6 +370,11 @@ embeddedRender st =
304370
, renderSelect (fst st.targetDate) (snd st.targetDate) st.visibility st.calendarItems
305371
]
306372

373+
firstMatch :: Array (Tuple (Fuzzy Date) Int) -> Maybe (Fuzzy Date)
374+
firstMatch = maybe Nothing (Just <<< fst) <<< find match'
375+
where
376+
match' (Tuple (Fuzzy { ratio }) _) = ratio == (1 % 1)
377+
307378
generateCalendarItem
308379
:: Maybe Date
309380
-> BoundaryStatus
@@ -323,12 +394,39 @@ generateCalendarRows
323394
-> Array CalendarItem
324395
generateCalendarRows selection y m = lastMonth <> thisMonth <> nextMonth
325396
where
326-
{ pre, body, post, all } = Utils.alignByWeek y m
397+
{ pre, body, post, all } = alignByWeek y m
327398
outOfBounds = map (generateCalendarItem selection OutOfBounds)
328399
lastMonth = outOfBounds pre
329400
nextMonth = outOfBounds post
330401
thisMonth = body <#> (generateCalendarItem selection InBounds)
331402

403+
guessDate :: Date -> MaxYears -> String -> Maybe Date
404+
guessDate start (MaxYears max) text =
405+
let text' :: String -- replace dashes and slashes with spaces
406+
text' = either
407+
(const text)
408+
(\r -> replace r " " text)
409+
(regex "-|\\/|," $ parseFlags "g")
410+
411+
text'' :: String -- consolidate all consecutive whitespaceg
412+
text'' = either
413+
(const text')
414+
(\r -> replace r " " text')
415+
(regex "\\s+" $ parseFlags "g")
416+
417+
matcher :: Int -> Date -> Tuple (Fuzzy Date) Int
418+
matcher i d = Tuple (Data.Fuzzy.match true toObject text'' d) i
419+
420+
guess :: Array Date -> Int -> Maybe Date
421+
guess dates = findIn (firstMatch $ sort $ matcher `mapWithIndex` dates)
422+
423+
findIn :: Maybe (Fuzzy Date) -> Int -> Maybe Date
424+
findIn (Just (Fuzzy { original })) _ = Just original
425+
findIn Nothing pass
426+
| pass > max = Nothing
427+
| otherwise = guess (dateRange (yearsForward pass start) (yearsForward (pass + 1) start)) (pass + 1)
428+
in guess (dateRange start $ nextYear start) 0
429+
332430
-- NOTE re-raise output messages from the embedded component
333431
handleAction :: forall m. Action -> ComponentM m Unit
334432
handleAction = case _ of
@@ -352,7 +450,7 @@ handleSearch = do
352450
today <- H.liftEffect nowDate
353451
_ <- case search of
354452
"" -> setSelection Nothing
355-
_ -> case Utils.guessDate today (Utils.MaxYears 5) search of
453+
_ -> case guessDate today (MaxYears 5) search of
356454
Nothing -> pure unit
357455
Just d -> do
358456
setSelection (Just d)
@@ -370,6 +468,32 @@ initialState { targetDate, selection, disabled } =
370468
, disabled
371469
}
372470

471+
-- Represents the number of days that will need to be "filled in"
472+
-- when the last day of the month is this weekday. For example, if the
473+
-- last day of the month is Tuesday, then Wednesday through Saturday
474+
-- will need to be padded
475+
padNext :: Weekday -> Number
476+
padNext Sunday = 6.0
477+
padNext Monday = 5.0
478+
padNext Tuesday = 4.0
479+
padNext Wednesday = 3.0
480+
padNext Thursday = 2.0
481+
padNext Friday = 1.0
482+
padNext Saturday = 0.0
483+
484+
-- Represents the number of days that will need to be "filled in"
485+
-- when the first day of the month is this weekday. For example, if the
486+
-- first day of the month is Tuesday, then Sunday and Monday will need
487+
-- to be padded
488+
padPrev :: Weekday -> Number
489+
padPrev Sunday = 0.0
490+
padPrev Monday = (-1.0)
491+
padPrev Tuesday = (-2.0)
492+
padPrev Wednesday = (-3.0)
493+
padPrev Thursday = (-4.0)
494+
padPrev Friday = (-5.0)
495+
padPrev Saturday = (-6.0)
496+
373497
render :: forall m. MonadAff m => ComponentRender m
374498
render st =
375499
HH.slot _select unit (S.component identity spec) (embeddedInput st) (Just <<< PassingOutput)
@@ -382,7 +506,7 @@ renderCalendar y m calendarItems =
382506
)
383507
[ calendarNav y m
384508
, calendarHeader
385-
, HH.div_ $ renderRows $ Utils.rowsFromArray calendarItems
509+
, HH.div_ $ renderRows $ rowsFromArray calendarItems
386510
]
387511
where
388512
dropdownClasses :: Array HH.ClassName
@@ -490,6 +614,17 @@ renderSelect y m visibility calendarItems =
490614
then [ renderCalendar y m calendarItems ]
491615
else []
492616

617+
-- Break a set of Sunday-aligned dates into rows, each 7 in length.
618+
rowsFromAligned :: Aligned -> Array (Array Date)
619+
rowsFromAligned { all } = rowsFromArray all
620+
621+
-- Break a set of Sunday-aligned dates into rows, each 7 in length.
622+
rowsFromArray :: a. Array a -> Array (Array a)
623+
rowsFromArray all = go all []
624+
where
625+
go [] acc = reverse acc
626+
go xs acc = go (drop 7 xs) ([take 7 xs] <> acc)
627+
493628
setSelection :: forall m. MonadAff m => Maybe Date -> CompositeComponentM m Unit
494629
setSelection selection = do
495630
setSelectionWithoutRaising selection
@@ -523,4 +658,20 @@ synchronize = do
523658
let update = case selection of
524659
Nothing -> identity
525660
Just date -> _ { search = ODT.formatDate date }
526-
H.modify_ (update <<< _ { calendarItems = calendarItems })
661+
H.modify_ (update <<< _ { calendarItems = calendarItems })
662+
663+
toObject :: Date -> Object String
664+
toObject d =
665+
fromFoldable
666+
[ Tuple "mdy1" $ sYearMonth <> " " <> sDay <> " " <> sYear
667+
, Tuple "mdy2" $ sMonth <> " " <> sDay <> " " <> sYear
668+
, Tuple "weekday" $ sWeekDay
669+
, Tuple "wmdy1" $ sWeekDay <> " " <> sYearMonth <> " " <> sDay <> " " <> sYear
670+
, Tuple "ymd" $ sYear <> " " <> sMonth <> " " <> sDay
671+
]
672+
where
673+
sYear = show $ fromEnum $ year d
674+
sMonth = show $ fromEnum $ month d
675+
sYearMonth = show $ month d
676+
sDay = show $ fromEnum $ day d
677+
sWeekDay = show $ weekday d

0 commit comments

Comments
 (0)