Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 105
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
PagerRenderer
0.00% covered (danger)
0.00%
0 / 105
0.00% covered (danger)
0.00%
0 / 5
342
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 render
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
20
 buildSelect
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
6
 buildButtonData
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
42
 buildHiddenFields
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2declare( strict_types = 1 );
3
4/**
5 * PagerRenderer.php
6 *
7 * This file is part of the Codex PHP library, which provides a PHP-based interface for creating
8 * UI components consistent with the Codex design system.
9 *
10 * The `PagerRenderer` class leverages the `TemplateParser` and `Sanitizer` utilities to ensure the
11 * component object is rendered according to Codex design system standards.
12 *
13 * @category Renderer
14 * @package  Codex\Renderer
15 * @since    0.1.0
16 * @author   Doğu Abaris <abaris@null.net>
17 * @license  https://www.gnu.org/copyleft/gpl.html GPL-2.0-or-later
18 * @link     https://doc.wikimedia.org/codex/main/ Codex Documentation
19 */
20
21namespace Wikimedia\Codex\Renderer;
22
23use InvalidArgumentException;
24use UnexpectedValueException;
25use Wikimedia\Codex\Component\Pager;
26use Wikimedia\Codex\Component\Select;
27use Wikimedia\Codex\Contract\Component;
28use Wikimedia\Codex\Contract\ILocalizer;
29use Wikimedia\Codex\Contract\Renderer;
30use Wikimedia\Codex\ParamValidator\ParamDefinitions;
31use Wikimedia\Codex\ParamValidator\ParamValidator;
32use Wikimedia\Codex\ParamValidator\ParamValidatorCallbacks;
33use Wikimedia\Codex\Parser\TemplateParser;
34use Wikimedia\Codex\Utility\Codex;
35use Wikimedia\Codex\Utility\Sanitizer;
36
37/**
38 * PagerRenderer is responsible for rendering the HTML markup
39 * for a Pager component using a Mustache template.
40 *
41 * This class uses the `TemplateParser` and `Sanitizer` utilities to manage
42 * the template rendering process, ensuring that the component object's HTML
43 * output adheres to the Codex design system's standards.
44 *
45 * @category Renderer
46 * @package  Codex\Renderer
47 * @since    0.1.0
48 * @author   Doğu Abaris <abaris@null.net>
49 * @license  https://www.gnu.org/copyleft/gpl.html GPL-2.0-or-later
50 * @link     https://doc.wikimedia.org/codex/main/ Codex Documentation
51 */
52class PagerRenderer extends Renderer {
53
54    /**
55     * The template parser instance.
56     */
57    private TemplateParser $templateParser;
58
59    /**
60     * The localization instance implementing ILocalizer.
61     */
62    private ILocalizer $localizer;
63
64    /**
65     * The Codex instance for utility methods.
66     */
67    private Codex $codex;
68
69    /**
70     * The param validator.
71     */
72    protected ParamValidator $paramValidator;
73
74    /**
75     * The param validator callbacks.
76     */
77    protected ParamValidatorCallbacks $paramValidatorCallbacks;
78
79    /**
80     * Array of icon classes for the pager buttons.
81     */
82    private const ICON_CLASSES = [
83        "first" => "cdx-table-pager__icon--first",
84        "previous" => "cdx-table-pager__icon--previous",
85        "next" => "cdx-table-pager__icon--next",
86        "last" => "cdx-table-pager__icon--last",
87    ];
88
89    /**
90     * Action for the first page.
91     */
92    private const ACTION_FIRST = 'first';
93
94    /**
95     * Action for the previous page.
96     */
97    private const ACTION_PREVIOUS = 'previous';
98
99    /**
100     * Action for the next page.
101     */
102    private const ACTION_NEXT = 'next';
103
104    /**
105     * Action for the last page.
106     */
107    private const ACTION_LAST = 'last';
108
109    /**
110     * Constructor to initialize the PagerRenderer with necessary dependencies.
111     *
112     * @since 0.1.0
113     * @param Sanitizer $sanitizer The sanitizer instance for cleaning user-provided data and attributes.
114     * @param TemplateParser $templateParser The template parser instance for rendering Mustache templates.
115     * @param ILocalizer $localizer The localizer instance for supporting translations and localization.
116     * @param Codex $codex The Codex instance for creating instances of other components
117     * @param ParamValidator $paramValidator The parameter validator instance for validating query parameters.
118     * @param ParamValidatorCallbacks $paramValidatorCallbacks The callback instance for accessing validated
119     *                                                         parameter values.
120     */
121    public function __construct(
122        Sanitizer $sanitizer,
123        TemplateParser $templateParser,
124        ILocalizer $localizer,
125        Codex $codex,
126        ParamValidator $paramValidator,
127        ParamValidatorCallbacks $paramValidatorCallbacks
128    ) {
129        parent::__construct( $sanitizer );
130        $this->templateParser = $templateParser;
131        $this->localizer = $localizer;
132        $this->codex = $codex;
133        $this->paramValidator = $paramValidator;
134        $this->paramValidatorCallbacks = $paramValidatorCallbacks;
135    }
136
137    /**
138     * Renders the HTML for a pager component.
139     *
140     * Uses the provided Pager to generate HTML markup adhering to the Codex design system.
141     *
142     * @since 0.1.0
143     * @param Component $component The Pager object to render.
144     * @return string The rendered HTML string for the component.
145     */
146    public function render( Component $component ): string {
147        if ( !$component instanceof Pager ) {
148            throw new InvalidArgumentException( "Expected instance of Pager, got " . get_class( $component ) );
149        }
150
151        $isPending = $component->getEndOrdinal() < $component->getStartOrdinal();
152        $totalResults = $component->getTotalResults();
153
154        $pagerData = [
155            'id' => $component->getId(),
156            'position' => $component->getPosition(),
157            'status' => $isPending ?
158                $this->localizer->msg( 'cdx-table-pagination-status-message-pending' ) :
159                $this->localizer->msg(
160                    $totalResults > 0 ?
161                        'cdx-table-pagination-status-message-determinate-long' :
162                        'cdx-table-pagination-status-message-indeterminate-long',
163                    $component->getStartOrdinal(),
164                    $component->getEndOrdinal(),
165                    $totalResults
166                ),
167            'select' => $this->buildSelect( $component )->getHtml(),
168            'firstButton' => $this->buildButtonData( $component, self::ACTION_FIRST ),
169            'prevButton' => $this->buildButtonData( $component, self::ACTION_PREVIOUS ),
170            'nextButton' => $this->buildButtonData( $component, self::ACTION_NEXT ),
171            'lastButton' => $this->buildButtonData( $component, self::ACTION_LAST ),
172            'hiddenFields' => $this->buildHiddenFields(),
173            'extraClasses' => $this->getExtraClasses( $component->getAttributes() ),
174            'attributes' => $this->getOtherAttributes( $component->getAttributes() ),
175        ];
176
177        return $this->templateParser->processTemplate( 'pager', $pagerData );
178    }
179
180    /**
181     * Build the select dropdown data to be passed to the Mustache template.
182     *
183     * @since 0.1.0
184     * @param Pager $pager
185     * @return Select The select component
186     */
187    protected function buildSelect( Pager $pager ): Select {
188        $sizeOptions = $pager->getPaginationSizeOptions();
189        $currentLimit = $pager->getLimit();
190
191        $options = [];
192
193        foreach ( $sizeOptions as $size ) {
194            $options[] = [
195                'value' => $size,
196                'text' => $this->localizer->msg(
197                    'cdx-table-pager-items-per-page-current', $size
198                ),
199                'selected' => ( $size == $currentLimit ),
200            ];
201        }
202
203        return $this->codex->select()
204            ->setOptions( $options )
205            ->setSelectedOption( (string)$currentLimit )
206            ->setAttributes( [
207                'name' => 'limit',
208                'onchange' => 'this.form.submit();',
209            ] );
210    }
211
212    /**
213     * Build an individual pagination button.
214     *
215     * Generates the data array for a single pagination button based on the action.
216     *
217     * @since 0.1.0
218     * @param Pager $pager The Pager object.
219     * @param string $action The action for the button (one of the ACTION_* constants).
220     * @return array The data array for the pagination button.
221     */
222    protected function buildButtonData( Pager $pager, string $action ): array {
223        $iconClass = self::ICON_CLASSES[$action] ?? '';
224        switch ( $action ) {
225            case self::ACTION_FIRST:
226                $disabled = $pager->isFirstDisabled();
227                $ariaLabelKey = 'cdx-table-pager-button-first-page';
228                $offset = $pager->getFirstOffset();
229                break;
230            case self::ACTION_PREVIOUS:
231                $disabled = $pager->isPrevDisabled();
232                $ariaLabelKey = 'cdx-table-pager-button-prev-page';
233                $offset = $pager->getPrevOffset();
234                break;
235            case self::ACTION_NEXT:
236                $disabled = $pager->isNextDisabled();
237                $ariaLabelKey = 'cdx-table-pager-button-next-page';
238                $offset = $pager->getNextOffset();
239                break;
240            case self::ACTION_LAST:
241                $disabled = $pager->isLastDisabled();
242                $ariaLabelKey = 'cdx-table-pager-button-last-page';
243                $offset = $pager->getLastOffset();
244                // FIXME this should maybe set dir=prev as well, but that's not easy with a button
245                break;
246            default:
247                throw new InvalidArgumentException( "Unknown action: $action" );
248        }
249
250        return [
251            'isDisabled' => $disabled,
252            'weight' => 'quiet',
253            'iconOnly' => true,
254            'iconClass' => $iconClass,
255            'type' => 'submit',
256            'name' => 'offset',
257            'value' => $offset,
258            'attributes' => $this->getOtherAttributes( [
259                'aria-label' => $this->localizer->msg( $ariaLabelKey ),
260            ] ),
261            'extraClasses' => '',
262        ];
263    }
264
265    /**
266     * Build hidden fields for the pagination form.
267     *
268     * This method generates the hidden input fields needed for the pagination form, including offset, direction,
269     * and other query parameters.
270     *
271     * @since 0.1.0
272     * @return array The generated HTML string for the hidden fields.
273     */
274    protected function buildHiddenFields(): array {
275        $definitions = ParamDefinitions::getDefinitionsForContext( 'table' );
276
277        foreach ( $definitions as $param => $rules ) {
278            try {
279                $this->paramValidator->validateValue(
280                    $param,
281                    $this->paramValidatorCallbacks->getValue(
282                        $param,
283                        $rules[ParamValidator::PARAM_DEFAULT],
284                        []
285                    ),
286                    $rules
287                );
288            } catch ( UnexpectedValueException $e ) {
289                throw new InvalidArgumentException( "Invalid value for parameter '$param': " . $e->getMessage() );
290            }
291        }
292
293        $fields = [];
294        $keys = [ 'sort', 'desc', 'asc' ];
295        foreach ( $keys as $key ) {
296            $value = $this->paramValidatorCallbacks->getValue( $key, '', [] );
297            if ( $value !== '' ) {
298                $fields[] = [
299                    'key' => $key,
300                    'value' => $value,
301                ];
302            }
303        }
304
305        return $fields;
306    }
307}