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
329import Prelude
4- import Data.Array ((!!), mapWithIndex )
30+ import Data.Array ((!!), drop , find , mapWithIndex , reverse , sort , take )
531import 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 )
733import Data.DateTime.Instant (fromDate , toDateTime )
834import Data.Either (either )
35+ import Data.Enum (fromEnum )
936import Data.Formatter.DateTime (formatDateTime )
37+ import Data.Fuzzy (Fuzzy (..))
38+ import Data.Fuzzy as Data.Fuzzy
1039import Data.Maybe (Maybe (..), fromMaybe , maybe )
40+ import Data.Rational ((%))
1141import Data.String (trim )
42+ import Data.String.Regex (parseFlags , regex , replace )
1243import Data.Tuple (Tuple (..), fst , snd )
1344import Data.Tuple.Nested (type (/\), (/\))
1445import Effect.Aff.Class (class MonadAff )
1546import Effect.Now (nowDate )
47+ import Foreign.Object (Object , fromFoldable )
1648import Halogen as H
1749import Halogen.HTML as HH
1850import Halogen.HTML.Events as HE
@@ -22,7 +54,7 @@ import Ocelot.Block.Format as Format
2254import Ocelot.Block.Icon as Icon
2355import Ocelot.Block.Input as Input
2456import 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 )
2658import Ocelot.Data.DateTime as ODT
2759import Ocelot.HTML.Properties (css )
2860import Select as S
@@ -40,6 +72,15 @@ import Web.UIEvent.KeyboardEvent as KE
4072data 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+
4384data BoundaryStatus
4485 = OutOfBounds
4586 | InBounds
@@ -65,10 +106,10 @@ type CompositeComponent m = H.Component HH.HTML CompositeQuery CompositeInput Ou
65106
66107type CompositeComponentHTML m = H.ComponentHTML CompositeAction EmbeddedChildSlots m
67108
68- type CompositeComponentRender m = CompositeState -> CompositeComponentHTML m
69-
70109type CompositeComponentM m a = H.HalogenM CompositeState CompositeAction EmbeddedChildSlots Output m a
71110
111+ type CompositeComponentRender m = CompositeState -> CompositeComponentHTML m
112+
72113type CompositeInput = S.Input StateRow
73114
74115type 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+
97143data 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-
118162type Slot = H.Slot Query Output
119163
164+ type Spec m = S.Spec StateRow Query EmbeddedAction EmbeddedChildSlots CompositeInput Output m
165+
120166type State = Record StateRow
121167
122168type 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+
147213calendarHeader :: forall m . CompositeComponentHTML m
148214calendarHeader =
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+
307378generateCalendarItem
308379 :: Maybe Date
309380 -> BoundaryStatus
@@ -323,12 +394,39 @@ generateCalendarRows
323394 -> Array CalendarItem
324395generateCalendarRows 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
333431handleAction :: forall m . Action -> ComponentM m Unit
334432handleAction = 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+
373497render :: forall m . MonadAff m => ComponentRender m
374498render 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+
493628setSelection :: forall m . MonadAff m => Maybe Date -> CompositeComponentM m Unit
494629setSelection 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