{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ExtendedDefaultRules #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE UndecidableSuperClasses #-}
{-# OPTIONS_GHC -Wno-type-defaults #-}
module Shpadoinkle.Widgets.Table.Lazy
( AssumedRowHeight (..)
, AssumedTableHeight (..)
, CurrentScrollY (..)
, LazyTabular (..)
, LazyTable (LazyTable)
, DebounceScroll
, LazyTableScrollConfig (..)
, Offset (..)
, Length (..)
, Page (..)
, RowsToShow (..)
, RowsLoaded (..)
, Paginator (..)
, lazyLoadingTable
, lazyTable
) where
import Prelude hiding (div)
import Control.Arrow (second)
import Control.Monad.Trans.Class (lift)
import Data.Aeson
import Data.List (sortBy)
import Data.Maybe (fromMaybe)
import Data.Proxy
import Data.Text hiding (filter, find, take)
import GHC.Generics
import Language.Javascript.JSaddle hiding (JSM, MonadJSM)
import Shpadoinkle
import Shpadoinkle.Html (div)
import Shpadoinkle.Widgets.Table
import Shpadoinkle.Widgets.Types
default (Text)
second3 :: (b -> b') -> (a, b, c) -> (a, b', c)
second3 f (x, y, z) = (x, f y, z)
class Tabular a => LazyTabular a where
countRows :: a -> Int
data LazyTable a =
LazyTable
{ tableData :: a
, tableHeight :: AssumedTableHeight
, rowHeight :: AssumedRowHeight
, scrollY :: CurrentScrollY
, rowsToShow :: RowsToShow
, _rowsLoaded :: RowsLoaded
, paginator :: Paginator a
, _sortCol :: SortCol a
, rows :: [Row (LazyTable a)]
}
newtype RowsToShow = RowsToShow { unRowsToShow :: Int }
deriving (Eq, Ord, Num, Real, Bounded, Enum, Read, Show, Generic, NFData)
instance ToJSON RowsToShow
instance FromJSON RowsToShow
instance ToJSVal RowsToShow
instance FromJSVal RowsToShow
newtype RowsLoaded = RowsLoaded { unRowsLoaded :: Int }
deriving (Eq, Ord, Num, Real, Bounded, Enum, Read, Show, Generic, NFData)
instance ToJSON RowsLoaded
instance FromJSON RowsLoaded
instance ToJSVal RowsLoaded
instance FromJSVal RowsLoaded
data instance (Row (LazyTable a)) = LazyRow (Row a) | FakeRow
newtype instance (Column (LazyTable a)) = LazyColumn (Column a)
unLazySortCol :: SortCol (LazyTable a) -> SortCol a
unLazySortCol (SortCol (LazyColumn col) ord) = SortCol col ord
instance Humanize (Column a) => Humanize (Column (LazyTable a)) where
humanize (LazyColumn c) = humanize c
instance Bounded (Column a) => Bounded (Column (LazyTable a)) where
minBound = LazyColumn minBound
maxBound = LazyColumn maxBound
instance Eq (Column a) => Eq (Column (LazyTable a)) where
(LazyColumn a) == (LazyColumn b) = a == b
instance Enum (Column a) => Enum (Column (LazyTable a)) where
toEnum = LazyColumn . toEnum
fromEnum (LazyColumn c) = fromEnum c
instance Ord (Column a) => Ord (Column (LazyTable a)) where
compare (LazyColumn a) (LazyColumn b) = compare a b
instance ( Tabular a ) => Tabular (LazyTable a) where
type Effect (LazyTable a) m = Effect a m
toRows LazyTable {rows} = rows ++ [FakeRow]
toCell LazyTable {tableData} (LazyRow r) (LazyColumn c) =
mapToLazyTable <$> toCell tableData r c
toCell _ FakeRow _ = []
sortTable sc (LazyRow a) (LazyRow b) = sortTable (fromLazySortCol sc) a b
sortTable _ FakeRow FakeRow = EQ
sortTable _ _ FakeRow = LT
sortTable _ FakeRow _ = GT
ascendingIcon _ = mapToLazyTableSc $ ascendingIcon Proxy
descendingIcon _ = mapToLazyTableSc $ descendingIcon Proxy
handleSort LazyTable {tableData, rowsToShow, paginator} sc = voidRunContinuationT $ do
tableData' <- lift $ unPaginator paginator tableData (unLazySortCol sc) (Page 0 (Length (unRowsToShow rowsToShow)))
commit . pur $ \(LazyTable _ th' rh' sy' rts' _ paginator' _ _, sc') ->
(LazyTable tableData' th' rh' sy' rts'
(RowsLoaded (unRowsToShow rowsToShow))
paginator'
(unLazySortCol sc')
(LazyRow <$> toRows tableData')
, sc')
newtype AssumedRowHeight = AssumedRowHeight Int
deriving (Eq, Ord, Generic, ToJSON, FromJSON, Read, Show, Num, Enum, Real, Integral, NFData)
newtype AssumedTableHeight = AssumedTableHeight Int
deriving (Eq, Ord, Generic, ToJSON, FromJSON, Read, Show, Num, Enum, Real, Integral, NFData)
type DebounceScroll m a = (RawNode -> RawEvent -> JSM (Continuation m a))
-> (RawNode -> RawEvent -> JSM (Continuation m a))
data LazyTableScrollConfig m a b = ContainerIsScrollable (DebounceScroll m (b, CurrentScrollY, RowsLoaded))
| TbodyIsScrollable (DebounceScroll m (LazyTable a, SortCol (LazyTable a)))
deriving Generic
toLazySortCol :: SortCol a -> SortCol (LazyTable a)
toLazySortCol (SortCol c' s') = SortCol (LazyColumn c') s'
fromLazySortCol :: SortCol (LazyTable a) -> SortCol a
fromLazySortCol (SortCol (LazyColumn c') s') = SortCol c' s'
mapFromLazyTableSc :: Tabular a => Functor m => Continuous f
=> LazyTable a
-> f m (LazyTable a, SortCol (LazyTable a))
-> f m ((a, SortCol a), CurrentScrollY, RowsLoaded)
mapFromLazyTableSc (LazyTable _xs tableHeight rowHeight _sy _rts _rl paginator _sc _rs) = liftC
(\(LazyTable tab _ _ sy _ rl _ _ _, sc') _ -> ((tab, fromLazySortCol sc'), sy, rl))
(\((tab, sc), sy, rl) -> ( toLazyTable tableHeight rowHeight sy rl paginator tab sc
, toLazySortCol sc ))
mapToLazyTable :: forall m a f. Functor m => Continuous f => Tabular a
=> f m a -> f m (LazyTable a)
mapToLazyTable = liftC
(\tab (LazyTable _ tableHeight rowHeight scrollY _ rowsLoaded paginator sc _)
-> toLazyTable tableHeight rowHeight scrollY rowsLoaded paginator tab sc)
(\(LazyTable tab _ _ _ _ _ _ _ _) -> tab)
mapToLazyTableSc :: Functor m => Continuous f => Tabular a
=> f m (a, SortCol a) -> f m (LazyTable a, SortCol (LazyTable a))
mapToLazyTableSc = liftC
(\(tab, sc) (LazyTable _ tableHeight rowHeight scrollY _ rowsLoaded paginator _ _, _)
-> ( toLazyTable tableHeight rowHeight scrollY rowsLoaded paginator tab sc
, toLazySortCol sc ))
(\(LazyTable {tableData}, sc) -> (tableData, fromLazySortCol sc))
toLazyTable :: Tabular a
=> AssumedTableHeight -> AssumedRowHeight -> CurrentScrollY
-> RowsLoaded -> Paginator a -> a -> SortCol a -> LazyTable a
toLazyTable tabh rowh sy rowsLoaded paginator xs sc
= LazyTable xs tabh rowh sy (RowsToShow numRows) rowsLoaded paginator sc
. fmap LazyRow
. take numRows
. sortBy (sortTable sc)
. filter (toFilter xs)
$ toRows xs
where numRows = computeRowsToShow tabh rowh sy
computeRowsToShow :: AssumedTableHeight -> AssumedRowHeight -> CurrentScrollY
-> Int
computeRowsToShow (AssumedTableHeight height) (AssumedRowHeight rowHeight) (CurrentScrollY scrollY) =
1 + truncate (pixelsToFill / fromIntegral rowHeight)
where
pixelsToFill :: Double
pixelsToFill = 8 * fromIntegral height + 1.5 * fromIntegral scrollY
newtype Paginator a = Paginator { unPaginator :: forall m. ( Applicative m, Effect a m ) => a -> SortCol a -> Page -> m a }
trivialPaginator :: Paginator a
trivialPaginator = Paginator (\x _ _ -> pure x)
lazyTable :: forall m a b.
( LazyTabular a
, Monad m
, Effect a m
, Humanize (Column a)
, Bounded (Column a)
, Ord (Column a)
, Enum (Column a) )
=> Theme m a
-> AssumedTableHeight
-> AssumedRowHeight
-> LazyTableScrollConfig m a b
-> (Html m ((a, SortCol a), CurrentScrollY) -> Html m (b, CurrentScrollY))
-> a
-> SortCol a
-> CurrentScrollY
-> Html m (b, CurrentScrollY)
lazyTable theme tableHeight rowHeight scrollConfig container xs sc scrollY
= removeRowsLoaded $
lazyLoadingTable trivialPaginator (RowsLoaded 0) theme tableHeight rowHeight scrollConfig
liftedContainer xs sc scrollY
where
liftedContainer = addRowsLoaded . container . removeRowsLoaded
addRowsLoaded :: Continuous f => f m (x, y) -> f m (x, y, RowsLoaded)
addRowsLoaded = liftC (\(x,y) (_,_,r) -> (x,y,r)) (\(x,y,_) -> (x,y))
removeRowsLoaded :: Continuous f => f m (x, y, RowsLoaded) -> f m (x, y)
removeRowsLoaded = liftC (\(x,y,_) _ -> (x,y)) (\(x,y) -> (x,y,0))
lazyLoadingTable :: forall m a b.
( LazyTabular a
, Monad m
, Effect a m
, Humanize (Column a)
, Bounded (Column a)
, Ord (Column a)
, Enum (Column a) )
=> Paginator a
-> RowsLoaded
-> Theme m a
-> AssumedTableHeight
-> AssumedRowHeight
-> LazyTableScrollConfig m a b
-> (Html m ((a, SortCol a), CurrentScrollY, RowsLoaded)
-> Html m (b, CurrentScrollY, RowsLoaded))
-> a
-> SortCol a
-> CurrentScrollY
-> Html m (b, CurrentScrollY, RowsLoaded)
lazyLoadingTable paginator rowsLoaded theme tableHeight rowHeight@(AssumedRowHeight rowHeight')
scrollConfig container xs sc@(SortCol c s) scrollY =
addContainerScrollHandler
. container
. addContainerFakeHeight
. mapFromLazyTableSc lazyTab
$ viewWith lazyTheme lazyTab (SortCol (LazyColumn c) s)
where
lazyTab@LazyTable {} = toLazyTable tableHeight rowHeight scrollY rowsLoaded paginator xs sc
totalRows = countRows xs
addContainerFakeHeight = case scrollConfig of
ContainerIsScrollable _ -> div [("style", textProp fakeHeightStyle)] . (:[])
TbodyIsScrollable _ -> id
addContainerScrollHandler = case scrollConfig of
ContainerIsScrollable debounceScroll ->
mapProps ([listenRaw "scroll" (debounceScroll scrollHandlerContainer)] <>)
TbodyIsScrollable _ -> id
scrollHandlerContainer (RawNode n) _ =
pur . second3 . const . CurrentScrollY . fromMaybe 0
<$> (fromJSVal =<< n ! "scrollTop")
scrollHandlerTbody :: RawNode -> RawEvent -> JSM (Continuation m (LazyTable a, SortCol (LazyTable a)))
scrollHandlerTbody (RawNode n) _ = do
sy <- CurrentScrollY . fromMaybe 0 <$> (fromJSVal =<< n ! "scrollTop")
let totalRows' = computeRowsToShow tableHeight rowHeight sy
offset = Offset $ unRowsLoaded rowsLoaded
newRows = Length $ totalRows' - unRowsLoaded rowsLoaded
if newRows > 0
then return . voidRunContinuationT $ do
xs' <- lift $ unPaginator paginator xs sc (Page offset newRows)
commit . pur $ \(LazyTable {tableHeight = tableHeight', rowHeight = rowHeight'', paginator = paginator'}, sc') ->
(LazyTable xs' tableHeight' rowHeight'' sy
(RowsToShow totalRows')
(RowsLoaded totalRows')
paginator'
(unLazySortCol sc')
(LazyRow <$> toRows xs')
, sc')
else return . pur $ \(tab, sc') -> (tab { scrollY = sy }, sc')
fakeHeightStyle =
"height: " <> pack (show (totalRows * rowHeight')) <> "px;"
fakeRowHeightStyle totalRows' (RowsToShow rts) =
"height: " <> pack (show ((totalRows' - rts) * rowHeight')) <> "px;"
lazyTheme :: Theme m (LazyTable a)
lazyTheme = case theme of
Theme tp hp hrp rp thp bp dp -> Theme
{ tableProps = \LazyTable {tableData = xs'} sc' ->
second mapToLazyTableSc <$> tp xs' (fromLazySortCol sc')
, headProps = \LazyTable {tableData = xs'} sc' ->
second mapToLazyTableSc <$> hp xs' (fromLazySortCol sc')
, htrProps = \LazyTable {tableData = xs'} sc' ->
second mapToLazyTableSc <$> hrp xs' (fromLazySortCol sc')
, trProps = \LazyTable {tableData = xs', rowsToShow = rts} sc' r ->
case r of
LazyRow r' -> second mapToLazyTableSc <$> rp xs' (fromLazySortCol sc') r'
FakeRow -> [("style", textProp (fakeRowHeightStyle (countRows xs') rts))]
, thProps = \LazyTable {tableData = xs'} sc' (LazyColumn c') ->
second mapToLazyTableSc <$> thp xs' (fromLazySortCol sc') c'
, bodyProps = \LazyTable {tableData = xs'} sc' ->
(second mapToLazyTableSc <$> bp xs' (fromLazySortCol sc'))
++
(case scrollConfig of
ContainerIsScrollable _ -> []
TbodyIsScrollable debounceScroll -> [ listenRaw "scroll" $ debounceScroll scrollHandlerTbody ])
, tdProps = \LazyTable {tableData = xs'} sc' r (LazyColumn c') ->
case r of
LazyRow r' -> second mapToLazyTable <$> dp xs' (fromLazySortCol sc') r' c'
FakeRow -> [] }