git.iko.soy/typeable/reflex-dom-extra
on master help sign in
5 files changed+514
since splitsnapshots
unifiedsplit
default.nix+28 reflex-dom-extra.cabal+35 shell.nix+1 src/Reflex/Dom/Extra.hs+277 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+ , lens
22+ , reflex-dom
23+ , semialign
24+ , text
25+ hs-source-dirs: src
26+ default-language: Haskell2010
27+ ghc-options: -Wall
28+ default-extensions: FlexibleContexts
29+ , MonoLocalBinds
30+ , OverloadedStrings
31+ , RecordWildCards
32+ , RecursiveDo
33+ , ViewPatterns
34+ if impl(ghc >= 8.10)
35+ ghc-options: -Wunused-packages
shell.nixnew file+1
@@ -0,0 +1 @@
1+(import ./default.nix { }).shells.ghc
src/Reflex/Dom/Extra.hsnew file+277
@@ -0,0 +1,277 @@
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.Bitraversable
7+import Data.Char
8+import Data.Either
9+import Data.Map as M
10+import Data.Maybe
11+import Data.Text as T hiding (zip, map)
12+import Data.Zip as Z
13+-- import Language.Javascript.JSaddle
14+import Prelude hiding (zip, unzip, zipWith)
15+import Reflex.Dom
16+-- import JavaScript.Object.Internal as JS
17+
18+-- | @<meta>@ element.
19+meta :: DomBuilder t m => Map Text Text -> m ()
20+meta attrs = elAttr "meta" attrs blank
21+
22+-- | Stylesheet link.
23+stylesheet :: DomBuilder t m => Text -> m ()
24+stylesheet ref = elAttr "link" ("rel" =: "stylesheet" <> "href" =: ref) blank
25+
26+-- | <script> tag. Do not use in dynamic contexts, or it will be executed twice.
27+script :: DomBuilder t m => Text -> m ()
28+script url = elAttr "script" ("type" =: "text/javascript" <> "src" =: url) blank
29+
30+-- | @<main>@ element with given CSS class.
31+mainClass :: DomBuilder t m => Text -> m a -> m a
32+mainClass = elClass "main"
33+
34+-- | @<span>@ element with given CSS class.
35+spanClass :: DomBuilder t m => Text -> m a -> m a
36+spanClass = elClass "span"
37+
38+-- | @<label>@ element with given CSS class.
39+labelClass :: DomBuilder t m => Text -> m a -> m a
40+labelClass = elClass "label"
41+
42+-- | @<section>@ element with given CSS class.
43+sectionClass :: DomBuilder t m => Text -> m a -> m a
44+sectionClass = elClass "section"
45+
46+-- | @<b>@ element with given CSS class.
47+bClass :: DomBuilder t m => Text -> m a -> m a
48+bClass = elClass "b"
49+
50+-- | @<tr>@ element with given CSS class.
51+trClass :: DomBuilder t m => Text -> m a -> m a
52+trClass = elClass "tr"
53+
54+-- | @<th>@ element with given CSS class.
55+thClass :: DomBuilder t m => Text -> m a -> m a
56+thClass = elClass "th"
57+
58+-- | @<td>@ element with given CSS class.
59+tdClass :: DomBuilder t m => Text -> m a -> m a
60+tdClass = elClass "td"
61+
62+-- | Line break (@<br>@).
63+br :: DomBuilder t m => m ()
64+br = el "br" blank
65+
66+-- | Button element, returns click event.
67+button
68+ :: DomBuilder t m
69+ => Text
70+ -- ^ Label.
71+ -> Map Text Text
72+ -- ^ Attributes.
73+ -> m (Event t ())
74+button label attrs = do
75+ (e, _) <- elAttr' "button" (attrs <> "type" =: "button") $ text label
76+ return $ domEvent Click e
77+
78+-- | Button element with only "class" attribute.
79+buttonClass
80+ :: DomBuilder t m
81+ => Text
82+ -- ^ Label.
83+ -> Text
84+ -- ^ CSS class.
85+ -> m (Event t ())
86+buttonClass label cls = do
87+ (e, _) <- elAttr' "button" ("class" =: cls <> "type" =: "button") $ text label
88+ return $ domEvent Click e
89+
90+-- | Button element with dynamic attributes.
91+buttonDynAttrs
92+ :: (DomBuilder t m, PostBuild t m)
93+ => Text
94+ -- ^ Label.
95+ -> Dynamic t (Map Text Text)
96+ -- ^ Attributes.
97+ -> m (Event t ())
98+buttonDynAttrs label dynAttrs = buttonDyn (constDyn label) dynAttrs
99+
100+-- | Button element with dynamic label.
101+buttonDynLabel
102+ :: (DomBuilder t m, PostBuild t m)
103+ => Dynamic t Text
104+ -- ^ Label.
105+ -> Map Text Text
106+ -- ^ Attributes.
107+ -> m (Event t ())
108+buttonDynLabel dynLabel attrs = buttonDyn dynLabel (constDyn attrs)
109+
110+-- | Button element with dynamic label and attributes.
111+buttonDyn
112+ :: (DomBuilder t m, PostBuild t m)
113+ => Dynamic t Text
114+ -- ^ Label.
115+ -> Dynamic t (Map Text Text)
116+ -- ^ Attributes.
117+ -> m (Event t ())
118+buttonDyn dynLabel dynAttrs = do
119+ (e, _) <- elDynAttr' "button" ((<> "type" =: "button") <$> dynAttrs)
120+ $ dynText dynLabel
121+ return $ domEvent Click e
122+
123+-- | Dynamic list widget that creates a list that supports the dynamic
124+-- addition and removal of items. This widget is completely general with zero
125+-- markup-specific choices. It handles all the event plumbing and lets you
126+-- completely determine the markup.
127+dynamicList
128+ :: MonadWidget t m
129+ => (Int -> a -> Event t a -> m b)
130+ -- ^ Widget used to display each item
131+ -> (b -> Event t ())
132+ -- ^ Function that gets a remove event from the return value of each item
133+ -> Event t a
134+ -- ^ Event that adds a new item to the list that is not based on an
135+ -- existing item.
136+ -> [a]
137+ -- ^ Initial list of items
138+ -> m (Dynamic t [b])
139+dynamicList w removeEvent addEvent initList = do
140+ let initMap = M.fromList $ zip [0..] initList
141+ rec
142+ let
143+ vals = mergeWith (<>)
144+ [ attachWith addNew (current res) addEvent
145+ , remove (current res) ]
146+ res <- listWithKeyShallowDiff initMap vals w
147+ return $ M.elems <$> res
148+ where
149+ addNew m a = M.singleton k (Just a)
150+ where
151+ k = if M.null m then 0 else fst (M.findMax m) + 1
152+ remove res = switch (mergeWith (<>) . fmap f . M.toList <$> res)
153+ where
154+ f (k,b) = M.singleton k Nothing <$ removeEvent b
155+
156+-- | Build widget using text value, if any. Otherwise stays blank.
157+maybeBlank :: DomBuilder t m => Maybe Text -> (Text -> m ()) -> m ()
158+maybeBlank mt w = maybe blank w mt
159+
160+-- | "cursor: pointer" style.
161+pointer :: Map Text Text
162+pointer = "style" =: "cursor:pointer;"
163+
164+table :: DomBuilder t m => m a -> m a
165+table = el "table"
166+
167+thead :: DomBuilder t m => m a -> m a
168+thead = el "thead"
169+
170+tbody :: DomBuilder t m => m a -> m a
171+tbody = el "tbody"
172+
173+th :: DomBuilder t m => m a -> m a
174+th = el "th"
175+
176+tr :: DomBuilder t m => m a -> m a
177+tr = el "tr"
178+
179+td :: DomBuilder t m => m a -> m a
180+td = el "td"
181+
182+form :: DomBuilder t m => Map Text Text -> m a -> m a
183+form attrs = elAttr "form" attrs
184+
185+-- | Join event of event into single event.
186+joinE :: (Reflex t, MonadHold t m) => Event t (Event t a) -> m (Event t a)
187+joinE = switchHold never
188+
189+joinEE
190+ :: (Reflex t, MonadHold t m)
191+ => Event t (Event t a, Event t b) -> m (Event t a, Event t b)
192+joinEE = bitraverse joinE joinE . Z.unzip
193+
194+-- | Separate an event to two sequences based on given predicate.
195+-- The first element of resulting pair keeps events for which the predicate
196+-- holds, the second -- those for which it doesn't.
197+separateE
198+ :: Reflex t
199+ => (a -> Bool)
200+ -> Event t a
201+ -> (Event t a, Event t a)
202+separateE p e = (ffilter p e, ffilter (not . p) e)
203+
204+-- | Separate an event with optional payload.
205+-- The first event in the resulting pair keeps @Just@s, the seconds keeps
206+-- @Nothing@s.
207+separateMaybeE
208+ :: Reflex t
209+ => Event t (Maybe a)
210+ -> (Event t (Maybe a), Event t (Maybe a))
211+separateMaybeE = separateE isJust
212+
213+-- | Filter only events with existing payload.
214+fromMaybeE
215+ :: Reflex t
216+ => Event t (Maybe a)
217+ -> Event t a
218+fromMaybeE = fmapMaybe id
219+
220+-- | Separate an event with alternative payload.
221+-- The first event in the resulting pair keeps @Left@s, the seconds keeps
222+-- @Right@s.
223+separateEitherE
224+ :: Reflex t
225+ => Event t (Either a b)
226+ -> (Event t (Either a b), Event t (Either a b))
227+separateEitherE = separateE isLeft
228+
229+{-
230+-- | Assigns HTML content to an element, allows embedding of arbitrary
231+-- HTML tags, so make sure you trust the source of the second argument
232+-- @
233+-- el <- _element_raw . fst <$> el' "span" blank
234+-- unsafeInnerHTML el "this text will be <i>italicised</i>"
235+-- @
236+unsafeInnerHTML :: (MonadJSM m, ToJSVal rawelem) => rawelem -> Text -> m ()
237+unsafeInnerHTML rel html = liftJSM $ do
238+ htmlVal <- toJSVal html
239+ relVal <- toJSVal rel
240+ JS.setProp "innerHTML" htmlVal (JS.Object relVal)
241+-}
242+
243+-- | input element which is changes while edited.
244+-- It is useful to input dates, phones etc
245+inputCorrect
246+ :: (MonadFix m, DomBuilder t m, MonadHold t m)
247+ => (Text -> Text -> Text) -- ^ old value and new value
248+ -> InputElementConfig er t (DomBuilderSpace m)
249+ -> m (InputElement er (DomBuilderSpace m) t)
250+inputCorrect correct ec = mdo
251+ ie <- inputElement $ ec & inputElementConfig_setValue .~ eNewValue
252+ dV <- holdUniqDyn $ value ie
253+ let
254+ eNewValue = attachWith correct (current dV)
255+ $ leftmost [ec ^. inputElementConfig_setValue, updated dV]
256+ pure ie
257+
258+-- | input element in form "__/__" (e.g. for credit card)
259+inputMonth
260+ :: (MonadFix m, DomBuilder t m, MonadHold t m)
261+ => InputElementConfig er t (DomBuilderSpace m)
262+ -> m (InputElement er (DomBuilderSpace m) t)
263+inputMonth ec = inputCorrect correct2 $ ec
264+ & initialAttributes %~ (<> "type" =: "text" <> "maxlength" =: "5")
265+ where
266+ correct2 (T.length -> ol) (T.foldl' f "" -> nv)
267+ | ol < 2 && nl == 2 = nv `T.snoc` '/'
268+ | ol > 3 && nl == 3 = T.init nv
269+ | otherwise = nv
270+ where
271+ nl = T.length nv
272+ f r c
273+ | (lr == 2 && isDigit c) = r `T.snoc` '/' `T.snoc` c
274+ | (lr == 2 && c == '/') || (isDigit c && lr < 5) = T.snoc r c
275+ | otherwise = r
276+ where
277+ lr = T.length r
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