| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[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 */ 15 #[AllowDynamicProperties] 16 class WP_List_Table { 17 18 /** 19 * The current list of items. 20 * 21 * @since 3.1.0 22 * 23 * @var array<int|string, mixed> 24 */ 25 public $items; 26 27 /** 28 * Various information about the current table. 29 * 30 * @since 3.1.0 31 * 32 * @var array<string, mixed> 33 */ 34 protected $_args; 35 36 /** 37 * Various information needed for displaying the pagination. 38 * 39 * @since 3.1.0 40 * 41 * @var array<string, mixed> 42 */ 43 protected $_pagination_args = array(); 44 45 /** 46 * The current screen. 47 * 48 * @since 3.1.0 49 * 50 * @var WP_Screen 51 */ 52 protected $screen; 53 54 /** 55 * Cached bulk actions. 56 * 57 * @since 3.1.0 58 * 59 * @var array<string, string|array<string, string>>|null 60 */ 61 private $_actions; 62 63 /** 64 * Cached pagination output. 65 * 66 * @since 3.1.0 67 * 68 * @var string 69 */ 70 private $_pagination; 71 72 /** 73 * The view switcher modes. 74 * 75 * @since 4.1.0 76 * 77 * @var array<string, string> 78 */ 79 protected $modes = array(); 80 81 /** 82 * Stores the value returned by {@see self::get_column_info()}. 83 * 84 * @since 4.2.0 85 * 86 * @var array<int, array|string>|null 87 */ 88 protected $_column_headers; 89 90 /** 91 * List of private properties made readable for backward compatibility. 92 * 93 * @since 4.2.0 94 * 95 * @var string[] 96 */ 97 protected $compat_fields = array( '_args', '_pagination_args', 'screen', '_actions', '_pagination' ); 98 99 /** 100 * List of private/protected methods made readable for backward compatibility. 101 * 102 * @since 4.2.0 103 * 104 * @var string[] 105 */ 106 protected $compat_methods = array( 107 'set_pagination_args', 108 'get_views', 109 'get_bulk_actions', 110 'bulk_actions', 111 'row_actions', 112 'months_dropdown', 113 'view_switcher', 114 'comments_bubble', 115 'get_items_per_page', 116 'pagination', 117 'get_sortable_columns', 118 'get_column_info', 119 'get_table_classes', 120 'display_tablenav', 121 'extra_tablenav', 122 'single_row_columns', 123 ); 124 125 /** 126 * Constructor. 127 * 128 * The child class should call this constructor from its own constructor to override 129 * the default $args. 130 * 131 * @since 3.2.0 132 * 133 * @param array|string $args { 134 * Array or string of arguments. 135 * 136 * @type string $plural Plural value used for labels and the objects being listed. 137 * This affects things such as CSS class-names and nonces used 138 * in the list table, e.g. 'posts'. Default empty. 139 * @type string $singular Singular label for an object being listed, e.g. 'post'. 140 * Default empty 141 * @type bool $ajax Whether the list table supports Ajax. This includes loading 142 * and sorting data, for example. If true, the class will call 143 * the _js_vars() method in the footer to provide variables 144 * to any scripts handling Ajax events. Default false. 145 * @type string $screen String containing the hook name used to determine the current 146 * screen. If left null, the current screen will be automatically set. 147 * Default null. 148 * } 149 */ 150 public function __construct( $args = array() ) { 151 $args = wp_parse_args( 152 $args, 153 array( 154 'plural' => '', 155 'singular' => '', 156 'ajax' => false, 157 'screen' => null, 158 ) 159 ); 160 161 $this->screen = convert_to_screen( $args['screen'] ); 162 163 add_filter( "manage_{$this->screen->id}_columns", array( $this, 'get_columns' ), 0 ); 164 165 if ( ! $args['plural'] ) { 166 $args['plural'] = $this->screen->base; 167 } 168 169 $args['plural'] = sanitize_key( $args['plural'] ); 170 $args['singular'] = sanitize_key( $args['singular'] ); 171 172 $this->_args = $args; 173 174 if ( $args['ajax'] ) { 175 // wp_enqueue_script( 'list-table' ); 176 add_action( 'admin_footer', array( $this, '_js_vars' ) ); 177 } 178 179 if ( empty( $this->modes ) ) { 180 $this->modes = array( 181 'list' => __( 'Compact view' ), 182 'excerpt' => __( 'Extended view' ), 183 ); 184 } 185 } 186 187 /** 188 * Makes private properties readable for backward compatibility. 189 * 190 * @since 4.0.0 191 * @since 6.4.0 Getting a dynamic property is deprecated. 192 * 193 * @param string $name Property to get. 194 * @return mixed Property. 195 */ 196 public function __get( $name ) { 197 if ( in_array( $name, $this->compat_fields, true ) ) { 198 return $this->$name; 199 } 200 201 wp_trigger_error( 202 __METHOD__, 203 "The property `{$name}` is not declared. Getting a dynamic property is " . 204 'deprecated since version 6.4.0! Instead, declare the property on the class.', 205 E_USER_DEPRECATED 206 ); 207 return null; 208 } 209 210 /** 211 * Makes private properties settable for backward compatibility. 212 * 213 * @since 4.0.0 214 * @since 6.4.0 Setting a dynamic property is deprecated. 215 * 216 * @param string $name Property to check if set. 217 * @param mixed $value Property value. 218 */ 219 public function __set( $name, $value ) { 220 if ( in_array( $name, $this->compat_fields, true ) ) { 221 $this->$name = $value; 222 return; 223 } 224 225 wp_trigger_error( 226 __METHOD__, 227 "The property `{$name}` is not declared. Setting a dynamic property is " . 228 'deprecated since version 6.4.0! Instead, declare the property on the class.', 229 E_USER_DEPRECATED 230 ); 231 } 232 233 /** 234 * Makes private properties checkable for backward compatibility. 235 * 236 * @since 4.0.0 237 * @since 6.4.0 Checking a dynamic property is deprecated. 238 * 239 * @param string $name Property to check if set. 240 * @return bool Whether the property is a back-compat property and it is set. 241 */ 242 public function __isset( $name ) { 243 if ( in_array( $name, $this->compat_fields, true ) ) { 244 return isset( $this->$name ); 245 } 246 247 wp_trigger_error( 248 __METHOD__, 249 "The property `{$name}` is not declared. Checking `isset()` on a dynamic property " . 250 'is deprecated since version 6.4.0! Instead, declare the property on the class.', 251 E_USER_DEPRECATED 252 ); 253 return false; 254 } 255 256 /** 257 * Makes private properties un-settable for backward compatibility. 258 * 259 * @since 4.0.0 260 * @since 6.4.0 Unsetting a dynamic property is deprecated. 261 * 262 * @param string $name Property to unset. 263 */ 264 public function __unset( $name ) { 265 if ( in_array( $name, $this->compat_fields, true ) ) { 266 unset( $this->$name ); 267 return; 268 } 269 270 wp_trigger_error( 271 __METHOD__, 272 "A property `{$name}` is not declared. Unsetting a dynamic property is " . 273 'deprecated since version 6.4.0! Instead, declare the property on the class.', 274 E_USER_DEPRECATED 275 ); 276 } 277 278 /** 279 * Makes private/protected methods readable for backward compatibility. 280 * 281 * @since 4.0.0 282 * 283 * @param string $name Method to call. 284 * @param array $arguments Arguments to pass when calling. 285 * @return mixed|bool Return value of the callback, false otherwise. 286 */ 287 public function __call( $name, $arguments ) { 288 if ( in_array( $name, $this->compat_methods, true ) ) { 289 return $this->$name( ...$arguments ); 290 } 291 return false; 292 } 293 294 /** 295 * Checks the current user's permissions. 296 * 297 * @since 3.1.0 298 * @abstract 299 */ 300 public function ajax_user_can() { 301 die( 'function WP_List_Table::ajax_user_can() must be overridden in a subclass.' ); 302 } 303 304 /** 305 * Prepares the list of items for displaying. 306 * 307 * @uses WP_List_Table::set_pagination_args() 308 * 309 * @since 3.1.0 310 * @abstract 311 */ 312 public function prepare_items() { 313 die( 'function WP_List_Table::prepare_items() must be overridden in a subclass.' ); 314 } 315 316 /** 317 * Sets all the necessary pagination arguments. 318 * 319 * @since 3.1.0 320 * 321 * @param array|string $args Array or string of arguments with information about the pagination. 322 */ 323 protected function set_pagination_args( $args ) { 324 $args = wp_parse_args( 325 $args, 326 array( 327 'total_items' => 0, 328 'total_pages' => 0, 329 'per_page' => 0, 330 ) 331 ); 332 333 if ( ! $args['total_pages'] && $args['per_page'] > 0 ) { 334 $args['total_pages'] = (int) ceil( $args['total_items'] / $args['per_page'] ); 335 } 336 337 // Redirect if page number is invalid and headers are not already sent. 338 if ( ! headers_sent() && ! wp_doing_ajax() && $args['total_pages'] > 0 && $this->get_pagenum() > $args['total_pages'] ) { 339 wp_redirect( add_query_arg( 'paged', $args['total_pages'] ) ); 340 exit; 341 } 342 343 $this->_pagination_args = $args; 344 } 345 346 /** 347 * Access the pagination args. 348 * 349 * @since 3.1.0 350 * 351 * @param string $key Pagination argument to retrieve. Common values include 'total_items', 352 * 'total_pages', 'per_page', or 'infinite_scroll'. 353 * @return int Number of items that correspond to the given pagination argument. 354 */ 355 public function get_pagination_arg( $key ) { 356 if ( 'page' === $key ) { 357 return $this->get_pagenum(); 358 } 359 return $this->_pagination_args[ $key ] ?? 0; 360 } 361 362 /** 363 * Determines whether the table has items to display or not. 364 * 365 * @since 3.1.0 366 * 367 * @return bool Whether the table has items to display. 368 */ 369 public function has_items() { 370 return ! empty( $this->items ); 371 } 372 373 /** 374 * Message to be displayed when there are no items. 375 * 376 * @since 3.1.0 377 */ 378 public function no_items() { 379 _e( 'No items found.' ); 380 } 381 382 /** 383 * Displays the search box. 384 * 385 * @since 3.1.0 386 * 387 * @param string $text The 'submit' button label. 388 * @param string $input_id ID attribute value for the search input field. 389 */ 390 public function search_box( $text, $input_id ) { 391 if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) { 392 return; 393 } 394 395 $input_id = $input_id . '-search-input'; 396 397 if ( ! empty( $_REQUEST['orderby'] ) ) { 398 if ( is_array( $_REQUEST['orderby'] ) ) { 399 foreach ( $_REQUEST['orderby'] as $key => $value ) { 400 echo '<input type="hidden" name="orderby[' . esc_attr( $key ) . ']" value="' . esc_attr( $value ) . '" />'; 401 } 402 } else { 403 echo '<input type="hidden" name="orderby" value="' . esc_attr( $_REQUEST['orderby'] ) . '" />'; 404 } 405 } 406 if ( ! empty( $_REQUEST['order'] ) ) { 407 echo '<input type="hidden" name="order" value="' . esc_attr( $_REQUEST['order'] ) . '" />'; 408 } 409 if ( ! empty( $_REQUEST['post_mime_type'] ) ) { 410 echo '<input type="hidden" name="post_mime_type" value="' . esc_attr( $_REQUEST['post_mime_type'] ) . '" />'; 411 } 412 if ( ! empty( $_REQUEST['detached'] ) ) { 413 echo '<input type="hidden" name="detached" value="' . esc_attr( $_REQUEST['detached'] ) . '" />'; 414 } 415 ?> 416 <p class="search-box"> 417 <label class="screen-reader-text" for="<?php echo esc_attr( $input_id ); ?>"><?php echo $text; ?>:</label> 418 <input type="search" id="<?php echo esc_attr( $input_id ); ?>" name="s" value="<?php _admin_search_query(); ?>" /> 419 <?php submit_button( $text, 'compact', '', false, array( 'id' => 'search-submit' ) ); ?> 420 </p> 421 <?php 422 } 423 424 /** 425 * Generates views links. 426 * 427 * @since 6.1.0 428 * 429 * @param array $link_data { 430 * An array of link data. 431 * 432 * @type string $url The link URL. 433 * @type string $label The link label. 434 * @type bool $current Optional. Whether this is the currently selected view. 435 * } 436 * @return string[] An array of link markup. Keys match the `$link_data` input array. 437 */ 438 protected function get_views_links( $link_data = array() ) { 439 if ( ! is_array( $link_data ) ) { 440 _doing_it_wrong( 441 __METHOD__, 442 sprintf( 443 /* translators: %s: The $link_data argument. */ 444 __( 'The %s argument must be an array.' ), 445 '<code>$link_data</code>' 446 ), 447 '6.1.0' 448 ); 449 450 return array( '' ); 451 } 452 453 $views_links = array(); 454 455 foreach ( $link_data as $view => $link ) { 456 if ( empty( $link['url'] ) || ! is_string( $link['url'] ) || '' === trim( $link['url'] ) ) { 457 _doing_it_wrong( 458 __METHOD__, 459 sprintf( 460 /* translators: %1$s: The argument name. %2$s: The view name. */ 461 __( 'The %1$s argument must be a non-empty string for %2$s.' ), 462 '<code>url</code>', 463 '<code>' . esc_html( $view ) . '</code>' 464 ), 465 '6.1.0' 466 ); 467 468 continue; 469 } 470 471 if ( empty( $link['label'] ) || ! is_string( $link['label'] ) || '' === trim( $link['label'] ) ) { 472 _doing_it_wrong( 473 __METHOD__, 474 sprintf( 475 /* translators: %1$s: The argument name. %2$s: The view name. */ 476 __( 'The %1$s argument must be a non-empty string for %2$s.' ), 477 '<code>label</code>', 478 '<code>' . esc_html( $view ) . '</code>' 479 ), 480 '6.1.0' 481 ); 482 483 continue; 484 } 485 486 $views_links[ $view ] = sprintf( 487 '<a href="%s"%s>%s</a>', 488 esc_url( $link['url'] ), 489 isset( $link['current'] ) && true === $link['current'] ? ' class="current" aria-current="page"' : '', 490 $link['label'] 491 ); 492 } 493 494 return $views_links; 495 } 496 497 /** 498 * Gets the list of views available on this table. 499 * 500 * The format is an associative array: 501 * - `'id' => 'link'` 502 * 503 * @since 3.1.0 504 * 505 * @return array<string, string> An associative array of views. 506 */ 507 protected function get_views() { 508 return array(); 509 } 510 511 /** 512 * Displays the list of views available on this table. 513 * 514 * @since 3.1.0 515 */ 516 public function views() { 517 $views = $this->get_views(); 518 /** 519 * Filters the list of available list table views. 520 * 521 * The dynamic portion of the hook name, `$this->screen->id`, refers 522 * to the ID of the current screen. 523 * 524 * @since 3.1.0 525 * 526 * @param string[] $views An array of available list table views. 527 */ 528 $views = apply_filters( "views_{$this->screen->id}", $views ); 529 530 if ( empty( $views ) ) { 531 return; 532 } 533 534 $this->screen->render_screen_reader_content( 'heading_views' ); 535 536 echo "<ul class='subsubsub'>\n"; 537 foreach ( $views as $class => $view ) { 538 $views[ $class ] = "\t<li class='$class'>$view"; 539 } 540 echo implode( " |</li>\n", $views ) . "</li>\n"; 541 echo '</ul>'; 542 } 543 544 /** 545 * Retrieves the list of bulk actions available for this table. 546 * 547 * The format is an associative array where each element represents either a top level option value and label, or 548 * an array representing an optgroup and its options. 549 * 550 * For a standard option, the array element key is the field value and the array element value is the field label. 551 * 552 * For an optgroup, the array element key is the label and the array element value is an associative array of 553 * options as above. 554 * 555 * Example: 556 * 557 * [ 558 * 'edit' => 'Edit', 559 * 'delete' => 'Delete', 560 * 'Change State' => [ 561 * 'feature' => 'Featured', 562 * 'sale' => 'On Sale', 563 * ] 564 * ] 565 * 566 * @since 3.1.0 567 * @since 5.6.0 A bulk action can now contain an array of options in order to create an optgroup. 568 * 569 * @return array<string, string|array<string, string>> An associative array of bulk actions. 570 */ 571 protected function get_bulk_actions() { 572 return array(); 573 } 574 575 /** 576 * Displays the bulk actions dropdown. 577 * 578 * @since 3.1.0 579 * 580 * @param string $which The location of the bulk actions: Either 'top' or 'bottom'. 581 * This is designated as optional for backward compatibility. 582 */ 583 protected function bulk_actions( $which = '' ) { 584 if ( is_null( $this->_actions ) ) { 585 $this->_actions = $this->get_bulk_actions(); 586 587 /** 588 * Filters the items in the bulk actions menu of the list table. 589 * 590 * The dynamic portion of the hook name, `$this->screen->id`, refers 591 * to the ID of the current screen. 592 * 593 * @since 3.1.0 594 * @since 5.6.0 A bulk action can now contain an array of options in order to create an optgroup. 595 * 596 * @param array $actions An array of the available bulk actions. 597 */ 598 $this->_actions = apply_filters( "bulk_actions-{$this->screen->id}", $this->_actions ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores 599 600 $two = ''; 601 } else { 602 $two = '2'; 603 } 604 605 if ( empty( $this->_actions ) ) { 606 return; 607 } 608 609 echo '<label for="bulk-action-selector-' . esc_attr( $which ) . '" class="screen-reader-text">' . 610 /* translators: Hidden accessibility text. */ 611 __( 'Select bulk action' ) . 612 '</label>'; 613 echo '<select name="action' . $two . '" id="bulk-action-selector-' . esc_attr( $which ) . "\">\n"; 614 echo '<option value="-1">' . __( 'Bulk actions' ) . "</option>\n"; 615 616 foreach ( $this->_actions as $key => $value ) { 617 if ( is_array( $value ) ) { 618 echo "\t" . '<optgroup label="' . esc_attr( $key ) . '">' . "\n"; 619 620 foreach ( $value as $name => $title ) { 621 $class = ( 'edit' === $name ) ? ' class="hide-if-no-js"' : ''; 622 623 echo "\t\t" . '<option value="' . esc_attr( $name ) . '"' . $class . '>' . $title . "</option>\n"; 624 } 625 echo "\t" . "</optgroup>\n"; 626 } else { 627 $class = ( 'edit' === $key ) ? ' class="hide-if-no-js"' : ''; 628 629 echo "\t" . '<option value="' . esc_attr( $key ) . '"' . $class . '>' . $value . "</option>\n"; 630 } 631 } 632 633 echo "</select>\n"; 634 635 submit_button( __( 'Apply' ), 'action compact', 'bulk_action', false, array( 'id' => "doaction$two" ) ); 636 echo "\n"; 637 } 638 639 /** 640 * Gets the current action selected from the bulk actions dropdown. 641 * 642 * @since 3.1.0 643 * 644 * @return string|false The action name. False if no action was selected. 645 */ 646 public function current_action() { 647 if ( isset( $_REQUEST['filter_action'] ) && ! empty( $_REQUEST['filter_action'] ) ) { 648 return false; 649 } 650 651 if ( isset( $_REQUEST['action'] ) && '-1' !== $_REQUEST['action'] ) { 652 return $_REQUEST['action']; 653 } 654 655 return false; 656 } 657 658 /** 659 * Generates the required HTML for a list of row action links. 660 * 661 * @since 3.1.0 662 * 663 * @param string[] $actions An array of action links. 664 * @param bool $always_visible Whether the actions should be always visible. 665 * @return string The HTML for the row actions. 666 */ 667 protected function row_actions( $actions, $always_visible = false ) { 668 $action_count = count( $actions ); 669 670 if ( ! $action_count ) { 671 return ''; 672 } 673 674 $mode = get_user_setting( 'posts_list_mode', 'list' ); 675 676 if ( 'excerpt' === $mode ) { 677 $always_visible = true; 678 } 679 680 $output = '<div class="' . ( $always_visible ? 'row-actions visible' : 'row-actions' ) . '">'; 681 682 $i = 0; 683 684 foreach ( $actions as $action => $link ) { 685 ++$i; 686 687 $separator = ( $i < $action_count ) ? ' | ' : ''; 688 689 $output .= "<span class='$action'>{$link}{$separator}</span>"; 690 } 691 692 $output .= '</div>'; 693 694 $output .= '<button type="button" class="toggle-row"><span class="screen-reader-text">' . 695 /* translators: Hidden accessibility text. */ 696 __( 'Show more details' ) . 697 '</span></button>'; 698 699 return $output; 700 } 701 702 /** 703 * Displays a dropdown for filtering items in the list table by month. 704 * 705 * @since 3.1.0 706 * 707 * @global wpdb $wpdb WordPress database abstraction object. 708 * @global WP_Locale $wp_locale WordPress date and time locale object. 709 * 710 * @param string $post_type The post type. 711 */ 712 protected function months_dropdown( $post_type ) { 713 global $wpdb, $wp_locale; 714 715 /** 716 * Filters whether to remove the 'Months' drop-down from the post list table. 717 * 718 * @since 4.2.0 719 * 720 * @param bool $disable Whether to disable the drop-down. Default false. 721 * @param string $post_type The post type. 722 */ 723 if ( apply_filters( 'disable_months_dropdown', false, $post_type ) ) { 724 return; 725 } 726 727 /** 728 * Filters whether to short-circuit performing the months dropdown query. 729 * 730 * @since 5.7.0 731 * 732 * @param object[]|false $months 'Months' drop-down results. Default false. 733 * @param string $post_type The post type. 734 */ 735 $months = apply_filters( 'pre_months_dropdown_query', false, $post_type ); 736 737 if ( ! is_array( $months ) ) { 738 $extra_checks = "AND post_status != 'auto-draft'"; 739 if ( ! isset( $_GET['post_status'] ) || 'trash' !== $_GET['post_status'] ) { 740 $extra_checks .= " AND post_status != 'trash'"; 741 } elseif ( isset( $_GET['post_status'] ) ) { 742 $extra_checks = $wpdb->prepare( ' AND post_status = %s', $_GET['post_status'] ); 743 } 744 745 $months = $wpdb->get_results( 746 $wpdb->prepare( 747 "SELECT DISTINCT YEAR( post_date ) AS year, MONTH( post_date ) AS month 748 FROM $wpdb->posts 749 WHERE post_type = %s 750 $extra_checks 751 ORDER BY post_date DESC", 752 $post_type 753 ) 754 ); 755 } 756 757 /** 758 * Filters the 'Months' drop-down results. 759 * 760 * @since 3.7.0 761 * 762 * @param object[] $months Array of the months drop-down query results. 763 * @param string $post_type The post type. 764 */ 765 $months = apply_filters( 'months_dropdown_results', $months, $post_type ); 766 767 $month_count = count( $months ); 768 769 if ( ! $month_count || ( 1 === $month_count && 0 === (int) $months[0]->month ) ) { 770 return; 771 } 772 773 $selected_month = isset( $_GET['m'] ) ? (int) $_GET['m'] : 0; 774 ?> 775 <label for="filter-by-date" class="screen-reader-text"><?php echo get_post_type_object( $post_type )->labels->filter_by_date; ?></label> 776 <select name="m" id="filter-by-date"> 777 <option<?php selected( $selected_month, 0 ); ?> value="0"><?php _e( 'All dates' ); ?></option> 778 <?php 779 foreach ( $months as $arc_row ) { 780 if ( 0 === (int) $arc_row->year ) { 781 continue; 782 } 783 784 $month = zeroise( $arc_row->month, 2 ); 785 $year = $arc_row->year; 786 787 printf( 788 "<option %s value='%s'>%s</option>\n", 789 selected( $selected_month, $year . $month, false ), 790 esc_attr( $year . $month ), 791 /* translators: 1: Month name, 2: 4-digit year. */ 792 esc_html( sprintf( __( '%1$s %2$d' ), $wp_locale->get_month( $month ), $year ) ) 793 ); 794 } 795 ?> 796 </select> 797 <?php 798 } 799 800 /** 801 * Displays a view switcher. 802 * 803 * @since 3.1.0 804 * 805 * @param string $current_mode The current view mode slug, e.g. 'list' or 'excerpt'. 806 */ 807 protected function view_switcher( $current_mode ) { 808 ?> 809 <input type="hidden" name="mode" value="<?php echo esc_attr( $current_mode ); ?>" /> 810 <div class="view-switch"> 811 <?php 812 foreach ( $this->modes as $mode => $title ) { 813 $classes = array( 'view-' . $mode ); 814 $aria_current = ''; 815 816 if ( $current_mode === $mode ) { 817 $classes[] = 'current'; 818 $aria_current = ' aria-current="page"'; 819 } 820 821 printf( 822 "<a href='%s' class='%s' id='view-switch-$mode'$aria_current>" . 823 "<span class='screen-reader-text'>%s</span>" . 824 "</a>\n", 825 esc_url( remove_query_arg( 'attachment-filter', add_query_arg( 'mode', $mode ) ) ), 826 implode( ' ', $classes ), 827 $title 828 ); 829 } 830 ?> 831 </div> 832 <?php 833 } 834 835 /** 836 * Displays a comment count bubble. 837 * 838 * @since 3.1.0 839 * 840 * @param int $post_id The post ID. 841 * @param int $pending_comments Number of pending comments. 842 */ 843 protected function comments_bubble( $post_id, $pending_comments ) { 844 $post_object = get_post( $post_id ); 845 $edit_post_cap = $post_object ? 'edit_post' : 'edit_posts'; 846 847 if ( ! current_user_can( $edit_post_cap, $post_id ) 848 && ( post_password_required( $post_id ) 849 || ! current_user_can( 'read_post', $post_id ) ) 850 ) { 851 // The user has no access to the post and thus cannot see the comments. 852 return false; 853 } 854 855 $approved_comments = get_comments_number(); 856 857 $approved_comments_number = number_format_i18n( $approved_comments ); 858 $pending_comments_number = number_format_i18n( $pending_comments ); 859 860 $approved_only_phrase = sprintf( 861 /* translators: %s: Number of comments. */ 862 _n( '%s comment', '%s comments', $approved_comments ), 863 $approved_comments_number 864 ); 865 866 $approved_phrase = sprintf( 867 /* translators: %s: Number of comments. */ 868 _n( '%s approved comment', '%s approved comments', $approved_comments ), 869 $approved_comments_number 870 ); 871 872 $pending_phrase = sprintf( 873 /* translators: %s: Number of comments. */ 874 _n( '%s pending comment', '%s pending comments', $pending_comments ), 875 $pending_comments_number 876 ); 877 878 if ( ! $approved_comments && ! $pending_comments ) { 879 // No comments at all. 880 printf( 881 '<span aria-hidden="true">—</span>' . 882 '<span class="screen-reader-text">%s</span>', 883 __( 'No comments' ) 884 ); 885 } elseif ( $approved_comments && 'trash' === get_post_status( $post_id ) ) { 886 // Don't link the comment bubble for a trashed post. 887 printf( 888 '<span class="post-com-count post-com-count-approved">' . 889 '<span class="comment-count-approved" aria-hidden="true">%s</span>' . 890 '<span class="screen-reader-text">%s</span>' . 891 '</span>', 892 $approved_comments_number, 893 $pending_comments ? $approved_phrase : $approved_only_phrase 894 ); 895 } elseif ( $approved_comments ) { 896 // Link the comment bubble to approved comments. 897 printf( 898 '<a href="%s" class="post-com-count post-com-count-approved">' . 899 '<span class="comment-count-approved" aria-hidden="true">%s</span>' . 900 '<span class="screen-reader-text">%s</span>' . 901 '</a>', 902 esc_url( 903 add_query_arg( 904 array( 905 'p' => $post_id, 906 'comment_status' => 'approved', 907 ), 908 admin_url( 'edit-comments.php' ) 909 ) 910 ), 911 $approved_comments_number, 912 $pending_comments ? $approved_phrase : $approved_only_phrase 913 ); 914 } else { 915 // Don't link the comment bubble when there are no approved comments. 916 printf( 917 '<span class="post-com-count post-com-count-no-comments">' . 918 '<span class="comment-count comment-count-no-comments" aria-hidden="true">%s</span>' . 919 '<span class="screen-reader-text">%s</span>' . 920 '</span>', 921 $approved_comments_number, 922 $pending_comments ? 923 /* translators: Hidden accessibility text. */ 924 __( 'No approved comments' ) : 925 /* translators: Hidden accessibility text. */ 926 __( 'No comments' ) 927 ); 928 } 929 930 if ( $pending_comments ) { 931 printf( 932 '<a href="%s" class="post-com-count post-com-count-pending">' . 933 '<span class="comment-count-pending" aria-hidden="true">%s</span>' . 934 '<span class="screen-reader-text">%s</span>' . 935 '</a>', 936 esc_url( 937 add_query_arg( 938 array( 939 'p' => $post_id, 940 'comment_status' => 'moderated', 941 ), 942 admin_url( 'edit-comments.php' ) 943 ) 944 ), 945 $pending_comments_number, 946 $pending_phrase 947 ); 948 } else { 949 printf( 950 '<span class="post-com-count post-com-count-pending post-com-count-no-pending">' . 951 '<span class="comment-count comment-count-no-pending" aria-hidden="true">%s</span>' . 952 '<span class="screen-reader-text">%s</span>' . 953 '</span>', 954 $pending_comments_number, 955 $approved_comments ? 956 /* translators: Hidden accessibility text. */ 957 __( 'No pending comments' ) : 958 /* translators: Hidden accessibility text. */ 959 __( 'No comments' ) 960 ); 961 } 962 } 963 964 /** 965 * Gets the current page number. 966 * 967 * @since 3.1.0 968 * 969 * @return int Current page number. 970 */ 971 public function get_pagenum() { 972 $pagenum = isset( $_REQUEST['paged'] ) ? absint( $_REQUEST['paged'] ) : 0; 973 974 if ( isset( $this->_pagination_args['total_pages'] ) && $pagenum > $this->_pagination_args['total_pages'] ) { 975 $pagenum = $this->_pagination_args['total_pages']; 976 } 977 978 return max( 1, $pagenum ); 979 } 980 981 /** 982 * Gets the number of items to display on a single page. 983 * 984 * @since 3.1.0 985 * 986 * @param string $option User option name. 987 * @param int $default_value Optional. The number of items to display. Default 20. 988 * @return int Number of items to display per page. 989 */ 990 protected function get_items_per_page( $option, $default_value = 20 ) { 991 $per_page = (int) get_user_option( $option ); 992 if ( empty( $per_page ) || $per_page < 1 ) { 993 $per_page = $default_value; 994 } 995 996 /** 997 * Filters the number of items to be displayed on each page of the list table. 998 * 999 * The dynamic hook name, `$option`, refers to the `per_page` option depending 1000 * on the type of list table in use. Possible filter names include: 1001 * 1002 * - `edit_comments_per_page` 1003 * - `sites_network_per_page` 1004 * - `site_themes_network_per_page` 1005 * - `themes_network_per_page` 1006 * - `users_network_per_page` 1007 * - `edit_post_per_page` 1008 * - `edit_page_per_page` 1009 * - `edit_{$post_type}_per_page` 1010 * - `edit_post_tag_per_page` 1011 * - `edit_category_per_page` 1012 * - `edit_{$taxonomy}_per_page` 1013 * - `site_users_network_per_page` 1014 * - `users_per_page` 1015 * 1016 * @since 2.9.0 1017 * 1018 * @param int $per_page Number of items to be displayed. Default 20. 1019 */ 1020 return (int) apply_filters( "{$option}", $per_page ); 1021 } 1022 1023 /** 1024 * Displays the pagination. 1025 * 1026 * @since 3.1.0 1027 * 1028 * @param string $which The location of the pagination: Either 'top' or 'bottom'. 1029 */ 1030 protected function pagination( $which ) { 1031 if ( empty( $this->_pagination_args['total_items'] ) ) { 1032 // translators: Number is a fixed value. This is default text when no items are found. 1033 echo '<div class="tablenav-pages no-pages"><span class="displaying-num">' . __( '0 items' ) . '</span></div>'; 1034 return; 1035 } 1036 1037 $total_items = $this->_pagination_args['total_items']; 1038 $total_pages = $this->_pagination_args['total_pages']; 1039 $infinite_scroll = false; 1040 if ( isset( $this->_pagination_args['infinite_scroll'] ) ) { 1041 $infinite_scroll = $this->_pagination_args['infinite_scroll']; 1042 } 1043 1044 if ( 'top' === $which && $total_pages > 1 ) { 1045 $this->screen->render_screen_reader_content( 'heading_pagination' ); 1046 } 1047 1048 $output = '<span class="displaying-num">' . sprintf( 1049 /* translators: %s: Number of items. */ 1050 _n( '%s item', '%s items', $total_items ), 1051 number_format_i18n( $total_items ) 1052 ) . '</span>'; 1053 1054 $current = $this->get_pagenum(); 1055 $removable_query_args = wp_removable_query_args(); 1056 1057 $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); 1058 1059 $current_url = remove_query_arg( $removable_query_args, $current_url ); 1060 1061 $page_links = array(); 1062 1063 $total_pages_before = '<span class="paging-input">'; 1064 $total_pages_after = '</span></span>'; 1065 1066 $disable_first = false; 1067 $disable_last = false; 1068 $disable_prev = false; 1069 $disable_next = false; 1070 1071 if ( 1 === $current ) { 1072 $disable_first = true; 1073 $disable_prev = true; 1074 } 1075 if ( $total_pages === $current ) { 1076 $disable_last = true; 1077 $disable_next = true; 1078 } 1079 1080 if ( $disable_first ) { 1081 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">«</span>'; 1082 } else { 1083 $page_links[] = sprintf( 1084 "<a class='first-page button' href='%s'>" . 1085 "<span class='screen-reader-text'>%s</span>" . 1086 "<span aria-hidden='true'>%s</span>" . 1087 '</a>', 1088 esc_url( remove_query_arg( 'paged', $current_url ) ), 1089 /* translators: Hidden accessibility text. */ 1090 __( 'First page' ), 1091 '«' 1092 ); 1093 } 1094 1095 if ( $disable_prev ) { 1096 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">‹</span>'; 1097 } else { 1098 $page_links[] = sprintf( 1099 "<a class='prev-page button' href='%s'>" . 1100 "<span class='screen-reader-text'>%s</span>" . 1101 "<span aria-hidden='true'>%s</span>" . 1102 '</a>', 1103 esc_url( add_query_arg( 'paged', max( 1, $current - 1 ), $current_url ) ), 1104 /* translators: Hidden accessibility text. */ 1105 __( 'Previous page' ), 1106 '‹' 1107 ); 1108 } 1109 1110 if ( 'bottom' === $which ) { 1111 $html_current_page = $current; 1112 $total_pages_before = sprintf( 1113 '<span class="screen-reader-text">%s</span>' . 1114 '<span id="table-paging" class="paging-input">' . 1115 '<span class="tablenav-paging-text">', 1116 /* translators: Hidden accessibility text. */ 1117 __( 'Current Page' ) 1118 ); 1119 } else { 1120 $html_current_page = sprintf( 1121 '<label for="current-page-selector" class="screen-reader-text">%s</label>' . 1122 "<input class='current-page' id='current-page-selector' type='text' 1123 name='paged' value='%s' size='%d' aria-describedby='table-paging' />" . 1124 "<span class='tablenav-paging-text'>", 1125 /* translators: Hidden accessibility text. */ 1126 __( 'Current Page' ), 1127 $current, 1128 strlen( $total_pages ) 1129 ); 1130 } 1131 1132 $html_total_pages = sprintf( "<span class='total-pages'>%s</span>", number_format_i18n( $total_pages ) ); 1133 1134 $page_links[] = $total_pages_before . sprintf( 1135 /* translators: 1: Current page, 2: Total pages. */ 1136 _x( '%1$s of %2$s', 'paging' ), 1137 $html_current_page, 1138 $html_total_pages 1139 ) . $total_pages_after; 1140 1141 if ( $disable_next ) { 1142 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">›</span>'; 1143 } else { 1144 $page_links[] = sprintf( 1145 "<a class='next-page button' href='%s'>" . 1146 "<span class='screen-reader-text'>%s</span>" . 1147 "<span aria-hidden='true'>%s</span>" . 1148 '</a>', 1149 esc_url( add_query_arg( 'paged', min( $total_pages, $current + 1 ), $current_url ) ), 1150 /* translators: Hidden accessibility text. */ 1151 __( 'Next page' ), 1152 '›' 1153 ); 1154 } 1155 1156 if ( $disable_last ) { 1157 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">»</span>'; 1158 } else { 1159 $page_links[] = sprintf( 1160 "<a class='last-page button' href='%s'>" . 1161 "<span class='screen-reader-text'>%s</span>" . 1162 "<span aria-hidden='true'>%s</span>" . 1163 '</a>', 1164 esc_url( add_query_arg( 'paged', $total_pages, $current_url ) ), 1165 /* translators: Hidden accessibility text. */ 1166 __( 'Last page' ), 1167 '»' 1168 ); 1169 } 1170 1171 $pagination_links_class = 'pagination-links'; 1172 if ( ! empty( $infinite_scroll ) ) { 1173 $pagination_links_class .= ' hide-if-js'; 1174 } 1175 $output .= "\n<span class='$pagination_links_class'>" . implode( "\n", $page_links ) . '</span>'; 1176 1177 if ( $total_pages ) { 1178 $page_class = $total_pages < 2 ? ' one-page' : ''; 1179 } else { 1180 $page_class = ' no-pages'; 1181 } 1182 $this->_pagination = "<div class='tablenav-pages{$page_class}'>$output</div>"; 1183 1184 echo $this->_pagination; 1185 } 1186 1187 /** 1188 * Gets a list of columns. 1189 * 1190 * The format is: 1191 * - `'internal-name' => 'Title'` 1192 * 1193 * @since 3.1.0 1194 * @abstract 1195 * 1196 * @return array<string, string> An associative array of columns. 1197 */ 1198 public function get_columns() { 1199 die( 'function WP_List_Table::get_columns() must be overridden in a subclass.' ); 1200 } 1201 1202 /** 1203 * Gets a list of sortable columns. 1204 * 1205 * The format is: 1206 * - `'internal-name' => 'orderby'` 1207 * - `'internal-name' => array( 'orderby', bool, 'abbr', 'orderby-text', 'initially-sorted-column-order' )` - 1208 * - `'internal-name' => array( 'orderby', 'asc' )` - The second element sets the initial sorting order. 1209 * - `'internal-name' => array( 'orderby', true )` - The second element makes the initial order descending. 1210 * 1211 * In the second format, passing true as second parameter will make the initial 1212 * sorting order be descending. Following parameters add a short column name to 1213 * be used as 'abbr' attribute, a translatable string for the current sorting, 1214 * and the initial order for the initial sorted column, 'asc' or 'desc' (default: false). 1215 * 1216 * @since 3.1.0 1217 * @since 6.3.0 Added 'abbr', 'orderby-text' and 'initially-sorted-column-order'. 1218 * 1219 * @return array<string, array<int, string|bool>|string> An associative array of sortable columns. 1220 */ 1221 protected function get_sortable_columns() { 1222 return array(); 1223 } 1224 1225 /** 1226 * Gets the name of the default primary column. 1227 * 1228 * @since 4.3.0 1229 * 1230 * @return string Name of the default primary column, in this case, an empty string. 1231 */ 1232 protected function get_default_primary_column_name() { 1233 $columns = $this->get_columns(); 1234 $column = ''; 1235 1236 if ( empty( $columns ) ) { 1237 return $column; 1238 } 1239 1240 /* 1241 * We need a primary defined so responsive views show something, 1242 * so let's fall back to the first non-checkbox column. 1243 */ 1244 foreach ( $columns as $col => $column_name ) { 1245 if ( 'cb' === $col ) { 1246 continue; 1247 } 1248 1249 $column = $col; 1250 break; 1251 } 1252 1253 return $column; 1254 } 1255 1256 /** 1257 * Gets the name of the primary column. 1258 * 1259 * Public wrapper for WP_List_Table::get_default_primary_column_name(). 1260 * 1261 * @since 4.4.0 1262 * 1263 * @return string Name of the default primary column. 1264 */ 1265 public function get_primary_column() { 1266 return $this->get_primary_column_name(); 1267 } 1268 1269 /** 1270 * Gets the name of the primary column. 1271 * 1272 * @since 4.3.0 1273 * 1274 * @return string The name of the primary column. 1275 */ 1276 protected function get_primary_column_name() { 1277 $columns = get_column_headers( $this->screen ); 1278 $default = $this->get_default_primary_column_name(); 1279 1280 /* 1281 * If the primary column doesn't exist, 1282 * fall back to the first non-checkbox column. 1283 */ 1284 if ( ! isset( $columns[ $default ] ) ) { 1285 $default = self::get_default_primary_column_name(); 1286 } 1287 1288 /** 1289 * Filters the name of the primary column for the current list table. 1290 * 1291 * @since 4.3.0 1292 * 1293 * @param string $default Column name default for the specific list table, e.g. 'name'. 1294 * @param string $context Screen ID for specific list table, e.g. 'plugins'. 1295 */ 1296 $column = apply_filters( 'list_table_primary_column', $default, $this->screen->id ); 1297 1298 if ( empty( $column ) || ! isset( $columns[ $column ] ) ) { 1299 $column = $default; 1300 } 1301 1302 return $column; 1303 } 1304 1305 /** 1306 * Gets a list of all, hidden, and sortable columns, with filter applied. 1307 * 1308 * @since 3.1.0 1309 * 1310 * @return array<int, array|string> Column information. 1311 */ 1312 protected function get_column_info() { 1313 // $_column_headers is already set / cached. 1314 if ( 1315 isset( $this->_column_headers ) && 1316 is_array( $this->_column_headers ) 1317 ) { 1318 /* 1319 * Backward compatibility for `$_column_headers` format prior to WordPress 4.3. 1320 * 1321 * In WordPress 4.3 the primary column name was added as a fourth item in the 1322 * column headers property. This ensures the primary column name is included 1323 * in plugins setting the property directly in the three item format. 1324 */ 1325 if ( 4 === count( $this->_column_headers ) ) { 1326 return $this->_column_headers; 1327 } 1328 1329 $column_headers = array( array(), array(), array(), $this->get_primary_column_name() ); 1330 foreach ( $this->_column_headers as $key => $value ) { 1331 $column_headers[ $key ] = $value; 1332 } 1333 1334 $this->_column_headers = $column_headers; 1335 1336 return $this->_column_headers; 1337 } 1338 1339 $columns = get_column_headers( $this->screen ); 1340 $hidden = get_hidden_columns( $this->screen ); 1341 1342 $sortable_columns = $this->get_sortable_columns(); 1343 /** 1344 * Filters the list table sortable columns for a specific screen. 1345 * 1346 * The dynamic portion of the hook name, `$this->screen->id`, refers 1347 * to the ID of the current screen. 1348 * 1349 * @since 3.1.0 1350 * 1351 * @param array $sortable_columns An array of sortable columns. 1352 */ 1353 $_sortable = apply_filters( "manage_{$this->screen->id}_sortable_columns", $sortable_columns ); 1354 1355 $sortable = array(); 1356 foreach ( $_sortable as $id => $data ) { 1357 if ( empty( $data ) ) { 1358 continue; 1359 } 1360 1361 $data = (array) $data; 1362 // Descending initial sorting. 1363 if ( ! isset( $data[1] ) ) { 1364 $data[1] = false; 1365 } 1366 // Current sorting translatable string. 1367 if ( ! isset( $data[2] ) ) { 1368 $data[2] = ''; 1369 } 1370 // Initial view sorted column and asc/desc order, default: false. 1371 if ( ! isset( $data[3] ) ) { 1372 $data[3] = false; 1373 } 1374 // Initial order for the initial sorted column, default: false. 1375 if ( ! isset( $data[4] ) ) { 1376 $data[4] = false; 1377 } 1378 1379 $sortable[ $id ] = $data; 1380 } 1381 1382 $primary = $this->get_primary_column_name(); 1383 $this->_column_headers = array( $columns, $hidden, $sortable, $primary ); 1384 1385 return $this->_column_headers; 1386 } 1387 1388 /** 1389 * Returns the number of visible columns. 1390 * 1391 * @since 3.1.0 1392 * 1393 * @return int The number of visible columns. 1394 */ 1395 public function get_column_count() { 1396 list ( $columns, $hidden ) = $this->get_column_info(); 1397 $hidden = array_intersect( array_keys( $columns ), array_filter( $hidden ) ); 1398 return count( $columns ) - count( $hidden ); 1399 } 1400 1401 /** 1402 * Prints column headers, accounting for hidden and sortable columns. 1403 * 1404 * @since 3.1.0 1405 * 1406 * @param bool $with_id Whether to set the ID attribute or not. Default true. 1407 */ 1408 public function print_column_headers( $with_id = true ) { 1409 list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info(); 1410 1411 $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); 1412 $current_url = remove_query_arg( 'paged', $current_url ); 1413 1414 // When users click on a column header to sort by other columns. 1415 if ( isset( $_GET['orderby'] ) ) { 1416 $current_orderby = $_GET['orderby']; 1417 // In the initial view there's no orderby parameter. 1418 } else { 1419 $current_orderby = ''; 1420 } 1421 1422 // Not in the initial view and descending order. 1423 if ( isset( $_GET['order'] ) && 'desc' === $_GET['order'] ) { 1424 $current_order = 'desc'; 1425 } else { 1426 // The initial view is not always 'asc', we'll take care of this below. 1427 $current_order = 'asc'; 1428 } 1429 1430 if ( ! empty( $columns['cb'] ) ) { 1431 static $cb_counter = 1; 1432 $columns['cb'] = '<input id="cb-select-all-' . $cb_counter . '" type="checkbox" /> 1433 <label for="cb-select-all-' . $cb_counter . '">' . 1434 '<span class="screen-reader-text">' . 1435 /* translators: Hidden accessibility text. */ 1436 __( 'Select All' ) . 1437 '</span>' . 1438 '</label>'; 1439 ++$cb_counter; 1440 } 1441 1442 foreach ( $columns as $column_key => $column_display_name ) { 1443 $class = array( 'manage-column', "column-$column_key" ); 1444 $aria_sort_attr = ''; 1445 $abbr_attr = ''; 1446 $order_text = ''; 1447 1448 if ( in_array( $column_key, $hidden, true ) ) { 1449 $class[] = 'hidden'; 1450 } 1451 1452 if ( 'cb' === $column_key ) { 1453 $class[] = 'check-column'; 1454 } elseif ( in_array( $column_key, array( 'posts', 'comments', 'links' ), true ) ) { 1455 $class[] = 'num'; 1456 } 1457 1458 if ( $column_key === $primary ) { 1459 $class[] = 'column-primary'; 1460 } 1461 1462 if ( isset( $sortable[ $column_key ] ) ) { 1463 $orderby = $sortable[ $column_key ][0] ?? ''; 1464 $desc_first = $sortable[ $column_key ][1] ?? false; 1465 $abbr = $sortable[ $column_key ][2] ?? ''; 1466 $orderby_text = $sortable[ $column_key ][3] ?? ''; 1467 $initial_order = $sortable[ $column_key ][4] ?? ''; 1468 1469 /* 1470 * We're in the initial view and there's no $_GET['orderby'] then check if the 1471 * initial sorting information is set in the sortable columns and use that. 1472 */ 1473 if ( '' === $current_orderby && $initial_order ) { 1474 // Use the initially sorted column $orderby as current orderby. 1475 $current_orderby = $orderby; 1476 // Use the initially sorted column asc/desc order as initial order. 1477 $current_order = $initial_order; 1478 } 1479 1480 /* 1481 * True in the initial view when an initial orderby is set via get_sortable_columns() 1482 * and true in the sorted views when the actual $_GET['orderby'] is equal to $orderby. 1483 */ 1484 if ( $current_orderby === $orderby ) { 1485 // The sorted column. The `aria-sort` attribute must be set only on the sorted column. 1486 if ( 'asc' === $current_order ) { 1487 $order = 'desc'; 1488 $aria_sort_attr = ' aria-sort="ascending"'; 1489 } else { 1490 $order = 'asc'; 1491 $aria_sort_attr = ' aria-sort="descending"'; 1492 } 1493 1494 $class[] = 'sorted'; 1495 $class[] = $current_order; 1496 } else { 1497 // The other sortable columns. 1498 $order = strtolower( $desc_first ); 1499 1500 if ( ! in_array( $order, array( 'desc', 'asc' ), true ) ) { 1501 $order = $desc_first ? 'desc' : 'asc'; 1502 } 1503 1504 $class[] = 'sortable'; 1505 $class[] = 'desc' === $order ? 'asc' : 'desc'; 1506 1507 /* translators: Hidden accessibility text. */ 1508 $asc_text = __( 'Sort ascending.' ); 1509 /* translators: Hidden accessibility text. */ 1510 $desc_text = __( 'Sort descending.' ); 1511 $order_text = 'asc' === $order ? $asc_text : $desc_text; 1512 } 1513 1514 if ( '' !== $order_text ) { 1515 $order_text = ' <span class="screen-reader-text">' . $order_text . '</span>'; 1516 } 1517 1518 // Print an 'abbr' attribute if a value is provided via get_sortable_columns(). 1519 $abbr_attr = $abbr ? ' abbr="' . esc_attr( $abbr ) . '"' : ''; 1520 1521 $column_display_name = sprintf( 1522 '<a href="%1$s">' . 1523 '<span>%2$s</span>' . 1524 '<span class="sorting-indicators">' . 1525 '<span class="sorting-indicator asc" aria-hidden="true"></span>' . 1526 '<span class="sorting-indicator desc" aria-hidden="true"></span>' . 1527 '</span>' . 1528 '%3$s' . 1529 '</a>', 1530 esc_url( add_query_arg( compact( 'orderby', 'order' ), $current_url ) ), 1531 $column_display_name, 1532 $order_text 1533 ); 1534 } 1535 1536 $tag = ( 'cb' === $column_key ) ? 'td' : 'th'; 1537 $scope = ( 'th' === $tag ) ? 'scope="col"' : ''; 1538 $id = $with_id ? "id='$column_key'" : ''; 1539 $class_attr = "class='" . implode( ' ', $class ) . "'"; 1540 1541 echo "<$tag $scope $id $class_attr $aria_sort_attr $abbr_attr>$column_display_name</$tag>"; 1542 } 1543 } 1544 1545 /** 1546 * Print a table description with information about current sorting and order. 1547 * 1548 * For the table initial view, information about initial orderby and order 1549 * should be provided via get_sortable_columns(). 1550 * 1551 * @since 6.3.0 1552 */ 1553 public function print_table_description() { 1554 list( $columns, $hidden, $sortable ) = $this->get_column_info(); 1555 1556 if ( empty( $sortable ) ) { 1557 return; 1558 } 1559 1560 // When users click on a column header to sort by other columns. 1561 if ( isset( $_GET['orderby'] ) ) { 1562 $current_orderby = $_GET['orderby']; 1563 // In the initial view there's no orderby parameter. 1564 } else { 1565 $current_orderby = ''; 1566 } 1567 1568 // Not in the initial view and descending order. 1569 if ( isset( $_GET['order'] ) && 'desc' === $_GET['order'] ) { 1570 $current_order = 'desc'; 1571 } else { 1572 // The initial view is not always 'asc', we'll take care of this below. 1573 $current_order = 'asc'; 1574 } 1575 1576 foreach ( array_keys( $columns ) as $column_key ) { 1577 1578 if ( isset( $sortable[ $column_key ] ) ) { 1579 $orderby = $sortable[ $column_key ][0] ?? ''; 1580 $desc_first = $sortable[ $column_key ][1] ?? false; 1581 $abbr = $sortable[ $column_key ][2] ?? ''; 1582 $orderby_text = $sortable[ $column_key ][3] ?? ''; 1583 $initial_order = $sortable[ $column_key ][4] ?? ''; 1584 1585 if ( ! is_string( $orderby_text ) || '' === $orderby_text ) { 1586 return; 1587 } 1588 /* 1589 * We're in the initial view and there's no $_GET['orderby'] then check if the 1590 * initial sorting information is set in the sortable columns and use that. 1591 */ 1592 if ( '' === $current_orderby && $initial_order ) { 1593 // Use the initially sorted column $orderby as current orderby. 1594 $current_orderby = $orderby; 1595 // Use the initially sorted column asc/desc order as initial order. 1596 $current_order = $initial_order; 1597 } 1598 1599 /* 1600 * True in the initial view when an initial orderby is set via get_sortable_columns() 1601 * and true in the sorted views when the actual $_GET['orderby'] is equal to $orderby. 1602 */ 1603 if ( $current_orderby === $orderby ) { 1604 /* translators: Hidden accessibility text. */ 1605 $asc_text = __( 'Ascending.' ); 1606 /* translators: Hidden accessibility text. */ 1607 $desc_text = __( 'Descending.' ); 1608 $order_text = 'asc' === $current_order ? $asc_text : $desc_text; 1609 echo '<caption class="screen-reader-text">' . $orderby_text . ' ' . $order_text . '</caption>'; 1610 1611 return; 1612 } 1613 } 1614 } 1615 } 1616 1617 /** 1618 * Displays the table. 1619 * 1620 * @since 3.1.0 1621 */ 1622 public function display() { 1623 $singular = $this->_args['singular']; 1624 1625 $this->display_tablenav( 'top' ); 1626 1627 $this->screen->render_screen_reader_content( 'heading_list' ); 1628 ?> 1629 <table class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>"> 1630 <?php $this->print_table_description(); ?> 1631 <thead> 1632 <tr> 1633 <?php $this->print_column_headers(); ?> 1634 </tr> 1635 </thead> 1636 1637 <tbody id="the-list" 1638 <?php 1639 if ( $singular ) { 1640 echo " data-wp-lists='list:$singular'"; 1641 } 1642 ?> 1643 > 1644 <?php $this->display_rows_or_placeholder(); ?> 1645 </tbody> 1646 1647 <tfoot> 1648 <tr> 1649 <?php $this->print_column_headers( false ); ?> 1650 </tr> 1651 </tfoot> 1652 1653 </table> 1654 <?php 1655 $this->display_tablenav( 'bottom' ); 1656 } 1657 1658 /** 1659 * Gets a list of CSS classes for the WP_List_Table table tag. 1660 * 1661 * @since 3.1.0 1662 * 1663 * @return string[] Array of CSS classes for the table tag. 1664 */ 1665 protected function get_table_classes() { 1666 $mode = get_user_setting( 'posts_list_mode', 'list' ); 1667 1668 $mode_class = esc_attr( 'table-view-' . $mode ); 1669 1670 return array( 'widefat', 'fixed', 'striped', $mode_class, $this->_args['plural'] ); 1671 } 1672 1673 /** 1674 * Generates the table navigation above or below the table. 1675 * 1676 * @since 3.1.0 1677 * 1678 * @param string $which The location of the navigation: Either 'top' or 'bottom'. 1679 */ 1680 protected function display_tablenav( $which ) { 1681 if ( 'bottom' === $which && ! $this->has_items() ) { 1682 return; 1683 } 1684 if ( 'top' === $which ) { 1685 wp_nonce_field( 'bulk-' . $this->_args['plural'] ); 1686 } 1687 ?> 1688 <div class="tablenav <?php echo esc_attr( $which ); ?>"> 1689 1690 <?php 1691 $visibility = ' hidden'; 1692 if ( $this->has_items() ) { 1693 $visibility = ''; 1694 } 1695 ?> 1696 <div class="alignleft actions bulkactions<?php echo $visibility; ?>"> 1697 <?php $this->bulk_actions( $which ); ?> 1698 </div> 1699 <?php 1700 $this->extra_tablenav( $which ); 1701 $this->pagination( $which ); 1702 ?> 1703 1704 <br class="clear" /> 1705 </div> 1706 <?php 1707 } 1708 1709 /** 1710 * Displays extra controls between bulk actions and pagination. 1711 * 1712 * @since 3.1.0 1713 * 1714 * @param string $which The location: 'top' or 'bottom'. 1715 */ 1716 protected function extra_tablenav( $which ) {} 1717 1718 /** 1719 * Generates the tbody element for the list table. 1720 * 1721 * @since 3.1.0 1722 */ 1723 public function display_rows_or_placeholder() { 1724 if ( $this->has_items() ) { 1725 $this->display_rows(); 1726 } else { 1727 echo '<tr class="no-items"><td class="colspanchange" colspan="' . $this->get_column_count() . '">'; 1728 $this->no_items(); 1729 echo '</td></tr>'; 1730 } 1731 } 1732 1733 /** 1734 * Generates the list table rows. 1735 * 1736 * @since 3.1.0 1737 */ 1738 public function display_rows() { 1739 foreach ( $this->items as $item ) { 1740 $this->single_row( $item ); 1741 } 1742 } 1743 1744 /** 1745 * Generates content for a single row of the table. 1746 * 1747 * @since 3.1.0 1748 * 1749 * @param object|array $item The current item 1750 */ 1751 public function single_row( $item ) { 1752 echo '<tr>'; 1753 $this->single_row_columns( $item ); 1754 echo '</tr>'; 1755 } 1756 1757 /** 1758 * Handles an unknown column. 1759 * 1760 * @since 4.2.0 1761 * 1762 * @param object|array $item The current item. 1763 * @param string $column_name Name of the column. 1764 */ 1765 protected function column_default( $item, $column_name ) {} 1766 1767 /** 1768 * Handles the checkbox column output. 1769 * 1770 * @since 4.2.0 1771 * 1772 * @param object|array $item The current item. 1773 */ 1774 protected function column_cb( $item ) {} 1775 1776 /** 1777 * Returns a clean, human-readable label for the primary column's row header. 1778 * 1779 * Used as the `aria-label` attribute value on the `<th scope="row">` element, 1780 * giving screen readers a concise cell name instead of computing it from 1781 * the full cell content (which may include row action links, excerpts, etc.). 1782 * 1783 * Subclasses should override this method to return the item's primary 1784 * identifier (e.g. post title, plugin name, username). Return an empty string 1785 * to omit the attribute. 1786 * 1787 * @since 6.9.0 1788 * 1789 * @param object|array $item The current item. 1790 * @return string The aria-label value, or an empty string. 1791 */ 1792 protected function get_primary_column_aria_label( $item ) { 1793 return ''; 1794 } 1795 1796 /** 1797 * Generates the columns for a single row of the table. 1798 * 1799 * @since 3.1.0 1800 * 1801 * @param object|array $item The current item. 1802 */ 1803 protected function single_row_columns( $item ) { 1804 list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info(); 1805 1806 foreach ( $columns as $column_name => $column_display_name ) { 1807 $classes = "$column_name column-$column_name"; 1808 if ( $primary === $column_name ) { 1809 $classes .= ' has-row-actions column-primary'; 1810 } 1811 1812 if ( in_array( $column_name, $hidden, true ) ) { 1813 $classes .= ' hidden'; 1814 } 1815 1816 /* 1817 * Comments column uses HTML in the display name with screen reader text. 1818 * Strip tags to get closer to a user-friendly string. 1819 */ 1820 $data = 'data-colname="' . esc_attr( wp_strip_all_tags( $column_display_name ) ) . '"'; 1821 1822 $attributes = "class='$classes' $data"; 1823 1824 if ( 'cb' === $column_name ) { 1825 echo '<td class="check-column">'; 1826 echo $this->column_cb( $item ); 1827 echo '</td>'; 1828 } elseif ( method_exists( $this, '_column_' . $column_name ) ) { 1829 echo call_user_func( 1830 array( $this, '_column_' . $column_name ), 1831 $item, 1832 $classes, 1833 $data, 1834 $primary 1835 ); 1836 } else { 1837 $is_primary = ( $primary === $column_name ); 1838 $tag = $is_primary ? 'th' : 'td'; 1839 $scope = $is_primary ? ' scope="row"' : ''; 1840 1841 $aria_label = ''; 1842 if ( $is_primary ) { 1843 $label = $this->get_primary_column_aria_label( $item ); 1844 if ( '' !== $label ) { 1845 $aria_label = ' aria-label="' . esc_attr( $label ) . '"'; 1846 } 1847 } 1848 1849 echo "<$tag $attributes$scope$aria_label>"; 1850 1851 if ( method_exists( $this, 'column_' . $column_name ) ) { 1852 echo call_user_func( array( $this, 'column_' . $column_name ), $item ); 1853 } else { 1854 echo $this->column_default( $item, $column_name ); 1855 } 1856 1857 echo $this->handle_row_actions( $item, $column_name, $primary ); 1858 echo "</$tag>"; 1859 } 1860 } 1861 } 1862 1863 /** 1864 * Generates and display row actions links for the list table. 1865 * 1866 * @since 4.3.0 1867 * 1868 * @param object|array $item The item being acted upon. 1869 * @param string $column_name Current column name. 1870 * @param string $primary Primary column name. 1871 * @return string The row actions HTML, or an empty string 1872 * if the current column is not the primary column. 1873 */ 1874 protected function handle_row_actions( $item, $column_name, $primary ) { 1875 return $column_name === $primary ? '<button type="button" class="toggle-row"><span class="screen-reader-text">' . 1876 /* translators: Hidden accessibility text. */ 1877 __( 'Show more details' ) . 1878 '</span></button>' : ''; 1879 } 1880 1881 /** 1882 * Handles an incoming ajax request (called from admin-ajax.php) 1883 * 1884 * @since 3.1.0 1885 * 1886 * @return never 1887 */ 1888 public function ajax_response() { 1889 $this->prepare_items(); 1890 1891 ob_start(); 1892 if ( ! empty( $_REQUEST['no_placeholder'] ) ) { 1893 $this->display_rows(); 1894 } else { 1895 $this->display_rows_or_placeholder(); 1896 } 1897 1898 $rows = ob_get_clean(); 1899 1900 $response = array( 'rows' => $rows ); 1901 1902 if ( isset( $this->_pagination_args['total_items'] ) ) { 1903 $response['total_items_i18n'] = sprintf( 1904 /* translators: Number of items. */ 1905 _n( '%s item', '%s items', $this->_pagination_args['total_items'] ), 1906 number_format_i18n( $this->_pagination_args['total_items'] ) 1907 ); 1908 } 1909 if ( isset( $this->_pagination_args['total_pages'] ) ) { 1910 $response['total_pages'] = $this->_pagination_args['total_pages']; 1911 $response['total_pages_i18n'] = number_format_i18n( $this->_pagination_args['total_pages'] ); 1912 } 1913 1914 die( wp_json_encode( $response ) ); 1915 } 1916 1917 /** 1918 * Sends required variables to JavaScript land. 1919 * 1920 * @since 3.1.0 1921 */ 1922 public function _js_vars() { 1923 $args = array( 1924 'class' => get_class( $this ), 1925 'screen' => array( 1926 'id' => $this->screen->id, 1927 'base' => $this->screen->base, 1928 ), 1929 ); 1930 1931 printf( "<script>list_args = %s;</script>\n", wp_json_encode( $args, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ) ); 1932 } 1933 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sun Jul 26 08:20:18 2026 | Cross-referenced by PHPXref |