[ Index ] |
PHP Cross Reference of WordPress |
[Summary view] [Print] [Text view]
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 */ 16 class 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 $extra_checks = "AND post_status != 'auto-draft'"; 599 if ( ! isset( $_GET['post_status'] ) || 'trash' !== $_GET['post_status'] ) { 600 $extra_checks .= " AND post_status != 'trash'"; 601 } elseif ( isset( $_GET['post_status'] ) ) { 602 $extra_checks = $wpdb->prepare( ' AND post_status = %s', $_GET['post_status'] ); 603 } 604 605 $months = $wpdb->get_results( 606 $wpdb->prepare( 607 " 608 SELECT DISTINCT YEAR( post_date ) AS year, MONTH( post_date ) AS month 609 FROM $wpdb->posts 610 WHERE post_type = %s 611 $extra_checks 612 ORDER BY post_date DESC 613 ", 614 $post_type 615 ) 616 ); 617 618 /** 619 * Filters the 'Months' drop-down results. 620 * 621 * @since 3.7.0 622 * 623 * @param object[] $months Array of the months drop-down query results. 624 * @param string $post_type The post type. 625 */ 626 $months = apply_filters( 'months_dropdown_results', $months, $post_type ); 627 628 $month_count = count( $months ); 629 630 if ( ! $month_count || ( 1 == $month_count && 0 == $months[0]->month ) ) { 631 return; 632 } 633 634 $m = isset( $_GET['m'] ) ? (int) $_GET['m'] : 0; 635 ?> 636 <label for="filter-by-date" class="screen-reader-text"><?php _e( 'Filter by date' ); ?></label> 637 <select name="m" id="filter-by-date"> 638 <option<?php selected( $m, 0 ); ?> value="0"><?php _e( 'All dates' ); ?></option> 639 <?php 640 foreach ( $months as $arc_row ) { 641 if ( 0 == $arc_row->year ) { 642 continue; 643 } 644 645 $month = zeroise( $arc_row->month, 2 ); 646 $year = $arc_row->year; 647 648 printf( 649 "<option %s value='%s'>%s</option>\n", 650 selected( $m, $year . $month, false ), 651 esc_attr( $arc_row->year . $month ), 652 /* translators: 1: Month name, 2: 4-digit year. */ 653 sprintf( __( '%1$s %2$d' ), $wp_locale->get_month( $month ), $year ) 654 ); 655 } 656 ?> 657 </select> 658 <?php 659 } 660 661 /** 662 * Displays a view switcher. 663 * 664 * @since 3.1.0 665 * 666 * @param string $current_mode 667 */ 668 protected function view_switcher( $current_mode ) { 669 ?> 670 <input type="hidden" name="mode" value="<?php echo esc_attr( $current_mode ); ?>" /> 671 <div class="view-switch"> 672 <?php 673 foreach ( $this->modes as $mode => $title ) { 674 $classes = array( 'view-' . $mode ); 675 $aria_current = ''; 676 677 if ( $current_mode === $mode ) { 678 $classes[] = 'current'; 679 $aria_current = ' aria-current="page"'; 680 } 681 682 printf( 683 "<a href='%s' class='%s' id='view-switch-$mode'$aria_current><span class='screen-reader-text'>%s</span></a>\n", 684 esc_url( remove_query_arg( 'attachment-filter', add_query_arg( 'mode', $mode ) ) ), 685 implode( ' ', $classes ), 686 $title 687 ); 688 } 689 ?> 690 </div> 691 <?php 692 } 693 694 /** 695 * Displays a comment count bubble. 696 * 697 * @since 3.1.0 698 * 699 * @param int $post_id The post ID. 700 * @param int $pending_comments Number of pending comments. 701 */ 702 protected function comments_bubble( $post_id, $pending_comments ) { 703 $approved_comments = get_comments_number(); 704 705 $approved_comments_number = number_format_i18n( $approved_comments ); 706 $pending_comments_number = number_format_i18n( $pending_comments ); 707 708 $approved_only_phrase = sprintf( 709 /* translators: %s: Number of comments. */ 710 _n( '%s comment', '%s comments', $approved_comments ), 711 $approved_comments_number 712 ); 713 714 $approved_phrase = sprintf( 715 /* translators: %s: Number of comments. */ 716 _n( '%s approved comment', '%s approved comments', $approved_comments ), 717 $approved_comments_number 718 ); 719 720 $pending_phrase = sprintf( 721 /* translators: %s: Number of comments. */ 722 _n( '%s pending comment', '%s pending comments', $pending_comments ), 723 $pending_comments_number 724 ); 725 726 if ( ! $approved_comments && ! $pending_comments ) { 727 // No comments at all. 728 printf( 729 '<span aria-hidden="true">—</span><span class="screen-reader-text">%s</span>', 730 __( 'No comments' ) 731 ); 732 } elseif ( $approved_comments && 'trash' === get_post_status( $post_id ) ) { 733 // Don't link the comment bubble for a trashed post. 734 printf( 735 '<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>', 736 $approved_comments_number, 737 $pending_comments ? $approved_phrase : $approved_only_phrase 738 ); 739 } elseif ( $approved_comments ) { 740 // Link the comment bubble to approved comments. 741 printf( 742 '<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>', 743 esc_url( 744 add_query_arg( 745 array( 746 'p' => $post_id, 747 'comment_status' => 'approved', 748 ), 749 admin_url( 'edit-comments.php' ) 750 ) 751 ), 752 $approved_comments_number, 753 $pending_comments ? $approved_phrase : $approved_only_phrase 754 ); 755 } else { 756 // Don't link the comment bubble when there are no approved comments. 757 printf( 758 '<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>', 759 $approved_comments_number, 760 $pending_comments ? __( 'No approved comments' ) : __( 'No comments' ) 761 ); 762 } 763 764 if ( $pending_comments ) { 765 printf( 766 '<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>', 767 esc_url( 768 add_query_arg( 769 array( 770 'p' => $post_id, 771 'comment_status' => 'moderated', 772 ), 773 admin_url( 'edit-comments.php' ) 774 ) 775 ), 776 $pending_comments_number, 777 $pending_phrase 778 ); 779 } else { 780 printf( 781 '<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>', 782 $pending_comments_number, 783 $approved_comments ? __( 'No pending comments' ) : __( 'No comments' ) 784 ); 785 } 786 } 787 788 /** 789 * Gets the current page number. 790 * 791 * @since 3.1.0 792 * 793 * @return int 794 */ 795 public function get_pagenum() { 796 $pagenum = isset( $_REQUEST['paged'] ) ? absint( $_REQUEST['paged'] ) : 0; 797 798 if ( isset( $this->_pagination_args['total_pages'] ) && $pagenum > $this->_pagination_args['total_pages'] ) { 799 $pagenum = $this->_pagination_args['total_pages']; 800 } 801 802 return max( 1, $pagenum ); 803 } 804 805 /** 806 * Gets the number of items to display on a single page. 807 * 808 * @since 3.1.0 809 * 810 * @param string $option 811 * @param int $default 812 * @return int 813 */ 814 protected function get_items_per_page( $option, $default = 20 ) { 815 $per_page = (int) get_user_option( $option ); 816 if ( empty( $per_page ) || $per_page < 1 ) { 817 $per_page = $default; 818 } 819 820 /** 821 * Filters the number of items to be displayed on each page of the list table. 822 * 823 * The dynamic hook name, `$option`, refers to the `per_page` option depending 824 * on the type of list table in use. Possible filter names include: 825 * 826 * - `edit_comments_per_page` 827 * - `sites_network_per_page` 828 * - `site_themes_network_per_page` 829 * - `themes_network_per_page'` 830 * - `users_network_per_page` 831 * - `edit_post_per_page` 832 * - `edit_page_per_page'` 833 * - `edit_{$post_type}_per_page` 834 * - `edit_post_tag_per_page` 835 * - `edit_category_per_page` 836 * - `edit_{$taxonomy}_per_page` 837 * - `site_users_network_per_page` 838 * - `users_per_page` 839 * 840 * @since 2.9.0 841 * 842 * @param int $per_page Number of items to be displayed. Default 20. 843 */ 844 return (int) apply_filters( "{$option}", $per_page ); 845 } 846 847 /** 848 * Displays the pagination. 849 * 850 * @since 3.1.0 851 * 852 * @param string $which 853 */ 854 protected function pagination( $which ) { 855 if ( empty( $this->_pagination_args ) ) { 856 return; 857 } 858 859 $total_items = $this->_pagination_args['total_items']; 860 $total_pages = $this->_pagination_args['total_pages']; 861 $infinite_scroll = false; 862 if ( isset( $this->_pagination_args['infinite_scroll'] ) ) { 863 $infinite_scroll = $this->_pagination_args['infinite_scroll']; 864 } 865 866 if ( 'top' === $which && $total_pages > 1 ) { 867 $this->screen->render_screen_reader_content( 'heading_pagination' ); 868 } 869 870 $output = '<span class="displaying-num">' . sprintf( 871 /* translators: %s: Number of items. */ 872 _n( '%s item', '%s items', $total_items ), 873 number_format_i18n( $total_items ) 874 ) . '</span>'; 875 876 $current = $this->get_pagenum(); 877 $removable_query_args = wp_removable_query_args(); 878 879 $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); 880 881 $current_url = remove_query_arg( $removable_query_args, $current_url ); 882 883 $page_links = array(); 884 885 $total_pages_before = '<span class="paging-input">'; 886 $total_pages_after = '</span></span>'; 887 888 $disable_first = false; 889 $disable_last = false; 890 $disable_prev = false; 891 $disable_next = false; 892 893 if ( 1 == $current ) { 894 $disable_first = true; 895 $disable_prev = true; 896 } 897 if ( 2 == $current ) { 898 $disable_first = true; 899 } 900 if ( $total_pages == $current ) { 901 $disable_last = true; 902 $disable_next = true; 903 } 904 if ( $total_pages - 1 == $current ) { 905 $disable_last = true; 906 } 907 908 if ( $disable_first ) { 909 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">«</span>'; 910 } else { 911 $page_links[] = sprintf( 912 "<a class='first-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>", 913 esc_url( remove_query_arg( 'paged', $current_url ) ), 914 __( 'First page' ), 915 '«' 916 ); 917 } 918 919 if ( $disable_prev ) { 920 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">‹</span>'; 921 } else { 922 $page_links[] = sprintf( 923 "<a class='prev-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>", 924 esc_url( add_query_arg( 'paged', max( 1, $current - 1 ), $current_url ) ), 925 __( 'Previous page' ), 926 '‹' 927 ); 928 } 929 930 if ( 'bottom' === $which ) { 931 $html_current_page = $current; 932 $total_pages_before = '<span class="screen-reader-text">' . __( 'Current Page' ) . '</span><span id="table-paging" class="paging-input"><span class="tablenav-paging-text">'; 933 } else { 934 $html_current_page = sprintf( 935 "%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'>", 936 '<label for="current-page-selector" class="screen-reader-text">' . __( 'Current Page' ) . '</label>', 937 $current, 938 strlen( $total_pages ) 939 ); 940 } 941 $html_total_pages = sprintf( "<span class='total-pages'>%s</span>", number_format_i18n( $total_pages ) ); 942 $page_links[] = $total_pages_before . sprintf( 943 /* translators: 1: Current page, 2: Total pages. */ 944 _x( '%1$s of %2$s', 'paging' ), 945 $html_current_page, 946 $html_total_pages 947 ) . $total_pages_after; 948 949 if ( $disable_next ) { 950 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">›</span>'; 951 } else { 952 $page_links[] = sprintf( 953 "<a class='next-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>", 954 esc_url( add_query_arg( 'paged', min( $total_pages, $current + 1 ), $current_url ) ), 955 __( 'Next page' ), 956 '›' 957 ); 958 } 959 960 if ( $disable_last ) { 961 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">»</span>'; 962 } else { 963 $page_links[] = sprintf( 964 "<a class='last-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>", 965 esc_url( add_query_arg( 'paged', $total_pages, $current_url ) ), 966 __( 'Last page' ), 967 '»' 968 ); 969 } 970 971 $pagination_links_class = 'pagination-links'; 972 if ( ! empty( $infinite_scroll ) ) { 973 $pagination_links_class .= ' hide-if-js'; 974 } 975 $output .= "\n<span class='$pagination_links_class'>" . implode( "\n", $page_links ) . '</span>'; 976 977 if ( $total_pages ) { 978 $page_class = $total_pages < 2 ? ' one-page' : ''; 979 } else { 980 $page_class = ' no-pages'; 981 } 982 $this->_pagination = "<div class='tablenav-pages{$page_class}'>$output</div>"; 983 984 echo $this->_pagination; 985 } 986 987 /** 988 * Gets a list of columns. 989 * 990 * The format is: 991 * - `'internal-name' => 'Title'` 992 * 993 * @since 3.1.0 994 * @abstract 995 * 996 * @return array 997 */ 998 public function get_columns() { 999 die( 'function WP_List_Table::get_columns() must be overridden in a subclass.' ); 1000 } 1001 1002 /** 1003 * Gets a list of sortable columns. 1004 * 1005 * The format is: 1006 * - `'internal-name' => 'orderby'` 1007 * - `'internal-name' => array( 'orderby', 'asc' )` - The second element sets the initial sorting order. 1008 * - `'internal-name' => array( 'orderby', true )` - The second element makes the initial order descending. 1009 * 1010 * @since 3.1.0 1011 * 1012 * @return array 1013 */ 1014 protected function get_sortable_columns() { 1015 return array(); 1016 } 1017 1018 /** 1019 * Gets the name of the default primary column. 1020 * 1021 * @since 4.3.0 1022 * 1023 * @return string Name of the default primary column, in this case, an empty string. 1024 */ 1025 protected function get_default_primary_column_name() { 1026 $columns = $this->get_columns(); 1027 $column = ''; 1028 1029 if ( empty( $columns ) ) { 1030 return $column; 1031 } 1032 1033 // We need a primary defined so responsive views show something, 1034 // so let's fall back to the first non-checkbox column. 1035 foreach ( $columns as $col => $column_name ) { 1036 if ( 'cb' === $col ) { 1037 continue; 1038 } 1039 1040 $column = $col; 1041 break; 1042 } 1043 1044 return $column; 1045 } 1046 1047 /** 1048 * Public wrapper for WP_List_Table::get_default_primary_column_name(). 1049 * 1050 * @since 4.4.0 1051 * 1052 * @return string Name of the default primary column. 1053 */ 1054 public function get_primary_column() { 1055 return $this->get_primary_column_name(); 1056 } 1057 1058 /** 1059 * Gets the name of the primary column. 1060 * 1061 * @since 4.3.0 1062 * 1063 * @return string The name of the primary column. 1064 */ 1065 protected function get_primary_column_name() { 1066 $columns = get_column_headers( $this->screen ); 1067 $default = $this->get_default_primary_column_name(); 1068 1069 // If the primary column doesn't exist, 1070 // fall back to the first non-checkbox column. 1071 if ( ! isset( $columns[ $default ] ) ) { 1072 $default = self::get_default_primary_column_name(); 1073 } 1074 1075 /** 1076 * Filters the name of the primary column for the current list table. 1077 * 1078 * @since 4.3.0 1079 * 1080 * @param string $default Column name default for the specific list table, e.g. 'name'. 1081 * @param string $context Screen ID for specific list table, e.g. 'plugins'. 1082 */ 1083 $column = apply_filters( 'list_table_primary_column', $default, $this->screen->id ); 1084 1085 if ( empty( $column ) || ! isset( $columns[ $column ] ) ) { 1086 $column = $default; 1087 } 1088 1089 return $column; 1090 } 1091 1092 /** 1093 * Gets a list of all, hidden, and sortable columns, with filter applied. 1094 * 1095 * @since 3.1.0 1096 * 1097 * @return array 1098 */ 1099 protected function get_column_info() { 1100 // $_column_headers is already set / cached. 1101 if ( isset( $this->_column_headers ) && is_array( $this->_column_headers ) ) { 1102 // Back-compat for list tables that have been manually setting $_column_headers for horse reasons. 1103 // In 4.3, we added a fourth argument for primary column. 1104 $column_headers = array( array(), array(), array(), $this->get_primary_column_name() ); 1105 foreach ( $this->_column_headers as $key => $value ) { 1106 $column_headers[ $key ] = $value; 1107 } 1108 1109 return $column_headers; 1110 } 1111 1112 $columns = get_column_headers( $this->screen ); 1113 $hidden = get_hidden_columns( $this->screen ); 1114 1115 $sortable_columns = $this->get_sortable_columns(); 1116 /** 1117 * Filters the list table sortable columns for a specific screen. 1118 * 1119 * The dynamic portion of the hook name, `$this->screen->id`, refers 1120 * to the ID of the current screen. 1121 * 1122 * @since 3.1.0 1123 * 1124 * @param array $sortable_columns An array of sortable columns. 1125 */ 1126 $_sortable = apply_filters( "manage_{$this->screen->id}_sortable_columns", $sortable_columns ); 1127 1128 $sortable = array(); 1129 foreach ( $_sortable as $id => $data ) { 1130 if ( empty( $data ) ) { 1131 continue; 1132 } 1133 1134 $data = (array) $data; 1135 if ( ! isset( $data[1] ) ) { 1136 $data[1] = false; 1137 } 1138 1139 $sortable[ $id ] = $data; 1140 } 1141 1142 $primary = $this->get_primary_column_name(); 1143 $this->_column_headers = array( $columns, $hidden, $sortable, $primary ); 1144 1145 return $this->_column_headers; 1146 } 1147 1148 /** 1149 * Returns the number of visible columns. 1150 * 1151 * @since 3.1.0 1152 * 1153 * @return int 1154 */ 1155 public function get_column_count() { 1156 list ( $columns, $hidden ) = $this->get_column_info(); 1157 $hidden = array_intersect( array_keys( $columns ), array_filter( $hidden ) ); 1158 return count( $columns ) - count( $hidden ); 1159 } 1160 1161 /** 1162 * Prints column headers, accounting for hidden and sortable columns. 1163 * 1164 * @since 3.1.0 1165 * 1166 * @param bool $with_id Whether to set the ID attribute or not 1167 */ 1168 public function print_column_headers( $with_id = true ) { 1169 list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info(); 1170 1171 $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); 1172 $current_url = remove_query_arg( 'paged', $current_url ); 1173 1174 if ( isset( $_GET['orderby'] ) ) { 1175 $current_orderby = $_GET['orderby']; 1176 } else { 1177 $current_orderby = ''; 1178 } 1179 1180 if ( isset( $_GET['order'] ) && 'desc' === $_GET['order'] ) { 1181 $current_order = 'desc'; 1182 } else { 1183 $current_order = 'asc'; 1184 } 1185 1186 if ( ! empty( $columns['cb'] ) ) { 1187 static $cb_counter = 1; 1188 $columns['cb'] = '<label class="screen-reader-text" for="cb-select-all-' . $cb_counter . '">' . __( 'Select All' ) . '</label>' 1189 . '<input id="cb-select-all-' . $cb_counter . '" type="checkbox" />'; 1190 $cb_counter++; 1191 } 1192 1193 foreach ( $columns as $column_key => $column_display_name ) { 1194 $class = array( 'manage-column', "column-$column_key" ); 1195 1196 if ( in_array( $column_key, $hidden, true ) ) { 1197 $class[] = 'hidden'; 1198 } 1199 1200 if ( 'cb' === $column_key ) { 1201 $class[] = 'check-column'; 1202 } elseif ( in_array( $column_key, array( 'posts', 'comments', 'links' ), true ) ) { 1203 $class[] = 'num'; 1204 } 1205 1206 if ( $column_key === $primary ) { 1207 $class[] = 'column-primary'; 1208 } 1209 1210 if ( isset( $sortable[ $column_key ] ) ) { 1211 list( $orderby, $desc_first ) = $sortable[ $column_key ]; 1212 1213 if ( $current_orderby === $orderby ) { 1214 $order = 'asc' === $current_order ? 'desc' : 'asc'; 1215 1216 $class[] = 'sorted'; 1217 $class[] = $current_order; 1218 } else { 1219 $order = strtolower( $desc_first ); 1220 1221 if ( ! in_array( $order, array( 'desc', 'asc' ), true ) ) { 1222 $order = $desc_first ? 'desc' : 'asc'; 1223 } 1224 1225 $class[] = 'sortable'; 1226 $class[] = 'desc' === $order ? 'asc' : 'desc'; 1227 } 1228 1229 $column_display_name = sprintf( 1230 '<a href="%s"><span>%s</span><span class="sorting-indicator"></span></a>', 1231 esc_url( add_query_arg( compact( 'orderby', 'order' ), $current_url ) ), 1232 $column_display_name 1233 ); 1234 } 1235 1236 $tag = ( 'cb' === $column_key ) ? 'td' : 'th'; 1237 $scope = ( 'th' === $tag ) ? 'scope="col"' : ''; 1238 $id = $with_id ? "id='$column_key'" : ''; 1239 1240 if ( ! empty( $class ) ) { 1241 $class = "class='" . implode( ' ', $class ) . "'"; 1242 } 1243 1244 echo "<$tag $scope $id $class>$column_display_name</$tag>"; 1245 } 1246 } 1247 1248 /** 1249 * Displays the table. 1250 * 1251 * @since 3.1.0 1252 */ 1253 public function display() { 1254 $singular = $this->_args['singular']; 1255 1256 $this->display_tablenav( 'top' ); 1257 1258 $this->screen->render_screen_reader_content( 'heading_list' ); 1259 ?> 1260 <table class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>"> 1261 <thead> 1262 <tr> 1263 <?php $this->print_column_headers(); ?> 1264 </tr> 1265 </thead> 1266 1267 <tbody id="the-list" 1268 <?php 1269 if ( $singular ) { 1270 echo " data-wp-lists='list:$singular'"; 1271 } 1272 ?> 1273 > 1274 <?php $this->display_rows_or_placeholder(); ?> 1275 </tbody> 1276 1277 <tfoot> 1278 <tr> 1279 <?php $this->print_column_headers( false ); ?> 1280 </tr> 1281 </tfoot> 1282 1283 </table> 1284 <?php 1285 $this->display_tablenav( 'bottom' ); 1286 } 1287 1288 /** 1289 * Gets a list of CSS classes for the WP_List_Table table tag. 1290 * 1291 * @since 3.1.0 1292 * 1293 * @return string[] Array of CSS classes for the table tag. 1294 */ 1295 protected function get_table_classes() { 1296 $mode = get_user_setting( 'posts_list_mode', 'list' ); 1297 1298 $mode_class = esc_attr( 'table-view-' . $mode ); 1299 1300 return array( 'widefat', 'fixed', 'striped', $mode_class, $this->_args['plural'] ); 1301 } 1302 1303 /** 1304 * Generates the table navigation above or below the table 1305 * 1306 * @since 3.1.0 1307 * @param string $which 1308 */ 1309 protected function display_tablenav( $which ) { 1310 if ( 'top' === $which ) { 1311 wp_nonce_field( 'bulk-' . $this->_args['plural'] ); 1312 } 1313 ?> 1314 <div class="tablenav <?php echo esc_attr( $which ); ?>"> 1315 1316 <?php if ( $this->has_items() ) : ?> 1317 <div class="alignleft actions bulkactions"> 1318 <?php $this->bulk_actions( $which ); ?> 1319 </div> 1320 <?php 1321 endif; 1322 $this->extra_tablenav( $which ); 1323 $this->pagination( $which ); 1324 ?> 1325 1326 <br class="clear" /> 1327 </div> 1328 <?php 1329 } 1330 1331 /** 1332 * Extra controls to be displayed between bulk actions and pagination. 1333 * 1334 * @since 3.1.0 1335 * 1336 * @param string $which 1337 */ 1338 protected function extra_tablenav( $which ) {} 1339 1340 /** 1341 * Generates the tbody element for the list table. 1342 * 1343 * @since 3.1.0 1344 */ 1345 public function display_rows_or_placeholder() { 1346 if ( $this->has_items() ) { 1347 $this->display_rows(); 1348 } else { 1349 echo '<tr class="no-items"><td class="colspanchange" colspan="' . $this->get_column_count() . '">'; 1350 $this->no_items(); 1351 echo '</td></tr>'; 1352 } 1353 } 1354 1355 /** 1356 * Generates the table rows. 1357 * 1358 * @since 3.1.0 1359 */ 1360 public function display_rows() { 1361 foreach ( $this->items as $item ) { 1362 $this->single_row( $item ); 1363 } 1364 } 1365 1366 /** 1367 * Generates content for a single row of the table. 1368 * 1369 * @since 3.1.0 1370 * 1371 * @param object|array $item The current item 1372 */ 1373 public function single_row( $item ) { 1374 echo '<tr>'; 1375 $this->single_row_columns( $item ); 1376 echo '</tr>'; 1377 } 1378 1379 /** 1380 * @param object|array $item 1381 * @param string $column_name 1382 */ 1383 protected function column_default( $item, $column_name ) {} 1384 1385 /** 1386 * @param object|array $item 1387 */ 1388 protected function column_cb( $item ) {} 1389 1390 /** 1391 * Generates the columns for a single row of the table. 1392 * 1393 * @since 3.1.0 1394 * 1395 * @param object|array $item The current item. 1396 */ 1397 protected function single_row_columns( $item ) { 1398 list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info(); 1399 1400 foreach ( $columns as $column_name => $column_display_name ) { 1401 $classes = "$column_name column-$column_name"; 1402 if ( $primary === $column_name ) { 1403 $classes .= ' has-row-actions column-primary'; 1404 } 1405 1406 if ( in_array( $column_name, $hidden, true ) ) { 1407 $classes .= ' hidden'; 1408 } 1409 1410 // Comments column uses HTML in the display name with screen reader text. 1411 // Instead of using esc_attr(), we strip tags to get closer to a user-friendly string. 1412 $data = 'data-colname="' . wp_strip_all_tags( $column_display_name ) . '"'; 1413 1414 $attributes = "class='$classes' $data"; 1415 1416 if ( 'cb' === $column_name ) { 1417 echo '<th scope="row" class="check-column">'; 1418 echo $this->column_cb( $item ); 1419 echo '</th>'; 1420 } elseif ( method_exists( $this, '_column_' . $column_name ) ) { 1421 echo call_user_func( 1422 array( $this, '_column_' . $column_name ), 1423 $item, 1424 $classes, 1425 $data, 1426 $primary 1427 ); 1428 } elseif ( method_exists( $this, 'column_' . $column_name ) ) { 1429 echo "<td $attributes>"; 1430 echo call_user_func( array( $this, 'column_' . $column_name ), $item ); 1431 echo $this->handle_row_actions( $item, $column_name, $primary ); 1432 echo '</td>'; 1433 } else { 1434 echo "<td $attributes>"; 1435 echo $this->column_default( $item, $column_name ); 1436 echo $this->handle_row_actions( $item, $column_name, $primary ); 1437 echo '</td>'; 1438 } 1439 } 1440 } 1441 1442 /** 1443 * Generates and display row actions links for the list table. 1444 * 1445 * @since 4.3.0 1446 * 1447 * @param object|array $item The item being acted upon. 1448 * @param string $column_name Current column name. 1449 * @param string $primary Primary column name. 1450 * @return string The row actions HTML, or an empty string 1451 * if the current column is not the primary column. 1452 */ 1453 protected function handle_row_actions( $item, $column_name, $primary ) { 1454 return $column_name === $primary ? '<button type="button" class="toggle-row"><span class="screen-reader-text">' . __( 'Show more details' ) . '</span></button>' : ''; 1455 } 1456 1457 /** 1458 * Handles an incoming ajax request (called from admin-ajax.php) 1459 * 1460 * @since 3.1.0 1461 */ 1462 public function ajax_response() { 1463 $this->prepare_items(); 1464 1465 ob_start(); 1466 if ( ! empty( $_REQUEST['no_placeholder'] ) ) { 1467 $this->display_rows(); 1468 } else { 1469 $this->display_rows_or_placeholder(); 1470 } 1471 1472 $rows = ob_get_clean(); 1473 1474 $response = array( 'rows' => $rows ); 1475 1476 if ( isset( $this->_pagination_args['total_items'] ) ) { 1477 $response['total_items_i18n'] = sprintf( 1478 /* translators: Number of items. */ 1479 _n( '%s item', '%s items', $this->_pagination_args['total_items'] ), 1480 number_format_i18n( $this->_pagination_args['total_items'] ) 1481 ); 1482 } 1483 if ( isset( $this->_pagination_args['total_pages'] ) ) { 1484 $response['total_pages'] = $this->_pagination_args['total_pages']; 1485 $response['total_pages_i18n'] = number_format_i18n( $this->_pagination_args['total_pages'] ); 1486 } 1487 1488 die( wp_json_encode( $response ) ); 1489 } 1490 1491 /** 1492 * Sends required variables to JavaScript land. 1493 * 1494 * @since 3.1.0 1495 */ 1496 public function _js_vars() { 1497 $args = array( 1498 'class' => get_class( $this ), 1499 'screen' => array( 1500 'id' => $this->screen->id, 1501 'base' => $this->screen->base, 1502 ), 1503 ); 1504 1505 printf( "<script type='text/javascript'>list_args = %s;</script>\n", wp_json_encode( $args ) ); 1506 } 1507 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Jan 28 01:00:03 2021 | Cross-referenced by PHPXref 0.7.1 |