yesod-paginator 0.11.0 → 1.1.0.0
raw patch · 11 files changed
+751/−336 lines, 11 filesdep +blaze-markupdep +doctestdep +path-piecesdep −data-defaultdep −resourcetdep −wai-extradep ~basedep ~persistentdep ~yesodPVP ok
version bump matches the API change (PVP)
Dependencies added: blaze-markup, doctest, path-pieces, safe, uri-encode, yesod-persistent
Dependencies removed: data-default, resourcet, wai-extra
Dependency ranges changed: base, persistent, yesod, yesod-core
API changes (from Hackage documentation)
- Yesod.Paginator: paginateWith :: Yesod m => PageWidget m -> Int -> [a] -> HandlerT m IO ([a], WidgetT m IO ())
- Yesod.Paginator: selectPaginatedWith :: (PersistEntity val, PersistEntityBackend val ~ BaseBackend (YesodPersistBackend m), PersistQuery (YesodPersistBackend m), Yesod m) => PageWidget m -> Int -> [Filter val] -> [SelectOpt val] -> YesodDB m ([Entity val], WidgetT m IO ())
- Yesod.Paginator.Widget: Disabled :: Text -> Text -> PageLink
- Yesod.Paginator.Widget: Enabled :: Int -> Text -> Text -> PageLink
- Yesod.Paginator.Widget: PageWidgetConfig :: Text -> Text -> Int -> Bool -> Bool -> [Text] -> PageWidgetConfig
- Yesod.Paginator.Widget: [ascending] :: PageWidgetConfig -> Bool
- Yesod.Paginator.Widget: [listClasses] :: PageWidgetConfig -> [Text]
- Yesod.Paginator.Widget: [nextText] :: PageWidgetConfig -> Text
- Yesod.Paginator.Widget: [pageCount] :: PageWidgetConfig -> Int
- Yesod.Paginator.Widget: [prevText] :: PageWidgetConfig -> Text
- Yesod.Paginator.Widget: [showEllipsis] :: PageWidgetConfig -> Bool
- Yesod.Paginator.Widget: data PageLink
- Yesod.Paginator.Widget: data PageWidgetConfig
- Yesod.Paginator.Widget: defaultPageWidgetConfig :: PageWidgetConfig
- Yesod.Paginator.Widget: defaultWidget :: Yesod m => PageWidget m
- Yesod.Paginator.Widget: getCurrentPage :: Yesod m => HandlerT m IO Int
- Yesod.Paginator.Widget: paginationWidget :: Yesod m => PageWidgetConfig -> PageWidget m
- Yesod.Paginator.Widget: showLink :: [(Text, Text)] -> PageLink -> WidgetT m IO ()
- Yesod.Paginator.Widget: simplePaginationWidget :: Yesod m => PageWidgetConfig -> PageWidget m
- Yesod.Paginator.Widget: type PageWidget m = Int -> Int -> Int -> WidgetT m IO ()
+ Yesod.Paginator: data ItemsCount
+ Yesod.Paginator: data Page a
+ Yesod.Paginator: data PageNumber
+ Yesod.Paginator: data Pages a
+ Yesod.Paginator: data PerPage
+ Yesod.Paginator: ellipsed :: Natural -> PaginationWidget m a
+ Yesod.Paginator: pageItems :: Page a -> [a]
+ Yesod.Paginator: pagesCurrent :: Pages a -> Page a
+ Yesod.Paginator: simple :: Natural -> PaginationWidget m a
+ Yesod.Paginator.Pages: data ItemsCount
+ Yesod.Paginator.Pages: data Page a
+ Yesod.Paginator.Pages: data PageNumber
+ Yesod.Paginator.Pages: data Pages a
+ Yesod.Paginator.Pages: data PerPage
+ Yesod.Paginator.Pages: getNextPage :: Pages a -> Maybe PageNumber
+ Yesod.Paginator.Pages: getPreviousPage :: Pages a -> Maybe PageNumber
+ Yesod.Paginator.Pages: instance GHC.Classes.Eq Yesod.Paginator.Pages.ItemsCount
+ Yesod.Paginator.Pages: instance GHC.Classes.Eq Yesod.Paginator.Pages.PageNumber
+ Yesod.Paginator.Pages: instance GHC.Classes.Eq Yesod.Paginator.Pages.PerPage
+ Yesod.Paginator.Pages: instance GHC.Classes.Eq a => GHC.Classes.Eq (Yesod.Paginator.Pages.Page a)
+ Yesod.Paginator.Pages: instance GHC.Classes.Eq a => GHC.Classes.Eq (Yesod.Paginator.Pages.Pages a)
+ Yesod.Paginator.Pages: instance GHC.Classes.Ord Yesod.Paginator.Pages.ItemsCount
+ Yesod.Paginator.Pages: instance GHC.Classes.Ord Yesod.Paginator.Pages.PageNumber
+ Yesod.Paginator.Pages: instance GHC.Classes.Ord Yesod.Paginator.Pages.PerPage
+ Yesod.Paginator.Pages: instance GHC.Enum.Enum Yesod.Paginator.Pages.ItemsCount
+ Yesod.Paginator.Pages: instance GHC.Enum.Enum Yesod.Paginator.Pages.PageNumber
+ Yesod.Paginator.Pages: instance GHC.Enum.Enum Yesod.Paginator.Pages.PerPage
+ Yesod.Paginator.Pages: instance GHC.Num.Num Yesod.Paginator.Pages.ItemsCount
+ Yesod.Paginator.Pages: instance GHC.Num.Num Yesod.Paginator.Pages.PageNumber
+ Yesod.Paginator.Pages: instance GHC.Num.Num Yesod.Paginator.Pages.PerPage
+ Yesod.Paginator.Pages: instance GHC.Read.Read Yesod.Paginator.Pages.ItemsCount
+ Yesod.Paginator.Pages: instance GHC.Read.Read Yesod.Paginator.Pages.PerPage
+ Yesod.Paginator.Pages: instance GHC.Real.Integral Yesod.Paginator.Pages.ItemsCount
+ Yesod.Paginator.Pages: instance GHC.Real.Integral Yesod.Paginator.Pages.PageNumber
+ Yesod.Paginator.Pages: instance GHC.Real.Integral Yesod.Paginator.Pages.PerPage
+ Yesod.Paginator.Pages: instance GHC.Real.Real Yesod.Paginator.Pages.ItemsCount
+ Yesod.Paginator.Pages: instance GHC.Real.Real Yesod.Paginator.Pages.PageNumber
+ Yesod.Paginator.Pages: instance GHC.Real.Real Yesod.Paginator.Pages.PerPage
+ Yesod.Paginator.Pages: instance GHC.Show.Show Yesod.Paginator.Pages.ItemsCount
+ Yesod.Paginator.Pages: instance GHC.Show.Show Yesod.Paginator.Pages.PageNumber
+ Yesod.Paginator.Pages: instance GHC.Show.Show Yesod.Paginator.Pages.PerPage
+ Yesod.Paginator.Pages: instance GHC.Show.Show a => GHC.Show.Show (Yesod.Paginator.Pages.Page a)
+ Yesod.Paginator.Pages: instance GHC.Show.Show a => GHC.Show.Show (Yesod.Paginator.Pages.Pages a)
+ Yesod.Paginator.Pages: instance Text.Blaze.ToMarkup Yesod.Paginator.Pages.PageNumber
+ Yesod.Paginator.Pages: instance Web.PathPieces.PathPiece Yesod.Paginator.Pages.ItemsCount
+ Yesod.Paginator.Pages: instance Web.PathPieces.PathPiece Yesod.Paginator.Pages.PerPage
+ Yesod.Paginator.Pages: pageItems :: Page a -> [a]
+ Yesod.Paginator.Pages: pageNumber :: Page a -> PageNumber
+ Yesod.Paginator.Pages: pageOffset :: PageNumber -> PerPage -> ItemsCount
+ Yesod.Paginator.Pages: pagesCurrent :: Pages a -> Page a
+ Yesod.Paginator.Pages: pagesLast :: Pages a -> PageNumber
+ Yesod.Paginator.Pages: takeNextPages :: Natural -> Pages a -> [PageNumber]
+ Yesod.Paginator.Pages: takePreviousPages :: Natural -> Pages a -> [PageNumber]
+ Yesod.Paginator.Pages: toPage :: [a] -> PageNumber -> Page a
+ Yesod.Paginator.Pages: toPages :: PageNumber -> PerPage -> ItemsCount -> [a] -> Pages a
+ Yesod.Paginator.Paginate: paginate :: Yesod site => PerPage -> [a] -> HandlerFor site (Pages a)
+ Yesod.Paginator.Paginate: paginate' :: PerPage -> [a] -> PageNumber -> Pages a
+ Yesod.Paginator.Paginate: selectPaginated :: (PersistEntity val, PersistEntityBackend val ~ BaseBackend (YesodPersistBackend site), PersistQuery (YesodPersistBackend site), Yesod site) => PerPage -> [Filter val] -> [SelectOpt val] -> YesodDB site (Pages (Entity val))
+ Yesod.Paginator.Paginate: selectPaginated' :: (MonadIO m, PersistEntity record, PersistEntityBackend record ~ BaseBackend backend, PersistQueryRead backend) => PerPage -> [Filter record] -> [SelectOpt record] -> PageNumber -> ReaderT backend m (Pages (Entity record))
+ Yesod.Paginator.Prelude: instance Web.PathPieces.PathPiece GHC.Natural.Natural
+ Yesod.Paginator.Prelude: nubOn :: Eq b => (a -> b) -> [a] -> [a]
+ Yesod.Paginator.Prelude: tshow :: Show a => a -> Text
+ Yesod.Paginator.Widgets: ellipsed :: Natural -> PaginationWidget m a
+ Yesod.Paginator.Widgets: simple :: Natural -> PaginationWidget m a
+ Yesod.Paginator.Widgets: type PaginationWidget site a = Pages a -> WidgetFor site ()
- Yesod.Paginator: paginate :: Yesod m => Int -> [a] -> HandlerT m IO ([a], WidgetT m IO ())
+ Yesod.Paginator: paginate :: Yesod site => PerPage -> [a] -> HandlerFor site (Pages a)
- Yesod.Paginator: selectPaginated :: (PersistEntity val, PersistEntityBackend val ~ BaseBackend (YesodPersistBackend m), PersistQuery (YesodPersistBackend m), Yesod m) => Int -> [Filter val] -> [SelectOpt val] -> YesodDB m ([Entity val], WidgetT m IO ())
+ Yesod.Paginator: selectPaginated :: (PersistEntity val, PersistEntityBackend val ~ BaseBackend (YesodPersistBackend site), PersistQuery (YesodPersistBackend site), Yesod site) => PerPage -> [Filter val] -> [SelectOpt val] -> YesodDB site (Pages (Entity val))
Files
- doctest/Main.hs +6/−0
- example/Main.hs +21/−14
- src/Yesod/Paginator.hs +49/−98
- src/Yesod/Paginator/Pages.hs +150/−0
- src/Yesod/Paginator/Paginate.hs +92/−0
- src/Yesod/Paginator/Prelude.hs +34/−0
- src/Yesod/Paginator/Widget.hs +0/−173
- src/Yesod/Paginator/Widgets.hs +170/−0
- test/SpecHelper.hs +39/−0
- test/Yesod/Paginator/WidgetsSpec.hs +100/−0
- yesod-paginator.cabal +90/−51
+ doctest/Main.hs view
@@ -0,0 +1,6 @@+module Main (main) where++import Test.DocTest++main :: IO ()+main = doctest ["-isrc", "src/"]
example/Main.hs view
@@ -1,13 +1,15 @@-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE MultiParamTypeClasses #-}-module Main where+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# OPTIONS_GHC -fno-warn-unused-top-binds #-} +module Main (main) where++import Network.Wai.Handler.Warp (run) import Yesod import Yesod.Paginator-import Network.Wai.Handler.Warp (run) data App = App @@ -29,27 +31,32 @@ <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" integrity="sha256-7s5uDGW3AHqw6xtJmNNtr+OBRJUlgkNJEo78P4b0yRw= sha512-nNo+yCHEyn0smMxSswnf/OnX6/KwJuZTlNZBjauKhTK0c+zT+q5JOCx0UFhXQ6rJR9jg6Es8gPuD2uZcYDLqSw==" crossorigin="anonymous"> ^{pageHead pc} <body>- ^{pageBody pc}+ <div .container>+ ^{pageBody pc} |] getRootR :: Handler Html getRootR = do- -- unneeded return here to match README- things' <- return [1..1142] :: Handler [Int]+ let things' = [1..1142] :: [Int] - (things, widget) <- paginate 3 things'+ pages <- paginate 3 things' defaultLayout $ do setTitle "My title" [whamlet|$newline never- <h1>Pagination- <p>The things:+ <h1>Pagination Examples+ <h2>The things: <ul>- $forall thing <- things+ $forall thing <- pageItems $ pagesCurrent pages <li>Thing #{show thing} + <h2>Simple navigation <div .pagination>- ^{widget}+ ^{simple 10 pages}++ <h2>Ellipsed navigation+ <div .pagination>+ ^{ellipsed 10 pages} |] main :: IO ()
src/Yesod/Paginator.hs view
@@ -1,113 +1,64 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE TypeFamilies #-}-------------------------------------------------------------------------------- -- | ----- Inspiration from a concept by ajdunlap:--- <http://hackage.haskell.org/package/yesod-paginate>+-- There are two pagination functions. One for arbitrary items where you provide+-- the list of things to be paginated: ----- But uses an entirely different approach.+-- @+-- getSomeRoute = do+-- -- 10 items per page+-- pages <- 'paginate' 10 =<< getAllThings ----- There are two pagination functions. One for arbitrary items where you--- provide the list of things to be paginated:+-- defaultLayout $ do+-- [whamlet|+-- $forall thing <- 'pageItems' $ 'pagesCurrent' pages+-- ^{showThing thing} ----- > getSomeRoute = do--- > things' <- getAllThings--- >--- > (things, widget) <- paginate 10 things'--- >--- > defaultLayout $ do--- > [whamlet|--- > $forall thing <- things--- > ^{showThing thing}--- >--- > <div .pagination>--- > ^{widget}--- > |]+-- $# display at most 5 page elements, with current page middle-ish+-- ^{'simple' 5 pages}+-- |]+-- @ ----- And another for paginating directly out of the database, you provide--- the same filters as you would to @selectList@.+-- And another for paginating directly out of the database, you provide the same+-- arguments as you would for @'selectList'@: ----- > getSomeRoute something = do--- > -- note: things is [Entity val] just like selectList returns--- > (things, widget) <- runDB $ selectPaginated 10 [SomeThing ==. something] []--- >--- > defaultLayout $ do--- > [whamlet|--- > $forall thing <- things--- > ^{showThing $ entityVal thing}--- >--- > <div .pagination>--- > ^{widget}--- > |]+-- @+-- getSomeRoute something = do+-- pages <- runDB $ 'selectPaginated' 10 [SomeThing ==. something] [] ----- Both functions return a tuple: the first element being the list of--- items (or Entities) to display on this page and the second being a--- widget showing the pagination navagation links.+-- defaultLayout $ do+-- [whamlet|+-- $forall thing <- 'pageItems' $ 'pagesCurrent' pages+-- ^{showThing $ entityVal thing} ----------------------------------------------------------------------------------+-- ^{'simple' 5 pages}+-- |]+-- @+-- module Yesod.Paginator- ( paginate- , paginateWith- , selectPaginated- , selectPaginatedWith- , module Yesod.Paginator.Widget- ) where--import Yesod-import Yesod.Paginator.Widget--paginate :: Yesod m => Int -> [a] -> HandlerT m IO ([a], WidgetT m IO ())-paginate = paginateWith defaultWidget--paginateWith :: Yesod m- => PageWidget m- -> Int- -> [a]- -> HandlerT m IO ([a], WidgetT m IO ())-paginateWith widget per items = do- p <- getCurrentPage+ (+ -- * Type-safe numerics+ PageNumber+ , PerPage+ , ItemsCount - let tot = length items- let xs = take per $ drop ((p - 1) * per) items+ -- * Paginated data+ , Pages+ , pagesCurrent - return (xs, widget p per tot)+ -- * The current page+ , Page+ , pageItems -selectPaginated :: ( PersistEntity val-#if MIN_VERSION_persistent(2, 5, 0)- , PersistEntityBackend val ~ BaseBackend (YesodPersistBackend m)-#else- , PersistEntityBackend val ~ YesodPersistBackend m-#endif- , PersistQuery (YesodPersistBackend m)- , Yesod m- )- => Int- -> [Filter val]- -> [SelectOpt val]- -> YesodDB m ([Entity val], WidgetT m IO ())-selectPaginated = selectPaginatedWith defaultWidget+ -- * Paginators+ , paginate+ , selectPaginated -selectPaginatedWith :: ( PersistEntity val-#if MIN_VERSION_persistent(2, 5, 0)- , PersistEntityBackend val ~ BaseBackend (YesodPersistBackend m)-#else- , PersistEntityBackend val ~ YesodPersistBackend m-#endif- , PersistQuery (YesodPersistBackend m)- , Yesod m- )- => PageWidget m- -> Int- -> [Filter val]- -> [SelectOpt val]- -> YesodDB m ([Entity val], WidgetT m IO ())-selectPaginatedWith widget per filters selectOpts = do- p <- lift getCurrentPage- tot <- count filters- xs <- selectList filters (selectOpts ++ [OffsetBy ((p-1)*per), LimitTo per])+ -- * Widgets+ , simple+ , ellipsed+ )+where - return (xs, widget p per tot)+import Yesod.Paginator.Pages+import Yesod.Paginator.Paginate+import Yesod.Paginator.Widgets
+ src/Yesod/Paginator/Pages.hs view
@@ -0,0 +1,150 @@+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++module Yesod.Paginator.Pages+ (+ -- * Type safe @'Natural'@s+ -- |+ --+ -- N.B. @'PageNumber'@ and @'PerPage'@ will currently allow @0@, but it's+ -- unclear if that's correct and may not be the case in the future.+ --+ PageNumber+ , PerPage+ , ItemsCount+ , pageOffset++ -- * Page+ , Page+ , pageItems+ , pageNumber+ , toPage++ -- * Pages+ , Pages+ , pagesCurrent+ , pagesLast+ , toPages++ -- * Safely accessing Pages data+ , takePreviousPages+ , takeNextPages+ , getPreviousPage+ , getNextPage+ )+where++import Yesod.Paginator.Prelude++import Text.Blaze (ToMarkup)+import Web.PathPieces++newtype PageNumber = PageNumber Natural+ deriving (Enum, Eq, Integral, Num, Ord, Real)+ deriving newtype (Show, ToMarkup)++newtype PerPage = PerPage Natural+ deriving (Enum, Eq, Integral, Num, Ord, Real)+ deriving newtype (Read, Show, PathPiece)++newtype ItemsCount = ItemsCount Natural+ deriving (Enum, Eq, Integral, Num, Ord, Real)+ deriving newtype (Read, Show, PathPiece)++data Page a = Page+ { pageItems :: [a]+ , pageNumber :: PageNumber+ }+ deriving (Eq, Show)++-- | @'Page'@ constructor+toPage :: [a] -> PageNumber -> Page a+toPage = Page++data Pages a = Pages+ { pagesCurrent :: Page a+ , pagesPrevious :: [PageNumber]+ , pagesNext :: [PageNumber]+ , pagesLast :: PageNumber+ }+ deriving (Eq, Show)++-- | Take previous pages, going back from current+--+-- >>> takePreviousPages 3 $ Pages (Page [] 5) [1,2,3,4] [6] 6+-- [2,3,4]+--+takePreviousPages :: Natural -> Pages a -> [PageNumber]+takePreviousPages n = reverse . genericTake n . reverse . pagesPrevious++-- | Take next pages, going forward from current+--+-- >>> takeNextPages 3 $ Pages (Page [] 2) [1] [3,4,5,6] 6+-- [3,4,5]+--+takeNextPages :: Natural -> Pages a -> [PageNumber]+takeNextPages n = genericTake n . pagesNext++-- | The previous page number, if it exists+--+-- >>> getPreviousPage $ Pages (Page [] 1) [] [2,3,4] 4+-- Nothing+--+-- >>> getPreviousPage $ Pages (Page [] 2) [1] [3,4] 4+-- Just 1+--+getPreviousPage :: Pages a -> Maybe PageNumber+getPreviousPage pages = do+ let prevPage = pageNumber (pagesCurrent pages) - 1+ firstPage <- headMay $ pagesPrevious pages+ prevPage <$ guard (prevPage >= firstPage)++-- | The next page number, if it exists+--+-- >>> getNextPage $ Pages (Page [] 4) [1,2,3] [] 4+-- Nothing+--+-- >>> getNextPage $ Pages (Page [] 3) [1,2] [4] 4+-- Just 4+--+getNextPage :: Pages a -> Maybe PageNumber+getNextPage pages = do+ let nextPage = pageNumber (pagesCurrent pages) + 1+ lastPage <- lastMay $ pagesNext pages+ nextPage <$ guard (nextPage <= lastPage)++-- | Construct a @'Pages' a@ from paginated data+--+-- >>> toPages 4 3 10 []+-- Pages {pagesCurrent = Page {pageItems = [], pageNumber = 4}, pagesPrevious = [1,2,3], pagesNext = [], pagesLast = 4}+--+toPages :: PageNumber -> PerPage -> ItemsCount -> [a] -> Pages a+toPages number per total items = Pages+ { pagesCurrent = toPage items number+ , pagesPrevious = [1 .. (number - 1)]+ , pagesNext = [(number + 1) .. lastPage]+ , pagesLast = lastPage+ }+ where lastPage = getLastPage total per++-- | Calculate the last page of some paginated data+--+-- >>> getLastPage 10 3+-- 4+--+-- >>> getLastPage 10 5+-- 2+--+getLastPage :: ItemsCount -> PerPage -> PageNumber+getLastPage total = fromIntegral . carry . (total `divMod`) . fromIntegral+ where+ carry (q, 0) = q+ carry (q, _) = q + 1++-- | Calculate a page's zero-based offset in the overall items+--+-- >>> pageOffset 4 3+-- 9+--+pageOffset :: PageNumber -> PerPage -> ItemsCount+pageOffset p per = fromIntegral $ (fromIntegral p - 1) * per
+ src/Yesod/Paginator/Paginate.hs view
@@ -0,0 +1,92 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies #-}++module Yesod.Paginator.Paginate+ ( paginate+ , paginate'+ , selectPaginated+ , selectPaginated'+ )+where++import Yesod.Paginator.Prelude++import Control.Monad.Trans.Reader (ReaderT)+import Database.Persist+import Yesod.Core+import Yesod.Paginator.Pages+import Yesod.Persist.Core++-- | Paginate a list of items+paginate :: Yesod site => PerPage -> [a] -> HandlerFor site (Pages a)+paginate per items = paginate' per items <$> getCurrentPage++-- | A version where the current page is given+--+-- This can be used to avoid the monadic context altogether.+--+-- >>> paginate' 3 ([1..10] :: [Int]) 1+-- Pages {pagesCurrent = Page {pageItems = [1,2,3], pageNumber = 1}, pagesPrevious = [], pagesNext = [2,3,4], pagesLast = 4}+--+-- >>> paginate' 3 ([1..10] :: [Int]) 2+-- Pages {pagesCurrent = Page {pageItems = [4,5,6], pageNumber = 2}, pagesPrevious = [1], pagesNext = [3,4], pagesLast = 4}+--+-- >>> paginate' 3 ([1..10] :: [Int]) 3+-- Pages {pagesCurrent = Page {pageItems = [7,8,9], pageNumber = 3}, pagesPrevious = [1,2], pagesNext = [4], pagesLast = 4}+--+-- >>> paginate' 3 ([1..10] :: [Int]) 4+-- Pages {pagesCurrent = Page {pageItems = [10], pageNumber = 4}, pagesPrevious = [1,2,3], pagesNext = [], pagesLast = 4}+--+-- >>> paginate' 3 ([1..10] :: [Int]) 5+-- Pages {pagesCurrent = Page {pageItems = [], pageNumber = 5}, pagesPrevious = [1,2,3,4], pagesNext = [], pagesLast = 4}+--+paginate' :: PerPage -> [a] -> PageNumber -> Pages a+paginate' per items p =+ toPages p per (genericLength items) $ genericTake per $ genericDrop+ (pageOffset p per)+ items++-- | Paginate out of a persistent database+selectPaginated+ :: ( PersistEntity val+ , PersistEntityBackend val ~ BaseBackend (YesodPersistBackend site)+ , PersistQuery (YesodPersistBackend site)+ , Yesod site+ )+ => PerPage+ -> [Filter val]+ -> [SelectOpt val]+ -> YesodDB site (Pages (Entity val))+selectPaginated per filters options =+ selectPaginated' per filters options =<< lift getCurrentPage++-- | A version where the current page is given+--+-- This can be used to avoid the Yesod context+--+selectPaginated'+ :: ( MonadIO m+ , PersistEntity record+ , PersistEntityBackend record ~ BaseBackend backend+ , PersistQueryRead backend+ )+ => PerPage+ -> [Filter record]+ -> [SelectOpt record]+ -> PageNumber+ -> ReaderT backend m (Pages (Entity record))+selectPaginated' per filters options p =+ toPages p per <$> (fromIntegral <$> count filters) <*> selectList+ filters+ (options+ <> [ OffsetBy $ fromIntegral $ pageOffset p per+ , LimitTo $ fromIntegral per+ ]+ )++getCurrentPage :: MonadHandler m => m PageNumber+getCurrentPage = fromMaybe 1 . go <$> lookupGetParam "p"+ where+ go :: Maybe Text -> Maybe PageNumber+ go mp = readIntegral . unpack =<< mp
+ src/Yesod/Paginator/Prelude.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE TypeApplications #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Yesod.Paginator.Prelude+ ( module X+ , module Yesod.Paginator.Prelude+ )+where++import Prelude as X++import Control.Monad as X+import Data.List as X+import Data.Maybe as X+import Data.Semigroup as X ((<>))+import Data.Text as X (Text, pack, unpack)+import Numeric.Natural as X+import Safe as X++import Data.Function (on)+import Web.PathPieces++instance PathPiece Natural where+ toPathPiece = toPathPiece @Int . fromIntegral+ fromPathPiece p = do+ n <- fromPathPiece @Int p+ guard $ n >= 0+ pure $ fromIntegral n++tshow :: Show a => a -> Text+tshow = pack . show++nubOn :: Eq b => (a -> b) -> [a] -> [a]+nubOn f = nubBy ((==) `on` f)
− src/Yesod/Paginator/Widget.hs
@@ -1,173 +0,0 @@-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE FlexibleContexts #-}-module Yesod.Paginator.Widget- ( getCurrentPage- , paginationWidget- , defaultWidget- , defaultPageWidgetConfig- , simplePaginationWidget- , PageWidget- , PageWidgetConfig(..)- , PageLink(..)- , showLink- ) where--import Yesod-import Control.Monad (when, liftM)-import Data.Maybe (fromMaybe)-import Data.Text (Text)--import qualified Data.Text as T---- | currentPage, itemsPerPage, totalItems -> widget-type PageWidget m = Int -> Int -> Int -> WidgetT m IO ()--data PageWidgetConfig = PageWidgetConfig- { prevText :: Text -- ^ The text for the 'previous page' link.- , nextText :: Text -- ^ The text for the 'next page' link.- , pageCount :: Int -- ^ The number of page links to show- , ascending :: Bool -- ^ Whether to list pages in ascending order.- , showEllipsis :: Bool -- ^ Whether to show an ellipsis if there are- -- more pages than pageCount- , listClasses :: [Text] -- ^ Additional classes for top level list- }---- | Individual links to pages need to follow strict (but sane) markup--- to be styled correctly by bootstrap. This type allows construction--- of such links in both enabled and disabled states.-data PageLink = Enabled Int Text Text -- ^ page, content, class- | Disabled Text Text -- ^ content, class---- | Correctly show one of the constructed links-showLink :: [(Text, Text)] -> PageLink -> WidgetT m IO ()-showLink params (Enabled pg cnt cls) = do- let param = ("p", showT pg)-- [whamlet|$newline never- <li .#{cls}>- <a href="#{updateGetParam params param}">#{cnt}- |]-- where- updateGetParam :: [(Text,Text)] -> (Text,Text) -> Text- updateGetParam getParams (p, n) = T.cons '?' . T.intercalate "&"- . map (\(k,v) -> k `T.append` "=" `T.append` v)- . (++ [(p, n)]) . filter ((/= p) . fst) $ getParams--showLink _ (Disabled cnt cls) =- [whamlet|$newline never- <li .#{cls} .disabled>- <a>#{cnt}- |]---- | Default widget config provided for easy overriding of only some fields.-defaultPageWidgetConfig :: PageWidgetConfig-defaultPageWidgetConfig = PageWidgetConfig { prevText = "«"- , nextText = "»"- , pageCount = 9- , ascending = True- , showEllipsis = True- , listClasses = ["pagination"]- }--defaultWidget :: Yesod m => PageWidget m-defaultWidget = paginationWidget defaultPageWidgetConfig---- | A widget showing pagination links. Follows bootstrap principles.--- Utilizes a \"p\" GET param but leaves all other GET params intact.-paginationWidget :: Yesod m => PageWidgetConfig -> PageWidget m-paginationWidget PageWidgetConfig {..} page per tot = do- -- total / per + 1 for any remainder- let pages = (\(n, r) -> n + min r 1) $ tot `divMod` per-- when (pages > 1) $ do- curParams <- handlerToWidget $ liftM reqGetParams getRequest-- [whamlet|$newline never- <ul class="#{cls}">- $forall link <- buildLinks page pages- ^{showLink curParams link}- |]-- where- -- | Concatenate all additional classes.- cls = T.intercalate " " listClasses-- -- | Build up each component of the overall list of links. We'll- -- use empty lists to denote ommissions along the way then- -- concatenate.- buildLinks :: Int -> Int -> [PageLink]- buildLinks pg pgs =- let prev = [1 .. pg - 1]- next = [pg + 1 .. pgs ]-- -- these always appear- prevLink = [(if null prev then Disabled else Enabled (pg - 1)) prevText "prev"]- nextLink = [(if null next then Disabled else Enabled (pg + 1)) nextText "next"]-- -- show first/last unless we're on it- firstLink = [ Enabled 1 "1" "prev" | pg > 1 ]- lastLink = [ Enabled pgs (showT pgs) "next" | pg < pgs ]-- -- we'll show ellipsis if there are enough links that some will- -- be ommitted from the list- prevEllipsis = [ Disabled "..." "prev" | showEllipsis && length prev > pageCount + 1 ]- nextEllipsis = [ Disabled "..." "next" | showEllipsis && length next > pageCount + 1 ]-- -- the middle lists, strip the first/last pages and- -- correctly take up to limit away from current- prevLinks = reverse . take pageCount . reverse . drop 1 $ map (\p -> Enabled p (showT p) "prev") prev- nextLinks = take pageCount . reverse . drop 1 . reverse $ map (\p -> Enabled p (showT p) "next") next-- -- finally, this page itself- curLink = [Disabled (showT pg) "active"]-- in concat $ (if ascending then id else reverse) [ prevLink- , firstLink- , prevEllipsis- , prevLinks- , curLink- , nextLinks- , nextEllipsis- , lastLink- , nextLink- ]---- | A simple widget that only shows next and previous links. Follows bootstrap--- principles. Utilizes a \"p\" GET param but leaves all other GET params--- intact. Uses the same config data type as the main pagination widget, but--- ignores all of the values with the exception of the text for the next and--- previous links.-simplePaginationWidget :: Yesod m => PageWidgetConfig -> PageWidget m-simplePaginationWidget PageWidgetConfig {..} page per tot = do- -- total number of pages- let pages = (\(n, r) -> n + min r 1) $ tot `divMod` per-- when (pages > 1) $ do- curParams <- handlerToWidget $ liftM reqGetParams getRequest-- [whamlet|$newline never- <ul class="pagination">- $forall link <- buildLinks page pages- ^{showLink curParams link}- |]-- where- buildLinks :: Int -> Int -> [PageLink]- buildLinks pg pgs =- [ Enabled (pg - 1) prevText "prev" | pg > 1 ] ++- [ Enabled (pg + 1) nextText "next" | pg < pgs ]---- | looks up the \"p\" GET param and converts it to an Int. returns a--- default of 1 when conversion fails.-getCurrentPage :: Yesod m => HandlerT m IO Int-getCurrentPage = liftM (fromMaybe 1 . go) $ lookupGetParam "p"-- where- go :: Maybe Text -> Maybe Int- go mp = readIntegral . T.unpack =<< mp--showT :: (Show a) => a -> Text-showT = T.pack . show
+ src/Yesod/Paginator/Widgets.hs view
@@ -0,0 +1,170 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}++module Yesod.Paginator.Widgets+ ( PaginationWidget+ , simple+ , ellipsed+ )+where++import Yesod.Paginator.Prelude++import qualified Data.Text as T+import Network.URI.Encode (encodeText)+import Yesod.Core+import Yesod.Paginator.Pages++type PaginationWidget site a = Pages a -> WidgetFor site ()++-- | Simple widget, limited to show the given number of total page elements+--+-- Pseudo-HTML for @'simple' 5@, on page 1:+--+-- @+-- \<ul .pagination>+-- \<li .prev .disabled>\<a>«+-- \<li .active .disabled>\<a>1+-- \<li .next>\<a href=\"?p=2\">2+-- \<li .next>\<a href=\"?p=3\">3+-- \<li .next>\<a href=\"?p=4\">4+-- \<li .next>\<a href=\"?p=5\">5+-- \<li .next>\<a href=\"?p=2\">»+-- @+--+-- And page 7:+--+-- @+-- \<ul .pagination>+-- \<li .prev>\<a href=\"?p=6\">«+-- \<li .prev>\<a href=\"?p=5\">5+-- \<li .prev>\<a href=\"?p=6\">6+-- \<li .active .disabled>\<a>7+-- \<li .next>\<a href=\"?p=8\">8+-- \<li .next>\<a href=\"?p=9\">9+-- \<li .next>\<a href=\"?p=8\">»+-- @+--+simple :: Natural -> PaginationWidget m a+simple elements pages = do+ updateGetParams <- getUpdateGetParams++ let (prevPages, nextPages) = getBalancedPages elements pages+ mPrevPage = getPreviousPage pages+ mNextPage = getNextPage pages++ [whamlet|$newline never+ <ul .pagination>+ $maybe prevPage <- mPrevPage+ <li .prev>+ <a href=#{renderGetParams $ updateGetParams prevPage}>«+ $nothing+ <li .prev .disabled>+ <a>«+ $forall number <- prevPages+ <li .prev >+ <a href=#{renderGetParams $ updateGetParams number}>#{number}+ $with number <- pageNumber $ pagesCurrent pages+ <li .active .disabled>+ <a>#{number}+ $forall number <- nextPages+ <li .next>+ <a href=#{renderGetParams $ updateGetParams number}>#{number}+ $maybe nextPage <- mNextPage+ <li .next>+ <a href=#{renderGetParams $ updateGetParams nextPage}>»+ $nothing+ <li .next .disabled>+ <a>»+ |]++-- | Show pages before and after, ellipsis, and first/last+ellipsed :: Natural -> PaginationWidget m a+ellipsed elements pages = do+ updateGetParams <- getUpdateGetParams++ let (prevPages, nextPages) = getBalancedPages elements pages++ mPrevPage = getPreviousPage pages+ mNextPage = getNextPage pages++ (mFirstPage, firstEllipses)+ | pageNumber (pagesCurrent pages) == 1 = (Nothing, False)+ | headMay prevPages == Just 1 = (Nothing, False)+ | headMay prevPages == Just 2 = (Just 1, False)+ | otherwise = (Just 1, True)++ (mLastPage, lastEllipses)+ | pageNumber (pagesCurrent pages) == pagesLast pages = (Nothing, False)+ | lastMay nextPages == Just (pagesLast pages) = (Nothing, False)+ | lastMay nextPages == Just (pagesLast pages - 1) = (Just $ pagesLast pages, False)+ | otherwise = (Just $ pagesLast pages, True)++ [whamlet|$newline never+ <ul .pagination>+ $maybe prevPage <- mPrevPage+ <li .prev>+ <a href=#{renderGetParams $ updateGetParams prevPage}>«+ $nothing+ <li .prev .disabled>+ <a>«+ $maybe firstPage <- mFirstPage+ <li .prev>+ <a href=#{renderGetParams $ updateGetParams firstPage}>#{firstPage}+ $if firstEllipses+ <li .prev .disabled>+ <a>…+ $forall number <- prevPages+ <li .prev >+ <a href=#{renderGetParams $ updateGetParams number}>#{number}+ $with number <- pageNumber $ pagesCurrent pages+ <li .active .disabled>+ <a>#{number}+ $forall number <- nextPages+ <li .next>+ <a href=#{renderGetParams $ updateGetParams number}>#{number}+ $maybe lastPage <- mLastPage+ $if lastEllipses+ <li .next .disabled>+ <a>…+ <li .next>+ <a href=#{renderGetParams $ updateGetParams lastPage}>#{lastPage}+ $maybe nextPage <- mNextPage+ <li .next>+ <a href=#{renderGetParams $ updateGetParams nextPage}>»+ $nothing+ <li .next .disabled>+ <a>»+ |]++-- | Calculate previous and next pages to produce an overall number of elements+--+-- >>> let page n = toPages n 2 20 [] :: Pages Int+-- >>> getBalancedPages 6 $ page 1+-- ([],[2,3,4,5,6])+--+-- >>> getBalancedPages 6 $ page 6+-- ([3,4,5],[7,8])+--+-- >>> getBalancedPages 6 $ page 10+-- ([5,6,7,8,9],[])+--+getBalancedPages :: Natural -> Pages a -> ([PageNumber], [PageNumber])+getBalancedPages elements pages =+ if genericLength nextPages >= (elements `div` 2)+ then (prevPagesNaive, nextPages)+ else (prevPagesCalcd, nextPages)+ where+ nextPages = takeNextPages (elements - genericLength prevPagesNaive - 1) pages+ prevPagesNaive = takePreviousPages (elements `div` 2) pages+ prevPagesCalcd = takePreviousPages (elements - genericLength nextPages - 1) pages++getUpdateGetParams :: WidgetFor site (PageNumber -> [(Text, Text)])+getUpdateGetParams = do+ params <- handlerToWidget $ reqGetParams <$> getRequest+ pure $ \number -> nubOn fst $ [("p", tshow number)] <> params++renderGetParams :: [(Text, Text)] -> Text+renderGetParams [] = ""+renderGetParams ps = "?" <> T.intercalate "&" (map renderGetParam ps)+ where renderGetParam (k, v) = encodeText k <> "=" <> encodeText v
+ test/SpecHelper.hs view
@@ -0,0 +1,39 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}++module SpecHelper+ ( module SpecHelper+ , module X+ )+where++import Test.Hspec as X+import Yesod.Core+import Yesod.Paginator as X+import Yesod.Paginator.Prelude as X+import Yesod.Test as X++data App = App++mkYesod "App" [parseRoutes|+ /simple/#ItemsCount/#PerPage/#Natural SimpleR GET+ /ellipsed/#ItemsCount/#PerPage/#Natural EllipsedR GET+|]++instance Yesod App++getSimpleR :: ItemsCount -> PerPage -> Natural -> Handler Html+getSimpleR total per elements = do+ pages <- paginate per $ genericReplicate total ()+ defaultLayout [whamlet|^{simple elements pages}|]++getEllipsedR :: ItemsCount -> PerPage -> Natural -> Handler Html+getEllipsedR total per elements = do+ pages <- paginate per $ genericReplicate total ()+ defaultLayout [whamlet|^{ellipsed elements pages}|]++withApp :: SpecWith (TestApp App) -> Spec+withApp = before $ pure (App, id)
+ test/Yesod/Paginator/WidgetsSpec.hs view
@@ -0,0 +1,100 @@+{-# LANGUAGE OverloadedStrings #-}+module Yesod.Paginator.WidgetsSpec+ ( spec+ )+where++import SpecHelper++spec :: Spec+spec = withApp $ do+ describe "simple" $ it "works" $ do+ get $ SimpleR 10 3 3++ statusIs 200+ bodyContains $ concat+ [ "<ul class=\"pagination\">"+ , "<li class=\"prev disabled\"><a>«</a></li>"+ , "<li class=\"active disabled\"><a>1</a></li>"+ , "<li class=\"next\"><a href=\"?p=2\">2</a></li>"+ , "<li class=\"next\"><a href=\"?p=3\">3</a></li>"+ , "<li class=\"next\"><a href=\"?p=2\">»</a></li>"+ , "</ul>"+ ]++ request $ do+ addGetParam "p" "3"+ setUrl $ SimpleR 10 3 3++ statusIs 200+ bodyContains $ concat+ [ "<ul class=\"pagination\">"+ , "<li class=\"prev\"><a href=\"?p=2\">«</a></li>"+ , "<li class=\"prev\"><a href=\"?p=2\">2</a></li>"+ , "<li class=\"active disabled\"><a>3</a></li>"+ , "<li class=\"next\"><a href=\"?p=4\">4</a></li>"+ , "<li class=\"next\"><a href=\"?p=4\">»</a></li>"+ , "</ul>"+ ]++ request $ do+ addGetParam "p" "4"+ setUrl $ SimpleR 10 3 3++ statusIs 200+ bodyContains $ concat+ [ "<ul class=\"pagination\">"+ , "<li class=\"prev\"><a href=\"?p=3\">«</a></li>"+ , "<li class=\"prev\"><a href=\"?p=2\">2</a></li>"+ , "<li class=\"prev\"><a href=\"?p=3\">3</a></li>"+ , "<li class=\"active disabled\"><a>4</a></li>"+ , "<li class=\"next disabled\"><a>»</a></li>"+ , "</ul>"+ ]++ describe "ellipsed" $ it "works" $ do+ get $ EllipsedR 10 3 3++ statusIs 200+ bodyContains $ concat+ [ "<ul class=\"pagination\">"+ , "<li class=\"prev disabled\"><a>«</a></li>"+ , "<li class=\"active disabled\"><a>1</a></li>"+ , "<li class=\"next\"><a href=\"?p=2\">2</a></li>"+ , "<li class=\"next\"><a href=\"?p=3\">3</a></li>"+ , "<li class=\"next\"><a href=\"?p=4\">4</a></li>"+ , "<li class=\"next\"><a href=\"?p=2\">»</a></li>"+ , "</ul>"+ ]++ get $ EllipsedR 15 3 3++ statusIs 200+ bodyContains $ concat+ [ "<ul class=\"pagination\">"+ , "<li class=\"prev disabled\"><a>«</a></li>"+ , "<li class=\"active disabled\"><a>1</a></li>"+ , "<li class=\"next\"><a href=\"?p=2\">2</a></li>"+ , "<li class=\"next\"><a href=\"?p=3\">3</a></li>"+ , "<li class=\"next disabled\"><a>…</a></li>"+ , "<li class=\"next\"><a href=\"?p=5\">5</a></li>"+ , "<li class=\"next\"><a href=\"?p=2\">»</a></li>"+ , "</ul>"+ ]++ request $ do+ addGetParam "p" "5"+ setUrl $ EllipsedR 15 3 3++ statusIs 200+ bodyContains $ concat+ [ "<ul class=\"pagination\">"+ , "<li class=\"prev\"><a href=\"?p=4\">«</a></li>"+ , "<li class=\"prev\"><a href=\"?p=1\">1</a></li>"+ , "<li class=\"prev disabled\"><a>…</a></li>"+ , "<li class=\"prev\"><a href=\"?p=3\">3</a></li>"+ , "<li class=\"prev\"><a href=\"?p=4\">4</a></li>"+ , "<li class=\"active disabled\"><a>5</a></li>"+ , "<li class=\"next disabled\"><a>»</a></li>"+ , "</ul>"+ ]
yesod-paginator.cabal view
@@ -1,61 +1,100 @@-name: yesod-paginator-version: 0.11.0-synopsis: A pagination approach for yesod-description: Paginate a list showing a per-item widget and links to other pages-homepage: http://github.com/pbrisbin/yesod-paginator-license: BSD3-license-file: LICENSE-author: Patrick Brisbin-maintainer: pbrisbin@gmail.com-category: Web, Yesod-build-type: Simple-cabal-version: >=1.8+-- This file has been generated from package.yaml by hpack version 0.28.2.+--+-- see: https://github.com/sol/hpack+--+-- hash: 4dbcdeb4bab34224d2c65b70f8a52d4759b83a23c55b5e77e4e616bce3152296 -flag example- description: Build the example application- default: False+name: yesod-paginator+version: 1.1.0.0+synopsis: A pagination approach for yesod+description: Paginate a list showing a per-item widget and links to other pages+category: Web, Yesod+homepage: http://github.com/pbrisbin/yesod-paginator+author: Patrick Brisbin+maintainer: pbrisbin@gmail.com+license: BSD3+license-file: LICENSE+build-type: Simple+cabal-version: >= 1.10 -library- hs-source-dirs: src- exposed-modules: Yesod.Paginator- Yesod.Paginator.Widget+source-repository head+ type: git+ location: git://github.com/pbrisbin/yesod-paginator.git - build-depends: base >= 4 && < 5- , text >= 0.11 && < 2.0- , yesod >= 1.4- , persistent >= 2.0- , resourcet >= 0.4.4- , transformers+flag examples+ description: Build the examples+ manual: False+ default: False +library+ exposed-modules:+ Yesod.Paginator+ Yesod.Paginator.Pages+ Yesod.Paginator.Paginate+ Yesod.Paginator.Prelude+ Yesod.Paginator.Widgets+ other-modules:+ Paths_yesod_paginator+ hs-source-dirs:+ src+ default-extensions: NoImplicitPrelude ghc-options: -Wall--test-suite test- type: exitcode-stdio-1.0- main-is: Spec.hs- hs-source-dirs: test- ghc-options: -Wall- build-depends: base- , hspec- , yesod-paginator- , data-default- , wai-extra- , yesod-core- , yesod-test+ build-depends:+ base >4.8.0 && <5+ , blaze-markup+ , path-pieces+ , persistent >=2.5+ , safe+ , text >=0.11 && <2.0+ , transformers+ , uri-encode+ , yesod-core >=1.4+ , yesod-persistent >=1.4+ default-language: Haskell2010 executable yesod-paginator-example- if flag(example)- buildable: True- else+ main-is: Main.hs+ other-modules:+ Paths_yesod_paginator+ hs-source-dirs:+ example+ ghc-options: -Wall+ build-depends:+ base >4.8.0 && <5+ , warp+ , yesod+ , yesod-paginator+ if !(flag(examples)) buildable: False+ default-language: Haskell2010 - main-is: Main.hs- hs-source-dirs: example- ghc-options: -Wall- build-depends: base- , yesod- , yesod-paginator- , warp+test-suite doctests+ type: exitcode-stdio-1.0+ main-is: Main.hs+ other-modules:+ Paths_yesod_paginator+ hs-source-dirs:+ doctest+ ghc-options: -Wall+ build-depends:+ base >4.8.0 && <5+ , doctest+ default-language: Haskell2010 -source-repository head- type: git- location: git://github.com/pbrisbin/yesod-paginator.git+test-suite test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ SpecHelper+ Yesod.Paginator.WidgetsSpec+ Paths_yesod_paginator+ hs-source-dirs:+ test+ ghc-options: -Wall+ build-depends:+ base >4.8.0 && <5+ , hspec+ , yesod-core+ , yesod-paginator+ , yesod-test+ default-language: Haskell2010