{-# 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 Data.Aeson import Data.Kind import Data.List (sortBy) import Data.Text import GHC.Generics import Shpadoinkle import Shpadoinkle.Html hiding (a, a', max, min, s, s') 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] toCell :: Effect a m => a -> Row a -> Column a -> [Html m a] sortTable :: SortCol a -> Row a -> Row a -> Ordering 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 :: [(Text, Prop m (a, SortCol a))] , headProps :: [(Text, Prop m (a, SortCol a))] , thProps :: Column a -> [(Text, Prop m (a, SortCol a))] , bodyProps :: [(Text, Prop m (a, SortCol a))] , tdProps :: Column a -> [(Text, Prop m a)] } deriving Generic instance Semigroup (Theme m a) where Theme v w x y z <> Theme v' w' x' y' z' = Theme (v <> v') (w <> w') (x <> x') (y <> y') (z <> z') instance Monoid (Theme m a) where mempty = Theme mempty mempty mempty mempty mempty view :: forall m a. ( Tabular a , Effect a m , Applicative 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 , Applicative 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 [ thead headProps [ tr_ $ cth_ <$> [minBound..maxBound] ] , tbody bodyProps $ do row <- sortBy (sortTable s) (toRows xs) return . (fmap (, s)) . tr_ $ (\c -> td (tdProps c) $ toCell xs row c) <$> [minBound..maxBound] ] where cth_ c = th (thProps c) . pure . Html.a [ onClick (xs, toggleSort c s) ] . mappend [ text (humanize c) ] . pure . text $ if c == sorton then case sortorder of ASC -> "↑"; DESC -> "↓" else ""