{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PolyKinds #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} module Shpadoinkle.Widgets.Table ( Sort (..) , SortCol (..) , compareOn , negateSort , Tabular (..) , Column, Row , Theme (..) , toggleSort , view , viewWith ) where import Control.Arrow (second) import Data.Aeson import Data.Functor.Identity import Data.Kind import Data.List (sortBy) import qualified Data.Map as M import Data.Proxy import Data.Text import GHC.Generics import Shpadoinkle import Shpadoinkle.Html hiding (a, a', max, min, s, s', u, u') import qualified Shpadoinkle.Html as Html import Shpadoinkle.Widgets.Types data Sort = ASC | DESC deriving (Show, Eq, Ord, Bounded, Enum, Generic, ToJSON, FromJSON) instance Semigroup Sort where (<>) = min instance Monoid Sort where mempty = maxBound negateSort :: Sort -> Sort negateSort ASC = DESC negateSort DESC = ASC data SortCol a = SortCol (Column a) Sort deriving instance Show (Column a) => Show (SortCol a) deriving instance Eq (Column a) => Eq (SortCol a) deriving instance Ord (Column a) => Ord (SortCol a) deriving instance Functor Column => Functor SortCol deriving instance Generic (SortCol a) instance (ToJSON (Column a)) => ToJSON (SortCol a) instance (FromJSON (Column a)) => FromJSON (SortCol a) instance Ord (Column a) => Semigroup (SortCol a) where SortCol a s <> SortCol a' s' = SortCol (max a a') (min s s') instance ( Bounded (Column a) , Ord (Column a) , Enum (Column a) ) => Monoid (SortCol a) where mempty = SortCol minBound maxBound compareOn :: Ord a => Sort -> a -> a -> Ordering compareOn DESC = compare compareOn ASC = flip compare data family Column (a :: Type) :: Type data family Row (a :: Type) :: Type class Tabular a where type Effect a (m :: Type -> Type) :: Constraint type Effect a m = Applicative m toRows :: a -> [Row a] toFilter :: a -> (Row a -> Bool) toFilter = const (const True) toCell :: Functor m => Effect a m => a -> Row a -> Column a -> [Html m a] sortTable :: SortCol a -> Row a -> Row a -> Ordering ascendingIcon :: Functor m => Effect a m => Proxy a -> Html m (a, SortCol a) ascendingIcon _ = TextNode "↑" descendingIcon :: Functor m => Effect a m => Proxy a -> Html m (a, SortCol a) descendingIcon _ = TextNode "↓" toggleSort :: Eq (Column a) => Column a -> SortCol a -> SortCol a toggleSort c (SortCol c' s) = if c == c' then SortCol c $ negateSort s else SortCol c mempty data Theme m a = Theme { tableProps :: a -> SortCol a -> [(Text, Prop m (a, SortCol a))] , headProps :: a -> SortCol a -> [(Text, Prop m (a, SortCol a))] , htrProps :: a -> SortCol a -> [(Text, Prop m (a, SortCol a))] , trProps :: a -> SortCol a -> Row a -> [(Text, Prop m (a, SortCol a))] , thProps :: a -> SortCol a -> Column a -> [(Text, Prop m (a, SortCol a))] , bodyProps :: a -> SortCol a -> [(Text, Prop m (a, SortCol a))] , tdProps :: a -> SortCol a -> Row a -> Column a -> [(Text, Prop m a)] } deriving Generic instance Semigroup (Theme m a) where Theme t u v w x y z <> Theme t' u' v' w' x' y' z' = Theme (t <> t') (u <> u') (v <> v') (w <> w') (x <> x') (y <> y') (z <> z') instance Monoid (Theme m a) where mempty = Theme mempty mempty mempty mempty mempty mempty mempty view :: forall m a. ( Tabular a , Effect a m , Monad m , Humanize (Column a) , Bounded (Column a) , Ord (Column a) , Enum (Column a) ) => a -> SortCol a -> Html m (a, SortCol a) view = viewWith mempty viewWith :: forall m a. ( Tabular a , Effect a m , Monad m , Humanize (Column a) , Bounded (Column a) , Ord (Column a) , Enum (Column a) ) => Theme m a -> a -> SortCol a -> Html m (a, SortCol a) viewWith Theme {..} xs s@(SortCol sorton sortorder) = table (tableProps xs s) [ thead (headProps xs s) [ tr (htrProps xs s) $ cth_ <$> [minBound..maxBound] ] , tbody (bodyProps xs s) $ do row <- sortBy (sortTable s) (toRows xs) return . filterRow row . tr (trProps xs s row) . fmap leftC $ (\c -> td (tdProps xs s row c) $ toCell xs row c) <$> [minBound..maxBound] ] where f = toFilter xs filterRow :: Row a -> Html m (a, SortCol a) -> Html m (a, SortCol a) filterRow row el = if f row then el else runIdentity $ props (Identity . addDisplayNoneStyle) el addDisplayNoneStyle :: [(Text, Prop m (a, SortCol a))] -> [(Text, Prop m (a, SortCol a))] addDisplayNoneStyle ps = let pMap = M.fromList ps styleProp = M.lookup "style" pMap in case styleProp of Just (PText sty) -> M.toList (M.insert "style" (textProp $ sty <> "; display: none") pMap) _ -> M.toList (M.insert "style" (textProp "display: none") pMap) cth_ c = th (thProps xs s c) . pure . Html.a [ second rightC . onClick $ toggleSort c s ] . mappend [ text (humanize c) ] . pure $ if c == sorton then case sortorder of ASC -> ascendingIcon Proxy; DESC -> descendingIcon Proxy else ""