Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 34
CRAP
0.00% covered (danger)
0.00%
0 / 1
Pager
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 34
1806
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isFirstDisabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 isPrevDisabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isNextDisabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 isLastDisabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 getCurrentOffset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFirstOffset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPrevOffset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNextOffset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLastOffset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getStartOrdinal
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getEndOrdinal
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTotalPages
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTotalResults
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLimit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPosition
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPaginationSizeOptions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPaginationSizeDefault
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAttributes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setId
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setTotalPages
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setTotalResults
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setLimit
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 setCurrentOffset
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setFirstOffset
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setPrevOffset
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setNextOffset
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setLastOffset
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setOrdinals
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 setPosition
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 setPaginationSizeOptions
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 setPaginationSizeDefault
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 setAttributes
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2declare( strict_types = 1 );
3
4/**
5 * Pager.php
6 *
7 * This file is part of the Codex design system, the official design system for Wikimedia projects.
8 * It contains the definition and implementation of the `Pager` class, responsible for managing
9 * the behavior and properties of the corresponding component.
10 *
11 * @category Component
12 * @package  Codex\Component
13 * @since    0.1.0
14 * @author   Doğu Abaris <abaris@null.net>
15 * @license  https://www.gnu.org/copyleft/gpl.html GPL-2.0-or-later
16 * @link     https://doc.wikimedia.org/codex/main/ Codex Documentation
17 */
18
19namespace Wikimedia\Codex\Component;
20
21use InvalidArgumentException;
22use Wikimedia\Codex\Contract\Component;
23use Wikimedia\Codex\Renderer\PagerRenderer;
24
25/**
26 * Pager
27 *
28 * @category Component
29 * @package  Codex\Component
30 * @since    0.1.0
31 * @author   Doğu Abaris <abaris@null.net>
32 * @license  https://www.gnu.org/copyleft/gpl.html GPL-2.0-or-later
33 * @link     https://doc.wikimedia.org/codex/main/ Codex Documentation
34 */
35class Pager extends Component {
36    private string $id = '';
37    /**
38     * Valid positions for pagination controls ('top', 'bottom', or 'both').
39     */
40    private const TABLE_PAGINATION_POSITIONS = [
41        'top',
42        'bottom',
43        'both',
44    ];
45
46    public function __construct(
47        PagerRenderer $renderer,
48        private array $paginationSizeOptions,
49        private int $paginationSizeDefault,
50        private int $totalPages,
51        private int $totalResults,
52        private string $position,
53        private int $limit,
54        private ?int $currentOffset,
55        private ?int $nextOffset,
56        private ?int $prevOffset,
57        private ?int $firstOffset,
58        private ?int $lastOffset,
59        private int $startOrdinal,
60        private int $endOrdinal,
61        private array $attributes,
62    ) {
63        parent::__construct( $renderer );
64    }
65
66    /**
67     * Get the Pager's HTML ID attribute.
68     *
69     * This method returns the ID assigned to the pager element, which is used
70     * for identifying the pager in the HTML document.
71     *
72     * @since 0.1.0
73     * @return string The ID of the Pager.
74     */
75    public function getId(): string {
76        return $this->id;
77    }
78
79    /**
80     * Determine if the first button should be disabled.
81     *
82     * This method checks whether the first button should be disabled based on the current page.
83     *
84     * @since 0.1.0
85     * @return bool Returns true if the first button should be disabled, false otherwise.
86     */
87    public function isFirstDisabled(): bool {
88        return $this->firstOffset == null && $this->prevOffset == null;
89    }
90
91    /**
92     * Determine if the previous button should be disabled.
93     *
94     * This method checks whether the previous button should be disabled based on the current page.
95     *
96     * @since 0.1.0
97     * @return bool Returns true if the previous button should be disabled, false otherwise.
98     */
99    public function isPrevDisabled(): bool {
100        return $this->prevOffset == null;
101    }
102
103    /**
104     * Determine if the next button should be disabled.
105     *
106     * This method checks whether the next button should be disabled based on the total results and the current page.
107     *
108     * @since 0.1.0
109     * @return bool Returns true if the next button should be disabled, false otherwise.
110     */
111    public function isNextDisabled(): bool {
112        return $this->nextOffset == null || $this->currentOffset == $this->lastOffset;
113    }
114
115    /**
116     * Determine if the last button should be disabled.
117     *
118     * This method checks whether the last button should be disabled based on the indeterminate state.
119     *
120     * @since 0.1.0
121     * @return bool Returns true if the last button should be disabled, false otherwise.
122     */
123    public function isLastDisabled(): bool {
124        return $this->nextOffset == null || $this->currentOffset == $this->lastOffset;
125    }
126
127    /**
128     * Get the current offset for the pager.
129     *
130     * This method returns the current offset value, which determines the
131     * starting point for the data on the current page. In cursor-based
132     * pagination, this offset is usually a timestamp or unique identifier.
133     *
134     * @since 0.1.0
135     * @return ?int The offset value for the current page.
136     */
137    public function getCurrentOffset(): ?int {
138        return $this->currentOffset;
139    }
140
141    /**
142     * Get the offset for the first page.
143     *
144     * This method returns the offset for the first page in cursor-based
145     * pagination. The first page offset usually represents the earliest
146     * timestamp or unique identifier in the dataset.
147     *
148     * @since 0.1.0
149     * @return ?int The offset value for the first page, or null if not set.
150     */
151    public function getFirstOffset(): ?int {
152        return $this->firstOffset;
153    }
154
155    /**
156     * Get the offset for the previous page.
157     *
158     * This method returns the offset for the previous page in cursor-based
159     * pagination. The previous page offset is typically the timestamp or
160     * unique identifier of the first item in the current page.
161     *
162     * @since 0.1.0
163     * @return ?int The offset value for the previous page, or null if not set.
164     */
165    public function getPrevOffset(): ?int {
166        return $this->prevOffset;
167    }
168
169    /**
170     * Get the offset for the next page.
171     *
172     * This method returns the offset for the next page in cursor-based
173     * pagination. The next page offset is typically the timestamp or
174     * unique identifier of the last item on the current page.
175     *
176     * @since 0.1.0
177     * @return ?int The offset value for the next page, or null if not set.
178     */
179    public function getNextOffset(): ?int {
180        return $this->nextOffset;
181    }
182
183    /**
184     * Get the offset for the last page.
185     *
186     * This method returns the offset for the last page in cursor-based
187     * pagination. The last page offset typically represents the timestamp
188     * or unique identifier of the last item in the dataset.
189     *
190     * @since 0.1.0
191     * @return ?int The offset value for the last page, or null if not set.
192     */
193    public function getLastOffset(): ?int {
194        return $this->lastOffset;
195    }
196
197    /**
198     * Get the start ordinal for the current page.
199     *
200     * @since 0.1.0
201     * @return int The start ordinal.
202     */
203    public function getStartOrdinal(): int {
204        return $this->startOrdinal;
205    }
206
207    /**
208     * Get the end ordinal for the current page.
209     *
210     * @since 0.1.0
211     * @return int The end ordinal.
212     */
213    public function getEndOrdinal(): int {
214        return $this->endOrdinal;
215    }
216
217    /**
218     * Get the total number of pages.
219     *
220     * This method returns the total number of pages available based on the dataset.
221     *
222     * @since 0.1.0
223     * @return int The total number of pages.
224     */
225    public function getTotalPages(): int {
226        return $this->totalPages;
227    }
228
229    /**
230     * Get the total number of results.
231     *
232     * This method returns the total number of results in the dataset.
233     *
234     * @since 0.1.0
235     * @return int The total number of results.
236     */
237    public function getTotalResults(): int {
238        return $this->totalResults;
239    }
240
241    /**
242     * Get the limit for the pager.
243     *
244     * This method returns the number of results to be displayed per page.
245     *
246     * @since 0.1.0
247     * @return int The number of results per page.
248     */
249    public function getLimit(): int {
250        return $this->limit;
251    }
252
253    /**
254     * Get the position of the pagination controls.
255     *
256     * This method returns the position where the pagination controls are displayed. Valid positions
257     * are 'top', 'bottom', or 'both'.
258     *
259     * @since 0.1.0
260     * @return string The position of the pagination controls.
261     */
262    public function getPosition(): string {
263        return $this->position;
264    }
265
266    /**
267     * Get the pagination size options.
268     *
269     * This method returns the available options for the number of results displayed per page.
270     * Users can select from these options in a dropdown.
271     *
272     * @since 0.1.0
273     * @return array The array of pagination size options.
274     */
275    public function getPaginationSizeOptions(): array {
276        return $this->paginationSizeOptions;
277    }
278
279    /**
280     * Get the default pagination size.
281     *
282     * This method returns the default number of rows displayed per page.
283     *
284     * @since 0.1.0
285     * @return int The default pagination size.
286     */
287    public function getPaginationSizeDefault(): int {
288        return $this->paginationSizeDefault;
289    }
290
291    /**
292     * Get the additional HTML attributes for the outer `<div>` element.
293     *
294     * This method returns an associative array of HTML attributes that are applied to the outer `<div>` element of the
295     * progress bar. These attributes can include `id`, `data-*`, `aria-*`, or any other valid HTML attributes.
296     *
297     * @since 0.1.0
298     * @return array The additional attributes as an array.
299     */
300    public function getAttributes(): array {
301        return $this->attributes;
302    }
303
304    /**
305     * Set the Pager's HTML ID attribute.
306     *
307     * @deprecated Use setAttributes() to set the ID
308     * @since 0.1.0
309     * @param string $id The ID for the Pager element.
310     * @return $this
311     */
312    public function setId( string $id ): self {
313        $this->id = $id;
314
315        return $this;
316    }
317
318    /**
319     * Set the total number of pages.
320     *
321     * The total number of pages available based on the dataset.
322     *
323     * @since 0.1.0
324     * @param int $totalPages The total number of pages.
325     * @return $this Returns the Pager instance for method chaining.
326     */
327    public function setTotalPages( int $totalPages ): self {
328        $this->totalPages = $totalPages;
329
330        return $this;
331    }
332
333    /**
334     * Set the total number of results.
335     *
336     * The total number of results in the dataset.
337     *
338     * @since 0.1.0
339     * @param int $totalResults The total number of results.
340     * @return $this Returns the Pager instance for method chaining.
341     */
342    public function setTotalResults( int $totalResults ): self {
343        $this->totalResults = $totalResults;
344
345        return $this;
346    }
347
348    /**
349     * Set the limit for the pager.
350     *
351     * The number of results to be displayed per page. The limit must be at least 1.
352     *
353     * @since 0.1.0
354     * @param int $limit The number of results per page.
355     * @return $this Returns the Pager instance for method chaining.
356     */
357    public function setLimit( int $limit ): self {
358        if ( $limit < 1 ) {
359            throw new InvalidArgumentException( 'The limit must be at least 1.' );
360        }
361
362        $this->limit = $limit;
363
364        return $this;
365    }
366
367    /**
368     * Set the current offset for the pager.
369     *
370     * This method sets the current offset, typically a timestamp or unique
371     * identifier, for cursor-based pagination. The offset represents the
372     * position in the dataset from which to start fetching the next page
373     * of results.
374     *
375     * Example usage:
376     *
377     *     $pager->setCurrentOffset('20240918135942');
378     *
379     * @since 0.1.0
380     * @param ?int $currentOffset The offset value (usually a timestamp).
381     * @return $this Returns the Pager instance for method chaining.
382     */
383    public function setCurrentOffset( ?int $currentOffset ): self {
384        $this->currentOffset = $currentOffset;
385
386        return $this;
387    }
388
389    /**
390     * Set the offset for the first page.
391     *
392     * This method sets the offset for the first page in cursor-based
393     * pagination. It usually represents the earliest timestamp in the
394     * dataset.
395     *
396     * Example usage:
397     *
398     *     $pager->setFirstOffset('20240918135942');
399     *
400     * @since 0.1.0
401     * @param ?int $firstOffset The offset for the first page.
402     * @return $this Returns the Pager instance for method chaining.
403     */
404    public function setFirstOffset( ?int $firstOffset ): self {
405        $this->firstOffset = $firstOffset;
406
407        return $this;
408    }
409
410    /**
411     * Set the offset for the previous page.
412     *
413     * This method sets the offset for the previous page in cursor-based
414     * pagination. The offset is typically the timestamp of the first
415     * item in the current page.
416     *
417     * Example usage:
418     *
419     *     $pager->setPrevOffset('20240918135942');
420     *
421     * @since 0.1.0
422     * @param ?int $prevOffset The offset for the previous page.
423     * @return $this Returns the Pager instance for method chaining.
424     */
425    public function setPrevOffset( ?int $prevOffset ): self {
426        $this->prevOffset = $prevOffset;
427
428        return $this;
429    }
430
431    /**
432     * Set the offset for the next page.
433     *
434     * This method sets the offset for the next page in cursor-based
435     * pagination. It is typically the timestamp of the last item on the
436     * current page.
437     *
438     * Example usage:
439     *
440     *     $pager->setNextOffset('20240918135942');
441     *
442     * @since 0.1.0
443     * @param ?int $nextOffset The offset for the next page.
444     * @return $this Returns the Pager instance for method chaining.
445     */
446    public function setNextOffset( ?int $nextOffset ): self {
447        $this->nextOffset = $nextOffset;
448
449        return $this;
450    }
451
452    /**
453     * Set the offset for the last page.
454     *
455     * This method sets the offset for the last page in cursor-based
456     * pagination. It typically represents the timestamp of the last
457     * item in the dataset.
458     *
459     * Example usage:
460     *
461     *     $pager->setLastOffset('20240918135942');
462     *
463     * @since 0.1.0
464     * @param ?int $lastOffset The offset for the last page.
465     * @return $this Returns the Pager instance for method chaining.
466     */
467    public function setLastOffset( ?int $lastOffset ): self {
468        $this->lastOffset = $lastOffset;
469
470        return $this;
471    }
472
473    /**
474     * Set the start and end ordinals for the current page.
475     *
476     * This method defines the range of items (ordinals) displayed on the
477     * current page of results. The ordinals represent the 1-based index
478     * of the first and last items shown on the page.
479     *
480     * Ordinals are typically determined based on the current page number
481     * and the limit, which is the number of items per page. The `startOrdinal`
482     * specifies the index of the first item on the page, while `endOrdinal`
483     * specifies the index of the last item. This ensures accurate display
484     * of the current page's item range.
485     *
486     * **Tip**: When working with cursor-based pagination (e.g., based on
487     * timestamps), ordinals can be calculated by determining the position
488     * of the current offset within the dataset. By tracking the relative
489     * position of items using their timestamps, the starting and ending
490     * ordinal values for each page can be derived.
491     *
492     * Example usage:
493     *
494     *     $pager->setOrdinals(6, 10);
495     *
496     * @since 0.1.0
497     * @param int $startOrdinal The 1-based index of the first item displayed.
498     * @param int $endOrdinal The 1-based index of the last item displayed.
499     * @return $this Returns the Pager instance for method chaining.
500     */
501    public function setOrdinals( int $startOrdinal, int $endOrdinal ): self {
502        $this->startOrdinal = $startOrdinal;
503        $this->endOrdinal = $endOrdinal;
504
505        return $this;
506    }
507
508    /**
509     * Set the position for the pager.
510     *
511     * This method specifies where the pagination controls should appear.
512     * Valid positions are 'top', 'bottom', or 'both'.
513     *
514     * Example usage:
515     *
516     *     $pager->setPosition('top');
517     *
518     * @since 0.1.0
519     * @param string $position The position of the pagination controls ('top', 'bottom', or 'both').
520     * @return $this Returns the Pager instance for method chaining.
521     */
522    public function setPosition( string $position ): self {
523        if ( !in_array( $position, self::TABLE_PAGINATION_POSITIONS, true ) ) {
524            throw new InvalidArgumentException( "Invalid pagination position: $position" );
525        }
526        $this->position = $position;
527
528        return $this;
529    }
530
531    /**
532     * Set the pagination size options.
533     *
534     * This method defines the available options for the number of results displayed per page.
535     * Users can select from these options in a dropdown, and the selected value will control
536     * how many items are displayed on each page.
537     *
538     * Example usage:
539     *
540     *     $pager->setPaginationSizeOptions([10, 20, 50]);
541     *
542     * @since 0.1.0
543     * @param array $paginationSizeOptions The array of pagination size options.
544     * @return $this Returns the Pager instance for method chaining.
545     */
546    public function setPaginationSizeOptions( array $paginationSizeOptions ): self {
547        if ( !$paginationSizeOptions ) {
548            throw new InvalidArgumentException( 'Pagination size options cannot be empty.' );
549        }
550        $this->paginationSizeOptions = $paginationSizeOptions;
551
552        return $this;
553    }
554
555    /**
556     * Set the default pagination size.
557     *
558     * This method specifies the default number of rows displayed per page.
559     *
560     * @since 0.1.0
561     * @param int $paginationSizeDefault The default number of rows per page.
562     * @return $this Returns the Pager instance for method chaining.
563     */
564    public function setPaginationSizeDefault( int $paginationSizeDefault ): self {
565        if ( !in_array( $paginationSizeDefault, $this->paginationSizeOptions, true ) ) {
566            throw new InvalidArgumentException( 'Default pagination size must be one of the pagination size options.' );
567        }
568        $this->paginationSizeDefault = $paginationSizeDefault;
569
570        return $this;
571    }
572
573    /**
574     * Set additional HTML attributes for the outer `<div>` element.
575     *
576     * This method allows custom HTML attributes to be added to the outer `<div>` element of the pager
577     * such as `id`, `data-*`, `aria-*`, or any other valid attributes. These attributes can be used to
578     * enhance accessibility or integrate with JavaScript.
579     *
580     * The values of these attributes are automatically escaped to prevent XSS vulnerabilities.
581     *
582     * Example usage:
583     *
584     *     $pager->setAttributes( [ 'class' => 'my-pager' ] );
585     *
586     * @since 0.8.0
587     * @param array $attributes An associative array of HTML attributes.
588     * @return $this Returns the Pager instance for method chaining.
589     */
590    public function setAttributes( array $attributes ): self {
591        foreach ( $attributes as $key => $value ) {
592            $this->attributes[$key] = $value;
593        }
594        return $this;
595    }
596}