diff --git a/typo3/sysext/core/Classes/Pagination/SlidingWindowPagination.php b/typo3/sysext/core/Classes/Pagination/SlidingWindowPagination.php new file mode 100644 index 0000000000000000000000000000000000000000..b017fce90ef8020b6aadb5eb414b1c3f5f32e64c --- /dev/null +++ b/typo3/sysext/core/Classes/Pagination/SlidingWindowPagination.php @@ -0,0 +1,149 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Core\Pagination; + +final class SlidingWindowPagination implements PaginationInterface +{ + protected int $displayRangeStart = 0; + protected int $displayRangeEnd = 0; + protected bool $hasLessPages = false; + protected bool $hasMorePages = false; + protected int $maximumNumberOfLinks = 0; + protected PaginatorInterface $paginator; + + public function __construct(PaginatorInterface $paginator, int $maximumNumberOfLinks = 0) + { + $this->paginator = $paginator; + + if ($maximumNumberOfLinks > 0) { + $this->maximumNumberOfLinks = $maximumNumberOfLinks; + } + + $this->calculateDisplayRange(); + } + + public function getPreviousPageNumber(): ?int + { + $previousPage = $this->paginator->getCurrentPageNumber() - 1; + + if ($previousPage > $this->paginator->getNumberOfPages()) { + return null; + } + + return $previousPage >= $this->getFirstPageNumber() ? $previousPage : null; + } + + public function getNextPageNumber(): ?int + { + $nextPage = $this->paginator->getCurrentPageNumber() + 1; + + return $nextPage <= $this->paginator->getNumberOfPages() ? $nextPage : null; + } + + public function getFirstPageNumber(): int + { + return 1; + } + + public function getLastPageNumber(): int + { + return $this->paginator->getNumberOfPages(); + } + + public function getStartRecordNumber(): int + { + if ($this->paginator->getCurrentPageNumber() > $this->paginator->getNumberOfPages()) { + return 0; + } + + return $this->paginator->getKeyOfFirstPaginatedItem() + 1; + } + + public function getEndRecordNumber(): int + { + if ($this->paginator->getCurrentPageNumber() > $this->paginator->getNumberOfPages()) { + return 0; + } + + return $this->paginator->getKeyOfLastPaginatedItem() + 1; + } + + public function getAllPageNumbers(): array + { + return range($this->displayRangeStart, $this->displayRangeEnd); + } + + public function getDisplayRangeStart(): int + { + return $this->displayRangeStart; + } + + public function getDisplayRangeEnd(): int + { + return $this->displayRangeEnd; + } + + public function getHasLessPages(): bool + { + return $this->hasLessPages; + } + + public function getHasMorePages(): bool + { + return $this->hasMorePages; + } + + public function getMaximumNumberOfLinks(): int + { + return $this->maximumNumberOfLinks; + } + + public function getPaginator(): PaginatorInterface + { + return $this->paginator; + } + + protected function calculateDisplayRange(): void + { + $maximumNumberOfLinks = $this->maximumNumberOfLinks; + $numberOfPages = $this->paginator->getNumberOfPages(); + + if ($maximumNumberOfLinks > $numberOfPages) { + $maximumNumberOfLinks = $numberOfPages; + } + + $currentPage = $this->paginator->getCurrentPageNumber(); + $delta = floor($maximumNumberOfLinks / 2); + + $this->displayRangeStart = (int)($currentPage - $delta); + $this->displayRangeEnd = (int)($currentPage + $delta - ($maximumNumberOfLinks % 2 === 0 ? 1 : 0)); + + if ($this->displayRangeStart < 1) { + $this->displayRangeEnd -= $this->displayRangeStart - 1; + } + + if ($this->displayRangeEnd > $numberOfPages) { + $this->displayRangeStart -= $this->displayRangeEnd - $numberOfPages; + } + + $this->displayRangeStart = (int)max($this->displayRangeStart, 1); + $this->displayRangeEnd = (int)min($this->displayRangeEnd, $numberOfPages); + $this->hasLessPages = $this->displayRangeStart > 2; + $this->hasMorePages = $this->displayRangeEnd + 1 < $this->paginator->getNumberOfPages(); + } +} diff --git a/typo3/sysext/core/Documentation/Changelog/12.0/Feature-94625-IntroduceSlidingWindowPagination.rst b/typo3/sysext/core/Documentation/Changelog/12.0/Feature-94625-IntroduceSlidingWindowPagination.rst new file mode 100644 index 0000000000000000000000000000000000000000..3de5e9f008641dbc616405edc580a3e7d3bae881 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/12.0/Feature-94625-IntroduceSlidingWindowPagination.rst @@ -0,0 +1,65 @@ +.. include:: ../../Includes.txt + +===================================================== +Feature: #94625 - Introduce sliding window pagination +===================================================== + +See :issue:`94625` + +Description +=========== + +Since TYPO3 10 a new `Pagination API <https://docs.typo3.org/m/typo3/reference-coreapi/master/en-us/ApiOverview/Pagination/Index.html>`__ +is shipped, which supersedes the pagination widget controller, which had +been removed in TYPO3 11. + +This patch provides an improved pagination which can be used to paginate array +items or query results from Extbase. The main advantage is that it reduces the +amount of pages shown. + +**Example**: Imagine 1000 records and 20 items per page which would lead to +50 links. Using the `SlidingWindowPagination`, you will get something like +`< 1 2 ... 21 22 23 24 ... 100 >`. + +Usage +===== + +Just replace the usage of :php:`SimplePagination` with +:php:`\TYPO3\CMS\Core\Pagination\SlidingWindowPagination` and you are done. +Set the 2nd argument to the maximum number of links which should be rendered. + +.. code-block:: php + + $currentPage = $this->request->hasArgument('currentPage') + ? (int)$this->request->getArgument('currentPage') + : 1; + $itemsPerPage = 10; + $maximumLinks = 15; + + $paginator = new \TYPO3\CMS\Extbase\Pagination\QueryResultPaginator( + $allItems, + $currentPage, + $itemsPerPage + ); + $pagination = new \TYPO3\CMS\Core\Pagination\SlidingWindowPagination( + $paginator, + $maximumLinks + ); + + $this->view->assign( + 'pagination', + [ + 'pagination' => $pagination, + 'paginator' => $paginator + ] + ); + +Credits +======= + +This patch is loosely based on the "`numbered_pagination <https://github.com/georgringer/numbered_pagination>`__" +extension by Georg Ringer. + +Thanks to him. + +.. index:: PHP-API, ext:core diff --git a/typo3/sysext/core/Tests/Unit/Pagination/SlidingWindowPaginationTest.php b/typo3/sysext/core/Tests/Unit/Pagination/SlidingWindowPaginationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..535467a1571918b3237eb2576e5fe3608f6f86ff --- /dev/null +++ b/typo3/sysext/core/Tests/Unit/Pagination/SlidingWindowPaginationTest.php @@ -0,0 +1,148 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Core\Tests\Unit\Pagination; + +use TYPO3\CMS\Core\Pagination\ArrayPaginator; +use TYPO3\CMS\Core\Pagination\SlidingWindowPagination; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +class SlidingWindowPaginationTest extends UnitTestCase +{ + protected $paginator = []; + + /** + * @test + */ + public function checkSlidingWindowPaginationWithAPaginatorWithDefaultSettings(): void + { + $pagination = new SlidingWindowPagination($this->paginator, 5); + + self::assertSame(1, $pagination->getStartRecordNumber()); + self::assertSame(10, $pagination->getEndRecordNumber()); + self::assertSame(1, $pagination->getFirstPageNumber()); + self::assertSame(2, $pagination->getLastPageNumber()); + self::assertNull($pagination->getPreviousPageNumber()); + self::assertSame(2, $pagination->getNextPageNumber()); + self::assertSame([1, 2], $pagination->getAllPageNumbers()); + self::assertSame(1, $pagination->getDisplayRangeStart()); + self::assertSame(2, $pagination->getDisplayRangeEnd()); + self::assertFalse($pagination->getHasLessPages()); + self::assertFalse($pagination->getHasMorePages()); + self::assertSame(5, $pagination->getMaximumNumberOfLinks()); + } + + /** + * @test + */ + public function checkSlidingWindowPaginationWithAnIncreasedCurrentPageNumber(): void + { + $paginator = $this->paginator->withCurrentPageNumber(2); + $pagination = new SlidingWindowPagination($paginator, 5); + + self::assertSame(11, $pagination->getStartRecordNumber()); + self::assertSame(14, $pagination->getEndRecordNumber()); + self::assertSame(1, $pagination->getFirstPageNumber()); + self::assertSame(2, $pagination->getLastPageNumber()); + self::assertSame(1, $pagination->getPreviousPageNumber()); + self::assertNull($pagination->getNextPageNumber()); + self::assertSame([1, 2], $pagination->getAllPageNumbers()); + self::assertSame(1, $pagination->getDisplayRangeStart()); + self::assertSame(2, $pagination->getDisplayRangeEnd()); + self::assertFalse($pagination->getHasLessPages()); + self::assertFalse($pagination->getHasMorePages()); + self::assertSame(5, $pagination->getMaximumNumberOfLinks()); + } + + /** + * @test + */ + public function checkSlidingWindowPaginationWithAnIncreasedCurrentPageNumberAndItemsPerPage(): void + { + $paginator = $this->paginator + ->withCurrentPageNumber(2) + ->withItemsPerPage(3); + $pagination = new SlidingWindowPagination($paginator, 5); + + self::assertSame(4, $pagination->getStartRecordNumber()); + self::assertSame(6, $pagination->getEndRecordNumber()); + self::assertSame(1, $pagination->getFirstPageNumber()); + self::assertSame(5, $pagination->getLastPageNumber()); + self::assertSame(1, $pagination->getPreviousPageNumber()); + self::assertSame(3, $pagination->getNextPageNumber()); + self::assertSame([1, 2, 3, 4, 5], $pagination->getAllPageNumbers()); + self::assertSame(1, $pagination->getDisplayRangeStart()); + self::assertSame(5, $pagination->getDisplayRangeEnd()); + self::assertFalse($pagination->getHasLessPages()); + self::assertFalse($pagination->getHasMorePages()); + self::assertSame(5, $pagination->getMaximumNumberOfLinks()); + } + + /** + * @test + */ + public function checkPaginationWithAPaginatorThatOnlyHasOnePage(): void + { + $paginator = $this->paginator->withItemsPerPage(50); + $pagination = new SlidingWindowPagination($paginator, 5); + + self::assertSame(1, $pagination->getStartRecordNumber()); + self::assertSame(14, $pagination->getEndRecordNumber()); + self::assertSame(1, $pagination->getFirstPageNumber()); + self::assertSame(1, $pagination->getLastPageNumber()); + self::assertNull($pagination->getPreviousPageNumber()); + self::assertNull($pagination->getNextPageNumber()); + self::assertSame([1], $pagination->getAllPageNumbers()); + self::assertSame(1, $pagination->getDisplayRangeStart()); + self::assertSame(1, $pagination->getDisplayRangeEnd()); + self::assertFalse($pagination->getHasLessPages()); + self::assertFalse($pagination->getHasMorePages()); + self::assertSame(5, $pagination->getMaximumNumberOfLinks()); + } + + /** + * @test + */ + public function checkPaginatorWithOutOfBoundsCurrentPage(): void + { + $paginator = $this->paginator + ->withItemsPerPage(5) + ->withCurrentPageNumber(100); + $pagination = new SlidingWindowPagination($paginator, 5); + + self::assertSame(11, $pagination->getStartRecordNumber()); + self::assertSame(14, $pagination->getEndRecordNumber()); + self::assertSame(3, $paginator->getCurrentPageNumber()); + self::assertSame(1, $pagination->getFirstPageNumber()); + self::assertSame(2, $pagination->getPreviousPageNumber()); + self::assertNull($pagination->getNextPageNumber()); + self::assertSame(3, $pagination->getLastPageNumber()); + self::assertSame([1, 2, 3], $pagination->getAllPageNumbers()); + self::assertSame(1, $pagination->getDisplayRangeStart()); + self::assertSame(3, $pagination->getDisplayRangeEnd()); + self::assertFalse($pagination->getHasLessPages()); + self::assertFalse($pagination->getHasMorePages()); + self::assertSame(5, $pagination->getMaximumNumberOfLinks()); + } + + protected function setUp(): void + { + parent::setUp(); + + $this->paginator = new ArrayPaginator(range(1, 14)); + } +}