git.iko.soy/typeable/reflex-dom-extra
on master help sign in
Add src
commit3d1461129395fc469903d132e82d4be142e5d7c2 authorDmitry Olshansky <olshanskydr@gmail.com> dateSun 30 Jan 2022, 14:00 (5 years ago) parent3288b52
5 files changed+500
default.nix+28 reflex-dom-extra.cabal+35 shell.nix+1 src/Reflex/Dom/Extra.hs+263 src/Reflex/Dom/Pagination.hs+173
default.nixnew file+28
@@ -0,0 +1,28 @@
1+{ reflex-platform ? ((import <nixpkgs> {}).fetchFromGitHub {
2+ owner = "reflex-frp";
3+ repo = "reflex-platform";
4+ rev = "efc6d923c633207d18bd4d8cae3e20110a377864";
5+ sha256 = "121rmnkx8nwiy96ipfyyv6vrgysv0zpr2br46y70zf4d0y1h1lz5";
6+ })
7+}:
8+(import reflex-platform {}).project ({ pkgs, ... }:{
9+ useWarp = true;
10+
11+ packages = {
12+ reflex-dom-extra = ../reflex-dom-extra;
13+ };
14+
15+ shellToolOverrides = ghc: super: {
16+ closure-compiler = null;
17+ haskell-ide-engine = null;
18+ hdevtools = null;
19+ hlint = null;
20+ stylish-haskell = null;
21+ hoogle = null;
22+ };
23+
24+ shells = {
25+ ghc = ["reflex-dom-extra"];
26+ ghcjs = ["reflex-dom-extra"];
27+ };
28+})
reflex-dom-extra.cabalnew file+35
@@ -0,0 +1,35 @@
1+cabal-version: 2.4
2+-- Initial package description 'reflex-extra.cabal' generated by 'cabal
3+-- init'. For further documentation, see
4+-- http://haskell.org/cabal/users-guide/
5+
6+name: reflex-dom-extra
7+version: 0.1
8+synopsis: Common Reflex-related functions
9+license-file: LICENSE
10+author: Catherine Galkina
11+maintainer: catherine@typeable.io
12+copyright: Antorica LLC
13+category: Web, GUI, FRP
14+build-type: Simple
15+
16+library
17+ exposed-modules: Reflex.Dom.Extra
18+ , Reflex.Dom.Pagination
19+ build-depends: base >=4.7 && <5
20+ , containers
21+ , jsaddle
22+ , lens
23+ , reflex-dom
24+ , text
25+ if impl(ghcjs)
26+ build-depends:
27+ ghcjs-base
28+ hs-source-dirs: src
29+ default-language: Haskell2010
30+ ghc-options: -Wall
31+ default-extensions: FlexibleContexts
32+ , MonoLocalBinds
33+ , OverloadedStrings
34+ , RecordWildCards
35+ , RecursiveDo
shell.nixnew file+1
@@ -0,0 +1 @@
1+(import ./default.nix { }).shells.ghc
src/Reflex/Dom/Extra.hsnew file+263
@@ -0,0 +1,263 @@
1+-- Common helpers to build DOM with Reflex.
2+module Reflex.Dom.Extra where
3+
4+import Control.Lens
5+import Control.Monad.Fix
6+import Data.Either
7+import Data.Map as M
8+import Data.Maybe
9+import Data.Text as T hiding (zip, map)
10+import Language.Javascript.JSaddle
11+import Reflex.Dom
12+import JavaScript.Object.Internal as JS
13+
14+-- | @<meta>@ element.
15+meta :: DomBuilder t m => Map Text Text -> m ()
16+meta attrs = elAttr "meta" attrs blank
17+
18+-- | Stylesheet link.
19+stylesheet :: DomBuilder t m => Text -> m ()
20+stylesheet ref = elAttr "link" ("rel" =: "stylesheet" <> "href" =: ref) blank
21+
22+-- | <script> tag. Do not use in dynamic contexts, or it will be executed twice.
23+script :: DomBuilder t m => Text -> m ()
24+script url = elAttr "script" ("type" =: "text/javascript" <> "src" =: url) blank
25+
26+-- | @<main>@ element with given CSS class.
27+mainClass :: DomBuilder t m => Text -> m a -> m a
28+mainClass = elClass "main"
29+
30+-- | @<span>@ element with given CSS class.
31+spanClass :: DomBuilder t m => Text -> m a -> m a
32+spanClass = elClass "span"
33+
34+-- | @<label>@ element with given CSS class.
35+labelClass :: DomBuilder t m => Text -> m a -> m a
36+labelClass = elClass "label"
37+
38+-- | @<section>@ element with given CSS class.
39+sectionClass :: DomBuilder t m => Text -> m a -> m a
40+sectionClass = elClass "section"
41+
42+-- | @<b>@ element with given CSS class.
43+bClass :: DomBuilder t m => Text -> m a -> m a
44+bClass = elClass "b"
45+
46+-- | @<tr>@ element with given CSS class.
47+trClass :: DomBuilder t m => Text -> m a -> m a
48+trClass = elClass "tr"
49+
50+-- | @<th>@ element with given CSS class.
51+thClass :: DomBuilder t m => Text -> m a -> m a
52+thClass = elClass "th"
53+
54+-- | @<td>@ element with given CSS class.
55+tdClass :: DomBuilder t m => Text -> m a -> m a
56+tdClass = elClass "td"
57+
58+-- | Line break (@<br>@).
59+br :: DomBuilder t m => m ()
60+br = el "br" blank
61+
62+-- | Button element, returns click event.
63+button
64+ :: DomBuilder t m
65+ => Text
66+ -- ^ Label.
67+ -> Map Text Text
68+ -- ^ Attributes.
69+ -> m (Event t ())
70+button label attrs = do
71+ (e, _) <- elAttr' "button" (attrs <> "type" =: "button") $ text label
72+ return $ domEvent Click e
73+
74+-- | Button element with only "class" attribute.
75+buttonClass
76+ :: DomBuilder t m
77+ => Text
78+ -- ^ Label.
79+ -> Text
80+ -- ^ CSS class.
81+ -> m (Event t ())
82+buttonClass label cls = do
83+ (e, _) <- elAttr' "button" ("class" =: cls <> "type" =: "button") $ text label
84+ return $ domEvent Click e
85+
86+-- | Button element with dynamic attributes.
87+buttonDynAttrs
88+ :: (DomBuilder t m, PostBuild t m)
89+ => Text
90+ -- ^ Label.
91+ -> Dynamic t (Map Text Text)
92+ -- ^ Attributes.
93+ -> m (Event t ())
94+buttonDynAttrs label dynAttrs = buttonDyn (constDyn label) dynAttrs
95+
96+-- | Button element with dynamic label.
97+buttonDynLabel
98+ :: (DomBuilder t m, PostBuild t m)
99+ => Dynamic t Text
100+ -- ^ Label.
101+ -> Map Text Text
102+ -- ^ Attributes.
103+ -> m (Event t ())
104+buttonDynLabel dynLabel attrs = buttonDyn dynLabel (constDyn attrs)
105+
106+-- | Button element with dynamic label and attributes.
107+buttonDyn
108+ :: (DomBuilder t m, PostBuild t m)
109+ => Dynamic t Text
110+ -- ^ Label.
111+ -> Dynamic t (Map Text Text)
112+ -- ^ Attributes.
113+ -> m (Event t ())
114+buttonDyn dynLabel dynAttrs = do
115+ (e, _) <- elDynAttr' "button" ((<> "type" =: "button") <$> dynAttrs)
116+ $ dynText dynLabel
117+ return $ domEvent Click e
118+
119+-- | Dynamic list widget that creates a list that supports the dynamic
120+-- addition and removal of items. This widget is completely general with zero
121+-- markup-specific choices. It handles all the event plumbing and lets you
122+-- completely determine the markup.
123+dynamicList
124+ :: MonadWidget t m
125+ => (Int -> a -> Event t a -> m b)
126+ -- ^ Widget used to display each item
127+ -> (b -> Event t ())
128+ -- ^ Function that gets a remove event from the return value of each item
129+ -> Event t a
130+ -- ^ Event that adds a new item to the list that is not based on an
131+ -- existing item.
132+ -> [a]
133+ -- ^ Initial list of items
134+ -> m (Dynamic t [b])
135+dynamicList w removeEvent addEvent initList = do
136+ let initMap = M.fromList $ zip [0..] initList
137+ rec
138+ let
139+ vals = mergeWith (<>)
140+ [ attachWith addNew (current res) addEvent
141+ , remove (current res) ]
142+ res <- listWithKeyShallowDiff initMap vals w
143+ return $ M.elems <$> res
144+ where
145+ addNew m a = M.singleton k (Just a)
146+ where
147+ k = if M.null m then 0 else fst (M.findMax m) + 1
148+ remove res = switch (mergeWith (<>) . fmap f . M.toList <$> res)
149+ where
150+ f (k,b) = M.singleton k Nothing <$ removeEvent b
151+
152+-- | Build widget using text value, if any. Otherwise stays blank.
153+maybeBlank :: DomBuilder t m => Maybe Text -> (Text -> m ()) -> m ()
154+maybeBlank mt w = maybe blank w mt
155+
156+-- | "cursor: pointer" style.
157+pointer :: Map Text Text
158+pointer = "style" =: "cursor:pointer;"
159+
160+table :: DomBuilder t m => m a -> m a
161+table = el "table"
162+
163+thead :: DomBuilder t m => m a -> m a
164+thead = el "thead"
165+
166+tbody :: DomBuilder t m => m a -> m a
167+tbody = el "tbody"
168+
169+th :: DomBuilder t m => m a -> m a
170+th = el "th"
171+
172+tr :: DomBuilder t m => m a -> m a
173+tr = el "tr"
174+
175+td :: DomBuilder t m => m a -> m a
176+td = el "td"
177+
178+form :: DomBuilder t m => Map Text Text -> m a -> m a
179+form attrs = elAttr "form" attrs
180+
181+-- | Join event of event into single event.
182+joinE
183+ :: (Reflex t, MonadHold t m)
184+ => Event t (Event t a)
185+ -> m (Event t a)
186+joinE = switchHold never
187+
188+-- | Separate an event to two sequences based on given predicate.
189+-- The first element of resulting pair keeps events for which the predicate
190+-- holds, the second -- those for which it doesn't.
191+separateE
192+ :: Reflex t
193+ => (a -> Bool)
194+ -> Event t a
195+ -> (Event t a, Event t a)
196+separateE p e = (ffilter p e, ffilter (not . p) e)
197+
198+-- | Separate an event with optional payload.
199+-- The first event in the resulting pair keeps @Just@s, the seconds keeps
200+-- @Nothing@s.
201+separateMaybeE
202+ :: Reflex t
203+ => Event t (Maybe a)
204+ -> (Event t (Maybe a), Event t (Maybe a))
205+separateMaybeE = separateE isJust
206+
207+-- | Filter only events with existing payload.
208+fromMaybeE
209+ :: Reflex t
210+ => Event t (Maybe a)
211+ -> Event t a
212+fromMaybeE = fmapMaybe id
213+
214+-- | Separate an event with alternative payload.
215+-- The first event in the resulting pair keeps @Left@s, the seconds keeps
216+-- @Right@s.
217+separateEitherE
218+ :: Reflex t
219+ => Event t (Either a b)
220+ -> (Event t (Either a b), Event t (Either a b))
221+separateEitherE = separateE isLeft
222+
223+-- | Assigns HTML content to an element, allows embedding of arbitrary
224+-- HTML tags, so make sure you trust the source of the second argument
225+-- @
226+-- el <- _element_raw . fst <$> el' "span" blank
227+-- unsafeInnerHTML el "this text will be <i>italicised</i>"
228+-- @
229+unsafeInnerHTML :: (MonadJSM m, ToJSVal rawelem) => rawelem -> Text -> m ()
230+unsafeInnerHTML rel html = liftJSM $ do
231+ htmlVal <- toJSVal html
232+ relVal <- toJSVal rel
233+ JS.setProp "innerHTML" htmlVal (JS.Object relVal)
234+
235+-- | input element which is changes while edited.
236+-- It is useful to input dates, phones etc
237+inputCorrect
238+ :: (MonadFix m, DomBuilder t m, MonadHold t m)
239+ => (Text -> Text -> Text) -- ^ old value and new value
240+ -> InputElementConfig er t (DomBuilderSpace m)
241+ -> m (InputElement er (DomBuilderSpace m) t)
242+inputCorrect correct ec = mdo
243+ ie <- inputElement $ ec & inputElementConfig_setValue .~ eNewValue
244+ dV <- holdUniqDyn $ value ie
245+ let
246+ eNewValue = attachWith correct (current dV)
247+ $ leftmost [ec ^. inputElementConfig_setValue, updated dV]
248+ pure ie
249+
250+-- | input element in form "__/__" (e.g. for credit card)
251+inputMonth
252+ :: (MonadFix m, DomBuilder t m, MonadHold t m)
253+ => InputElementConfig er t (DomBuilderSpace m)
254+ -> m (InputElement er (DomBuilderSpace m) t)
255+inputMonth ec = inputCorrect correct $ ec
256+ & initialAttributes %~ (<> "type" =: "text" <> "maxlength" =: "5")
257+ where
258+ correct ov nv = case (T.length ov, T.length nv) of
259+ (x,2) | x < 2 -> nv <> "/"
260+ (x,3) | x > 3 -> T.take 2 nv
261+ (_, x) | x > 2 && T.head (T.drop 2 nv) /= '/' ->
262+ T.take 5 $ T.take 2 nv <> "/" <> T.drop 2 nv
263+ _ -> nv
src/Reflex/Dom/Pagination.hsnew file+173
@@ -0,0 +1,173 @@
1+-- Pagination panel widget.
2+module Reflex.Dom.Pagination where
3+
4+import Control.Monad (join)
5+import qualified Data.Text as T
6+import Reflex.Dom
7+
8+type PageNumber = Int
9+
10+-- | Pagination configuration.
11+data PaginationConfig t m = PaginationConfig
12+ { pagesNumber :: PageNumber
13+ -- ^ Total number of pages.
14+ , initPage :: PageNumber
15+ -- ^ Initial page number.
16+ , pageNumberElem
17+ :: Dynamic t PageNumber
18+ -> PageNumber
19+ -> m (Element EventResult (DomBuilderSpace m) t)
20+ -- ^ Element to display every page number given current page.
21+ , nextPageElem
22+ :: Dynamic t PageNumber
23+ -> m (Element EventResult (DomBuilderSpace m) t)
24+ -- ^ "Next page" element.
25+ , prevPageElem
26+ :: Dynamic t PageNumber
27+ -> m (Element EventResult (DomBuilderSpace m) t)
28+ -- ^ "Previous page" element.
29+ , ellipsisWidget :: m ()
30+ -- ^ Ellipsis between pages.
31+ , pageUpdateEvent :: Event t PageNumber
32+ -- ^ Event to update page number.
33+ , pageNumberSegmentSize :: Int
34+ -- ^ Number of pages after first page, before and after selected page
35+ -- and before last page. Skipped pages are replaced with ellipsis.
36+ , hideWhenEmpty :: Bool
37+ -- ^ Hide pagination panel when there are zero total pages?
38+ }
39+
40+-- | Sanity check for config.
41+checkConfig :: PaginationConfig t m -> Bool
42+checkConfig PaginationConfig{..} =
43+ pagesNumber >= 0 &&
44+ initPage > 0 &&
45+ pageNumberSegmentSize >= 0
46+
47+-- | Default element to display page number (current page is darker).
48+defaultPageNumberWidget
49+ :: MonadWidget t m
50+ => Dynamic t PageNumber
51+ -> PageNumber
52+ -> m (Element EventResult (DomBuilderSpace m) t)
53+defaultPageNumberWidget curDyn pageNum = fmap fst . elDynAttr' "button"
54+ (mkAttrs <$> curDyn) . text . show' $ pageNum
55+ where
56+ show' = T.pack . show
57+ mkAttrs cur = "type" =: "button" <>
58+ if pageNum == cur
59+ then "style" =: "background-color: #CCCCCC;"
60+ else mempty
61+
62+-- | Default element for previous page (button with text "Prev").
63+defaultPrevPage
64+ :: MonadWidget t m
65+ => Dynamic t PageNumber
66+ -> m (Element EventResult (DomBuilderSpace m) t)
67+defaultPrevPage curDyn = fmap fst $
68+ elDynAttr' "button" (mkAttrs <$> curDyn) $ text "Prev"
69+ where
70+ mkAttrs cur = "type" =: "button" <>
71+ if cur == 1
72+ then "disabled" =: "" <> "style" =: "pointer-events:none;"
73+ else mempty
74+
75+-- | Default element for next page (button with text "Next").
76+defaultNextPage
77+ :: MonadWidget t m
78+ => PageNumber
79+ -> Dynamic t PageNumber
80+ -> m (Element EventResult (DomBuilderSpace m) t)
81+defaultNextPage total curDyn = fmap fst $
82+ elDynAttr' "button" (mkAttrs <$> curDyn) $ text "Next"
83+ where
84+ mkAttrs cur = "type" =: "button" <>
85+ if cur == total
86+ then "disabled" =: "" <> "style" =: "pointer-events:none;"
87+ else mempty
88+
89+-- | Default ellipsis.
90+defaultEllipsis
91+ :: MonadWidget t m
92+ => m ()
93+defaultEllipsis = text "..."
94+
95+-- | Default config based on total number of items and page size.
96+defaultPaginationConfig
97+ :: MonadWidget t m
98+ => Int
99+ -> Int
100+ -> PaginationConfig t m
101+defaultPaginationConfig totalItems pageSize = PaginationConfig
102+ { pagesNumber = totalPages
103+ , initPage = 1
104+ , pageNumberElem = defaultPageNumberWidget
105+ , nextPageElem = defaultNextPage totalPages
106+ , prevPageElem = defaultPrevPage
107+ , ellipsisWidget = defaultEllipsis
108+ , pageUpdateEvent = never
109+ , pageNumberSegmentSize = 1
110+ , hideWhenEmpty = True }
111+ where
112+ totalPages = calcPages totalItems pageSize
113+
114+-- | Calculate total number of pages given input length and page size.
115+calcPages :: Int -> Int -> Int
116+calcPages totalSize pageSize = (totalSize + pageSize - 1) `div` pageSize
117+
118+-- | Pagination widget that takes dynamic config (useful when total number
119+-- of items may change dynamically).
120+paginationDyn
121+ :: MonadWidget t m
122+ => Dynamic t (PaginationConfig t m)
123+ -> m (Dynamic t PageNumber)
124+paginationDyn dynCfg = do
125+ pageEvDyn <- dyn $ pagination <$> dynCfg
126+ pageDynDyn <- holdDyn (initPage <$> dynCfg) pageEvDyn
127+ return (join pageDynDyn)
128+
129+-- | Pagination widget that renders page numbers and returns current page
130+-- number.
131+pagination
132+ :: MonadWidget t m
133+ => PaginationConfig t m
134+ -> m (Dynamic t PageNumber)
135+pagination cfg@PaginationConfig{..}
136+ | not (checkConfig cfg) = return (constDyn initPage)
137+ | pagesNumber == 0 && hideWhenEmpty = return (constDyn initPage)
138+ | otherwise = mdo
139+ let shownPagesDyn = mkShownPages <$> currentPage
140+ prevEl <- prevPageElem currentPage
141+ pageSwitchEe <- (fmap leftmost) <$>
142+ (dyn $ mapM (pageSelector currentPage) <$> shownPagesDyn)
143+ pageSelectEvent <- switchHold never pageSwitchEe
144+ nextEl <- nextPageElem currentPage
145+ let
146+ prevPageEvent = domEvent Click prevEl
147+ nextPageEvent = domEvent Click nextEl
148+ currentPage <- foldDyn ($) initPage $ leftmost
149+ [ decPage <$ prevPageEvent
150+ , setPage <$> pageSelectEvent
151+ , incPage <$ nextPageEvent
152+ , const <$> pageUpdateEvent ]
153+ return currentPage
154+ where
155+ decPage n = max 1 (n-1)
156+ incPage n = min (n+1) pagesNumber
157+ setPage newPage currentPage = if newPage `elem` [1 .. pagesNumber]
158+ then newPage
159+ else currentPage
160+ pageSelector _ Nothing = ellipsisWidget >> return never
161+ pageSelector cur (Just n) = do
162+ selElem <- pageNumberElem cur n
163+ return (n <$ domEvent Click selElem)
164+ mkShownPages cur = fixPages $
165+ [1 .. min (1+pageNumberSegmentSize) pagesNumber] ++
166+ [max 1 (cur-pageNumberSegmentSize) ..
167+ min (cur+pageNumberSegmentSize) pagesNumber] ++
168+ [max 1 (pagesNumber-pageNumberSegmentSize) .. pagesNumber]
169+ fixPages (x : y : rest)
170+ | y <= x = fixPages (x : rest)
171+ | x + 1 < y = Just x : Nothing : fixPages (y : rest)
172+ | otherwise = Just x : fixPages (y :rest)
173+ fixPages xs = map Just xs