class-wp-list-table.php 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323
  1. <?php
  2. /**
  3. * Administration API: WP_List_Table class
  4. *
  5. * @package WordPress
  6. * @subpackage List_Table
  7. * @since 3.1.0
  8. */
  9. /**
  10. * Base class for displaying a list of items in an ajaxified HTML table.
  11. *
  12. * @since 3.1.0
  13. * @access private
  14. */
  15. class WP_List_Table {
  16. /**
  17. * The current list of items.
  18. *
  19. * @since 3.1.0
  20. * @var array
  21. */
  22. public $items;
  23. /**
  24. * Various information about the current table.
  25. *
  26. * @since 3.1.0
  27. * @var array
  28. */
  29. protected $_args;
  30. /**
  31. * Various information needed for displaying the pagination.
  32. *
  33. * @since 3.1.0
  34. * @var array
  35. */
  36. protected $_pagination_args = array();
  37. /**
  38. * The current screen.
  39. *
  40. * @since 3.1.0
  41. * @var object
  42. */
  43. protected $screen;
  44. /**
  45. * Cached bulk actions.
  46. *
  47. * @since 3.1.0
  48. * @var array
  49. */
  50. private $_actions;
  51. /**
  52. * Cached pagination output.
  53. *
  54. * @since 3.1.0
  55. * @var string
  56. */
  57. private $_pagination;
  58. /**
  59. * The view switcher modes.
  60. *
  61. * @since 4.1.0
  62. * @var array
  63. */
  64. protected $modes = array();
  65. /**
  66. * Stores the value returned by ->get_column_info().
  67. *
  68. * @since 4.1.0
  69. * @var array
  70. */
  71. protected $_column_headers;
  72. /**
  73. * {@internal Missing Summary}
  74. *
  75. * @var array
  76. */
  77. protected $compat_fields = array( '_args', '_pagination_args', 'screen', '_actions', '_pagination' );
  78. /**
  79. * {@internal Missing Summary}
  80. *
  81. * @var array
  82. */
  83. protected $compat_methods = array( 'set_pagination_args', 'get_views', 'get_bulk_actions', 'bulk_actions',
  84. 'row_actions', 'months_dropdown', 'view_switcher', 'comments_bubble', 'get_items_per_page', 'pagination',
  85. 'get_sortable_columns', 'get_column_info', 'get_table_classes', 'display_tablenav', 'extra_tablenav',
  86. 'single_row_columns' );
  87. /**
  88. * Constructor.
  89. *
  90. * The child class should call this constructor from its own constructor to override
  91. * the default $args.
  92. *
  93. * @since 3.1.0
  94. *
  95. * @param array|string $args {
  96. * Array or string of arguments.
  97. *
  98. * @type string $plural Plural value used for labels and the objects being listed.
  99. * This affects things such as CSS class-names and nonces used
  100. * in the list table, e.g. 'posts'. Default empty.
  101. * @type string $singular Singular label for an object being listed, e.g. 'post'.
  102. * Default empty
  103. * @type bool $ajax Whether the list table supports Ajax. This includes loading
  104. * and sorting data, for example. If true, the class will call
  105. * the _js_vars() method in the footer to provide variables
  106. * to any scripts handling Ajax events. Default false.
  107. * @type string $screen String containing the hook name used to determine the current
  108. * screen. If left null, the current screen will be automatically set.
  109. * Default null.
  110. * }
  111. */
  112. public function __construct( $args = array() ) {
  113. $args = wp_parse_args( $args, array(
  114. 'plural' => '',
  115. 'singular' => '',
  116. 'ajax' => false,
  117. 'screen' => null,
  118. ) );
  119. $this->screen = convert_to_screen( $args['screen'] );
  120. add_filter( "manage_{$this->screen->id}_columns", array( $this, 'get_columns' ), 0 );
  121. if ( !$args['plural'] )
  122. $args['plural'] = $this->screen->base;
  123. $args['plural'] = sanitize_key( $args['plural'] );
  124. $args['singular'] = sanitize_key( $args['singular'] );
  125. $this->_args = $args;
  126. if ( $args['ajax'] ) {
  127. // wp_enqueue_script( 'list-table' );
  128. add_action( 'admin_footer', array( $this, '_js_vars' ) );
  129. }
  130. if ( empty( $this->modes ) ) {
  131. $this->modes = array(
  132. 'list' => __( 'List View' ),
  133. 'excerpt' => __( 'Excerpt View' )
  134. );
  135. }
  136. }
  137. /**
  138. * Make private properties readable for backward compatibility.
  139. *
  140. * @since 4.0.0
  141. *
  142. * @param string $name Property to get.
  143. * @return mixed Property.
  144. */
  145. public function __get( $name ) {
  146. if ( in_array( $name, $this->compat_fields ) ) {
  147. return $this->$name;
  148. }
  149. }
  150. /**
  151. * Make private properties settable for backward compatibility.
  152. *
  153. * @since 4.0.0
  154. *
  155. * @param string $name Property to check if set.
  156. * @param mixed $value Property value.
  157. * @return mixed Newly-set property.
  158. */
  159. public function __set( $name, $value ) {
  160. if ( in_array( $name, $this->compat_fields ) ) {
  161. return $this->$name = $value;
  162. }
  163. }
  164. /**
  165. * Make private properties checkable for backward compatibility.
  166. *
  167. * @since 4.0.0
  168. *
  169. * @param string $name Property to check if set.
  170. * @return bool Whether the property is set.
  171. */
  172. public function __isset( $name ) {
  173. if ( in_array( $name, $this->compat_fields ) ) {
  174. return isset( $this->$name );
  175. }
  176. }
  177. /**
  178. * Make private properties un-settable for backward compatibility.
  179. *
  180. * @since 4.0.0
  181. *
  182. * @param string $name Property to unset.
  183. */
  184. public function __unset( $name ) {
  185. if ( in_array( $name, $this->compat_fields ) ) {
  186. unset( $this->$name );
  187. }
  188. }
  189. /**
  190. * Make private/protected methods readable for backward compatibility.
  191. *
  192. * @since 4.0.0
  193. *
  194. * @param callable $name Method to call.
  195. * @param array $arguments Arguments to pass when calling.
  196. * @return mixed|bool Return value of the callback, false otherwise.
  197. */
  198. public function __call( $name, $arguments ) {
  199. if ( in_array( $name, $this->compat_methods ) ) {
  200. return call_user_func_array( array( $this, $name ), $arguments );
  201. }
  202. return false;
  203. }
  204. /**
  205. * Checks the current user's permissions
  206. *
  207. * @since 3.1.0
  208. * @abstract
  209. */
  210. public function ajax_user_can() {
  211. die( 'function WP_List_Table::ajax_user_can() must be over-ridden in a sub-class.' );
  212. }
  213. /**
  214. * Prepares the list of items for displaying.
  215. * @uses WP_List_Table::set_pagination_args()
  216. *
  217. * @since 3.1.0
  218. * @abstract
  219. */
  220. public function prepare_items() {
  221. die( 'function WP_List_Table::prepare_items() must be over-ridden in a sub-class.' );
  222. }
  223. /**
  224. * An internal method that sets all the necessary pagination arguments
  225. *
  226. * @since 3.1.0
  227. *
  228. * @param array|string $args Array or string of arguments with information about the pagination.
  229. */
  230. protected function set_pagination_args( $args ) {
  231. $args = wp_parse_args( $args, array(
  232. 'total_items' => 0,
  233. 'total_pages' => 0,
  234. 'per_page' => 0,
  235. ) );
  236. if ( !$args['total_pages'] && $args['per_page'] > 0 )
  237. $args['total_pages'] = ceil( $args['total_items'] / $args['per_page'] );
  238. // Redirect if page number is invalid and headers are not already sent.
  239. if ( ! headers_sent() && ! wp_doing_ajax() && $args['total_pages'] > 0 && $this->get_pagenum() > $args['total_pages'] ) {
  240. wp_redirect( add_query_arg( 'paged', $args['total_pages'] ) );
  241. exit;
  242. }
  243. $this->_pagination_args = $args;
  244. }
  245. /**
  246. * Access the pagination args.
  247. *
  248. * @since 3.1.0
  249. *
  250. * @param string $key Pagination argument to retrieve. Common values include 'total_items',
  251. * 'total_pages', 'per_page', or 'infinite_scroll'.
  252. * @return int Number of items that correspond to the given pagination argument.
  253. */
  254. public function get_pagination_arg( $key ) {
  255. if ( 'page' === $key ) {
  256. return $this->get_pagenum();
  257. }
  258. if ( isset( $this->_pagination_args[$key] ) ) {
  259. return $this->_pagination_args[$key];
  260. }
  261. }
  262. /**
  263. * Whether the table has items to display or not
  264. *
  265. * @since 3.1.0
  266. *
  267. * @return bool
  268. */
  269. public function has_items() {
  270. return !empty( $this->items );
  271. }
  272. /**
  273. * Message to be displayed when there are no items
  274. *
  275. * @since 3.1.0
  276. */
  277. public function no_items() {
  278. _e( 'No items found.' );
  279. }
  280. /**
  281. * Displays the search box.
  282. *
  283. * @since 3.1.0
  284. *
  285. * @param string $text The 'submit' button label.
  286. * @param string $input_id ID attribute value for the search input field.
  287. */
  288. public function search_box( $text, $input_id ) {
  289. if ( empty( $_REQUEST['s'] ) && !$this->has_items() )
  290. return;
  291. $input_id = $input_id . '-search-input';
  292. if ( ! empty( $_REQUEST['orderby'] ) )
  293. echo '<input type="hidden" name="orderby" value="' . esc_attr( $_REQUEST['orderby'] ) . '" />';
  294. if ( ! empty( $_REQUEST['order'] ) )
  295. echo '<input type="hidden" name="order" value="' . esc_attr( $_REQUEST['order'] ) . '" />';
  296. if ( ! empty( $_REQUEST['post_mime_type'] ) )
  297. echo '<input type="hidden" name="post_mime_type" value="' . esc_attr( $_REQUEST['post_mime_type'] ) . '" />';
  298. if ( ! empty( $_REQUEST['detached'] ) )
  299. echo '<input type="hidden" name="detached" value="' . esc_attr( $_REQUEST['detached'] ) . '" />';
  300. ?>
  301. <p class="search-box">
  302. <label class="screen-reader-text" for="<?php echo esc_attr( $input_id ); ?>"><?php echo $text; ?>:</label>
  303. <input type="search" id="<?php echo esc_attr( $input_id ); ?>" name="s" value="<?php _admin_search_query(); ?>" />
  304. <?php submit_button( $text, '', '', false, array( 'id' => 'search-submit' ) ); ?>
  305. </p>
  306. <?php
  307. }
  308. /**
  309. * Get an associative array ( id => link ) with the list
  310. * of views available on this table.
  311. *
  312. * @since 3.1.0
  313. *
  314. * @return array
  315. */
  316. protected function get_views() {
  317. return array();
  318. }
  319. /**
  320. * Display the list of views available on this table.
  321. *
  322. * @since 3.1.0
  323. */
  324. public function views() {
  325. $views = $this->get_views();
  326. /**
  327. * Filters the list of available list table views.
  328. *
  329. * The dynamic portion of the hook name, `$this->screen->id`, refers
  330. * to the ID of the current screen, usually a string.
  331. *
  332. * @since 3.5.0
  333. *
  334. * @param array $views An array of available list table views.
  335. */
  336. $views = apply_filters( "views_{$this->screen->id}", $views );
  337. if ( empty( $views ) )
  338. return;
  339. $this->screen->render_screen_reader_content( 'heading_views' );
  340. echo "<ul class='subsubsub'>\n";
  341. foreach ( $views as $class => $view ) {
  342. $views[ $class ] = "\t<li class='$class'>$view";
  343. }
  344. echo implode( " |</li>\n", $views ) . "</li>\n";
  345. echo "</ul>";
  346. }
  347. /**
  348. * Get an associative array ( option_name => option_title ) with the list
  349. * of bulk actions available on this table.
  350. *
  351. * @since 3.1.0
  352. *
  353. * @return array
  354. */
  355. protected function get_bulk_actions() {
  356. return array();
  357. }
  358. /**
  359. * Display the bulk actions dropdown.
  360. *
  361. * @since 3.1.0
  362. *
  363. * @param string $which The location of the bulk actions: 'top' or 'bottom'.
  364. * This is designated as optional for backward compatibility.
  365. */
  366. protected function bulk_actions( $which = '' ) {
  367. if ( is_null( $this->_actions ) ) {
  368. $this->_actions = $this->get_bulk_actions();
  369. /**
  370. * Filters the list table Bulk Actions drop-down.
  371. *
  372. * The dynamic portion of the hook name, `$this->screen->id`, refers
  373. * to the ID of the current screen, usually a string.
  374. *
  375. * This filter can currently only be used to remove bulk actions.
  376. *
  377. * @since 3.5.0
  378. *
  379. * @param array $actions An array of the available bulk actions.
  380. */
  381. $this->_actions = apply_filters( "bulk_actions-{$this->screen->id}", $this->_actions );
  382. $two = '';
  383. } else {
  384. $two = '2';
  385. }
  386. if ( empty( $this->_actions ) )
  387. return;
  388. echo '<label for="bulk-action-selector-' . esc_attr( $which ) . '" class="screen-reader-text">' . __( 'Select bulk action' ) . '</label>';
  389. echo '<select name="action' . $two . '" id="bulk-action-selector-' . esc_attr( $which ) . "\">\n";
  390. echo '<option value="-1">' . __( 'Bulk Actions' ) . "</option>\n";
  391. foreach ( $this->_actions as $name => $title ) {
  392. $class = 'edit' === $name ? ' class="hide-if-no-js"' : '';
  393. echo "\t" . '<option value="' . $name . '"' . $class . '>' . $title . "</option>\n";
  394. }
  395. echo "</select>\n";
  396. submit_button( __( 'Apply' ), 'action', '', false, array( 'id' => "doaction$two" ) );
  397. echo "\n";
  398. }
  399. /**
  400. * Get the current action selected from the bulk actions dropdown.
  401. *
  402. * @since 3.1.0
  403. *
  404. * @return string|false The action name or False if no action was selected
  405. */
  406. public function current_action() {
  407. if ( isset( $_REQUEST['filter_action'] ) && ! empty( $_REQUEST['filter_action'] ) )
  408. return false;
  409. if ( isset( $_REQUEST['action'] ) && -1 != $_REQUEST['action'] )
  410. return $_REQUEST['action'];
  411. if ( isset( $_REQUEST['action2'] ) && -1 != $_REQUEST['action2'] )
  412. return $_REQUEST['action2'];
  413. return false;
  414. }
  415. /**
  416. * Generate row actions div
  417. *
  418. * @since 3.1.0
  419. *
  420. * @param array $actions The list of actions
  421. * @param bool $always_visible Whether the actions should be always visible
  422. * @return string
  423. */
  424. protected function row_actions( $actions, $always_visible = false ) {
  425. $action_count = count( $actions );
  426. $i = 0;
  427. if ( !$action_count )
  428. return '';
  429. $out = '<div class="' . ( $always_visible ? 'row-actions visible' : 'row-actions' ) . '">';
  430. foreach ( $actions as $action => $link ) {
  431. ++$i;
  432. ( $i == $action_count ) ? $sep = '' : $sep = ' | ';
  433. $out .= "<span class='$action'>$link$sep</span>";
  434. }
  435. $out .= '</div>';
  436. $out .= '<button type="button" class="toggle-row"><span class="screen-reader-text">' . __( 'Show more details' ) . '</span></button>';
  437. return $out;
  438. }
  439. /**
  440. * Display a monthly dropdown for filtering items
  441. *
  442. * @since 3.1.0
  443. *
  444. * @global wpdb $wpdb
  445. * @global WP_Locale $wp_locale
  446. *
  447. * @param string $post_type
  448. */
  449. protected function months_dropdown( $post_type ) {
  450. global $wpdb, $wp_locale;
  451. /**
  452. * Filters whether to remove the 'Months' drop-down from the post list table.
  453. *
  454. * @since 4.2.0
  455. *
  456. * @param bool $disable Whether to disable the drop-down. Default false.
  457. * @param string $post_type The post type.
  458. */
  459. if ( apply_filters( 'disable_months_dropdown', false, $post_type ) ) {
  460. return;
  461. }
  462. $extra_checks = "AND post_status != 'auto-draft'";
  463. if ( ! isset( $_GET['post_status'] ) || 'trash' !== $_GET['post_status'] ) {
  464. $extra_checks .= " AND post_status != 'trash'";
  465. } elseif ( isset( $_GET['post_status'] ) ) {
  466. $extra_checks = $wpdb->prepare( ' AND post_status = %s', $_GET['post_status'] );
  467. }
  468. $months = $wpdb->get_results( $wpdb->prepare( "
  469. SELECT DISTINCT YEAR( post_date ) AS year, MONTH( post_date ) AS month
  470. FROM $wpdb->posts
  471. WHERE post_type = %s
  472. $extra_checks
  473. ORDER BY post_date DESC
  474. ", $post_type ) );
  475. /**
  476. * Filters the 'Months' drop-down results.
  477. *
  478. * @since 3.7.0
  479. *
  480. * @param object $months The months drop-down query results.
  481. * @param string $post_type The post type.
  482. */
  483. $months = apply_filters( 'months_dropdown_results', $months, $post_type );
  484. $month_count = count( $months );
  485. if ( !$month_count || ( 1 == $month_count && 0 == $months[0]->month ) )
  486. return;
  487. $m = isset( $_GET['m'] ) ? (int) $_GET['m'] : 0;
  488. ?>
  489. <label for="filter-by-date" class="screen-reader-text"><?php _e( 'Filter by date' ); ?></label>
  490. <select name="m" id="filter-by-date">
  491. <option<?php selected( $m, 0 ); ?> value="0"><?php _e( 'All dates' ); ?></option>
  492. <?php
  493. foreach ( $months as $arc_row ) {
  494. if ( 0 == $arc_row->year )
  495. continue;
  496. $month = zeroise( $arc_row->month, 2 );
  497. $year = $arc_row->year;
  498. printf( "<option %s value='%s'>%s</option>\n",
  499. selected( $m, $year . $month, false ),
  500. esc_attr( $arc_row->year . $month ),
  501. /* translators: 1: month name, 2: 4-digit year */
  502. sprintf( __( '%1$s %2$d' ), $wp_locale->get_month( $month ), $year )
  503. );
  504. }
  505. ?>
  506. </select>
  507. <?php
  508. }
  509. /**
  510. * Display a view switcher
  511. *
  512. * @since 3.1.0
  513. *
  514. * @param string $current_mode
  515. */
  516. protected function view_switcher( $current_mode ) {
  517. ?>
  518. <input type="hidden" name="mode" value="<?php echo esc_attr( $current_mode ); ?>" />
  519. <div class="view-switch">
  520. <?php
  521. foreach ( $this->modes as $mode => $title ) {
  522. $classes = array( 'view-' . $mode );
  523. if ( $current_mode === $mode )
  524. $classes[] = 'current';
  525. printf(
  526. "<a href='%s' class='%s' id='view-switch-$mode'><span class='screen-reader-text'>%s</span></a>\n",
  527. esc_url( add_query_arg( 'mode', $mode ) ),
  528. implode( ' ', $classes ),
  529. $title
  530. );
  531. }
  532. ?>
  533. </div>
  534. <?php
  535. }
  536. /**
  537. * Display a comment count bubble
  538. *
  539. * @since 3.1.0
  540. *
  541. * @param int $post_id The post ID.
  542. * @param int $pending_comments Number of pending comments.
  543. */
  544. protected function comments_bubble( $post_id, $pending_comments ) {
  545. $approved_comments = get_comments_number();
  546. $approved_comments_number = number_format_i18n( $approved_comments );
  547. $pending_comments_number = number_format_i18n( $pending_comments );
  548. $approved_only_phrase = sprintf( _n( '%s comment', '%s comments', $approved_comments ), $approved_comments_number );
  549. $approved_phrase = sprintf( _n( '%s approved comment', '%s approved comments', $approved_comments ), $approved_comments_number );
  550. $pending_phrase = sprintf( _n( '%s pending comment', '%s pending comments', $pending_comments ), $pending_comments_number );
  551. // No comments at all.
  552. if ( ! $approved_comments && ! $pending_comments ) {
  553. printf( '<span aria-hidden="true">&#8212;</span><span class="screen-reader-text">%s</span>',
  554. __( 'No comments' )
  555. );
  556. // Approved comments have different display depending on some conditions.
  557. } elseif ( $approved_comments ) {
  558. printf( '<a href="%s" class="post-com-count post-com-count-approved"><span class="comment-count-approved" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></a>',
  559. esc_url( add_query_arg( array( 'p' => $post_id, 'comment_status' => 'approved' ), admin_url( 'edit-comments.php' ) ) ),
  560. $approved_comments_number,
  561. $pending_comments ? $approved_phrase : $approved_only_phrase
  562. );
  563. } else {
  564. printf( '<span class="post-com-count post-com-count-no-comments"><span class="comment-count comment-count-no-comments" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></span>',
  565. $approved_comments_number,
  566. $pending_comments ? __( 'No approved comments' ) : __( 'No comments' )
  567. );
  568. }
  569. if ( $pending_comments ) {
  570. printf( '<a href="%s" class="post-com-count post-com-count-pending"><span class="comment-count-pending" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></a>',
  571. esc_url( add_query_arg( array( 'p' => $post_id, 'comment_status' => 'moderated' ), admin_url( 'edit-comments.php' ) ) ),
  572. $pending_comments_number,
  573. $pending_phrase
  574. );
  575. } else {
  576. printf( '<span class="post-com-count post-com-count-pending post-com-count-no-pending"><span class="comment-count comment-count-no-pending" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></span>',
  577. $pending_comments_number,
  578. $approved_comments ? __( 'No pending comments' ) : __( 'No comments' )
  579. );
  580. }
  581. }
  582. /**
  583. * Get the current page number
  584. *
  585. * @since 3.1.0
  586. *
  587. * @return int
  588. */
  589. public function get_pagenum() {
  590. $pagenum = isset( $_REQUEST['paged'] ) ? absint( $_REQUEST['paged'] ) : 0;
  591. if ( isset( $this->_pagination_args['total_pages'] ) && $pagenum > $this->_pagination_args['total_pages'] )
  592. $pagenum = $this->_pagination_args['total_pages'];
  593. return max( 1, $pagenum );
  594. }
  595. /**
  596. * Get number of items to display on a single page
  597. *
  598. * @since 3.1.0
  599. *
  600. * @param string $option
  601. * @param int $default
  602. * @return int
  603. */
  604. protected function get_items_per_page( $option, $default = 20 ) {
  605. $per_page = (int) get_user_option( $option );
  606. if ( empty( $per_page ) || $per_page < 1 )
  607. $per_page = $default;
  608. /**
  609. * Filters the number of items to be displayed on each page of the list table.
  610. *
  611. * The dynamic hook name, $option, refers to the `per_page` option depending
  612. * on the type of list table in use. Possible values include: 'edit_comments_per_page',
  613. * 'sites_network_per_page', 'site_themes_network_per_page', 'themes_network_per_page',
  614. * 'users_network_per_page', 'edit_post_per_page', 'edit_page_per_page',
  615. * 'edit_{$post_type}_per_page', etc.
  616. *
  617. * @since 2.9.0
  618. *
  619. * @param int $per_page Number of items to be displayed. Default 20.
  620. */
  621. return (int) apply_filters( "{$option}", $per_page );
  622. }
  623. /**
  624. * Display the pagination.
  625. *
  626. * @since 3.1.0
  627. *
  628. * @param string $which
  629. */
  630. protected function pagination( $which ) {
  631. if ( empty( $this->_pagination_args ) ) {
  632. return;
  633. }
  634. $total_items = $this->_pagination_args['total_items'];
  635. $total_pages = $this->_pagination_args['total_pages'];
  636. $infinite_scroll = false;
  637. if ( isset( $this->_pagination_args['infinite_scroll'] ) ) {
  638. $infinite_scroll = $this->_pagination_args['infinite_scroll'];
  639. }
  640. if ( 'top' === $which && $total_pages > 1 ) {
  641. $this->screen->render_screen_reader_content( 'heading_pagination' );
  642. }
  643. $output = '<span class="displaying-num">' . sprintf( _n( '%s item', '%s items', $total_items ), number_format_i18n( $total_items ) ) . '</span>';
  644. $current = $this->get_pagenum();
  645. $removable_query_args = wp_removable_query_args();
  646. $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
  647. $current_url = remove_query_arg( $removable_query_args, $current_url );
  648. $page_links = array();
  649. $total_pages_before = '<span class="paging-input">';
  650. $total_pages_after = '</span></span>';
  651. $disable_first = $disable_last = $disable_prev = $disable_next = false;
  652. if ( $current == 1 ) {
  653. $disable_first = true;
  654. $disable_prev = true;
  655. }
  656. if ( $current == 2 ) {
  657. $disable_first = true;
  658. }
  659. if ( $current == $total_pages ) {
  660. $disable_last = true;
  661. $disable_next = true;
  662. }
  663. if ( $current == $total_pages - 1 ) {
  664. $disable_last = true;
  665. }
  666. if ( $disable_first ) {
  667. $page_links[] = '<span class="tablenav-pages-navspan" aria-hidden="true">&laquo;</span>';
  668. } else {
  669. $page_links[] = sprintf( "<a class='first-page' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
  670. esc_url( remove_query_arg( 'paged', $current_url ) ),
  671. __( 'First page' ),
  672. '&laquo;'
  673. );
  674. }
  675. if ( $disable_prev ) {
  676. $page_links[] = '<span class="tablenav-pages-navspan" aria-hidden="true">&lsaquo;</span>';
  677. } else {
  678. $page_links[] = sprintf( "<a class='prev-page' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
  679. esc_url( add_query_arg( 'paged', max( 1, $current-1 ), $current_url ) ),
  680. __( 'Previous page' ),
  681. '&lsaquo;'
  682. );
  683. }
  684. if ( 'bottom' === $which ) {
  685. $html_current_page = $current;
  686. $total_pages_before = '<span class="screen-reader-text">' . __( 'Current Page' ) . '</span><span id="table-paging" class="paging-input"><span class="tablenav-paging-text">';
  687. } else {
  688. $html_current_page = sprintf( "%s<input class='current-page' id='current-page-selector' type='text' name='paged' value='%s' size='%d' aria-describedby='table-paging' /><span class='tablenav-paging-text'>",
  689. '<label for="current-page-selector" class="screen-reader-text">' . __( 'Current Page' ) . '</label>',
  690. $current,
  691. strlen( $total_pages )
  692. );
  693. }
  694. $html_total_pages = sprintf( "<span class='total-pages'>%s</span>", number_format_i18n( $total_pages ) );
  695. $page_links[] = $total_pages_before . sprintf( _x( '%1$s of %2$s', 'paging' ), $html_current_page, $html_total_pages ) . $total_pages_after;
  696. if ( $disable_next ) {
  697. $page_links[] = '<span class="tablenav-pages-navspan" aria-hidden="true">&rsaquo;</span>';
  698. } else {
  699. $page_links[] = sprintf( "<a class='next-page' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
  700. esc_url( add_query_arg( 'paged', min( $total_pages, $current+1 ), $current_url ) ),
  701. __( 'Next page' ),
  702. '&rsaquo;'
  703. );
  704. }
  705. if ( $disable_last ) {
  706. $page_links[] = '<span class="tablenav-pages-navspan" aria-hidden="true">&raquo;</span>';
  707. } else {
  708. $page_links[] = sprintf( "<a class='last-page' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
  709. esc_url( add_query_arg( 'paged', $total_pages, $current_url ) ),
  710. __( 'Last page' ),
  711. '&raquo;'
  712. );
  713. }
  714. $pagination_links_class = 'pagination-links';
  715. if ( ! empty( $infinite_scroll ) ) {
  716. $pagination_links_class .= ' hide-if-js';
  717. }
  718. $output .= "\n<span class='$pagination_links_class'>" . join( "\n", $page_links ) . '</span>';
  719. if ( $total_pages ) {
  720. $page_class = $total_pages < 2 ? ' one-page' : '';
  721. } else {
  722. $page_class = ' no-pages';
  723. }
  724. $this->_pagination = "<div class='tablenav-pages{$page_class}'>$output</div>";
  725. echo $this->_pagination;
  726. }
  727. /**
  728. * Get a list of columns. The format is:
  729. * 'internal-name' => 'Title'
  730. *
  731. * @since 3.1.0
  732. * @abstract
  733. *
  734. * @return array
  735. */
  736. public function get_columns() {
  737. die( 'function WP_List_Table::get_columns() must be over-ridden in a sub-class.' );
  738. }
  739. /**
  740. * Get a list of sortable columns. The format is:
  741. * 'internal-name' => 'orderby'
  742. * or
  743. * 'internal-name' => array( 'orderby', true )
  744. *
  745. * The second format will make the initial sorting order be descending
  746. *
  747. * @since 3.1.0
  748. *
  749. * @return array
  750. */
  751. protected function get_sortable_columns() {
  752. return array();
  753. }
  754. /**
  755. * Gets the name of the default primary column.
  756. *
  757. * @since 4.3.0
  758. *
  759. * @return string Name of the default primary column, in this case, an empty string.
  760. */
  761. protected function get_default_primary_column_name() {
  762. $columns = $this->get_columns();
  763. $column = '';
  764. if ( empty( $columns ) ) {
  765. return $column;
  766. }
  767. // We need a primary defined so responsive views show something,
  768. // so let's fall back to the first non-checkbox column.
  769. foreach ( $columns as $col => $column_name ) {
  770. if ( 'cb' === $col ) {
  771. continue;
  772. }
  773. $column = $col;
  774. break;
  775. }
  776. return $column;
  777. }
  778. /**
  779. * Public wrapper for WP_List_Table::get_default_primary_column_name().
  780. *
  781. * @since 4.4.0
  782. *
  783. * @return string Name of the default primary column.
  784. */
  785. public function get_primary_column() {
  786. return $this->get_primary_column_name();
  787. }
  788. /**
  789. * Gets the name of the primary column.
  790. *
  791. * @since 4.3.0
  792. *
  793. * @return string The name of the primary column.
  794. */
  795. protected function get_primary_column_name() {
  796. $columns = get_column_headers( $this->screen );
  797. $default = $this->get_default_primary_column_name();
  798. // If the primary column doesn't exist fall back to the
  799. // first non-checkbox column.
  800. if ( ! isset( $columns[ $default ] ) ) {
  801. $default = WP_List_Table::get_default_primary_column_name();
  802. }
  803. /**
  804. * Filters the name of the primary column for the current list table.
  805. *
  806. * @since 4.3.0
  807. *
  808. * @param string $default Column name default for the specific list table, e.g. 'name'.
  809. * @param string $context Screen ID for specific list table, e.g. 'plugins'.
  810. */
  811. $column = apply_filters( 'list_table_primary_column', $default, $this->screen->id );
  812. if ( empty( $column ) || ! isset( $columns[ $column ] ) ) {
  813. $column = $default;
  814. }
  815. return $column;
  816. }
  817. /**
  818. * Get a list of all, hidden and sortable columns, with filter applied
  819. *
  820. * @since 3.1.0
  821. *
  822. * @return array
  823. */
  824. protected function get_column_info() {
  825. // $_column_headers is already set / cached
  826. if ( isset( $this->_column_headers ) && is_array( $this->_column_headers ) ) {
  827. // Back-compat for list tables that have been manually setting $_column_headers for horse reasons.
  828. // In 4.3, we added a fourth argument for primary column.
  829. $column_headers = array( array(), array(), array(), $this->get_primary_column_name() );
  830. foreach ( $this->_column_headers as $key => $value ) {
  831. $column_headers[ $key ] = $value;
  832. }
  833. return $column_headers;
  834. }
  835. $columns = get_column_headers( $this->screen );
  836. $hidden = get_hidden_columns( $this->screen );
  837. $sortable_columns = $this->get_sortable_columns();
  838. /**
  839. * Filters the list table sortable columns for a specific screen.
  840. *
  841. * The dynamic portion of the hook name, `$this->screen->id`, refers
  842. * to the ID of the current screen, usually a string.
  843. *
  844. * @since 3.5.0
  845. *
  846. * @param array $sortable_columns An array of sortable columns.
  847. */
  848. $_sortable = apply_filters( "manage_{$this->screen->id}_sortable_columns", $sortable_columns );
  849. $sortable = array();
  850. foreach ( $_sortable as $id => $data ) {
  851. if ( empty( $data ) )
  852. continue;
  853. $data = (array) $data;
  854. if ( !isset( $data[1] ) )
  855. $data[1] = false;
  856. $sortable[$id] = $data;
  857. }
  858. $primary = $this->get_primary_column_name();
  859. $this->_column_headers = array( $columns, $hidden, $sortable, $primary );
  860. return $this->_column_headers;
  861. }
  862. /**
  863. * Return number of visible columns
  864. *
  865. * @since 3.1.0
  866. *
  867. * @return int
  868. */
  869. public function get_column_count() {
  870. list ( $columns, $hidden ) = $this->get_column_info();
  871. $hidden = array_intersect( array_keys( $columns ), array_filter( $hidden ) );
  872. return count( $columns ) - count( $hidden );
  873. }
  874. /**
  875. * Print column headers, accounting for hidden and sortable columns.
  876. *
  877. * @since 3.1.0
  878. *
  879. * @staticvar int $cb_counter
  880. *
  881. * @param bool $with_id Whether to set the id attribute or not
  882. */
  883. public function print_column_headers( $with_id = true ) {
  884. list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();
  885. $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
  886. $current_url = remove_query_arg( 'paged', $current_url );
  887. if ( isset( $_GET['orderby'] ) ) {
  888. $current_orderby = $_GET['orderby'];
  889. } else {
  890. $current_orderby = '';
  891. }
  892. if ( isset( $_GET['order'] ) && 'desc' === $_GET['order'] ) {
  893. $current_order = 'desc';
  894. } else {
  895. $current_order = 'asc';
  896. }
  897. if ( ! empty( $columns['cb'] ) ) {
  898. static $cb_counter = 1;
  899. $columns['cb'] = '<label class="screen-reader-text" for="cb-select-all-' . $cb_counter . '">' . __( 'Select All' ) . '</label>'
  900. . '<input id="cb-select-all-' . $cb_counter . '" type="checkbox" />';
  901. $cb_counter++;
  902. }
  903. foreach ( $columns as $column_key => $column_display_name ) {
  904. $class = array( 'manage-column', "column-$column_key" );
  905. if ( in_array( $column_key, $hidden ) ) {
  906. $class[] = 'hidden';
  907. }
  908. if ( 'cb' === $column_key )
  909. $class[] = 'check-column';
  910. elseif ( in_array( $column_key, array( 'posts', 'comments', 'links' ) ) )
  911. $class[] = 'num';
  912. if ( $column_key === $primary ) {
  913. $class[] = 'column-primary';
  914. }
  915. if ( isset( $sortable[$column_key] ) ) {
  916. list( $orderby, $desc_first ) = $sortable[$column_key];
  917. if ( $current_orderby === $orderby ) {
  918. $order = 'asc' === $current_order ? 'desc' : 'asc';
  919. $class[] = 'sorted';
  920. $class[] = $current_order;
  921. } else {
  922. $order = $desc_first ? 'desc' : 'asc';
  923. $class[] = 'sortable';
  924. $class[] = $desc_first ? 'asc' : 'desc';
  925. }
  926. $column_display_name = '<a href="' . esc_url( add_query_arg( compact( 'orderby', 'order' ), $current_url ) ) . '"><span>' . $column_display_name . '</span><span class="sorting-indicator"></span></a>';
  927. }
  928. $tag = ( 'cb' === $column_key ) ? 'td' : 'th';
  929. $scope = ( 'th' === $tag ) ? 'scope="col"' : '';
  930. $id = $with_id ? "id='$column_key'" : '';
  931. if ( !empty( $class ) )
  932. $class = "class='" . join( ' ', $class ) . "'";
  933. echo "<$tag $scope $id $class>$column_display_name</$tag>";
  934. }
  935. }
  936. /**
  937. * Display the table
  938. *
  939. * @since 3.1.0
  940. */
  941. public function display() {
  942. $singular = $this->_args['singular'];
  943. $this->display_tablenav( 'top' );
  944. $this->screen->render_screen_reader_content( 'heading_list' );
  945. ?>
  946. <table class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>">
  947. <thead>
  948. <tr>
  949. <?php $this->print_column_headers(); ?>
  950. </tr>
  951. </thead>
  952. <tbody id="the-list"<?php
  953. if ( $singular ) {
  954. echo " data-wp-lists='list:$singular'";
  955. } ?>>
  956. <?php $this->display_rows_or_placeholder(); ?>
  957. </tbody>
  958. <tfoot>
  959. <tr>
  960. <?php $this->print_column_headers( false ); ?>
  961. </tr>
  962. </tfoot>
  963. </table>
  964. <?php
  965. $this->display_tablenav( 'bottom' );
  966. }
  967. /**
  968. * Get a list of CSS classes for the WP_List_Table table tag.
  969. *
  970. * @since 3.1.0
  971. *
  972. * @return array List of CSS classes for the table tag.
  973. */
  974. protected function get_table_classes() {
  975. return array( 'widefat', 'fixed', 'striped', $this->_args['plural'] );
  976. }
  977. /**
  978. * Generate the table navigation above or below the table
  979. *
  980. * @since 3.1.0
  981. * @param string $which
  982. */
  983. protected function display_tablenav( $which ) {
  984. if ( 'top' === $which ) {
  985. wp_nonce_field( 'bulk-' . $this->_args['plural'] );
  986. }
  987. ?>
  988. <div class="tablenav <?php echo esc_attr( $which ); ?>">
  989. <?php if ( $this->has_items() ): ?>
  990. <div class="alignleft actions bulkactions">
  991. <?php $this->bulk_actions( $which ); ?>
  992. </div>
  993. <?php endif;
  994. $this->extra_tablenav( $which );
  995. $this->pagination( $which );
  996. ?>
  997. <br class="clear" />
  998. </div>
  999. <?php
  1000. }
  1001. /**
  1002. * Extra controls to be displayed between bulk actions and pagination
  1003. *
  1004. * @since 3.1.0
  1005. *
  1006. * @param string $which
  1007. */
  1008. protected function extra_tablenav( $which ) {}
  1009. /**
  1010. * Generate the tbody element for the list table.
  1011. *
  1012. * @since 3.1.0
  1013. */
  1014. public function display_rows_or_placeholder() {
  1015. if ( $this->has_items() ) {
  1016. $this->display_rows();
  1017. } else {
  1018. echo '<tr class="no-items"><td class="colspanchange" colspan="' . $this->get_column_count() . '">';
  1019. $this->no_items();
  1020. echo '</td></tr>';
  1021. }
  1022. }
  1023. /**
  1024. * Generate the table rows
  1025. *
  1026. * @since 3.1.0
  1027. */
  1028. public function display_rows() {
  1029. foreach ( $this->items as $item )
  1030. $this->single_row( $item );
  1031. }
  1032. /**
  1033. * Generates content for a single row of the table
  1034. *
  1035. * @since 3.1.0
  1036. *
  1037. * @param object $item The current item
  1038. */
  1039. public function single_row( $item ) {
  1040. echo '<tr>';
  1041. $this->single_row_columns( $item );
  1042. echo '</tr>';
  1043. }
  1044. /**
  1045. *
  1046. * @param object $item
  1047. * @param string $column_name
  1048. */
  1049. protected function column_default( $item, $column_name ) {}
  1050. /**
  1051. *
  1052. * @param object $item
  1053. */
  1054. protected function column_cb( $item ) {}
  1055. /**
  1056. * Generates the columns for a single row of the table
  1057. *
  1058. * @since 3.1.0
  1059. *
  1060. * @param object $item The current item
  1061. */
  1062. protected function single_row_columns( $item ) {
  1063. list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();
  1064. foreach ( $columns as $column_name => $column_display_name ) {
  1065. $classes = "$column_name column-$column_name";
  1066. if ( $primary === $column_name ) {
  1067. $classes .= ' has-row-actions column-primary';
  1068. }
  1069. if ( in_array( $column_name, $hidden ) ) {
  1070. $classes .= ' hidden';
  1071. }
  1072. // Comments column uses HTML in the display name with screen reader text.
  1073. // Instead of using esc_attr(), we strip tags to get closer to a user-friendly string.
  1074. $data = 'data-colname="' . wp_strip_all_tags( $column_display_name ) . '"';
  1075. $attributes = "class='$classes' $data";
  1076. if ( 'cb' === $column_name ) {
  1077. echo '<th scope="row" class="check-column">';
  1078. echo $this->column_cb( $item );
  1079. echo '</th>';
  1080. } elseif ( method_exists( $this, '_column_' . $column_name ) ) {
  1081. echo call_user_func(
  1082. array( $this, '_column_' . $column_name ),
  1083. $item,
  1084. $classes,
  1085. $data,
  1086. $primary
  1087. );
  1088. } elseif ( method_exists( $this, 'column_' . $column_name ) ) {
  1089. echo "<td $attributes>";
  1090. echo call_user_func( array( $this, 'column_' . $column_name ), $item );
  1091. echo $this->handle_row_actions( $item, $column_name, $primary );
  1092. echo "</td>";
  1093. } else {
  1094. echo "<td $attributes>";
  1095. echo $this->column_default( $item, $column_name );
  1096. echo $this->handle_row_actions( $item, $column_name, $primary );
  1097. echo "</td>";
  1098. }
  1099. }
  1100. }
  1101. /**
  1102. * Generates and display row actions links for the list table.
  1103. *
  1104. * @since 4.3.0
  1105. *
  1106. * @param object $item The item being acted upon.
  1107. * @param string $column_name Current column name.
  1108. * @param string $primary Primary column name.
  1109. * @return string The row actions HTML, or an empty string if the current column is the primary column.
  1110. */
  1111. protected function handle_row_actions( $item, $column_name, $primary ) {
  1112. return $column_name === $primary ? '<button type="button" class="toggle-row"><span class="screen-reader-text">' . __( 'Show more details' ) . '</span></button>' : '';
  1113. }
  1114. /**
  1115. * Handle an incoming ajax request (called from admin-ajax.php)
  1116. *
  1117. * @since 3.1.0
  1118. */
  1119. public function ajax_response() {
  1120. $this->prepare_items();
  1121. ob_start();
  1122. if ( ! empty( $_REQUEST['no_placeholder'] ) ) {
  1123. $this->display_rows();
  1124. } else {
  1125. $this->display_rows_or_placeholder();
  1126. }
  1127. $rows = ob_get_clean();
  1128. $response = array( 'rows' => $rows );
  1129. if ( isset( $this->_pagination_args['total_items'] ) ) {
  1130. $response['total_items_i18n'] = sprintf(
  1131. _n( '%s item', '%s items', $this->_pagination_args['total_items'] ),
  1132. number_format_i18n( $this->_pagination_args['total_items'] )
  1133. );
  1134. }
  1135. if ( isset( $this->_pagination_args['total_pages'] ) ) {
  1136. $response['total_pages'] = $this->_pagination_args['total_pages'];
  1137. $response['total_pages_i18n'] = number_format_i18n( $this->_pagination_args['total_pages'] );
  1138. }
  1139. die( wp_json_encode( $response ) );
  1140. }
  1141. /**
  1142. * Send required variables to JavaScript land
  1143. *
  1144. */
  1145. public function _js_vars() {
  1146. $args = array(
  1147. 'class' => get_class( $this ),
  1148. 'screen' => array(
  1149. 'id' => $this->screen->id,
  1150. 'base' => $this->screen->base,
  1151. )
  1152. );
  1153. printf( "<script type='text/javascript'>list_args = %s;</script>\n", wp_json_encode( $args ) );
  1154. }
  1155. }