{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NoMonomorphismRestriction #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecursiveDo #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeFamilies #-} module Reflex.Dom.Contrib.Widgets.BoundedList ( boundedSelectList , boundedSelectList' , mkHiding , keyToMaybe ) where ------------------------------------------------------------------------------ import Control.Applicative import Control.Monad import Data.Bifunctor import Data.List import Data.Map (Map) import qualified Data.Map as M import Data.Monoid import Reflex import Reflex.Dom ------------------------------------------------------------------------------ import Reflex.Contrib.Interfaces import Reflex.Contrib.Utils import Reflex.Dom.Contrib.Utils ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ findCurItem :: Ord k => Map k v -> k -> Maybe (k,v) findCurItem m k = M.lookupLE k m <|> M.lookupGT k m -- Limit on the number of items in the DOM. Might make this more -- sophisticated in the future. type Limit = Maybe Int -- An Int counter that we use in lieu of a timestamp for LRU calculations type BornAt = Int ------------------------------------------------------------------------------ limitMap :: Ord k => Map k v -> Limit -> Map k v limitMap m Nothing = m limitMap m (Just lim) = M.fromList $ take lim $ M.toList m ------------------------------------------------------------------------------ boundedInsert :: Ord k => Limit -> (BornAt, (k,v)) -> Map k (BornAt,v) -> Map k (BornAt,v) boundedInsert Nothing (c, (k, v)) m = M.insert k (c,v) m boundedInsert (Just lim) (c, (k, v)) m = if M.size m < lim then ins m else ins pruned where ins = M.insert k (c,v) pruned = M.fromList $ tail $ sortOn (fst . snd) $ M.toList m ------------------------------------------------------------------------------ -- | A widget with generalized handling for dynamically sized lists. There -- are many possible approaches to rendering lists that have one visible -- current selection. One way is to keep all the items in the DOM and manage -- the selection by managing visibility through something like display:none or -- visibility:hidden. Another way is to only keep the currently selected item -- in the DOM and swap it out every time the selection is changed. -- -- The problem with keeping all items in the DOM is that this might use too -- much memory either because there are many items or the items are large. -- The problem with keeping only the currently selected item in the DOM is -- that performance might be too slow if removing the old item's DOM elements -- and building the new one takes too long. -- -- This widget provides a middle ground. It lets the user decide how many -- elements are kept in the DOM at any one time and prunes the least recently -- used items if that size is exceeded. boundedSelectList' :: (MonadWidget t m, Show k, Ord k, Show v) => Limit -- ^ Maximum number of items to keep in the DOM at a time -> Dynamic t k -- ^ Currently selected item -> Event t (Map k v -> Map k v) -- ^ Event that updates individual item values -> ReflexMap t k v -- ^ Interface for updating the list -> (k -> Dynamic t v -> Dynamic t Bool -> m a) -- ^ Function to render a single item -> m (Dynamic t (Map k a)) boundedSelectList' itemLimit curSelected updateEvent ReflexMap{..} renderSingle = do -- Map holding the full item list. items <- foldDyn ($) rmInitialItems $ leftmost [ M.union . M.fromList <$> rmInsertItems , rmDeleteFunc <$> rmDeleteItems , updateEvent ] counter <- count $ updated curSelected curItem <- combineDyn findCurItem items curSelected let addCounter c (k,v) = (k, ((-c), (k, v))) taggedInitial = M.fromList $ zipWith addCounter [1..] $ M.toList rmInitialItems let initMap = limitMap taggedInitial itemLimit activeItems <- foldDyn ($) initMap $ boundedInsert itemLimit <$> attachDynWith (\c (k,v) -> (c, (k, (k,v)))) counter (fmapMaybe id $ updated curItem) listWithKeyAndSelection curSelected activeItems wrapSingle where --wrapSingle :: k -> Dynamic t (BornAt, (k,v)) -> Dynamic t Bool -> m a wrapSingle k v b = do v' <- mapDyn (snd . snd) =<< filterDyn (\x -> fst (snd x) == k) v renderSingle k v' b ------------------------------------------------------------------------------ -- | Implements a common use of boundedSelectList' where only the currently -- selected item from a list is displayed. In this case a Dynamic -- representing the current selection is used to drive insertions and they are -- never deleted externally. Instead of returning a Map of all the item -- results, this function only returns the result for the item that is -- currently selected. boundedSelectList0 :: (MonadWidget t m, Show k, Ord k, Show v) => Limit -- ^ Maximum number of items to keep in the DOM at a time -> Dynamic t a -- ^ Currently selected item. New items are added to the list when the -- currently selected item changes and the new item is not already in the -- list. -> Event t (Map k v -> Map k v) -> (a -> k) -- ^ Gets the portion of a used as the key for the map of items -> (a -> Maybe a) -- ^ Decides whether to run expensiveGetNew in the case that the key is -- already in the cache. -> (Event t a -> m (Event t (k,v))) -- ^ Gets a new key/value pair. This function is run when curSelected -- changes. -> (k -> Dynamic t v -> Dynamic t Bool -> m b) -- ^ Function to render a single item -> m (Dynamic t (Map k b)) boundedSelectList0 itemLimit curSelected updateEvent getKey shouldRunExpensive expensiveGetNew renderSingle = do pb <- getPostBuild rec let insertEvent = leftmost [ fmapMaybe id $ attachDynWith isAlreadyPresent res (updated curSelected) , tagDyn curSelected pb ] newVal <- expensiveGetNew insertEvent let rm = ReflexMap mempty ((:[]) <$> newVal) never curK <- mapDyn getKey curSelected res :: Dynamic t (Map k b) <- boundedSelectList' itemLimit curK updateEvent rm renderSingle return res where isAlreadyPresent fieldListMap cur = case M.lookup (getKey cur) fieldListMap of Nothing -> Just cur Just _ -> shouldRunExpensive cur ------------------------------------------------------------------------------ -- | Implements a common use of boundedSelectList' where only the currently -- selected item from a list is displayed. In this case a Dynamic -- representing the current selection is used to drive insertions and they are -- never deleted externally. Instead of returning a Map of all the item -- results, this function only returns the result for the item that is -- currently selected. boundedSelectList :: (MonadWidget t m, Show k, Ord k, Show v) => Limit -- ^ Maximum number of items to keep in the DOM at a time -> Dynamic t a -- ^ Currently selected item. New items are added to the list when the -- currently selected item changes and the new item is not already in the -- list. -> Event t (Map k v -> Map k v) -> (a -> k) -- ^ Gets the portion of a used as the key for the map of items -> (a -> Maybe a) -- ^ Decides whether to run expensiveGetNew in the case that the key is -- already in the cache. -> (Event t a -> m (Event t (k,v))) -- ^ Gets a new key/value pair. This function is run when curSelected -- changes. -> b -- ^ Default value to return if nothing is in the list -> (k -> Dynamic t v -> Dynamic t Bool -> m b) -- ^ Function to render a single item -> m (Dynamic t b) boundedSelectList itemLimit curSelected updateEvent getKey shouldRunExpensive expensiveGetNew defaultVal renderSingle = do res <- boundedSelectList0 itemLimit curSelected updateEvent getKey shouldRunExpensive expensiveGetNew renderSingle combineDyn getCurrent curSelected res where getCurrent cur listMap = case M.lookup (getKey cur) listMap of Nothing -> defaultVal Just v -> v ------------------------------------------------------------------------------ -- | Wraps a widget with a dynamically hidden div that uses display:none to -- hide. mkHiding :: (MonadWidget t m) => Map String String -> m a -> Dynamic t Bool -- ^ Function of a dynamic active flag -> m a mkHiding staticAttrs w active = do attrs <- mapDyn mkAttrs active elDynAttr "div" attrs w where mkAttrs True = staticAttrs mkAttrs False = staticAttrs <> "style" =: "display:none" ------------------------------------------------------------------------------ -- | Small helper for a common pattern that comes up with the expensiveGetNew -- parameter to boundedSelectList. keyToMaybe :: MonadWidget t m => (Event t a -> m (Event t (b,c))) -> Event t (Maybe a) -> m (Event t (Maybe b, c)) keyToMaybe f = liftM (fmap $ first Just) . f . fmapMaybe id