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