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/** 11 * Base class for displaying a list of items in an ajaxified HTML table. 12 * 13 * @since 3.1.0 14 * @access private 15 */ 16class WP_List_Table { 17 18 /** 19 * The current list of items. 20 * 21 * @since 3.1.0 22 * @var array 23 */ 24 public $items; 25 26 /** 27 * Various information about the current table. 28 * 29 * @since 3.1.0 30 * @var array 31 */ 32 protected $_args; 33 34 /** 35 * Various information needed for displaying the pagination. 36 * 37 * @since 3.1.0 38 * @var array 39 */ 40 protected $_pagination_args = array(); 41 42 /** 43 * The current screen. 44 * 45 * @since 3.1.0 46 * @var WP_Screen 47 */ 48 protected $screen; 49 50 /** 51 * Cached bulk actions. 52 * 53 * @since 3.1.0 54 * @var array 55 */ 56 private $_actions; 57 58 /** 59 * Cached pagination output. 60 * 61 * @since 3.1.0 62 * @var string 63 */ 64 private $_pagination; 65 66 /** 67 * The view switcher modes. 68 * 69 * @since 4.1.0 70 * @var array 71 */ 72 protected $modes = array(); 73 74 /** 75 * Stores the value returned by ->get_column_info(). 76 * 77 * @since 4.1.0 78 * @var array 79 */ 80 protected $_column_headers; 81 82 /** 83 * {@internal Missing Summary} 84 * 85 * @var array 86 */ 87 protected $compat_fields = array( '_args', '_pagination_args', 'screen', '_actions', '_pagination' ); 88 89 /** 90 * {@internal Missing Summary} 91 * 92 * @var array 93 */ 94 protected $compat_methods = array( 95 'set_pagination_args', 96 'get_views', 97 'get_bulk_actions', 98 'bulk_actions', 99 'row_actions', 100 'months_dropdown', 101 'view_switcher', 102 'comments_bubble', 103 'get_items_per_page', 104 'pagination', 105 'get_sortable_columns', 106 'get_column_info', 107 'get_table_classes', 108 'display_tablenav', 109 'extra_tablenav', 110 'single_row_columns', 111 ); 112 113 /** 114 * Constructor. 115 * 116 * The child class should call this constructor from its own constructor to override 117 * the default $args. 118 * 119 * @since 3.1.0 120 * 121 * @param array|string $args { 122 * Array or string of arguments. 123 * 124 * @type string $plural Plural value used for labels and the objects being listed. 125 * This affects things such as CSS class-names and nonces used 126 * in the list table, e.g. 'posts'. Default empty. 127 * @type string $singular Singular label for an object being listed, e.g. 'post'. 128 * Default empty 129 * @type bool $ajax Whether the list table supports Ajax. This includes loading 130 * and sorting data, for example. If true, the class will call 131 * the _js_vars() method in the footer to provide variables 132 * to any scripts handling Ajax events. Default false. 133 * @type string $screen String containing the hook name used to determine the current 134 * screen. If left null, the current screen will be automatically set. 135 * Default null. 136 * } 137 */ 138 public function __construct( $args = array() ) { 139 $args = wp_parse_args( 140 $args, 141 array( 142 'plural' => '', 143 'singular' => '', 144 'ajax' => false, 145 'screen' => null, 146 ) 147 ); 148 149 $this->screen = convert_to_screen( $args['screen'] ); 150 151 add_filter( "manage_{$this->screen->id}_columns", array( $this, 'get_columns' ), 0 ); 152 153 if ( ! $args['plural'] ) { 154 $args['plural'] = $this->screen->base; 155 } 156 157 $args['plural'] = sanitize_key( $args['plural'] ); 158 $args['singular'] = sanitize_key( $args['singular'] ); 159 160 $this->_args = $args; 161 162 if ( $args['ajax'] ) { 163 // wp_enqueue_script( 'list-table' ); 164 add_action( 'admin_footer', array( $this, '_js_vars' ) ); 165 } 166 167 if ( empty( $this->modes ) ) { 168 $this->modes = array( 169 'list' => __( 'Compact view' ), 170 'excerpt' => __( 'Extended view' ), 171 ); 172 } 173 } 174 175 /** 176 * Make private properties readable for backward compatibility. 177 * 178 * @since 4.0.0 179 * 180 * @param string $name Property to get. 181 * @return mixed Property. 182 */ 183 public function __get( $name ) { 184 if ( in_array( $name, $this->compat_fields, true ) ) { 185 return $this->$name; 186 } 187 } 188 189 /** 190 * Make private properties settable for backward compatibility. 191 * 192 * @since 4.0.0 193 * 194 * @param string $name Property to check if set. 195 * @param mixed $value Property value. 196 * @return mixed Newly-set property. 197 */ 198 public function __set( $name, $value ) { 199 if ( in_array( $name, $this->compat_fields, true ) ) { 200 return $this->$name = $value; 201 } 202 } 203 204 /** 205 * Make private properties checkable for backward compatibility. 206 * 207 * @since 4.0.0 208 * 209 * @param string $name Property to check if set. 210 * @return bool Whether the property is set. 211 */ 212 public function __isset( $name ) { 213 if ( in_array( $name, $this->compat_fields, true ) ) { 214 return isset( $this->$name ); 215 } 216 } 217 218 /** 219 * Make private properties un-settable for backward compatibility. 220 * 221 * @since 4.0.0 222 * 223 * @param string $name Property to unset. 224 */ 225 public function __unset( $name ) { 226 if ( in_array( $name, $this->compat_fields, true ) ) { 227 unset( $this->$name ); 228 } 229 } 230 231 /** 232 * Make private/protected methods readable for backward compatibility. 233 * 234 * @since 4.0.0 235 * 236 * @param string $name Method to call. 237 * @param array $arguments Arguments to pass when calling. 238 * @return mixed|bool Return value of the callback, false otherwise. 239 */ 240 public function __call( $name, $arguments ) { 241 if ( in_array( $name, $this->compat_methods, true ) ) { 242 return $this->$name( ...$arguments ); 243 } 244 return false; 245 } 246 247 /** 248 * Checks the current user's permissions 249 * 250 * @since 3.1.0 251 * @abstract 252 */ 253 public function ajax_user_can() { 254 die( 'function WP_List_Table::ajax_user_can() must be overridden in a subclass.' ); 255 } 256 257 /** 258 * Prepares the list of items for displaying. 259 * 260 * @uses WP_List_Table::set_pagination_args() 261 * 262 * @since 3.1.0 263 * @abstract 264 */ 265 public function prepare_items() { 266 die( 'function WP_List_Table::prepare_items() must be overridden in a subclass.' ); 267 } 268 269 /** 270 * An internal method that sets all the necessary pagination arguments 271 * 272 * @since 3.1.0 273 * 274 * @param array|string $args Array or string of arguments with information about the pagination. 275 */ 276 protected function set_pagination_args( $args ) { 277 $args = wp_parse_args( 278 $args, 279 array( 280 'total_items' => 0, 281 'total_pages' => 0, 282 'per_page' => 0, 283 ) 284 ); 285 286 if ( ! $args['total_pages'] && $args['per_page'] > 0 ) { 287 $args['total_pages'] = ceil( $args['total_items'] / $args['per_page'] ); 288 } 289 290 // Redirect if page number is invalid and headers are not already sent. 291 if ( ! headers_sent() && ! wp_doing_ajax() && $args['total_pages'] > 0 && $this->get_pagenum() > $args['total_pages'] ) { 292 wp_redirect( add_query_arg( 'paged', $args['total_pages'] ) ); 293 exit; 294 } 295 296 $this->_pagination_args = $args; 297 } 298 299 /** 300 * Access the pagination args. 301 * 302 * @since 3.1.0 303 * 304 * @param string $key Pagination argument to retrieve. Common values include 'total_items', 305 * 'total_pages', 'per_page', or 'infinite_scroll'. 306 * @return int Number of items that correspond to the given pagination argument. 307 */ 308 public function get_pagination_arg( $key ) { 309 if ( 'page' === $key ) { 310 return $this->get_pagenum(); 311 } 312 313 if ( isset( $this->_pagination_args[ $key ] ) ) { 314 return $this->_pagination_args[ $key ]; 315 } 316 } 317 318 /** 319 * Whether the table has items to display or not 320 * 321 * @since 3.1.0 322 * 323 * @return bool 324 */ 325 public function has_items() { 326 return ! empty( $this->items ); 327 } 328 329 /** 330 * Message to be displayed when there are no items 331 * 332 * @since 3.1.0 333 */ 334 public function no_items() { 335 _e( 'No items found.' ); 336 } 337 338 /** 339 * Displays the search box. 340 * 341 * @since 3.1.0 342 * 343 * @param string $text The 'submit' button label. 344 * @param string $input_id ID attribute value for the search input field. 345 */ 346 public function search_box( $text, $input_id ) { 347 if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) { 348 return; 349 } 350 351 $input_id = $input_id . '-search-input'; 352 353 if ( ! empty( $_REQUEST['orderby'] ) ) { 354 echo '<input type="hidden" name="orderby" value="' . esc_attr( $_REQUEST['orderby'] ) . '" />'; 355 } 356 if ( ! empty( $_REQUEST['order'] ) ) { 357 echo '<input type="hidden" name="order" value="' . esc_attr( $_REQUEST['order'] ) . '" />'; 358 } 359 if ( ! empty( $_REQUEST['post_mime_type'] ) ) { 360 echo '<input type="hidden" name="post_mime_type" value="' . esc_attr( $_REQUEST['post_mime_type'] ) . '" />'; 361 } 362 if ( ! empty( $_REQUEST['detached'] ) ) { 363 echo '<input type="hidden" name="detached" value="' . esc_attr( $_REQUEST['detached'] ) . '" />'; 364 } 365 ?> 366<p class="search-box"> 367 <label class="screen-reader-text" for="<?php echo esc_attr( $input_id ); ?>"><?php echo $text; ?>:</label> 368 <input type="search" id="<?php echo esc_attr( $input_id ); ?>" name="s" value="<?php _admin_search_query(); ?>" /> 369 <?php submit_button( $text, '', '', false, array( 'id' => 'search-submit' ) ); ?> 370</p> 371 <?php 372 } 373 374 /** 375 * Gets the list of views available on this table. 376 * 377 * The format is an associative array: 378 * - `'id' => 'link'` 379 * 380 * @since 3.1.0 381 * 382 * @return array 383 */ 384 protected function get_views() { 385 return array(); 386 } 387 388 /** 389 * Displays the list of views available on this table. 390 * 391 * @since 3.1.0 392 */ 393 public function views() { 394 $views = $this->get_views(); 395 /** 396 * Filters the list of available list table views. 397 * 398 * The dynamic portion of the hook name, `$this->screen->id`, refers 399 * to the ID of the current screen. 400 * 401 * @since 3.1.0 402 * 403 * @param string[] $views An array of available list table views. 404 */ 405 $views = apply_filters( "views_{$this->screen->id}", $views ); 406 407 if ( empty( $views ) ) { 408 return; 409 } 410 411 $this->screen->render_screen_reader_content( 'heading_views' ); 412 413 echo "<ul class='subsubsub'>\n"; 414 foreach ( $views as $class => $view ) { 415 $views[ $class ] = "\t<li class='$class'>$view"; 416 } 417 echo implode( " |</li>\n", $views ) . "</li>\n"; 418 echo '</ul>'; 419 } 420 421 /** 422 * Retrieves the list of bulk actions available for this table. 423 * 424 * The format is an associative array where each element represents either a top level option value and label, or 425 * an array representing an optgroup and its options. 426 * 427 * For a standard option, the array element key is the field value and the array element value is the field label. 428 * 429 * For an optgroup, the array element key is the label and the array element value is an associative array of 430 * options as above. 431 * 432 * Example: 433 * 434 * [ 435 * 'edit' => 'Edit', 436 * 'delete' => 'Delete', 437 * 'Change State' => [ 438 * 'feature' => 'Featured', 439 * 'sale' => 'On Sale', 440 * ] 441 * ] 442 * 443 * @since 3.1.0 444 * @since 5.6.0 A bulk action can now contain an array of options in order to create an optgroup. 445 * 446 * @return array 447 */ 448 protected function get_bulk_actions() { 449 return array(); 450 } 451 452 /** 453 * Displays the bulk actions dropdown. 454 * 455 * @since 3.1.0 456 * 457 * @param string $which The location of the bulk actions: 'top' or 'bottom'. 458 * This is designated as optional for backward compatibility. 459 */ 460 protected function bulk_actions( $which = '' ) { 461 if ( is_null( $this->_actions ) ) { 462 $this->_actions = $this->get_bulk_actions(); 463 464 /** 465 * Filters the items in the bulk actions menu of the list table. 466 * 467 * The dynamic portion of the hook name, `$this->screen->id`, refers 468 * to the ID of the current screen. 469 * 470 * @since 3.1.0 471 * @since 5.6.0 A bulk action can now contain an array of options in order to create an optgroup. 472 * 473 * @param array $actions An array of the available bulk actions. 474 */ 475 $this->_actions = apply_filters( "bulk_actions-{$this->screen->id}", $this->_actions ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores 476 477 $two = ''; 478 } else { 479 $two = '2'; 480 } 481 482 if ( empty( $this->_actions ) ) { 483 return; 484 } 485 486 echo '<label for="bulk-action-selector-' . esc_attr( $which ) . '" class="screen-reader-text">' . __( 'Select bulk action' ) . '</label>'; 487 echo '<select name="action' . $two . '" id="bulk-action-selector-' . esc_attr( $which ) . "\">\n"; 488 echo '<option value="-1">' . __( 'Bulk actions' ) . "</option>\n"; 489 490 foreach ( $this->_actions as $key => $value ) { 491 if ( is_array( $value ) ) { 492 echo "\t" . '<optgroup label="' . esc_attr( $key ) . '">' . "\n"; 493 494 foreach ( $value as $name => $title ) { 495 $class = ( 'edit' === $name ) ? ' class="hide-if-no-js"' : ''; 496 497 echo "\t\t" . '<option value="' . esc_attr( $name ) . '"' . $class . '>' . $title . "</option>\n"; 498 } 499 echo "\t" . "</optgroup>\n"; 500 } else { 501 $class = ( 'edit' === $key ) ? ' class="hide-if-no-js"' : ''; 502 503 echo "\t" . '<option value="' . esc_attr( $key ) . '"' . $class . '>' . $value . "</option>\n"; 504 } 505 } 506 507 echo "</select>\n"; 508 509 submit_button( __( 'Apply' ), 'action', '', false, array( 'id' => "doaction$two" ) ); 510 echo "\n"; 511 } 512 513 /** 514 * Gets the current action selected from the bulk actions dropdown. 515 * 516 * @since 3.1.0 517 * 518 * @return string|false The action name. False if no action was selected. 519 */ 520 public function current_action() { 521 if ( isset( $_REQUEST['filter_action'] ) && ! empty( $_REQUEST['filter_action'] ) ) { 522 return false; 523 } 524 525 if ( isset( $_REQUEST['action'] ) && -1 != $_REQUEST['action'] ) { 526 return $_REQUEST['action']; 527 } 528 529 return false; 530 } 531 532 /** 533 * Generates the required HTML for a list of row action links. 534 * 535 * @since 3.1.0 536 * 537 * @param string[] $actions An array of action links. 538 * @param bool $always_visible Whether the actions should be always visible. 539 * @return string The HTML for the row actions. 540 */ 541 protected function row_actions( $actions, $always_visible = false ) { 542 $action_count = count( $actions ); 543 544 if ( ! $action_count ) { 545 return ''; 546 } 547 548 $mode = get_user_setting( 'posts_list_mode', 'list' ); 549 550 if ( 'excerpt' === $mode ) { 551 $always_visible = true; 552 } 553 554 $out = '<div class="' . ( $always_visible ? 'row-actions visible' : 'row-actions' ) . '">'; 555 556 $i = 0; 557 558 foreach ( $actions as $action => $link ) { 559 ++$i; 560 561 $sep = ( $i < $action_count ) ? ' | ' : ''; 562 563 $out .= "<span class='$action'>$link$sep</span>"; 564 } 565 566 $out .= '</div>'; 567 568 $out .= '<button type="button" class="toggle-row"><span class="screen-reader-text">' . __( 'Show more details' ) . '</span></button>'; 569 570 return $out; 571 } 572 573 /** 574 * Displays a dropdown for filtering items in the list table by month. 575 * 576 * @since 3.1.0 577 * 578 * @global wpdb $wpdb WordPress database abstraction object. 579 * @global WP_Locale $wp_locale WordPress date and time locale object. 580 * 581 * @param string $post_type The post type. 582 */ 583 protected function months_dropdown( $post_type ) { 584 global $wpdb, $wp_locale; 585 586 /** 587 * Filters whether to remove the 'Months' drop-down from the post list table. 588 * 589 * @since 4.2.0 590 * 591 * @param bool $disable Whether to disable the drop-down. Default false. 592 * @param string $post_type The post type. 593 */ 594 if ( apply_filters( 'disable_months_dropdown', false, $post_type ) ) { 595 return; 596 } 597 598 /** 599 * Filters to short-circuit performing the months dropdown query. 600 * 601 * @since 5.7.0 602 * 603 * @param object[]|false $months 'Months' drop-down results. Default false. 604 * @param string $post_type The post type. 605 */ 606 $months = apply_filters( 'pre_months_dropdown_query', false, $post_type ); 607 608 if ( ! is_array( $months ) ) { 609 $extra_checks = "AND post_status != 'auto-draft'"; 610 if ( ! isset( $_GET['post_status'] ) || 'trash' !== $_GET['post_status'] ) { 611 $extra_checks .= " AND post_status != 'trash'"; 612 } elseif ( isset( $_GET['post_status'] ) ) { 613 $extra_checks = $wpdb->prepare( ' AND post_status = %s', $_GET['post_status'] ); 614 } 615 616 $months = $wpdb->get_results( 617 $wpdb->prepare( 618 " 619 SELECT DISTINCT YEAR( post_date ) AS year, MONTH( post_date ) AS month 620 FROM $wpdb->posts 621 WHERE post_type = %s 622 $extra_checks 623 ORDER BY post_date DESC 624 ", 625 $post_type 626 ) 627 ); 628 } 629 630 /** 631 * Filters the 'Months' drop-down results. 632 * 633 * @since 3.7.0 634 * 635 * @param object[] $months Array of the months drop-down query results. 636 * @param string $post_type The post type. 637 */ 638 $months = apply_filters( 'months_dropdown_results', $months, $post_type ); 639 640 $month_count = count( $months ); 641 642 if ( ! $month_count || ( 1 == $month_count && 0 == $months[0]->month ) ) { 643 return; 644 } 645 646 $m = isset( $_GET['m'] ) ? (int) $_GET['m'] : 0; 647 ?> 648 <label for="filter-by-date" class="screen-reader-text"><?php echo get_post_type_object( $post_type )->labels->filter_by_date; ?></label> 649 <select name="m" id="filter-by-date"> 650 <option<?php selected( $m, 0 ); ?> value="0"><?php _e( 'All dates' ); ?></option> 651 <?php 652 foreach ( $months as $arc_row ) { 653 if ( 0 == $arc_row->year ) { 654 continue; 655 } 656 657 $month = zeroise( $arc_row->month, 2 ); 658 $year = $arc_row->year; 659 660 printf( 661 "<option %s value='%s'>%s</option>\n", 662 selected( $m, $year . $month, false ), 663 esc_attr( $arc_row->year . $month ), 664 /* translators: 1: Month name, 2: 4-digit year. */ 665 sprintf( __( '%1$s %2$d' ), $wp_locale->get_month( $month ), $year ) 666 ); 667 } 668 ?> 669 </select> 670 <?php 671 } 672 673 /** 674 * Displays a view switcher. 675 * 676 * @since 3.1.0 677 * 678 * @param string $current_mode 679 */ 680 protected function view_switcher( $current_mode ) { 681 ?> 682 <input type="hidden" name="mode" value="<?php echo esc_attr( $current_mode ); ?>" /> 683 <div class="view-switch"> 684 <?php 685 foreach ( $this->modes as $mode => $title ) { 686 $classes = array( 'view-' . $mode ); 687 $aria_current = ''; 688 689 if ( $current_mode === $mode ) { 690 $classes[] = 'current'; 691 $aria_current = ' aria-current="page"'; 692 } 693 694 printf( 695 "<a href='%s' class='%s' id='view-switch-$mode'$aria_current><span class='screen-reader-text'>%s</span></a>\n", 696 esc_url( remove_query_arg( 'attachment-filter', add_query_arg( 'mode', $mode ) ) ), 697 implode( ' ', $classes ), 698 $title 699 ); 700 } 701 ?> 702 </div> 703 <?php 704 } 705 706 /** 707 * Displays a comment count bubble. 708 * 709 * @since 3.1.0 710 * 711 * @param int $post_id The post ID. 712 * @param int $pending_comments Number of pending comments. 713 */ 714 protected function comments_bubble( $post_id, $pending_comments ) { 715 $approved_comments = get_comments_number(); 716 717 $approved_comments_number = number_format_i18n( $approved_comments ); 718 $pending_comments_number = number_format_i18n( $pending_comments ); 719 720 $approved_only_phrase = sprintf( 721 /* translators: %s: Number of comments. */ 722 _n( '%s comment', '%s comments', $approved_comments ), 723 $approved_comments_number 724 ); 725 726 $approved_phrase = sprintf( 727 /* translators: %s: Number of comments. */ 728 _n( '%s approved comment', '%s approved comments', $approved_comments ), 729 $approved_comments_number 730 ); 731 732 $pending_phrase = sprintf( 733 /* translators: %s: Number of comments. */ 734 _n( '%s pending comment', '%s pending comments', $pending_comments ), 735 $pending_comments_number 736 ); 737 738 if ( ! $approved_comments && ! $pending_comments ) { 739 // No comments at all. 740 printf( 741 '<span aria-hidden="true">—</span><span class="screen-reader-text">%s</span>', 742 __( 'No comments' ) 743 ); 744 } elseif ( $approved_comments && 'trash' === get_post_status( $post_id ) ) { 745 // Don't link the comment bubble for a trashed post. 746 printf( 747 '<span 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></span>', 748 $approved_comments_number, 749 $pending_comments ? $approved_phrase : $approved_only_phrase 750 ); 751 } elseif ( $approved_comments ) { 752 // Link the comment bubble to approved comments. 753 printf( 754 '<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>', 755 esc_url( 756 add_query_arg( 757 array( 758 'p' => $post_id, 759 'comment_status' => 'approved', 760 ), 761 admin_url( 'edit-comments.php' ) 762 ) 763 ), 764 $approved_comments_number, 765 $pending_comments ? $approved_phrase : $approved_only_phrase 766 ); 767 } else { 768 // Don't link the comment bubble when there are no approved comments. 769 printf( 770 '<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>', 771 $approved_comments_number, 772 $pending_comments ? __( 'No approved comments' ) : __( 'No comments' ) 773 ); 774 } 775 776 if ( $pending_comments ) { 777 printf( 778 '<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>', 779 esc_url( 780 add_query_arg( 781 array( 782 'p' => $post_id, 783 'comment_status' => 'moderated', 784 ), 785 admin_url( 'edit-comments.php' ) 786 ) 787 ), 788 $pending_comments_number, 789 $pending_phrase 790 ); 791 } else { 792 printf( 793 '<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>', 794 $pending_comments_number, 795 $approved_comments ? __( 'No pending comments' ) : __( 'No comments' ) 796 ); 797 } 798 } 799 800 /** 801 * Gets the current page number. 802 * 803 * @since 3.1.0 804 * 805 * @return int 806 */ 807 public function get_pagenum() { 808 $pagenum = isset( $_REQUEST['paged'] ) ? absint( $_REQUEST['paged'] ) : 0; 809 810 if ( isset( $this->_pagination_args['total_pages'] ) && $pagenum > $this->_pagination_args['total_pages'] ) { 811 $pagenum = $this->_pagination_args['total_pages']; 812 } 813 814 return max( 1, $pagenum ); 815 } 816 817 /** 818 * Gets the number of items to display on a single page. 819 * 820 * @since 3.1.0 821 * 822 * @param string $option 823 * @param int $default 824 * @return int 825 */ 826 protected function get_items_per_page( $option, $default = 20 ) { 827 $per_page = (int) get_user_option( $option ); 828 if ( empty( $per_page ) || $per_page < 1 ) { 829 $per_page = $default; 830 } 831 832 /** 833 * Filters the number of items to be displayed on each page of the list table. 834 * 835 * The dynamic hook name, `$option`, refers to the `per_page` option depending 836 * on the type of list table in use. Possible filter names include: 837 * 838 * - `edit_comments_per_page` 839 * - `sites_network_per_page` 840 * - `site_themes_network_per_page` 841 * - `themes_network_per_page'` 842 * - `users_network_per_page` 843 * - `edit_post_per_page` 844 * - `edit_page_per_page'` 845 * - `edit_{$post_type}_per_page` 846 * - `edit_post_tag_per_page` 847 * - `edit_category_per_page` 848 * - `edit_{$taxonomy}_per_page` 849 * - `site_users_network_per_page` 850 * - `users_per_page` 851 * 852 * @since 2.9.0 853 * 854 * @param int $per_page Number of items to be displayed. Default 20. 855 */ 856 return (int) apply_filters( "{$option}", $per_page ); 857 } 858 859 /** 860 * Displays the pagination. 861 * 862 * @since 3.1.0 863 * 864 * @param string $which 865 */ 866 protected function pagination( $which ) { 867 if ( empty( $this->_pagination_args ) ) { 868 return; 869 } 870 871 $total_items = $this->_pagination_args['total_items']; 872 $total_pages = $this->_pagination_args['total_pages']; 873 $infinite_scroll = false; 874 if ( isset( $this->_pagination_args['infinite_scroll'] ) ) { 875 $infinite_scroll = $this->_pagination_args['infinite_scroll']; 876 } 877 878 if ( 'top' === $which && $total_pages > 1 ) { 879 $this->screen->render_screen_reader_content( 'heading_pagination' ); 880 } 881 882 $output = '<span class="displaying-num">' . sprintf( 883 /* translators: %s: Number of items. */ 884 _n( '%s item', '%s items', $total_items ), 885 number_format_i18n( $total_items ) 886 ) . '</span>'; 887 888 $current = $this->get_pagenum(); 889 $removable_query_args = wp_removable_query_args(); 890 891 $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); 892 893 $current_url = remove_query_arg( $removable_query_args, $current_url ); 894 895 $page_links = array(); 896 897 $total_pages_before = '<span class="paging-input">'; 898 $total_pages_after = '</span></span>'; 899 900 $disable_first = false; 901 $disable_last = false; 902 $disable_prev = false; 903 $disable_next = false; 904 905 if ( 1 == $current ) { 906 $disable_first = true; 907 $disable_prev = true; 908 } 909 if ( 2 == $current ) { 910 $disable_first = true; 911 } 912 if ( $total_pages == $current ) { 913 $disable_last = true; 914 $disable_next = true; 915 } 916 if ( $total_pages - 1 == $current ) { 917 $disable_last = true; 918 } 919 920 if ( $disable_first ) { 921 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">«</span>'; 922 } else { 923 $page_links[] = sprintf( 924 "<a class='first-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>", 925 esc_url( remove_query_arg( 'paged', $current_url ) ), 926 __( 'First page' ), 927 '«' 928 ); 929 } 930 931 if ( $disable_prev ) { 932 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">‹</span>'; 933 } else { 934 $page_links[] = sprintf( 935 "<a class='prev-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>", 936 esc_url( add_query_arg( 'paged', max( 1, $current - 1 ), $current_url ) ), 937 __( 'Previous page' ), 938 '‹' 939 ); 940 } 941 942 if ( 'bottom' === $which ) { 943 $html_current_page = $current; 944 $total_pages_before = '<span class="screen-reader-text">' . __( 'Current Page' ) . '</span><span id="table-paging" class="paging-input"><span class="tablenav-paging-text">'; 945 } else { 946 $html_current_page = sprintf( 947 "%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'>", 948 '<label for="current-page-selector" class="screen-reader-text">' . __( 'Current Page' ) . '</label>', 949 $current, 950 strlen( $total_pages ) 951 ); 952 } 953 $html_total_pages = sprintf( "<span class='total-pages'>%s</span>", number_format_i18n( $total_pages ) ); 954 $page_links[] = $total_pages_before . sprintf( 955 /* translators: 1: Current page, 2: Total pages. */ 956 _x( '%1$s of %2$s', 'paging' ), 957 $html_current_page, 958 $html_total_pages 959 ) . $total_pages_after; 960 961 if ( $disable_next ) { 962 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">›</span>'; 963 } else { 964 $page_links[] = sprintf( 965 "<a class='next-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>", 966 esc_url( add_query_arg( 'paged', min( $total_pages, $current + 1 ), $current_url ) ), 967 __( 'Next page' ), 968 '›' 969 ); 970 } 971 972 if ( $disable_last ) { 973 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">»</span>'; 974 } else { 975 $page_links[] = sprintf( 976 "<a class='last-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>", 977 esc_url( add_query_arg( 'paged', $total_pages, $current_url ) ), 978 __( 'Last page' ), 979 '»' 980 ); 981 } 982 983 $pagination_links_class = 'pagination-links'; 984 if ( ! empty( $infinite_scroll ) ) { 985 $pagination_links_class .= ' hide-if-js'; 986 } 987 $output .= "\n<span class='$pagination_links_class'>" . implode( "\n", $page_links ) . '</span>'; 988 989 if ( $total_pages ) { 990 $page_class = $total_pages < 2 ? ' one-page' : ''; 991 } else { 992 $page_class = ' no-pages'; 993 } 994 $this->_pagination = "<div class='tablenav-pages{$page_class}'>$output</div>"; 995 996 echo $this->_pagination; 997 } 998 999 /** 1000 * Gets a list of columns. 1001 * 1002 * The format is: 1003 * - `'internal-name' => 'Title'` 1004 * 1005 * @since 3.1.0 1006 * @abstract 1007 * 1008 * @return array 1009 */ 1010 public function get_columns() { 1011 die( 'function WP_List_Table::get_columns() must be overridden in a subclass.' ); 1012 } 1013 1014 /** 1015 * Gets a list of sortable columns. 1016 * 1017 * The format is: 1018 * - `'internal-name' => 'orderby'` 1019 * - `'internal-name' => array( 'orderby', 'asc' )` - The second element sets the initial sorting order. 1020 * - `'internal-name' => array( 'orderby', true )` - The second element makes the initial order descending. 1021 * 1022 * @since 3.1.0 1023 * 1024 * @return array 1025 */ 1026 protected function get_sortable_columns() { 1027 return array(); 1028 } 1029 1030 /** 1031 * Gets the name of the default primary column. 1032 * 1033 * @since 4.3.0 1034 * 1035 * @return string Name of the default primary column, in this case, an empty string. 1036 */ 1037 protected function get_default_primary_column_name() { 1038 $columns = $this->get_columns(); 1039 $column = ''; 1040 1041 if ( empty( $columns ) ) { 1042 return $column; 1043 } 1044 1045 // We need a primary defined so responsive views show something, 1046 // so let's fall back to the first non-checkbox column. 1047 foreach ( $columns as $col => $column_name ) { 1048 if ( 'cb' === $col ) { 1049 continue; 1050 } 1051 1052 $column = $col; 1053 break; 1054 } 1055 1056 return $column; 1057 } 1058 1059 /** 1060 * Public wrapper for WP_List_Table::get_default_primary_column_name(). 1061 * 1062 * @since 4.4.0 1063 * 1064 * @return string Name of the default primary column. 1065 */ 1066 public function get_primary_column() { 1067 return $this->get_primary_column_name(); 1068 } 1069 1070 /** 1071 * Gets the name of the primary column. 1072 * 1073 * @since 4.3.0 1074 * 1075 * @return string The name of the primary column. 1076 */ 1077 protected function get_primary_column_name() { 1078 $columns = get_column_headers( $this->screen ); 1079 $default = $this->get_default_primary_column_name(); 1080 1081 // If the primary column doesn't exist, 1082 // fall back to the first non-checkbox column. 1083 if ( ! isset( $columns[ $default ] ) ) { 1084 $default = self::get_default_primary_column_name(); 1085 } 1086 1087 /** 1088 * Filters the name of the primary column for the current list table. 1089 * 1090 * @since 4.3.0 1091 * 1092 * @param string $default Column name default for the specific list table, e.g. 'name'. 1093 * @param string $context Screen ID for specific list table, e.g. 'plugins'. 1094 */ 1095 $column = apply_filters( 'list_table_primary_column', $default, $this->screen->id ); 1096 1097 if ( empty( $column ) || ! isset( $columns[ $column ] ) ) { 1098 $column = $default; 1099 } 1100 1101 return $column; 1102 } 1103 1104 /** 1105 * Gets a list of all, hidden, and sortable columns, with filter applied. 1106 * 1107 * @since 3.1.0 1108 * 1109 * @return array 1110 */ 1111 protected function get_column_info() { 1112 // $_column_headers is already set / cached. 1113 if ( isset( $this->_column_headers ) && is_array( $this->_column_headers ) ) { 1114 /* 1115 * Backward compatibility for `$_column_headers` format prior to WordPress 4.3. 1116 * 1117 * In WordPress 4.3 the primary column name was added as a fourth item in the 1118 * column headers property. This ensures the primary column name is included 1119 * in plugins setting the property directly in the three item format. 1120 */ 1121 $column_headers = array( array(), array(), array(), $this->get_primary_column_name() ); 1122 foreach ( $this->_column_headers as $key => $value ) { 1123 $column_headers[ $key ] = $value; 1124 } 1125 1126 return $column_headers; 1127 } 1128 1129 $columns = get_column_headers( $this->screen ); 1130 $hidden = get_hidden_columns( $this->screen ); 1131 1132 $sortable_columns = $this->get_sortable_columns(); 1133 /** 1134 * Filters the list table sortable columns for a specific screen. 1135 * 1136 * The dynamic portion of the hook name, `$this->screen->id`, refers 1137 * to the ID of the current screen. 1138 * 1139 * @since 3.1.0 1140 * 1141 * @param array $sortable_columns An array of sortable columns. 1142 */ 1143 $_sortable = apply_filters( "manage_{$this->screen->id}_sortable_columns", $sortable_columns ); 1144 1145 $sortable = array(); 1146 foreach ( $_sortable as $id => $data ) { 1147 if ( empty( $data ) ) { 1148 continue; 1149 } 1150 1151 $data = (array) $data; 1152 if ( ! isset( $data[1] ) ) { 1153 $data[1] = false; 1154 } 1155 1156 $sortable[ $id ] = $data; 1157 } 1158 1159 $primary = $this->get_primary_column_name(); 1160 $this->_column_headers = array( $columns, $hidden, $sortable, $primary ); 1161 1162 return $this->_column_headers; 1163 } 1164 1165 /** 1166 * Returns the number of visible columns. 1167 * 1168 * @since 3.1.0 1169 * 1170 * @return int 1171 */ 1172 public function get_column_count() { 1173 list ( $columns, $hidden ) = $this->get_column_info(); 1174 $hidden = array_intersect( array_keys( $columns ), array_filter( $hidden ) ); 1175 return count( $columns ) - count( $hidden ); 1176 } 1177 1178 /** 1179 * Prints column headers, accounting for hidden and sortable columns. 1180 * 1181 * @since 3.1.0 1182 * 1183 * @param bool $with_id Whether to set the ID attribute or not 1184 */ 1185 public function print_column_headers( $with_id = true ) { 1186 list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info(); 1187 1188 $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); 1189 $current_url = remove_query_arg( 'paged', $current_url ); 1190 1191 if ( isset( $_GET['orderby'] ) ) { 1192 $current_orderby = $_GET['orderby']; 1193 } else { 1194 $current_orderby = ''; 1195 } 1196 1197 if ( isset( $_GET['order'] ) && 'desc' === $_GET['order'] ) { 1198 $current_order = 'desc'; 1199 } else { 1200 $current_order = 'asc'; 1201 } 1202 1203 if ( ! empty( $columns['cb'] ) ) { 1204 static $cb_counter = 1; 1205 $columns['cb'] = '<label class="screen-reader-text" for="cb-select-all-' . $cb_counter . '">' . __( 'Select All' ) . '</label>' 1206 . '<input id="cb-select-all-' . $cb_counter . '" type="checkbox" />'; 1207 $cb_counter++; 1208 } 1209 1210 foreach ( $columns as $column_key => $column_display_name ) { 1211 $class = array( 'manage-column', "column-$column_key" ); 1212 1213 if ( in_array( $column_key, $hidden, true ) ) { 1214 $class[] = 'hidden'; 1215 } 1216 1217 if ( 'cb' === $column_key ) { 1218 $class[] = 'check-column'; 1219 } elseif ( in_array( $column_key, array( 'posts', 'comments', 'links' ), true ) ) { 1220 $class[] = 'num'; 1221 } 1222 1223 if ( $column_key === $primary ) { 1224 $class[] = 'column-primary'; 1225 } 1226 1227 if ( isset( $sortable[ $column_key ] ) ) { 1228 list( $orderby, $desc_first ) = $sortable[ $column_key ]; 1229 1230 if ( $current_orderby === $orderby ) { 1231 $order = 'asc' === $current_order ? 'desc' : 'asc'; 1232 1233 $class[] = 'sorted'; 1234 $class[] = $current_order; 1235 } else { 1236 $order = strtolower( $desc_first ); 1237 1238 if ( ! in_array( $order, array( 'desc', 'asc' ), true ) ) { 1239 $order = $desc_first ? 'desc' : 'asc'; 1240 } 1241 1242 $class[] = 'sortable'; 1243 $class[] = 'desc' === $order ? 'asc' : 'desc'; 1244 } 1245 1246 $column_display_name = sprintf( 1247 '<a href="%s"><span>%s</span><span class="sorting-indicator"></span></a>', 1248 esc_url( add_query_arg( compact( 'orderby', 'order' ), $current_url ) ), 1249 $column_display_name 1250 ); 1251 } 1252 1253 $tag = ( 'cb' === $column_key ) ? 'td' : 'th'; 1254 $scope = ( 'th' === $tag ) ? 'scope="col"' : ''; 1255 $id = $with_id ? "id='$column_key'" : ''; 1256 1257 if ( ! empty( $class ) ) { 1258 $class = "class='" . implode( ' ', $class ) . "'"; 1259 } 1260 1261 echo "<$tag $scope $id $class>$column_display_name</$tag>"; 1262 } 1263 } 1264 1265 /** 1266 * Displays the table. 1267 * 1268 * @since 3.1.0 1269 */ 1270 public function display() { 1271 $singular = $this->_args['singular']; 1272 1273 $this->display_tablenav( 'top' ); 1274 1275 $this->screen->render_screen_reader_content( 'heading_list' ); 1276 ?> 1277<table class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>"> 1278 <thead> 1279 <tr> 1280 <?php $this->print_column_headers(); ?> 1281 </tr> 1282 </thead> 1283 1284 <tbody id="the-list" 1285 <?php 1286 if ( $singular ) { 1287 echo " data-wp-lists='list:$singular'"; 1288 } 1289 ?> 1290 > 1291 <?php $this->display_rows_or_placeholder(); ?> 1292 </tbody> 1293 1294 <tfoot> 1295 <tr> 1296 <?php $this->print_column_headers( false ); ?> 1297 </tr> 1298 </tfoot> 1299 1300</table> 1301 <?php 1302 $this->display_tablenav( 'bottom' ); 1303 } 1304 1305 /** 1306 * Gets a list of CSS classes for the WP_List_Table table tag. 1307 * 1308 * @since 3.1.0 1309 * 1310 * @return string[] Array of CSS classes for the table tag. 1311 */ 1312 protected function get_table_classes() { 1313 $mode = get_user_setting( 'posts_list_mode', 'list' ); 1314 1315 $mode_class = esc_attr( 'table-view-' . $mode ); 1316 1317 return array( 'widefat', 'fixed', 'striped', $mode_class, $this->_args['plural'] ); 1318 } 1319 1320 /** 1321 * Generates the table navigation above or below the table 1322 * 1323 * @since 3.1.0 1324 * @param string $which 1325 */ 1326 protected function display_tablenav( $which ) { 1327 if ( 'top' === $which ) { 1328 wp_nonce_field( 'bulk-' . $this->_args['plural'] ); 1329 } 1330 ?> 1331 <div class="tablenav <?php echo esc_attr( $which ); ?>"> 1332 1333 <?php if ( $this->has_items() ) : ?> 1334 <div class="alignleft actions bulkactions"> 1335 <?php $this->bulk_actions( $which ); ?> 1336 </div> 1337 <?php 1338 endif; 1339 $this->extra_tablenav( $which ); 1340 $this->pagination( $which ); 1341 ?> 1342 1343 <br class="clear" /> 1344 </div> 1345 <?php 1346 } 1347 1348 /** 1349 * Extra controls to be displayed between bulk actions and pagination. 1350 * 1351 * @since 3.1.0 1352 * 1353 * @param string $which 1354 */ 1355 protected function extra_tablenav( $which ) {} 1356 1357 /** 1358 * Generates the tbody element for the list table. 1359 * 1360 * @since 3.1.0 1361 */ 1362 public function display_rows_or_placeholder() { 1363 if ( $this->has_items() ) { 1364 $this->display_rows(); 1365 } else { 1366 echo '<tr class="no-items"><td class="colspanchange" colspan="' . $this->get_column_count() . '">'; 1367 $this->no_items(); 1368 echo '</td></tr>'; 1369 } 1370 } 1371 1372 /** 1373 * Generates the table rows. 1374 * 1375 * @since 3.1.0 1376 */ 1377 public function display_rows() { 1378 foreach ( $this->items as $item ) { 1379 $this->single_row( $item ); 1380 } 1381 } 1382 1383 /** 1384 * Generates content for a single row of the table. 1385 * 1386 * @since 3.1.0 1387 * 1388 * @param object|array $item The current item 1389 */ 1390 public function single_row( $item ) { 1391 echo '<tr>'; 1392 $this->single_row_columns( $item ); 1393 echo '</tr>'; 1394 } 1395 1396 /** 1397 * @param object|array $item 1398 * @param string $column_name 1399 */ 1400 protected function column_default( $item, $column_name ) {} 1401 1402 /** 1403 * @param object|array $item 1404 */ 1405 protected function column_cb( $item ) {} 1406 1407 /** 1408 * Generates the columns for a single row of the table. 1409 * 1410 * @since 3.1.0 1411 * 1412 * @param object|array $item The current item. 1413 */ 1414 protected function single_row_columns( $item ) { 1415 list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info(); 1416 1417 foreach ( $columns as $column_name => $column_display_name ) { 1418 $classes = "$column_name column-$column_name"; 1419 if ( $primary === $column_name ) { 1420 $classes .= ' has-row-actions column-primary'; 1421 } 1422 1423 if ( in_array( $column_name, $hidden, true ) ) { 1424 $classes .= ' hidden'; 1425 } 1426 1427 // Comments column uses HTML in the display name with screen reader text. 1428 // Strip tags to get closer to a user-friendly string. 1429 $data = 'data-colname="' . esc_attr( wp_strip_all_tags( $column_display_name ) ) . '"'; 1430 1431 $attributes = "class='$classes' $data"; 1432 1433 if ( 'cb' === $column_name ) { 1434 echo '<th scope="row" class="check-column">'; 1435 echo $this->column_cb( $item ); 1436 echo '</th>'; 1437 } elseif ( method_exists( $this, '_column_' . $column_name ) ) { 1438 echo call_user_func( 1439 array( $this, '_column_' . $column_name ), 1440 $item, 1441 $classes, 1442 $data, 1443 $primary 1444 ); 1445 } elseif ( method_exists( $this, 'column_' . $column_name ) ) { 1446 echo "<td $attributes>"; 1447 echo call_user_func( array( $this, 'column_' . $column_name ), $item ); 1448 echo $this->handle_row_actions( $item, $column_name, $primary ); 1449 echo '</td>'; 1450 } else { 1451 echo "<td $attributes>"; 1452 echo $this->column_default( $item, $column_name ); 1453 echo $this->handle_row_actions( $item, $column_name, $primary ); 1454 echo '</td>'; 1455 } 1456 } 1457 } 1458 1459 /** 1460 * Generates and display row actions links for the list table. 1461 * 1462 * @since 4.3.0 1463 * 1464 * @param object|array $item The item being acted upon. 1465 * @param string $column_name Current column name. 1466 * @param string $primary Primary column name. 1467 * @return string The row actions HTML, or an empty string 1468 * if the current column is not the primary column. 1469 */ 1470 protected function handle_row_actions( $item, $column_name, $primary ) { 1471 return $column_name === $primary ? '<button type="button" class="toggle-row"><span class="screen-reader-text">' . __( 'Show more details' ) . '</span></button>' : ''; 1472 } 1473 1474 /** 1475 * Handles an incoming ajax request (called from admin-ajax.php) 1476 * 1477 * @since 3.1.0 1478 */ 1479 public function ajax_response() { 1480 $this->prepare_items(); 1481 1482 ob_start(); 1483 if ( ! empty( $_REQUEST['no_placeholder'] ) ) { 1484 $this->display_rows(); 1485 } else { 1486 $this->display_rows_or_placeholder(); 1487 } 1488 1489 $rows = ob_get_clean(); 1490 1491 $response = array( 'rows' => $rows ); 1492 1493 if ( isset( $this->_pagination_args['total_items'] ) ) { 1494 $response['total_items_i18n'] = sprintf( 1495 /* translators: Number of items. */ 1496 _n( '%s item', '%s items', $this->_pagination_args['total_items'] ), 1497 number_format_i18n( $this->_pagination_args['total_items'] ) 1498 ); 1499 } 1500 if ( isset( $this->_pagination_args['total_pages'] ) ) { 1501 $response['total_pages'] = $this->_pagination_args['total_pages']; 1502 $response['total_pages_i18n'] = number_format_i18n( $this->_pagination_args['total_pages'] ); 1503 } 1504 1505 die( wp_json_encode( $response ) ); 1506 } 1507 1508 /** 1509 * Sends required variables to JavaScript land. 1510 * 1511 * @since 3.1.0 1512 */ 1513 public function _js_vars() { 1514 $args = array( 1515 'class' => get_class( $this ), 1516 'screen' => array( 1517 'id' => $this->screen->id, 1518 'base' => $this->screen->base, 1519 ), 1520 ); 1521 1522 printf( "<script type='text/javascript'>list_args = %s;</script>\n", wp_json_encode( $args ) ); 1523 } 1524} 1525