WP_List_Table

Base class for displaying a list of items in an ajaxified HTML table.

Defined (1)

The class is defined in the following location(s).

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