Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
34.29% |
48 / 140 |
|
40.91% |
9 / 22 |
CRAP | |
0.00% |
0 / 1 |
TablePager | |
34.53% |
48 / 139 |
|
40.91% |
9 / 22 |
613.20 | |
0.00% |
0 / 1 |
__construct | |
72.73% |
8 / 11 |
|
0.00% |
0 / 1 |
6.73 | |||
getBodyOutput | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getFullOutput | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getStartBody | |
44.83% |
13 / 29 |
|
0.00% |
0 / 1 |
12.05 | |||
getEndBody | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getEmptyBody | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
formatRow | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
3.01 | |||
getRowClass | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getRowAttrs | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCurrentRow | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCellAttrs | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getIndexField | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTableClass | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getNavClass | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSortHeaderClass | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getNavigationBar | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
30 | |||
getModuleStyles | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getLimitSelect | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getLimitSelectList | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
getHiddenFields | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
getLimitForm | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
getLimitDropdown | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
isFieldSortable | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
formatValue | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getDefaultSort | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getFieldNames | n/a |
0 / 0 |
n/a |
0 / 0 |
0 |
1 | <?php |
2 | /** |
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. |
7 | * |
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. |
12 | * |
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 |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\Pager; |
22 | |
23 | use MediaWiki\Context\IContextSource; |
24 | use MediaWiki\Html\Html; |
25 | use MediaWiki\Linker\LinkRenderer; |
26 | use MediaWiki\Parser\ParserOutput; |
27 | use MediaWiki\Xml\XmlSelect; |
28 | use OOUI\ButtonGroupWidget; |
29 | use OOUI\ButtonWidget; |
30 | use stdClass; |
31 | |
32 | /** |
33 | * Table-based display with a user-selectable sort order |
34 | * |
35 | * @stable to extend |
36 | * @ingroup Pager |
37 | */ |
38 | abstract class TablePager extends IndexPager { |
39 | /** @var string */ |
40 | protected $mSort; |
41 | |
42 | /** @var stdClass */ |
43 | protected $mCurrentRow; |
44 | |
45 | /** |
46 | * @stable to call |
47 | * |
48 | * @param IContextSource|null $context |
49 | * @param LinkRenderer|null $linkRenderer |
50 | */ |
51 | public function __construct( ?IContextSource $context = null, ?LinkRenderer $linkRenderer = null ) { |
52 | if ( $context ) { |
53 | $this->setContext( $context ); |
54 | } |
55 | |
56 | $this->mSort = $this->getRequest()->getText( 'sort' ); |
57 | if ( !array_key_exists( $this->mSort, $this->getFieldNames() ) |
58 | || !$this->isFieldSortable( $this->mSort ) |
59 | ) { |
60 | $this->mSort = $this->getDefaultSort(); |
61 | } |
62 | if ( $this->getRequest()->getBool( 'asc' ) ) { |
63 | $this->mDefaultDirection = IndexPager::DIR_ASCENDING; |
64 | } elseif ( $this->getRequest()->getBool( 'desc' ) ) { |
65 | $this->mDefaultDirection = IndexPager::DIR_DESCENDING; |
66 | } /* Else leave it at whatever the class default is */ |
67 | |
68 | // Parent constructor needs mSort set, so we call it last |
69 | parent::__construct( null, $linkRenderer ); |
70 | } |
71 | |
72 | /** |
73 | * Get the formatted result list. |
74 | * |
75 | * Calls getBody() and getModuleStyles() and builds a ParserOutput object. (This is a bit hacky |
76 | * but works well.) |
77 | * |
78 | * @since 1.24 |
79 | * @return ParserOutput |
80 | */ |
81 | public function getBodyOutput() { |
82 | $body = parent::getBody(); |
83 | |
84 | $pout = new ParserOutput; |
85 | $pout->setRawText( $body ); |
86 | return $pout; |
87 | } |
88 | |
89 | /** |
90 | * Get the formatted result list, with navigation bars. |
91 | * |
92 | * Calls getBody(), getNavigationBar() and getModuleStyles() and |
93 | * builds a ParserOutput object. (This is a bit hacky but works well.) |
94 | * |
95 | * @since 1.24 |
96 | * @return ParserOutput |
97 | */ |
98 | public function getFullOutput() { |
99 | $navigation = $this->getNavigationBar(); |
100 | $body = parent::getBody(); |
101 | |
102 | $pout = new ParserOutput; |
103 | $pout->setRawText( $navigation . $body . $navigation ); |
104 | $pout->addModuleStyles( $this->getModuleStyles() ); |
105 | return $pout; |
106 | } |
107 | |
108 | /** |
109 | * @stable to override |
110 | * @return string |
111 | */ |
112 | protected function getStartBody() { |
113 | $sortClass = $this->getSortHeaderClass(); |
114 | |
115 | $s = ''; |
116 | $fields = $this->getFieldNames(); |
117 | |
118 | // Make table header |
119 | foreach ( $fields as $field => $name ) { |
120 | if ( strval( $name ) == '' ) { |
121 | $s .= Html::rawElement( 'th', [], "\u{00A0}" ) . "\n"; |
122 | } elseif ( $this->isFieldSortable( $field ) ) { |
123 | $query = [ 'sort' => $field, 'limit' => $this->mLimit ]; |
124 | $linkType = null; |
125 | $class = null; |
126 | |
127 | if ( $this->mSort == $field ) { |
128 | // The table is sorted by this field already, make a link to sort in the other direction |
129 | // We don't actually know in which direction other fields will be sorted by default… |
130 | if ( $this->mDefaultDirection == IndexPager::DIR_DESCENDING ) { |
131 | $linkType = 'asc'; |
132 | $class = "$sortClass mw-datatable-is-sorted mw-datatable-is-descending"; |
133 | $query['asc'] = '1'; |
134 | $query['desc'] = ''; |
135 | } else { |
136 | $linkType = 'desc'; |
137 | $class = "$sortClass mw-datatable-is-sorted mw-datatable-is-ascending"; |
138 | $query['asc'] = ''; |
139 | $query['desc'] = '1'; |
140 | } |
141 | } |
142 | |
143 | $link = $this->makeLink( htmlspecialchars( $name ), $query, $linkType ); |
144 | $s .= Html::rawElement( 'th', [ 'class' => $class ], $link ) . "\n"; |
145 | } else { |
146 | $s .= Html::element( 'th', [], $name ) . "\n"; |
147 | } |
148 | } |
149 | |
150 | $ret = Html::openElement( 'table', [ |
151 | 'class' => $this->getTableClass() ] |
152 | ); |
153 | $ret .= Html::rawElement( 'thead', [], Html::rawElement( 'tr', [], "\n" . $s . "\n" ) ); |
154 | $ret .= Html::openElement( 'tbody' ) . "\n"; |
155 | |
156 | return $ret; |
157 | } |
158 | |
159 | /** |
160 | * @stable to override |
161 | * @return string |
162 | */ |
163 | protected function getEndBody() { |
164 | return "</tbody></table>\n"; |
165 | } |
166 | |
167 | /** |
168 | * @return string |
169 | */ |
170 | protected function getEmptyBody() { |
171 | $colspan = count( $this->getFieldNames() ); |
172 | $msgEmpty = $this->msg( 'table_pager_empty' )->text(); |
173 | return Html::rawElement( 'tr', [], |
174 | Html::element( 'td', [ 'colspan' => $colspan ], $msgEmpty ) ); |
175 | } |
176 | |
177 | /** |
178 | * @stable to override |
179 | * @param stdClass $row |
180 | * @return string HTML |
181 | */ |
182 | public function formatRow( $row ) { |
183 | $this->mCurrentRow = $row; // In case formatValue etc need to know |
184 | $s = Html::openElement( 'tr', $this->getRowAttrs( $row ) ) . "\n"; |
185 | $fieldNames = $this->getFieldNames(); |
186 | |
187 | foreach ( $fieldNames as $field => $name ) { |
188 | $value = $row->$field ?? null; |
189 | $formatted = strval( $this->formatValue( $field, $value ) ); |
190 | |
191 | if ( $formatted == '' ) { |
192 | $formatted = "\u{00A0}"; |
193 | } |
194 | |
195 | $s .= Html::rawElement( 'td', $this->getCellAttrs( $field, $value ), $formatted ) . "\n"; |
196 | } |
197 | |
198 | $s .= Html::closeElement( 'tr' ) . "\n"; |
199 | |
200 | return $s; |
201 | } |
202 | |
203 | /** |
204 | * Get a class name to be applied to the given row. |
205 | * |
206 | * @stable to override |
207 | * |
208 | * @param stdClass $row The database result row |
209 | * @return string |
210 | */ |
211 | protected function getRowClass( $row ) { |
212 | return ''; |
213 | } |
214 | |
215 | /** |
216 | * Get attributes to be applied to the given row. |
217 | * |
218 | * @stable to override |
219 | * |
220 | * @param stdClass $row The database result row |
221 | * @return array Array of attribute => value |
222 | */ |
223 | protected function getRowAttrs( $row ) { |
224 | return [ 'class' => $this->getRowClass( $row ) ]; |
225 | } |
226 | |
227 | /** |
228 | * @return stdClass |
229 | */ |
230 | protected function getCurrentRow() { |
231 | return $this->mCurrentRow; |
232 | } |
233 | |
234 | /** |
235 | * Get any extra attributes to be applied to the given cell. Don't |
236 | * take this as an excuse to hardcode styles; use classes and |
237 | * CSS instead. Row context is available in $this->mCurrentRow |
238 | * |
239 | * @stable to override |
240 | * |
241 | * @param string $field The column |
242 | * @param string $value The cell contents |
243 | * @return array Array of attr => value |
244 | */ |
245 | protected function getCellAttrs( $field, $value ) { |
246 | return [ 'class' => 'TablePager_col_' . $field ]; |
247 | } |
248 | |
249 | /** |
250 | * @inheritDoc |
251 | * @stable to override |
252 | */ |
253 | public function getIndexField() { |
254 | return $this->mSort; |
255 | } |
256 | |
257 | /** |
258 | * TablePager relies on `mw-datatable` for styling, see T214208 |
259 | * |
260 | * @stable to override |
261 | * @return string |
262 | */ |
263 | protected function getTableClass() { |
264 | return 'mw-datatable'; |
265 | } |
266 | |
267 | /** |
268 | * @stable to override |
269 | * @return string |
270 | */ |
271 | protected function getNavClass() { |
272 | return 'TablePager_nav'; |
273 | } |
274 | |
275 | /** |
276 | * @stable to override |
277 | * @return string |
278 | */ |
279 | protected function getSortHeaderClass() { |
280 | return 'TablePager_sort'; |
281 | } |
282 | |
283 | /** |
284 | * A navigation bar with images |
285 | * |
286 | * @stable to override |
287 | * @return string HTML |
288 | */ |
289 | public function getNavigationBar() { |
290 | if ( !$this->isNavigationBarShown() ) { |
291 | return ''; |
292 | } |
293 | |
294 | $this->getOutput()->enableOOUI(); |
295 | |
296 | $types = [ 'first', 'prev', 'next', 'last' ]; |
297 | |
298 | $queries = $this->getPagingQueries(); |
299 | |
300 | $buttons = []; |
301 | |
302 | $title = $this->getTitle(); |
303 | |
304 | foreach ( $types as $type ) { |
305 | $buttons[] = new ButtonWidget( [ |
306 | // Messages used here: |
307 | // * table_pager_first |
308 | // * table_pager_prev |
309 | // * table_pager_next |
310 | // * table_pager_last |
311 | 'classes' => [ 'TablePager-button-' . $type ], |
312 | 'flags' => [ 'progressive' ], |
313 | 'framed' => false, |
314 | 'label' => $this->msg( 'table_pager_' . $type )->text(), |
315 | 'href' => $queries[ $type ] ? |
316 | $title->getLinkURL( $queries[ $type ] + $this->getDefaultQuery() ) : |
317 | null, |
318 | 'icon' => $type === 'prev' ? 'previous' : $type, |
319 | 'disabled' => $queries[ $type ] === false |
320 | ] ); |
321 | } |
322 | return new ButtonGroupWidget( [ |
323 | 'classes' => [ $this->getNavClass() ], |
324 | 'items' => $buttons, |
325 | ] ); |
326 | } |
327 | |
328 | /** |
329 | * @inheritDoc |
330 | */ |
331 | public function getModuleStyles() { |
332 | return array_merge( |
333 | parent::getModuleStyles(), [ 'oojs-ui.styles.icons-movement' ] |
334 | ); |
335 | } |
336 | |
337 | /** |
338 | * Get a "<select>" element which has options for each of the allowed limits |
339 | * |
340 | * @param string[] $attribs Extra attributes to set |
341 | * @return string HTML fragment |
342 | */ |
343 | public function getLimitSelect( array $attribs = [] ): string { |
344 | $select = new XmlSelect( 'limit', false, $this->mLimit ); |
345 | $select->addOptions( $this->getLimitSelectList() ); |
346 | foreach ( $attribs as $name => $value ) { |
347 | $select->setAttribute( $name, $value ); |
348 | } |
349 | return $select->getHTML(); |
350 | } |
351 | |
352 | /** |
353 | * Get a list of items to show in a "<select>" element of limits. |
354 | * This can be passed directly to XmlSelect::addOptions(). |
355 | * |
356 | * @since 1.22 |
357 | * @return array |
358 | */ |
359 | public function getLimitSelectList() { |
360 | # Add the current limit from the query string |
361 | # to avoid that the limit is lost after clicking Go next time |
362 | if ( !in_array( $this->mLimit, $this->mLimitsShown ) ) { |
363 | $this->mLimitsShown[] = $this->mLimit; |
364 | sort( $this->mLimitsShown ); |
365 | } |
366 | $ret = []; |
367 | foreach ( $this->mLimitsShown as $key => $value ) { |
368 | # The pair is either $index => $limit, in which case the $value |
369 | # will be numeric, or $limit => $text, in which case the $value |
370 | # will be a string. |
371 | if ( is_int( $value ) ) { |
372 | $limit = $value; |
373 | $text = $this->getLanguage()->formatNum( $limit ); |
374 | } else { |
375 | $limit = $key; |
376 | $text = $value; |
377 | } |
378 | $ret[$text] = $limit; |
379 | } |
380 | return $ret; |
381 | } |
382 | |
383 | /** |
384 | * Get \<input type="hidden"\> elements for use in a method="get" form. |
385 | * Resubmits all defined elements of the query string, except for a |
386 | * exclusion list, passed in the $noResubmit parameter. |
387 | * Also array values are discarded for security reasons (per WebRequest::getVal) |
388 | * |
389 | * @param array $noResubmit Parameters from the request query which should not be resubmitted |
390 | * @return string HTML fragment |
391 | */ |
392 | public function getHiddenFields( $noResubmit = [] ) { |
393 | $noResubmit = (array)$noResubmit; |
394 | $query = $this->getRequest()->getQueryValues(); |
395 | foreach ( $noResubmit as $name ) { |
396 | unset( $query[$name] ); |
397 | } |
398 | $s = ''; |
399 | foreach ( $query as $name => $value ) { |
400 | if ( is_array( $value ) ) { |
401 | // Per WebRequest::getVal: Array values are discarded for security reasons. |
402 | continue; |
403 | } |
404 | $s .= Html::hidden( $name, $value ) . "\n"; |
405 | } |
406 | return $s; |
407 | } |
408 | |
409 | /** |
410 | * Get a form containing a limit selection dropdown |
411 | * |
412 | * @return string HTML fragment |
413 | */ |
414 | public function getLimitForm() { |
415 | return Html::rawElement( |
416 | 'form', |
417 | [ |
418 | 'method' => 'get', |
419 | 'action' => wfScript(), |
420 | ], |
421 | "\n" . $this->getLimitDropdown() |
422 | ) . "\n"; |
423 | } |
424 | |
425 | /** |
426 | * Gets a limit selection dropdown |
427 | * |
428 | * @return string |
429 | */ |
430 | private function getLimitDropdown() { |
431 | # Make the select with some explanatory text |
432 | $msgSubmit = $this->msg( 'table_pager_limit_submit' )->escaped(); |
433 | |
434 | return $this->msg( 'table_pager_limit' ) |
435 | ->rawParams( $this->getLimitSelect() )->escaped() . |
436 | "\n<input type=\"submit\" value=\"$msgSubmit\"/>\n" . |
437 | $this->getHiddenFields( [ 'limit' ] ); |
438 | } |
439 | |
440 | /** |
441 | * Return true if the named field should be sortable by the UI, false |
442 | * otherwise |
443 | * |
444 | * @param string $field |
445 | * @return bool |
446 | */ |
447 | abstract protected function isFieldSortable( $field ); |
448 | |
449 | /** |
450 | * Format a table cell. The return value should be HTML, but use an empty |
451 | * string not   for empty cells. Do not include the <td> and </td>. |
452 | * |
453 | * The current result row is available as $this->mCurrentRow, in case you |
454 | * need more context. |
455 | * |
456 | * @param string $name The database field name |
457 | * @param string|null $value The value retrieved from the database, or null if |
458 | * the row doesn't contain this field |
459 | */ |
460 | abstract public function formatValue( $name, $value ); |
461 | |
462 | /** |
463 | * The database field name used as a default sort order. |
464 | * |
465 | * Note that this field will only be sorted on if isFieldSortable returns |
466 | * true for this field. If not (e.g. paginating on multiple columns), this |
467 | * should return empty string, and getIndexField should be overridden. |
468 | * |
469 | * @return string |
470 | */ |
471 | abstract public function getDefaultSort(); |
472 | |
473 | /** |
474 | * An array mapping database field names to a textual description of the |
475 | * field name, for use in the table header. The description should be plain |
476 | * text, it will be HTML-escaped later. |
477 | * |
478 | * @return string[] |
479 | */ |
480 | abstract protected function getFieldNames(); |
481 | } |
482 | |
483 | /** @deprecated class alias since 1.41 */ |
484 | class_alias( TablePager::class, 'TablePager' ); |