3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
21 namespace MediaWiki\Pager
;
23 use InvalidArgumentException
;
24 use MediaWiki\Context\IContextSource
;
25 use MediaWiki\Html\Html
;
26 use MediaWiki\Linker\LinkRenderer
;
27 use MediaWiki\Parser\ParserOutput
;
30 * Codex Table display of sortable data with pagination.
35 abstract class CodexTablePager
extends TablePager
{
36 protected string $mCaption;
41 * @param string $caption Text for visually-hidden caption element
42 * @param ?IContextSource $context
43 * @param ?LinkRenderer $linkRenderer
45 public function __construct(
47 ?IContextSource
$context = null,
48 ?LinkRenderer
$linkRenderer = null
50 if ( trim( $caption ) === '' ) {
51 throw new InvalidArgumentException( 'Table caption is required.' );
53 $this->mCaption
= $caption;
55 $this->getOutput()->addModules( 'mediawiki.pager.codex' );
57 parent
::__construct( $context, $linkRenderer );
59 $this->getOutput()->addJsConfigVars( [
60 'wgCodexTablePagerLimit' => $this->mLimit
,
65 * Get the entire Codex table markup, including the wrapper element, pagers, table wrapper
66 * (which enables horizontal scroll), and the table element.
70 public function getFullOutput(): ParserOutput
{
72 $navigation = $this->getNavigationBar();
73 // `<table>` element and its contents.
74 $body = parent
::getBody();
76 $pout = new ParserOutput();
78 Html
::openElement( 'div', [ 'class' => 'cdx-table' ] ) . "\n" .
79 // In the future, a visible caption + header content could go here.
81 Html
::openElement( 'div', [ 'class' => 'cdx-table__table-wrapper' ] ) . "\n" .
83 Html
::closeElement( 'div' ) . "\n" .
84 // In the future, footer content could go here.
86 Html
::closeElement( 'div' )
88 $pout->addModuleStyles( $this->getModuleStyles() );
93 * Generate the `<thead>` element.
95 * This creates a thead with a single tr and includes sort buttons if applicable. To customize
96 * the thead layout, override this method.
100 protected function getThead(): string {
102 $fields = $this->getFieldNames();
104 // Build each th element.
105 foreach ( $fields as $field => $name ) {
106 if ( $name === '' ) {
107 // th with no label (not advised).
108 $theadContent .= Html
::rawElement( 'th', $this->getCellAttrs( $field, $name ), "\u{00A0}" ) . "\n";
109 } elseif ( $this->isFieldSortable( $field ) ) {
111 $query = [ 'sort' => $field, 'limit' => $this->mLimit
];
112 $sortIconClasses = [ 'cdx-table__table__sort-icon' ];
114 if ( $this->mSort
== $field ) {
115 // Set data for the currently sorted column.
116 if ( $this->mDefaultDirection
== IndexPager
::DIR_DESCENDING
) {
117 $sortIconClasses[] = 'cdx-table__table__sort-icon--desc';
121 $sortIconClasses[] = 'cdx-table__table__sort-icon--asc';
123 $query['desc'] = '1';
126 $sortIconClasses[] = 'cdx-table__table__sort-icon--unsorted';
129 // Build the label and icon span that go inside the link.
130 $linkContents = Html
::rawElement( 'span',
131 [ 'class' => 'cdx-table__table__sort-label' ],
132 htmlspecialchars( $name )
134 Html
::rawElement( 'span',
135 [ 'class' => $sortIconClasses, 'aria-hidden' => true ]
138 // Build the link that goes inside the th.
139 $link = Html
::rawElement( 'a',
141 'class' => [ 'cdx-table__table__sort-button' ],
143 'href' => $this->getTitle()->getLinkURL( $query +
$this->getDefaultQuery() ),
149 $thAttrs = $this->getCellAttrs( $field, $name );
150 $thAttrs[ 'class' ][] = $this->getSortHeaderClass();
151 $theadContent .= Html
::rawElement( 'th', $thAttrs, $link ) . "\n";
153 // Unsortable column.
154 $theadContent .= Html
::element( 'th', $this->getCellAttrs( $field, $name ), $name ) . "\n";
157 return Html
::rawElement( 'thead', [], Html
::rawElement( 'tr', [], "\n" . $theadContent . "\n" ) );
161 * Append text to the caption if any fields are sortable.
163 * @param string $captionText Caption provided for the table
165 private function getFullCaption( string $captionText ): string {
166 $fields = $this->getFieldNames();
169 foreach ( $fields as $field => $name ) {
170 if ( $this->isFieldSortable( $field ) === true ) {
171 return $this->msg( 'cdx-table-sort-caption', $captionText )->text();
179 * Get the opening table tag through the opening tbody tag.
181 * This method should generally not be overridden: use getThead() to create a custom `<thead>`
182 * and getTableClass to set additional classes on the `<table>` element.
184 * @stable to override
186 protected function getStartBody(): string {
187 $ret = Html
::openElement( 'table', [
188 'class' => $this->getTableClass() ]
190 $ret .= Html
::rawElement( 'caption', [], $this->getFullCaption( $this->mCaption
) );
191 $ret .= $this->getThead();
192 $ret .= Html
::openElement( 'tbody' ) . "\n";
198 * Override to add a `<tfoot>` element.
200 * @stable to override
202 protected function getTfoot(): string {
207 * Get the closing tbody tag through the closing table tag.
209 * @stable to override
211 protected function getEndBody(): string {
212 return "</tbody>" . $this->getTfoot() . "</table>\n";
216 * Get markup for the "no results" UI. This is placed inside the tbody tag.
218 protected function getEmptyBody(): string {
219 $colspan = count( $this->getFieldNames() );
220 $msgEmpty = $this->msg( 'table_pager_empty' )->text();
221 return Html
::rawElement( 'tr', [ 'class' => 'cdx-table__table__empty-state' ],
224 [ 'class' => 'cdx-table__table__empty-state-content', 'colspan' => $colspan ],
230 * Add alignment per column.
232 * @param string $field The column
233 * @return string start (default), center, end, or number (always to the right)
235 protected function getCellAlignment( string $field ): string {
240 * Add extra attributes to be applied to the given cell.
242 * @stable to override
244 * @param string $field The column
245 * @param string $value The cell contents
246 * @return array Array of attr => value
248 protected function getCellAttrs( $field, $value ): array {
251 'cdx-table-pager__col--' . $field,
252 'cdx-table__table__cell--align-' . $this->getCellAlignment( $field )
258 * Class for the `<table>` element.
260 * @stable to override
262 protected function getTableClass(): string {
263 return 'cdx-table__table';
267 * Class for the outermost element of the pager UI.
269 * @stable to override
271 protected function getNavClass(): string {
272 return 'cdx-table-pager';
276 * Class for th elements of sortable columns.
278 * @stable to override
280 protected function getSortHeaderClass(): string {
281 return 'cdx-table__table__cell--has-sort';
285 * Pager bar with per-page limit and pager buttons.
287 * @stable to override
289 * @return string HTML for the pager UI
291 public function getNavigationBar(): string {
292 if ( !$this->isNavigationBarShown() ) {
296 $types = [ 'first', 'prev', 'next', 'last' ];
297 $queries = $this->getPagingQueries();
298 $title = $this->getTitle();
302 foreach ( $types as $type ) {
303 // TODO: Update Codex class suffix for previous to 'prev' so we don't have to do this.
304 $classSuffix = $type === 'prev' ?
'previous' : $type;
305 $isDisabled = $queries[ $type ] === false;
306 $buttons[] = Html
::rawElement( 'a',
310 'cdx-button--fake-button',
311 'cdx-button--fake-button--' . ( $isDisabled ?
'disabled' : 'enabled' ),
312 'cdx-button--weight-quiet',
313 'cdx-button--icon-only'
316 'disabled' => $queries[ $type ] === false,
317 'aria-label' => $this->msg( 'table_pager_' . $type )->text(),
318 'href' => $queries[ $type ] ?
319 $title->getLinkURL( $queries[ $type ] +
$this->getDefaultQuery() ) :
322 Html
::rawElement( 'span',
323 [ 'class' => [ 'cdx-button__icon', 'cdx-table-pager__icon--' . $classSuffix ] ]
328 return Html
::openElement( 'div', [ 'class' => $this->getNavClass() ] ) . "\n" .
329 Html
::rawElement( 'div', [ 'class' => 'cdx-table-pager__start' ], $this->getLimitForm() ) . "\n" .
330 Html
::rawElement( 'div', [ 'class' => 'cdx-table-pager__end' ], implode( '', $buttons ) ) . "\n" .
331 Html
::closeElement( 'div' );
335 * Get a `<select>` element with options for items-per-page limits.
337 * @param string[] $attribs Extra attributes to set
338 * @return string HTML fragment
340 public function getLimitSelect( array $attribs = [] ): string {
341 return parent
::getLimitSelect( [ 'class' => 'cdx-select' ] );
345 * Get a list of items to show as options in the item-per-page select.
347 public function getLimitSelectList(): array {
348 $options = parent
::getLimitSelectList();
350 foreach ( $options as $key => $option ) {
351 // Add new option with "rows" after the number.
352 $options[ $this->msg( 'cdx-table-pager-items-per-page-current', $option )->text() ] = $option;
353 // Remove the old option.
354 unset( $options[ $key ] );
361 * Get a form with the items-per-page select.
363 * @return string HTML fragment
365 public function getLimitForm(): string {
366 return Html
::rawElement(
369 'id' => 'cdx-table-pager-limit-form',
371 'action' => wfScript(),
372 'class' => 'cdx-table-pager__limit-form'
374 "\n" . $this->getLimitSelect() . "\n" .
375 Html
::element( 'button',
376 [ 'class' => [ 'cdx-button', 'cdx-table-pager__limit-form__submit' ] ],
377 $this->msg( 'table_pager_limit_submit' )->text()
379 $this->getHiddenFields( [ 'limit' ] )
386 public function getModuleStyles(): array {
387 return [ 'mediawiki.pager.codex.styles' ];
391 /** @deprecated class alias since 1.41 */
392 class_alias( CodexTablePager
::class, 'CodexTablePager' );