From b60cf59fbe7875aff5ee1ba4c56155301694d6b8 Mon Sep 17 00:00:00 2001
From: Eric Chavaillaz <echavaillaz@wideagency.com>
Date: Mon, 26 Jul 2021 11:12:07 +0200
Subject: [PATCH] [FEATURE] Introduce sliding window pagination

This adds the NumberedPagination class from the
ext:numbered_pagination of Georg Ringer as
"SlidingWindowPagination" to the Core.

Resolves: #94625
Releases: master
Change-Id: If8631da405d95103c7a2f3a58a831835f08e45b9
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/70069
Tested-by: core-ci <typo3@b13.com>
Tested-by: Oliver Bartsch <bo@cedev.de>
Tested-by: Torben Hansen <derhansen@gmail.com>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Reviewed-by: Oliver Bartsch <bo@cedev.de>
Reviewed-by: Torben Hansen <derhansen@gmail.com>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
---
 .../Pagination/SlidingWindowPagination.php    | 149 ++++++++++++++++++
 ...94625-IntroduceSlidingWindowPagination.rst |  65 ++++++++
 .../SlidingWindowPaginationTest.php           | 148 +++++++++++++++++
 3 files changed, 362 insertions(+)
 create mode 100644 typo3/sysext/core/Classes/Pagination/SlidingWindowPagination.php
 create mode 100644 typo3/sysext/core/Documentation/Changelog/12.0/Feature-94625-IntroduceSlidingWindowPagination.rst
 create mode 100644 typo3/sysext/core/Tests/Unit/Pagination/SlidingWindowPaginationTest.php

diff --git a/typo3/sysext/core/Classes/Pagination/SlidingWindowPagination.php b/typo3/sysext/core/Classes/Pagination/SlidingWindowPagination.php
new file mode 100644
index 000000000000..b017fce90ef8
--- /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 000000000000..3de5e9f00864
--- /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 000000000000..535467a15719
--- /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));
+    }
+}
-- 
GitLab