TablePager.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. <?php
  2. /**
  3. * Efficient paging for SQL queries.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. * @ingroup Pager
  22. */
  23. use MediaWiki\Linker\LinkRenderer;
  24. /**
  25. * Table-based display with a user-selectable sort order
  26. * @ingroup Pager
  27. */
  28. abstract class TablePager extends IndexPager {
  29. /** @var string */
  30. protected $mSort;
  31. /** @var stdClass */
  32. protected $mCurrentRow;
  33. public function __construct( IContextSource $context = null, LinkRenderer $linkRenderer = null ) {
  34. if ( $context ) {
  35. $this->setContext( $context );
  36. }
  37. $this->mSort = $this->getRequest()->getText( 'sort' );
  38. if ( !array_key_exists( $this->mSort, $this->getFieldNames() )
  39. || !$this->isFieldSortable( $this->mSort )
  40. ) {
  41. $this->mSort = $this->getDefaultSort();
  42. }
  43. if ( $this->getRequest()->getBool( 'asc' ) ) {
  44. $this->mDefaultDirection = IndexPager::DIR_ASCENDING;
  45. } elseif ( $this->getRequest()->getBool( 'desc' ) ) {
  46. $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
  47. } /* Else leave it at whatever the class default is */
  48. // Parent constructor needs mSort set, so we call it last
  49. parent::__construct( null, $linkRenderer );
  50. }
  51. /**
  52. * Get the formatted result list. Calls getStartBody(), formatRow() and getEndBody(), concatenates
  53. * the results and returns them.
  54. *
  55. * Also adds the required styles to our OutputPage object (this means that if context wasn't
  56. * passed to constructor or otherwise set up, you will get a pager with missing styles).
  57. *
  58. * This method has been made 'final' in 1.24. There's no reason to override it, and if there exist
  59. * any subclasses that do, the style loading hack is probably broken in them. Let's fail fast
  60. * rather than mysteriously render things wrong.
  61. *
  62. * @deprecated since 1.24, use getBodyOutput() or getFullOutput() instead
  63. * @return string
  64. */
  65. final public function getBody() {
  66. $this->getOutput()->addModuleStyles( $this->getModuleStyles() );
  67. return parent::getBody();
  68. }
  69. /**
  70. * Get the formatted result list.
  71. *
  72. * Calls getBody() and getModuleStyles() and builds a ParserOutput object. (This is a bit hacky
  73. * but works well.)
  74. *
  75. * @since 1.24
  76. * @return ParserOutput
  77. */
  78. public function getBodyOutput() {
  79. $body = parent::getBody();
  80. $pout = new ParserOutput;
  81. $pout->setText( $body );
  82. $pout->addModuleStyles( $this->getModuleStyles() );
  83. return $pout;
  84. }
  85. /**
  86. * Get the formatted result list, with navigation bars.
  87. *
  88. * Calls getBody(), getNavigationBar() and getModuleStyles() and
  89. * builds a ParserOutput object. (This is a bit hacky but works well.)
  90. *
  91. * @since 1.24
  92. * @return ParserOutput
  93. */
  94. public function getFullOutput() {
  95. $navigation = $this->getNavigationBar();
  96. $body = parent::getBody();
  97. $pout = new ParserOutput;
  98. $pout->setText( $navigation . $body . $navigation );
  99. $pout->addModuleStyles( $this->getModuleStyles() );
  100. return $pout;
  101. }
  102. /**
  103. * @protected
  104. * @return string
  105. */
  106. protected function getStartBody() {
  107. $sortClass = $this->getSortHeaderClass();
  108. $s = '';
  109. $fields = $this->getFieldNames();
  110. // Make table header
  111. foreach ( $fields as $field => $name ) {
  112. if ( strval( $name ) == '' ) {
  113. $s .= Html::rawElement( 'th', [], "\u{00A0}" ) . "\n";
  114. } elseif ( $this->isFieldSortable( $field ) ) {
  115. $query = [ 'sort' => $field, 'limit' => $this->mLimit ];
  116. $linkType = null;
  117. $class = null;
  118. if ( $this->mSort == $field ) {
  119. // The table is sorted by this field already, make a link to sort in the other direction
  120. // We don't actually know in which direction other fields will be sorted by default…
  121. if ( $this->mDefaultDirection == IndexPager::DIR_DESCENDING ) {
  122. $linkType = 'asc';
  123. $class = "$sortClass mw-datatable-is-sorted mw-datatable-is-descending";
  124. $query['asc'] = '1';
  125. $query['desc'] = '';
  126. } else {
  127. $linkType = 'desc';
  128. $class = "$sortClass mw-datatable-is-sorted mw-datatable-is-ascending";
  129. $query['asc'] = '';
  130. $query['desc'] = '1';
  131. }
  132. }
  133. $link = $this->makeLink( htmlspecialchars( $name ), $query, $linkType );
  134. $s .= Html::rawElement( 'th', [ 'class' => $class ], $link ) . "\n";
  135. } else {
  136. $s .= Html::element( 'th', [], $name ) . "\n";
  137. }
  138. }
  139. $tableClass = $this->getTableClass();
  140. $ret = Html::openElement( 'table', [
  141. 'class' => " $tableClass" ]
  142. );
  143. $ret .= Html::rawElement( 'thead', [], Html::rawElement( 'tr', [], "\n" . $s . "\n" ) );
  144. $ret .= Html::openElement( 'tbody' ) . "\n";
  145. return $ret;
  146. }
  147. /**
  148. * @protected
  149. * @return string
  150. */
  151. protected function getEndBody() {
  152. return "</tbody></table>\n";
  153. }
  154. /**
  155. * @protected
  156. * @return string
  157. */
  158. function getEmptyBody() {
  159. $colspan = count( $this->getFieldNames() );
  160. $msgEmpty = $this->msg( 'table_pager_empty' )->text();
  161. return Html::rawElement( 'tr', [],
  162. Html::element( 'td', [ 'colspan' => $colspan ], $msgEmpty ) );
  163. }
  164. /**
  165. * @protected
  166. * @param stdClass $row
  167. * @return string HTML
  168. */
  169. function formatRow( $row ) {
  170. $this->mCurrentRow = $row; // In case formatValue etc need to know
  171. $s = Html::openElement( 'tr', $this->getRowAttrs( $row ) ) . "\n";
  172. $fieldNames = $this->getFieldNames();
  173. foreach ( $fieldNames as $field => $name ) {
  174. $value = $row->$field ?? null;
  175. $formatted = strval( $this->formatValue( $field, $value ) );
  176. if ( $formatted == '' ) {
  177. $formatted = "\u{00A0}";
  178. }
  179. $s .= Html::rawElement( 'td', $this->getCellAttrs( $field, $value ), $formatted ) . "\n";
  180. }
  181. $s .= Html::closeElement( 'tr' ) . "\n";
  182. return $s;
  183. }
  184. /**
  185. * Get a class name to be applied to the given row.
  186. *
  187. * @protected
  188. *
  189. * @param object $row The database result row
  190. * @return string
  191. */
  192. function getRowClass( $row ) {
  193. return '';
  194. }
  195. /**
  196. * Get attributes to be applied to the given row.
  197. *
  198. * @protected
  199. *
  200. * @param object $row The database result row
  201. * @return array Array of attribute => value
  202. */
  203. function getRowAttrs( $row ) {
  204. $class = $this->getRowClass( $row );
  205. if ( $class === '' ) {
  206. // Return an empty array to avoid clutter in HTML like class=""
  207. return [];
  208. } else {
  209. return [ 'class' => $this->getRowClass( $row ) ];
  210. }
  211. }
  212. /**
  213. * @return stdClass
  214. */
  215. protected function getCurrentRow() {
  216. return $this->mCurrentRow;
  217. }
  218. /**
  219. * Get any extra attributes to be applied to the given cell. Don't
  220. * take this as an excuse to hardcode styles; use classes and
  221. * CSS instead. Row context is available in $this->mCurrentRow
  222. *
  223. * @protected
  224. *
  225. * @param string $field The column
  226. * @param string $value The cell contents
  227. * @return array Array of attr => value
  228. */
  229. function getCellAttrs( $field, $value ) {
  230. return [ 'class' => 'TablePager_col_' . $field ];
  231. }
  232. /**
  233. * @protected
  234. * @return string
  235. */
  236. function getIndexField() {
  237. return $this->mSort;
  238. }
  239. /**
  240. * TablePager relies on `mw-datatable` for styling, see T214208
  241. * @return string
  242. */
  243. protected function getTableClass() {
  244. return 'mw-datatable';
  245. }
  246. /**
  247. * @return string
  248. */
  249. protected function getNavClass() {
  250. return 'TablePager_nav';
  251. }
  252. /**
  253. * @return string
  254. */
  255. protected function getSortHeaderClass() {
  256. return 'TablePager_sort';
  257. }
  258. /**
  259. * A navigation bar with images
  260. * @return string HTML
  261. */
  262. public function getNavigationBar() {
  263. if ( !$this->isNavigationBarShown() ) {
  264. return '';
  265. }
  266. $this->getOutput()->enableOOUI();
  267. $types = [ 'first', 'prev', 'next', 'last' ];
  268. $queries = $this->getPagingQueries();
  269. $buttons = [];
  270. $title = $this->getTitle();
  271. foreach ( $types as $type ) {
  272. $buttons[] = new \OOUI\ButtonWidget( [
  273. // Messages used here:
  274. // * table_pager_first
  275. // * table_pager_prev
  276. // * table_pager_next
  277. // * table_pager_last
  278. 'classes' => [ 'TablePager-button-' . $type ],
  279. 'flags' => [ 'progressive' ],
  280. 'framed' => false,
  281. 'label' => $this->msg( 'table_pager_' . $type )->text(),
  282. 'href' => $queries[ $type ] ?
  283. $title->getLinkURL( $queries[ $type ] + $this->getDefaultQuery() ) :
  284. null,
  285. 'icon' => $type === 'prev' ? 'previous' : $type,
  286. 'disabled' => $queries[ $type ] === false
  287. ] );
  288. }
  289. return new \OOUI\ButtonGroupWidget( [
  290. 'classes' => [ $this->getNavClass() ],
  291. 'items' => $buttons,
  292. ] );
  293. }
  294. /**
  295. * ResourceLoader modules that must be loaded to provide correct styling for this pager
  296. * @since 1.24
  297. * @return string[]
  298. */
  299. public function getModuleStyles() {
  300. return [ 'mediawiki.pager.tablePager', 'oojs-ui.styles.icons-movement' ];
  301. }
  302. /**
  303. * Get a "<select>" element which has options for each of the allowed limits
  304. *
  305. * @param string[] $attribs Extra attributes to set
  306. * @return string HTML fragment
  307. */
  308. public function getLimitSelect( $attribs = [] ) {
  309. $select = new XmlSelect( 'limit', false, $this->mLimit );
  310. $select->addOptions( $this->getLimitSelectList() );
  311. foreach ( $attribs as $name => $value ) {
  312. $select->setAttribute( $name, $value );
  313. }
  314. return $select->getHTML();
  315. }
  316. /**
  317. * Get a list of items to show in a "<select>" element of limits.
  318. * This can be passed directly to XmlSelect::addOptions().
  319. *
  320. * @since 1.22
  321. * @return array
  322. */
  323. public function getLimitSelectList() {
  324. # Add the current limit from the query string
  325. # to avoid that the limit is lost after clicking Go next time
  326. if ( !in_array( $this->mLimit, $this->mLimitsShown ) ) {
  327. $this->mLimitsShown[] = $this->mLimit;
  328. sort( $this->mLimitsShown );
  329. }
  330. $ret = [];
  331. foreach ( $this->mLimitsShown as $key => $value ) {
  332. # The pair is either $index => $limit, in which case the $value
  333. # will be numeric, or $limit => $text, in which case the $value
  334. # will be a string.
  335. if ( is_int( $value ) ) {
  336. $limit = $value;
  337. $text = $this->getLanguage()->formatNum( $limit );
  338. } else {
  339. $limit = $key;
  340. $text = $value;
  341. }
  342. $ret[$text] = $limit;
  343. }
  344. return $ret;
  345. }
  346. /**
  347. * Get \<input type="hidden"\> elements for use in a method="get" form.
  348. * Resubmits all defined elements of the query string, except for a
  349. * blacklist, passed in the $blacklist parameter.
  350. *
  351. * @param array $blacklist Parameters from the request query which should not be resubmitted
  352. * @return string HTML fragment
  353. */
  354. function getHiddenFields( $blacklist = [] ) {
  355. $blacklist = (array)$blacklist;
  356. $query = $this->getRequest()->getQueryValues();
  357. foreach ( $blacklist as $name ) {
  358. unset( $query[$name] );
  359. }
  360. $s = '';
  361. foreach ( $query as $name => $value ) {
  362. $s .= Html::hidden( $name, $value ) . "\n";
  363. }
  364. return $s;
  365. }
  366. /**
  367. * Get a form containing a limit selection dropdown
  368. *
  369. * @return string HTML fragment
  370. */
  371. function getLimitForm() {
  372. return Html::rawElement(
  373. 'form',
  374. [
  375. 'method' => 'get',
  376. 'action' => wfScript(),
  377. ],
  378. "\n" . $this->getLimitDropdown()
  379. ) . "\n";
  380. }
  381. /**
  382. * Gets a limit selection dropdown
  383. *
  384. * @return string
  385. */
  386. function getLimitDropdown() {
  387. # Make the select with some explanatory text
  388. $msgSubmit = $this->msg( 'table_pager_limit_submit' )->escaped();
  389. return $this->msg( 'table_pager_limit' )
  390. ->rawParams( $this->getLimitSelect() )->escaped() .
  391. "\n<input type=\"submit\" value=\"$msgSubmit\"/>\n" .
  392. $this->getHiddenFields( [ 'limit' ] );
  393. }
  394. /**
  395. * Return true if the named field should be sortable by the UI, false
  396. * otherwise
  397. *
  398. * @param string $field
  399. */
  400. abstract function isFieldSortable( $field );
  401. /**
  402. * Format a table cell. The return value should be HTML, but use an empty
  403. * string not &#160; for empty cells. Do not include the <td> and </td>.
  404. *
  405. * The current result row is available as $this->mCurrentRow, in case you
  406. * need more context.
  407. *
  408. * @protected
  409. *
  410. * @param string $name The database field name
  411. * @param string $value The value retrieved from the database
  412. */
  413. abstract function formatValue( $name, $value );
  414. /**
  415. * The database field name used as a default sort order.
  416. *
  417. * @protected
  418. *
  419. * @return string
  420. */
  421. abstract function getDefaultSort();
  422. /**
  423. * An array mapping database field names to a textual description of the
  424. * field name, for use in the table header. The description should be plain
  425. * text, it will be HTML-escaped later.
  426. *
  427. * @return array
  428. */
  429. abstract function getFieldNames();
  430. }