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() && ( ! defined( 'DOING_AJAX' ) || ! 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, 'button', '', 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. $no_new_actions = $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. $this->_actions = array_intersect_assoc( $this->_actions, $no_new_actions ); 
  363. $two = ''; 
  364. } else { 
  365. $two = '2'; 
  366.  
  367. if ( empty( $this->_actions ) ) 
  368. return; 
  369.  
  370. echo '<label for="bulk-action-selector-' . esc_attr( $which ) . '" class="screen-reader-text">' . __( 'Select bulk action' ) . '</label>'; 
  371. echo '<select name="action' . $two . '" id="bulk-action-selector-' . esc_attr( $which ) . "\">\n"; 
  372. echo '<option value="-1">' . __( 'Bulk Actions' ) . "</option>\n"; 
  373.  
  374. foreach ( $this->_actions as $name => $title ) { 
  375. $class = 'edit' === $name ? ' class="hide-if-no-js"' : ''; 
  376.  
  377. echo "\t" . '<option value="' . $name . '"' . $class . '>' . $title . "</option>\n"; 
  378.  
  379. echo "</select>\n"; 
  380.  
  381. submit_button( __( 'Apply' ), 'action', '', false, array( 'id' => "doaction$two" ) ); 
  382. echo "\n"; 
  383.  
  384. /** 
  385. * Get the current action selected from the bulk actions dropdown. 
  386. * @since 3.1.0 
  387. * @access public 
  388. * @return string|false The action name or False if no action was selected 
  389. */ 
  390. public function current_action() { 
  391. if ( isset( $_REQUEST['filter_action'] ) && ! empty( $_REQUEST['filter_action'] ) ) 
  392. return false; 
  393.  
  394. if ( isset( $_REQUEST['action'] ) && -1 != $_REQUEST['action'] ) 
  395. return $_REQUEST['action']; 
  396.  
  397. if ( isset( $_REQUEST['action2'] ) && -1 != $_REQUEST['action2'] ) 
  398. return $_REQUEST['action2']; 
  399.  
  400. return false; 
  401.  
  402. /** 
  403. * Generate row actions div 
  404. * @since 3.1.0 
  405. * @access protected 
  406. * @param array $actions The list of actions 
  407. * @param bool $always_visible Whether the actions should be always visible 
  408. * @return string 
  409. */ 
  410. protected function row_actions( $actions, $always_visible = false ) { 
  411. $action_count = count( $actions ); 
  412. $i = 0; 
  413.  
  414. if ( !$action_count ) 
  415. return ''; 
  416.  
  417. $out = '<div class="' . ( $always_visible ? 'row-actions visible' : 'row-actions' ) . '">'; 
  418. foreach ( $actions as $action => $link ) { 
  419. ++$i; 
  420. ( $i == $action_count ) ? $sep = '' : $sep = ' | '; 
  421. $out .= "<span class='$action'>$link$sep</span>"; 
  422. $out .= '</div>'; 
  423.  
  424. $out .= '<button type="button" class="toggle-row"><span class="screen-reader-text">' . __( 'Show more details' ) . '</span></button>'; 
  425.  
  426. return $out; 
  427.  
  428. /** 
  429. * Display a monthly dropdown for filtering items 
  430. * @since 3.1.0 
  431. * @access protected 
  432. * @global wpdb $wpdb 
  433. * @global WP_Locale $wp_locale 
  434. * @param string $post_type 
  435. */ 
  436. protected function months_dropdown( $post_type ) { 
  437. global $wpdb, $wp_locale; 
  438.  
  439. /** 
  440. * Filters whether to remove the 'Months' drop-down from the post list table. 
  441. * @since 4.2.0 
  442. * @param bool $disable Whether to disable the drop-down. Default false. 
  443. * @param string $post_type The post type. 
  444. */ 
  445. if ( apply_filters( 'disable_months_dropdown', false, $post_type ) ) { 
  446. return; 
  447.  
  448. $extra_checks = "AND post_status != 'auto-draft'"; 
  449. if ( ! isset( $_GET['post_status'] ) || 'trash' !== $_GET['post_status'] ) { 
  450. $extra_checks .= " AND post_status != 'trash'"; 
  451. } elseif ( isset( $_GET['post_status'] ) ) { 
  452. $extra_checks = $wpdb->prepare( ' AND post_status = %s', $_GET['post_status'] ); 
  453.  
  454. $months = $wpdb->get_results( $wpdb->prepare( " 
  455. SELECT DISTINCT YEAR( post_date ) AS year, MONTH( post_date ) AS month 
  456. FROM $wpdb->posts 
  457. WHERE post_type = %s 
  458. $extra_checks 
  459. ORDER BY post_date DESC 
  460. ", $post_type ) ); 
  461.  
  462. /** 
  463. * Filters the 'Months' drop-down results. 
  464. * @since 3.7.0 
  465. * @param object $months The months drop-down query results. 
  466. * @param string $post_type The post type. 
  467. */ 
  468. $months = apply_filters( 'months_dropdown_results', $months, $post_type ); 
  469.  
  470. $month_count = count( $months ); 
  471.  
  472. if ( !$month_count || ( 1 == $month_count && 0 == $months[0]->month ) ) 
  473. return; 
  474.  
  475. $m = isset( $_GET['m'] ) ? (int) $_GET['m'] : 0; 
  476. ?> 
  477. <label for="filter-by-date" class="screen-reader-text"><?php _e( 'Filter by date' ); ?></label> 
  478. <select name="m" id="filter-by-date"> 
  479. <option<?php selected( $m, 0 ); ?> value="0"><?php _e( 'All dates' ); ?></option> 
  480. <?php 
  481. foreach ( $months as $arc_row ) { 
  482. if ( 0 == $arc_row->year ) 
  483. continue; 
  484.  
  485. $month = zeroise( $arc_row->month, 2 ); 
  486. $year = $arc_row->year; 
  487.  
  488. printf( "<option %s value='%s'>%s</option>\n",  
  489. selected( $m, $year . $month, false ),  
  490. esc_attr( $arc_row->year . $month ),  
  491. /** translators: 1: month name, 2: 4-digit year */ 
  492. sprintf( __( '%1$s %2$d' ), $wp_locale->get_month( $month ), $year ) 
  493. ); 
  494. ?> 
  495. </select> 
  496. <?php 
  497.  
  498. /** 
  499. * Display a view switcher 
  500. * @since 3.1.0 
  501. * @access protected 
  502. * @param string $current_mode 
  503. */ 
  504. protected function view_switcher( $current_mode ) { 
  505. ?> 
  506. <input type="hidden" name="mode" value="<?php echo esc_attr( $current_mode ); ?>" /> 
  507. <div class="view-switch"> 
  508. <?php 
  509. foreach ( $this->modes as $mode => $title ) { 
  510. $classes = array( 'view-' . $mode ); 
  511. if ( $current_mode === $mode ) 
  512. $classes[] = 'current'; 
  513. printf( 
  514. "<a href='%s' class='%s' id='view-switch-$mode'><span class='screen-reader-text'>%s</span></a>\n",  
  515. esc_url( add_query_arg( 'mode', $mode ) ),  
  516. implode( ' ', $classes ),  
  517. $title 
  518. ); 
  519. ?> 
  520. </div> 
  521. <?php 
  522.  
  523. /** 
  524. * Display a comment count bubble 
  525. * @since 3.1.0 
  526. * @access protected 
  527. * @param int $post_id The post ID. 
  528. * @param int $pending_comments Number of pending comments. 
  529. */ 
  530. protected function comments_bubble( $post_id, $pending_comments ) { 
  531. $approved_comments = get_comments_number(); 
  532.  
  533. $approved_comments_number = number_format_i18n( $approved_comments ); 
  534. $pending_comments_number = number_format_i18n( $pending_comments ); 
  535.  
  536. $approved_only_phrase = sprintf( _n( '%s comment', '%s comments', $approved_comments ), $approved_comments_number ); 
  537. $approved_phrase = sprintf( _n( '%s approved comment', '%s approved comments', $approved_comments ), $approved_comments_number ); 
  538. $pending_phrase = sprintf( _n( '%s pending comment', '%s pending comments', $pending_comments ), $pending_comments_number ); 
  539.  
  540. // No comments at all. 
  541. if ( ! $approved_comments && ! $pending_comments ) { 
  542. printf( '<span aria-hidden="true">*</span><span class="screen-reader-text">%s</span>',  
  543. __( 'No comments' ) 
  544. ); 
  545. // Approved comments have different display depending on some conditions. 
  546. } elseif ( $approved_comments ) { 
  547. 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>',  
  548. esc_url( add_query_arg( array( 'p' => $post_id, 'comment_status' => 'approved' ), admin_url( 'edit-comments.php' ) ) ),  
  549. $approved_comments_number,  
  550. $pending_comments ? $approved_phrase : $approved_only_phrase 
  551. ); 
  552. } else { 
  553. 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>',  
  554. $approved_comments_number,  
  555. $pending_comments ? __( 'No approved comments' ) : __( 'No comments' ) 
  556. ); 
  557.  
  558. if ( $pending_comments ) { 
  559. 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>',  
  560. esc_url( add_query_arg( array( 'p' => $post_id, 'comment_status' => 'moderated' ), admin_url( 'edit-comments.php' ) ) ),  
  561. $pending_comments_number,  
  562. $pending_phrase 
  563. ); 
  564. } else { 
  565. 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>',  
  566. $pending_comments_number,  
  567. $approved_comments ? __( 'No pending comments' ) : __( 'No comments' ) 
  568. ); 
  569.  
  570. /** 
  571. * Get the current page number 
  572. * @since 3.1.0 
  573. * @access public 
  574. * @return int 
  575. */ 
  576. public function get_pagenum() { 
  577. $pagenum = isset( $_REQUEST['paged'] ) ? absint( $_REQUEST['paged'] ) : 0; 
  578.  
  579. if ( isset( $this->_pagination_args['total_pages'] ) && $pagenum > $this->_pagination_args['total_pages'] ) 
  580. $pagenum = $this->_pagination_args['total_pages']; 
  581.  
  582. return max( 1, $pagenum ); 
  583.  
  584. /** 
  585. * Get number of items to display on a single page 
  586. * @since 3.1.0 
  587. * @access protected 
  588. * @param string $option 
  589. * @param int $default 
  590. * @return int 
  591. */ 
  592. protected function get_items_per_page( $option, $default = 20 ) { 
  593. $per_page = (int) get_user_option( $option ); 
  594. if ( empty( $per_page ) || $per_page < 1 ) 
  595. $per_page = $default; 
  596.  
  597. /** 
  598. * Filters the number of items to be displayed on each page of the list table. 
  599. * The dynamic hook name, $option, refers to the `per_page` option depending 
  600. * on the type of list table in use. Possible values include: 'edit_comments_per_page',  
  601. * 'sites_network_per_page', 'site_themes_network_per_page', 'themes_network_per_page',  
  602. * 'users_network_per_page', 'edit_post_per_page', 'edit_page_per_page',  
  603. * 'edit_{$post_type}_per_page', etc. 
  604. * @since 2.9.0 
  605. * @param int $per_page Number of items to be displayed. Default 20. 
  606. */ 
  607. return (int) apply_filters( $option, $per_page ); 
  608.  
  609. /** 
  610. * Display the pagination. 
  611. * @since 3.1.0 
  612. * @access protected 
  613. * @param string $which 
  614. */ 
  615. protected function pagination( $which ) { 
  616. if ( empty( $this->_pagination_args ) ) { 
  617. return; 
  618.  
  619. $total_items = $this->_pagination_args['total_items']; 
  620. $total_pages = $this->_pagination_args['total_pages']; 
  621. $infinite_scroll = false; 
  622. if ( isset( $this->_pagination_args['infinite_scroll'] ) ) { 
  623. $infinite_scroll = $this->_pagination_args['infinite_scroll']; 
  624.  
  625. if ( 'top' === $which && $total_pages > 1 ) { 
  626. $this->screen->render_screen_reader_content( 'heading_pagination' ); 
  627.  
  628. $output = '<span class="displaying-num">' . sprintf( _n( '%s item', '%s items', $total_items ), number_format_i18n( $total_items ) ) . '</span>'; 
  629.  
  630. $current = $this->get_pagenum(); 
  631. $removable_query_args = wp_removable_query_args(); 
  632.  
  633. $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); 
  634.  
  635. $current_url = remove_query_arg( $removable_query_args, $current_url ); 
  636.  
  637. $page_links = array(); 
  638.  
  639. $total_pages_before = '<span class="paging-input">'; 
  640. $total_pages_after = '</span></span>'; 
  641.  
  642. $disable_first = $disable_last = $disable_prev = $disable_next = false; 
  643.  
  644. if ( $current == 1 ) { 
  645. $disable_first = true; 
  646. $disable_prev = true; 
  647. if ( $current == 2 ) { 
  648. $disable_first = true; 
  649. if ( $current == $total_pages ) { 
  650. $disable_last = true; 
  651. $disable_next = true; 
  652. if ( $current == $total_pages - 1 ) { 
  653. $disable_last = true; 
  654.  
  655. if ( $disable_first ) { 
  656. $page_links[] = '<span class="tablenav-pages-navspan" aria-hidden="true">«</span>'; 
  657. } else { 
  658. $page_links[] = sprintf( "<a class='first-page' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",  
  659. esc_url( remove_query_arg( 'paged', $current_url ) ),  
  660. __( 'First page' ),  
  661. '«' 
  662. ); 
  663.  
  664. if ( $disable_prev ) { 
  665. $page_links[] = '<span class="tablenav-pages-navspan" aria-hidden="true">‹</span>'; 
  666. } else { 
  667. $page_links[] = sprintf( "<a class='prev-page' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",  
  668. esc_url( add_query_arg( 'paged', max( 1, $current-1 ), $current_url ) ),  
  669. __( 'Previous page' ),  
  670. '‹' 
  671. ); 
  672.  
  673. if ( 'bottom' === $which ) { 
  674. $html_current_page = $current; 
  675. $total_pages_before = '<span class="screen-reader-text">' . __( 'Current Page' ) . '</span><span id="table-paging" class="paging-input"><span class="tablenav-paging-text">'; 
  676. } else { 
  677. $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'>",  
  678. '<label for="current-page-selector" class="screen-reader-text">' . __( 'Current Page' ) . '</label>',  
  679. $current,  
  680. strlen( $total_pages ) 
  681. ); 
  682. $html_total_pages = sprintf( "<span class='total-pages'>%s</span>", number_format_i18n( $total_pages ) ); 
  683. $page_links[] = $total_pages_before . sprintf( _x( '%1$s of %2$s', 'paging' ), $html_current_page, $html_total_pages ) . $total_pages_after; 
  684.  
  685. if ( $disable_next ) { 
  686. $page_links[] = '<span class="tablenav-pages-navspan" aria-hidden="true">›</span>'; 
  687. } else { 
  688. $page_links[] = sprintf( "<a class='next-page' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",  
  689. esc_url( add_query_arg( 'paged', min( $total_pages, $current+1 ), $current_url ) ),  
  690. __( 'Next page' ),  
  691. '›' 
  692. ); 
  693.  
  694. if ( $disable_last ) { 
  695. $page_links[] = '<span class="tablenav-pages-navspan" aria-hidden="true">»</span>'; 
  696. } else { 
  697. $page_links[] = sprintf( "<a class='last-page' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",  
  698. esc_url( add_query_arg( 'paged', $total_pages, $current_url ) ),  
  699. __( 'Last page' ),  
  700. '»' 
  701. ); 
  702.  
  703. $pagination_links_class = 'pagination-links'; 
  704. if ( ! empty( $infinite_scroll ) ) { 
  705. $pagination_links_class = ' hide-if-js'; 
  706. $output .= "\n<span class='$pagination_links_class'>" . join( "\n", $page_links ) . '</span>'; 
  707.  
  708. if ( $total_pages ) { 
  709. $page_class = $total_pages < 2 ? ' one-page' : ''; 
  710. } else { 
  711. $page_class = ' no-pages'; 
  712. $this->_pagination = "<div class='tablenav-pages{$page_class}'>$output</div>"; 
  713.  
  714. echo $this->_pagination; 
  715.  
  716. /** 
  717. * Get a list of columns. The format is: 
  718. * 'internal-name' => 'Title' 
  719. * @since 3.1.0 
  720. * @access public 
  721. * @abstract 
  722. * @return array 
  723. */ 
  724. public function get_columns() { 
  725. die( 'function WP_List_Table::get_columns() must be over-ridden in a sub-class.' ); 
  726.  
  727. /** 
  728. * Get a list of sortable columns. The format is: 
  729. * 'internal-name' => 'orderby' 
  730. * or 
  731. * 'internal-name' => array( 'orderby', true ) 
  732. * The second format will make the initial sorting order be descending 
  733. * @since 3.1.0 
  734. * @access protected 
  735. * @return array 
  736. */ 
  737. protected function get_sortable_columns() { 
  738. return array(); 
  739.  
  740. /** 
  741. * Gets the name of the default primary column. 
  742. * @since 4.3.0 
  743. * @access protected 
  744. * @return string Name of the default primary column, in this case, an empty string. 
  745. */ 
  746. protected function get_default_primary_column_name() { 
  747. $columns = $this->get_columns(); 
  748. $column = ''; 
  749.  
  750. if ( empty( $columns ) ) { 
  751. return $column; 
  752.  
  753. // We need a primary defined so responsive views show something,  
  754. // so let's fall back to the first non-checkbox column. 
  755. foreach ( $columns as $col => $column_name ) { 
  756. if ( 'cb' === $col ) { 
  757. continue; 
  758.  
  759. $column = $col; 
  760. break; 
  761.  
  762. return $column; 
  763.  
  764. /** 
  765. * Public wrapper for WP_List_Table::get_default_primary_column_name(). 
  766. * @since 4.4.0 
  767. * @access public 
  768. * @return string Name of the default primary column. 
  769. */ 
  770. public function get_primary_column() { 
  771. return $this->get_primary_column_name(); 
  772.  
  773. /** 
  774. * Gets the name of the primary column. 
  775. * @since 4.3.0 
  776. * @access protected 
  777. * @return string The name of the primary column. 
  778. */ 
  779. protected function get_primary_column_name() { 
  780. $columns = get_column_headers( $this->screen ); 
  781. $default = $this->get_default_primary_column_name(); 
  782.  
  783. // If the primary column doesn't exist fall back to the 
  784. // first non-checkbox column. 
  785. if ( ! isset( $columns[ $default ] ) ) { 
  786. $default = WP_List_Table::get_default_primary_column_name(); 
  787.  
  788. /** 
  789. * Filters the name of the primary column for the current list table. 
  790. * @since 4.3.0 
  791. * @param string $default Column name default for the specific list table, e.g. 'name'. 
  792. * @param string $context Screen ID for specific list table, e.g. 'plugins'. 
  793. */ 
  794. $column = apply_filters( 'list_table_primary_column', $default, $this->screen->id ); 
  795.  
  796. if ( empty( $column ) || ! isset( $columns[ $column ] ) ) { 
  797. $column = $default; 
  798.  
  799. return $column; 
  800.  
  801. /** 
  802. * Get a list of all, hidden and sortable columns, with filter applied 
  803. * @since 3.1.0 
  804. * @access protected 
  805. * @return array 
  806. */ 
  807. protected function get_column_info() { 
  808. // $_column_headers is already set / cached 
  809. if ( isset( $this->_column_headers ) && is_array( $this->_column_headers ) ) { 
  810. // Back-compat for list tables that have been manually setting $_column_headers for horse reasons. 
  811. // In 4.3, we added a fourth argument for primary column. 
  812. $column_headers = array( array(), array(), array(), $this->get_primary_column_name() ); 
  813. foreach ( $this->_column_headers as $key => $value ) { 
  814. $column_headers[ $key ] = $value; 
  815.  
  816. return $column_headers; 
  817.  
  818. $columns = get_column_headers( $this->screen ); 
  819. $hidden = get_hidden_columns( $this->screen ); 
  820.  
  821. $sortable_columns = $this->get_sortable_columns(); 
  822. /** 
  823. * Filters the list table sortable columns for a specific screen. 
  824. * The dynamic portion of the hook name, `$this->screen->id`, refers 
  825. * to the ID of the current screen, usually a string. 
  826. * @since 3.5.0 
  827. * @param array $sortable_columns An array of sortable columns. 
  828. */ 
  829. $_sortable = apply_filters( "manage_{$this->screen->id}_sortable_columns", $sortable_columns ); 
  830.  
  831. $sortable = array(); 
  832. foreach ( $_sortable as $id => $data ) { 
  833. if ( empty( $data ) ) 
  834. continue; 
  835.  
  836. $data = (array) $data; 
  837. if ( !isset( $data[1] ) ) 
  838. $data[1] = false; 
  839.  
  840. $sortable[$id] = $data; 
  841.  
  842. $primary = $this->get_primary_column_name(); 
  843. $this->_column_headers = array( $columns, $hidden, $sortable, $primary ); 
  844.  
  845. return $this->_column_headers; 
  846.  
  847. /** 
  848. * Return number of visible columns 
  849. * @since 3.1.0 
  850. * @access public 
  851. * @return int 
  852. */ 
  853. public function get_column_count() { 
  854. list ( $columns, $hidden ) = $this->get_column_info(); 
  855. $hidden = array_intersect( array_keys( $columns ), array_filter( $hidden ) ); 
  856. return count( $columns ) - count( $hidden ); 
  857.  
  858. /** 
  859. * Print column headers, accounting for hidden and sortable columns. 
  860. * @since 3.1.0 
  861. * @access public 
  862. * @staticvar int $cb_counter 
  863. * @param bool $with_id Whether to set the id attribute or not 
  864. */ 
  865. public function print_column_headers( $with_id = true ) { 
  866. list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info(); 
  867.  
  868. $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); 
  869. $current_url = remove_query_arg( 'paged', $current_url ); 
  870.  
  871. if ( isset( $_GET['orderby'] ) ) { 
  872. $current_orderby = $_GET['orderby']; 
  873. } else { 
  874. $current_orderby = ''; 
  875.  
  876. if ( isset( $_GET['order'] ) && 'desc' === $_GET['order'] ) { 
  877. $current_order = 'desc'; 
  878. } else { 
  879. $current_order = 'asc'; 
  880.  
  881. if ( ! empty( $columns['cb'] ) ) { 
  882. static $cb_counter = 1; 
  883. $columns['cb'] = '<label class="screen-reader-text" for="cb-select-all-' . $cb_counter . '">' . __( 'Select All' ) . '</label>' 
  884. . '<input id="cb-select-all-' . $cb_counter . '" type="checkbox" />'; 
  885. $cb_counter++; 
  886.  
  887. foreach ( $columns as $column_key => $column_display_name ) { 
  888. $class = array( 'manage-column', "column-$column_key" ); 
  889.  
  890. if ( in_array( $column_key, $hidden ) ) { 
  891. $class[] = 'hidden'; 
  892.  
  893. if ( 'cb' === $column_key ) 
  894. $class[] = 'check-column'; 
  895. elseif ( in_array( $column_key, array( 'posts', 'comments', 'links' ) ) ) 
  896. $class[] = 'num'; 
  897.  
  898. if ( $column_key === $primary ) { 
  899. $class[] = 'column-primary'; 
  900.  
  901. if ( isset( $sortable[$column_key] ) ) { 
  902. list( $orderby, $desc_first ) = $sortable[$column_key]; 
  903.  
  904. if ( $current_orderby === $orderby ) { 
  905. $order = 'asc' === $current_order ? 'desc' : 'asc'; 
  906. $class[] = 'sorted'; 
  907. $class[] = $current_order; 
  908. } else { 
  909. $order = $desc_first ? 'desc' : 'asc'; 
  910. $class[] = 'sortable'; 
  911. $class[] = $desc_first ? 'asc' : 'desc'; 
  912.  
  913. $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>'; 
  914.  
  915. $tag = ( 'cb' === $column_key ) ? 'td' : 'th'; 
  916. $scope = ( 'th' === $tag ) ? 'scope="col"' : ''; 
  917. $id = $with_id ? "id='$column_key'" : ''; 
  918.  
  919. if ( !empty( $class ) ) 
  920. $class = "class='" . join( ' ', $class ) . "'"; 
  921.  
  922. echo "<$tag $scope $id $class>$column_display_name</$tag>"; 
  923.  
  924. /** 
  925. * Display the table 
  926. * @since 3.1.0 
  927. * @access public 
  928. */ 
  929. public function display() { 
  930. $singular = $this->_args['singular']; 
  931.  
  932. $this->display_tablenav( 'top' ); 
  933.  
  934. $this->screen->render_screen_reader_content( 'heading_list' ); 
  935. ?> 
  936. <table class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>"> 
  937. <thead> 
  938. <tr> 
  939. <?php $this->print_column_headers(); ?> 
  940. </tr> 
  941. </thead> 
  942.  
  943. <tbody id="the-list"<?php 
  944. if ( $singular ) { 
  945. echo " data-wp-lists='list:$singular'"; 
  946. } ?>> 
  947. <?php $this->display_rows_or_placeholder(); ?> 
  948. </tbody> 
  949.  
  950. <tfoot> 
  951. <tr> 
  952. <?php $this->print_column_headers( false ); ?> 
  953. </tr> 
  954. </tfoot> 
  955.  
  956. </table> 
  957. <?php 
  958. $this->display_tablenav( 'bottom' ); 
  959.  
  960. /** 
  961. * Get a list of CSS classes for the WP_List_Table table tag. 
  962. * @since 3.1.0 
  963. * @access protected 
  964. * @return array List of CSS classes for the table tag. 
  965. */ 
  966. protected function get_table_classes() { 
  967. return array( 'widefat', 'fixed', 'striped', $this->_args['plural'] ); 
  968.  
  969. /** 
  970. * Generate the table navigation above or below the table 
  971. * @since 3.1.0 
  972. * @access protected 
  973. * @param string $which 
  974. */ 
  975. protected function display_tablenav( $which ) { 
  976. if ( 'top' === $which ) { 
  977. wp_nonce_field( 'bulk-' . $this->_args['plural'] ); 
  978. ?> 
  979. <div class="tablenav <?php echo esc_attr( $which ); ?>"> 
  980.  
  981. <?php if ( $this->has_items() ): ?> 
  982. <div class="alignleft actions bulkactions"> 
  983. <?php $this->bulk_actions( $which ); ?> 
  984. </div> 
  985. <?php endif; 
  986. $this->extra_tablenav( $which ); 
  987. $this->pagination( $which ); 
  988. ?> 
  989.  
  990. <br class="clear" /> 
  991. </div> 
  992. <?php 
  993.  
  994. /** 
  995. * Extra controls to be displayed between bulk actions and pagination 
  996. * @since 3.1.0 
  997. * @access protected 
  998. * @param string $which 
  999. */ 
  1000. protected function extra_tablenav( $which ) {} 
  1001.  
  1002. /** 
  1003. * Generate the tbody element for the list table. 
  1004. * @since 3.1.0 
  1005. * @access public 
  1006. */ 
  1007. public function display_rows_or_placeholder() { 
  1008. if ( $this->has_items() ) { 
  1009. $this->display_rows(); 
  1010. } else { 
  1011. echo '<tr class="no-items"><td class="colspanchange" colspan="' . $this->get_column_count() . '">'; 
  1012. $this->no_items(); 
  1013. echo '</td></tr>'; 
  1014.  
  1015. /** 
  1016. * Generate the table rows 
  1017. * @since 3.1.0 
  1018. * @access public 
  1019. */ 
  1020. public function display_rows() { 
  1021. foreach ( $this->items as $item ) 
  1022. $this->single_row( $item ); 
  1023.  
  1024. /** 
  1025. * Generates content for a single row of the table 
  1026. * @since 3.1.0 
  1027. * @access public 
  1028. * @param object $item The current item 
  1029. */ 
  1030. public function single_row( $item ) { 
  1031. echo '<tr>'; 
  1032. $this->single_row_columns( $item ); 
  1033. echo '</tr>'; 
  1034.  
  1035. /** 
  1036. * @param object $item 
  1037. * @param string $column_name 
  1038. */ 
  1039. protected function column_default( $item, $column_name ) {} 
  1040.  
  1041. /** 
  1042. * @param object $item 
  1043. */ 
  1044. protected function column_cb( $item ) {} 
  1045.  
  1046. /** 
  1047. * Generates the columns for a single row of the table 
  1048. * @since 3.1.0 
  1049. * @access protected 
  1050. * @param object $item The current item 
  1051. */ 
  1052. protected function single_row_columns( $item ) { 
  1053. list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info(); 
  1054.  
  1055. foreach ( $columns as $column_name => $column_display_name ) { 
  1056. $classes = "$column_name column-$column_name"; 
  1057. if ( $primary === $column_name ) { 
  1058. $classes .= ' has-row-actions column-primary'; 
  1059.  
  1060. if ( in_array( $column_name, $hidden ) ) { 
  1061. $classes .= ' hidden'; 
  1062.  
  1063. // Comments column uses HTML in the display name with screen reader text. 
  1064. // Instead of using esc_attr(), we strip tags to get closer to a user-friendly string. 
  1065. $data = 'data-colname="' . wp_strip_all_tags( $column_display_name ) . '"'; 
  1066.  
  1067. $attributes = "class='$classes' $data"; 
  1068.  
  1069. if ( 'cb' === $column_name ) { 
  1070. echo '<th scope="row" class="check-column">'; 
  1071. echo $this->column_cb( $item ); 
  1072. echo '</th>'; 
  1073. } elseif ( method_exists( $this, '_column_' . $column_name ) ) { 
  1074. echo call_user_func( 
  1075. array( $this, '_column_' . $column_name ),  
  1076. $item,  
  1077. $classes,  
  1078. $data,  
  1079. $primary 
  1080. ); 
  1081. } elseif ( method_exists( $this, 'column_' . $column_name ) ) { 
  1082. echo "<td $attributes>"; 
  1083. echo call_user_func( array( $this, 'column_' . $column_name ), $item ); 
  1084. echo $this->handle_row_actions( $item, $column_name, $primary ); 
  1085. echo "</td>"; 
  1086. } else { 
  1087. echo "<td $attributes>"; 
  1088. echo $this->column_default( $item, $column_name ); 
  1089. echo $this->handle_row_actions( $item, $column_name, $primary ); 
  1090. echo "</td>"; 
  1091.  
  1092. /** 
  1093. * Generates and display row actions links for the list table. 
  1094. * @since 4.3.0 
  1095. * @access protected 
  1096. * @param object $item The item being acted upon. 
  1097. * @param string $column_name Current column name. 
  1098. * @param string $primary Primary column name. 
  1099. * @return string The row actions HTML, or an empty string if the current column is the primary column. 
  1100. */ 
  1101. protected function handle_row_actions( $item, $column_name, $primary ) { 
  1102. return $column_name === $primary ? '<button type="button" class="toggle-row"><span class="screen-reader-text">' . __( 'Show more details' ) . '</span></button>' : ''; 
  1103.  
  1104. /** 
  1105. * Handle an incoming ajax request (called from admin-ajax.php) 
  1106. * @since 3.1.0 
  1107. * @access public 
  1108. */ 
  1109. public function ajax_response() { 
  1110. $this->prepare_items(); 
  1111.  
  1112. ob_start(); 
  1113. if ( ! empty( $_REQUEST['no_placeholder'] ) ) { 
  1114. $this->display_rows(); 
  1115. } else { 
  1116. $this->display_rows_or_placeholder(); 
  1117.  
  1118. $rows = ob_get_clean(); 
  1119.  
  1120. $response = array( 'rows' => $rows ); 
  1121.  
  1122. if ( isset( $this->_pagination_args['total_items'] ) ) { 
  1123. $response['total_items_i18n'] = sprintf( 
  1124. _n( '%s item', '%s items', $this->_pagination_args['total_items'] ),  
  1125. number_format_i18n( $this->_pagination_args['total_items'] ) 
  1126. ); 
  1127. if ( isset( $this->_pagination_args['total_pages'] ) ) { 
  1128. $response['total_pages'] = $this->_pagination_args['total_pages']; 
  1129. $response['total_pages_i18n'] = number_format_i18n( $this->_pagination_args['total_pages'] ); 
  1130.  
  1131. die( wp_json_encode( $response ) ); 
  1132.  
  1133. /** 
  1134. * Send required variables to JavaScript land 
  1135. * @access public 
  1136. */ 
  1137. public function _js_vars() { 
  1138. $args = array( 
  1139. 'class' => get_class( $this ),  
  1140. 'screen' => array( 
  1141. 'id' => $this->screen->id,  
  1142. 'base' => $this->screen->base,  
  1143. ); 
  1144.  
  1145. printf( "<script type='text/javascript'>list_args = %s;</script>\n", wp_json_encode( $args ) );