| [ 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, 'button-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 button-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 return; 1033 } 1034 1035 $total_items = $this->_pagination_args['total_items']; 1036 $total_pages = $this->_pagination_args['total_pages']; 1037 $infinite_scroll = false; 1038 if ( isset( $this->_pagination_args['infinite_scroll'] ) ) { 1039 $infinite_scroll = $this->_pagination_args['infinite_scroll']; 1040 } 1041 1042 if ( 'top' === $which && $total_pages > 1 ) { 1043 $this->screen->render_screen_reader_content( 'heading_pagination' ); 1044 } 1045 1046 $output = '<span class="displaying-num">' . sprintf( 1047 /* translators: %s: Number of items. */ 1048 _n( '%s item', '%s items', $total_items ), 1049 number_format_i18n( $total_items ) 1050 ) . '</span>'; 1051 1052 $current = $this->get_pagenum(); 1053 $removable_query_args = wp_removable_query_args(); 1054 1055 $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); 1056 1057 $current_url = remove_query_arg( $removable_query_args, $current_url ); 1058 1059 $page_links = array(); 1060 1061 $total_pages_before = '<span class="paging-input">'; 1062 $total_pages_after = '</span></span>'; 1063 1064 $disable_first = false; 1065 $disable_last = false; 1066 $disable_prev = false; 1067 $disable_next = false; 1068 1069 if ( 1 === $current ) { 1070 $disable_first = true; 1071 $disable_prev = true; 1072 } 1073 if ( $total_pages === $current ) { 1074 $disable_last = true; 1075 $disable_next = true; 1076 } 1077 1078 if ( $disable_first ) { 1079 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">«</span>'; 1080 } else { 1081 $page_links[] = sprintf( 1082 "<a class='first-page button' href='%s'>" . 1083 "<span class='screen-reader-text'>%s</span>" . 1084 "<span aria-hidden='true'>%s</span>" . 1085 '</a>', 1086 esc_url( remove_query_arg( 'paged', $current_url ) ), 1087 /* translators: Hidden accessibility text. */ 1088 __( 'First page' ), 1089 '«' 1090 ); 1091 } 1092 1093 if ( $disable_prev ) { 1094 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">‹</span>'; 1095 } else { 1096 $page_links[] = sprintf( 1097 "<a class='prev-page button' href='%s'>" . 1098 "<span class='screen-reader-text'>%s</span>" . 1099 "<span aria-hidden='true'>%s</span>" . 1100 '</a>', 1101 esc_url( add_query_arg( 'paged', max( 1, $current - 1 ), $current_url ) ), 1102 /* translators: Hidden accessibility text. */ 1103 __( 'Previous page' ), 1104 '‹' 1105 ); 1106 } 1107 1108 if ( 'bottom' === $which ) { 1109 $html_current_page = $current; 1110 $total_pages_before = sprintf( 1111 '<span class="screen-reader-text">%s</span>' . 1112 '<span id="table-paging" class="paging-input">' . 1113 '<span class="tablenav-paging-text">', 1114 /* translators: Hidden accessibility text. */ 1115 __( 'Current Page' ) 1116 ); 1117 } else { 1118 $html_current_page = sprintf( 1119 '<label for="current-page-selector" class="screen-reader-text">%s</label>' . 1120 "<input class='current-page' id='current-page-selector' type='text' 1121 name='paged' value='%s' size='%d' aria-describedby='table-paging' />" . 1122 "<span class='tablenav-paging-text'>", 1123 /* translators: Hidden accessibility text. */ 1124 __( 'Current Page' ), 1125 $current, 1126 strlen( $total_pages ) 1127 ); 1128 } 1129 1130 $html_total_pages = sprintf( "<span class='total-pages'>%s</span>", number_format_i18n( $total_pages ) ); 1131 1132 $page_links[] = $total_pages_before . sprintf( 1133 /* translators: 1: Current page, 2: Total pages. */ 1134 _x( '%1$s of %2$s', 'paging' ), 1135 $html_current_page, 1136 $html_total_pages 1137 ) . $total_pages_after; 1138 1139 if ( $disable_next ) { 1140 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">›</span>'; 1141 } else { 1142 $page_links[] = sprintf( 1143 "<a class='next-page button' href='%s'>" . 1144 "<span class='screen-reader-text'>%s</span>" . 1145 "<span aria-hidden='true'>%s</span>" . 1146 '</a>', 1147 esc_url( add_query_arg( 'paged', min( $total_pages, $current + 1 ), $current_url ) ), 1148 /* translators: Hidden accessibility text. */ 1149 __( 'Next page' ), 1150 '›' 1151 ); 1152 } 1153 1154 if ( $disable_last ) { 1155 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">»</span>'; 1156 } else { 1157 $page_links[] = sprintf( 1158 "<a class='last-page button' href='%s'>" . 1159 "<span class='screen-reader-text'>%s</span>" . 1160 "<span aria-hidden='true'>%s</span>" . 1161 '</a>', 1162 esc_url( add_query_arg( 'paged', $total_pages, $current_url ) ), 1163 /* translators: Hidden accessibility text. */ 1164 __( 'Last page' ), 1165 '»' 1166 ); 1167 } 1168 1169 $pagination_links_class = 'pagination-links'; 1170 if ( ! empty( $infinite_scroll ) ) { 1171 $pagination_links_class .= ' hide-if-js'; 1172 } 1173 $output .= "\n<span class='$pagination_links_class'>" . implode( "\n", $page_links ) . '</span>'; 1174 1175 if ( $total_pages ) { 1176 $page_class = $total_pages < 2 ? ' one-page' : ''; 1177 } else { 1178 $page_class = ' no-pages'; 1179 } 1180 $this->_pagination = "<div class='tablenav-pages{$page_class}'>$output</div>"; 1181 1182 echo $this->_pagination; 1183 } 1184 1185 /** 1186 * Gets a list of columns. 1187 * 1188 * The format is: 1189 * - `'internal-name' => 'Title'` 1190 * 1191 * @since 3.1.0 1192 * @abstract 1193 * 1194 * @return array<string, string> An associative array of columns. 1195 */ 1196 public function get_columns() { 1197 die( 'function WP_List_Table::get_columns() must be overridden in a subclass.' ); 1198 } 1199 1200 /** 1201 * Gets a list of sortable columns. 1202 * 1203 * The format is: 1204 * - `'internal-name' => 'orderby'` 1205 * - `'internal-name' => array( 'orderby', bool, 'abbr', 'orderby-text', 'initially-sorted-column-order' )` - 1206 * - `'internal-name' => array( 'orderby', 'asc' )` - The second element sets the initial sorting order. 1207 * - `'internal-name' => array( 'orderby', true )` - The second element makes the initial order descending. 1208 * 1209 * In the second format, passing true as second parameter will make the initial 1210 * sorting order be descending. Following parameters add a short column name to 1211 * be used as 'abbr' attribute, a translatable string for the current sorting, 1212 * and the initial order for the initial sorted column, 'asc' or 'desc' (default: false). 1213 * 1214 * @since 3.1.0 1215 * @since 6.3.0 Added 'abbr', 'orderby-text' and 'initially-sorted-column-order'. 1216 * 1217 * @return array<string, array<int, string|bool>|string> An associative array of sortable columns. 1218 */ 1219 protected function get_sortable_columns() { 1220 return array(); 1221 } 1222 1223 /** 1224 * Gets the name of the default primary column. 1225 * 1226 * @since 4.3.0 1227 * 1228 * @return string Name of the default primary column, in this case, an empty string. 1229 */ 1230 protected function get_default_primary_column_name() { 1231 $columns = $this->get_columns(); 1232 $column = ''; 1233 1234 if ( empty( $columns ) ) { 1235 return $column; 1236 } 1237 1238 /* 1239 * We need a primary defined so responsive views show something, 1240 * so let's fall back to the first non-checkbox column. 1241 */ 1242 foreach ( $columns as $col => $column_name ) { 1243 if ( 'cb' === $col ) { 1244 continue; 1245 } 1246 1247 $column = $col; 1248 break; 1249 } 1250 1251 return $column; 1252 } 1253 1254 /** 1255 * Gets the name of the primary column. 1256 * 1257 * Public wrapper for WP_List_Table::get_default_primary_column_name(). 1258 * 1259 * @since 4.4.0 1260 * 1261 * @return string Name of the default primary column. 1262 */ 1263 public function get_primary_column() { 1264 return $this->get_primary_column_name(); 1265 } 1266 1267 /** 1268 * Gets the name of the primary column. 1269 * 1270 * @since 4.3.0 1271 * 1272 * @return string The name of the primary column. 1273 */ 1274 protected function get_primary_column_name() { 1275 $columns = get_column_headers( $this->screen ); 1276 $default = $this->get_default_primary_column_name(); 1277 1278 /* 1279 * If the primary column doesn't exist, 1280 * fall back to the first non-checkbox column. 1281 */ 1282 if ( ! isset( $columns[ $default ] ) ) { 1283 $default = self::get_default_primary_column_name(); 1284 } 1285 1286 /** 1287 * Filters the name of the primary column for the current list table. 1288 * 1289 * @since 4.3.0 1290 * 1291 * @param string $default Column name default for the specific list table, e.g. 'name'. 1292 * @param string $context Screen ID for specific list table, e.g. 'plugins'. 1293 */ 1294 $column = apply_filters( 'list_table_primary_column', $default, $this->screen->id ); 1295 1296 if ( empty( $column ) || ! isset( $columns[ $column ] ) ) { 1297 $column = $default; 1298 } 1299 1300 return $column; 1301 } 1302 1303 /** 1304 * Gets a list of all, hidden, and sortable columns, with filter applied. 1305 * 1306 * @since 3.1.0 1307 * 1308 * @return array<int, array|string> Column information. 1309 */ 1310 protected function get_column_info() { 1311 // $_column_headers is already set / cached. 1312 if ( 1313 isset( $this->_column_headers ) && 1314 is_array( $this->_column_headers ) 1315 ) { 1316 /* 1317 * Backward compatibility for `$_column_headers` format prior to WordPress 4.3. 1318 * 1319 * In WordPress 4.3 the primary column name was added as a fourth item in the 1320 * column headers property. This ensures the primary column name is included 1321 * in plugins setting the property directly in the three item format. 1322 */ 1323 if ( 4 === count( $this->_column_headers ) ) { 1324 return $this->_column_headers; 1325 } 1326 1327 $column_headers = array( array(), array(), array(), $this->get_primary_column_name() ); 1328 foreach ( $this->_column_headers as $key => $value ) { 1329 $column_headers[ $key ] = $value; 1330 } 1331 1332 $this->_column_headers = $column_headers; 1333 1334 return $this->_column_headers; 1335 } 1336 1337 $columns = get_column_headers( $this->screen ); 1338 $hidden = get_hidden_columns( $this->screen ); 1339 1340 $sortable_columns = $this->get_sortable_columns(); 1341 /** 1342 * Filters the list table sortable columns for a specific screen. 1343 * 1344 * The dynamic portion of the hook name, `$this->screen->id`, refers 1345 * to the ID of the current screen. 1346 * 1347 * @since 3.1.0 1348 * 1349 * @param array $sortable_columns An array of sortable columns. 1350 */ 1351 $_sortable = apply_filters( "manage_{$this->screen->id}_sortable_columns", $sortable_columns ); 1352 1353 $sortable = array(); 1354 foreach ( $_sortable as $id => $data ) { 1355 if ( empty( $data ) ) { 1356 continue; 1357 } 1358 1359 $data = (array) $data; 1360 // Descending initial sorting. 1361 if ( ! isset( $data[1] ) ) { 1362 $data[1] = false; 1363 } 1364 // Current sorting translatable string. 1365 if ( ! isset( $data[2] ) ) { 1366 $data[2] = ''; 1367 } 1368 // Initial view sorted column and asc/desc order, default: false. 1369 if ( ! isset( $data[3] ) ) { 1370 $data[3] = false; 1371 } 1372 // Initial order for the initial sorted column, default: false. 1373 if ( ! isset( $data[4] ) ) { 1374 $data[4] = false; 1375 } 1376 1377 $sortable[ $id ] = $data; 1378 } 1379 1380 $primary = $this->get_primary_column_name(); 1381 $this->_column_headers = array( $columns, $hidden, $sortable, $primary ); 1382 1383 return $this->_column_headers; 1384 } 1385 1386 /** 1387 * Returns the number of visible columns. 1388 * 1389 * @since 3.1.0 1390 * 1391 * @return int The number of visible columns. 1392 */ 1393 public function get_column_count() { 1394 list ( $columns, $hidden ) = $this->get_column_info(); 1395 $hidden = array_intersect( array_keys( $columns ), array_filter( $hidden ) ); 1396 return count( $columns ) - count( $hidden ); 1397 } 1398 1399 /** 1400 * Prints column headers, accounting for hidden and sortable columns. 1401 * 1402 * @since 3.1.0 1403 * 1404 * @param bool $with_id Whether to set the ID attribute or not. Default true. 1405 */ 1406 public function print_column_headers( $with_id = true ) { 1407 list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info(); 1408 1409 $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); 1410 $current_url = remove_query_arg( 'paged', $current_url ); 1411 1412 // When users click on a column header to sort by other columns. 1413 if ( isset( $_GET['orderby'] ) ) { 1414 $current_orderby = $_GET['orderby']; 1415 // In the initial view there's no orderby parameter. 1416 } else { 1417 $current_orderby = ''; 1418 } 1419 1420 // Not in the initial view and descending order. 1421 if ( isset( $_GET['order'] ) && 'desc' === $_GET['order'] ) { 1422 $current_order = 'desc'; 1423 } else { 1424 // The initial view is not always 'asc', we'll take care of this below. 1425 $current_order = 'asc'; 1426 } 1427 1428 if ( ! empty( $columns['cb'] ) ) { 1429 static $cb_counter = 1; 1430 $columns['cb'] = '<input id="cb-select-all-' . $cb_counter . '" type="checkbox" /> 1431 <label for="cb-select-all-' . $cb_counter . '">' . 1432 '<span class="screen-reader-text">' . 1433 /* translators: Hidden accessibility text. */ 1434 __( 'Select All' ) . 1435 '</span>' . 1436 '</label>'; 1437 ++$cb_counter; 1438 } 1439 1440 foreach ( $columns as $column_key => $column_display_name ) { 1441 $class = array( 'manage-column', "column-$column_key" ); 1442 $aria_sort_attr = ''; 1443 $abbr_attr = ''; 1444 $order_text = ''; 1445 1446 if ( in_array( $column_key, $hidden, true ) ) { 1447 $class[] = 'hidden'; 1448 } 1449 1450 if ( 'cb' === $column_key ) { 1451 $class[] = 'check-column'; 1452 } elseif ( in_array( $column_key, array( 'posts', 'comments', 'links' ), true ) ) { 1453 $class[] = 'num'; 1454 } 1455 1456 if ( $column_key === $primary ) { 1457 $class[] = 'column-primary'; 1458 } 1459 1460 if ( isset( $sortable[ $column_key ] ) ) { 1461 $orderby = $sortable[ $column_key ][0] ?? ''; 1462 $desc_first = $sortable[ $column_key ][1] ?? false; 1463 $abbr = $sortable[ $column_key ][2] ?? ''; 1464 $orderby_text = $sortable[ $column_key ][3] ?? ''; 1465 $initial_order = $sortable[ $column_key ][4] ?? ''; 1466 1467 /* 1468 * We're in the initial view and there's no $_GET['orderby'] then check if the 1469 * initial sorting information is set in the sortable columns and use that. 1470 */ 1471 if ( '' === $current_orderby && $initial_order ) { 1472 // Use the initially sorted column $orderby as current orderby. 1473 $current_orderby = $orderby; 1474 // Use the initially sorted column asc/desc order as initial order. 1475 $current_order = $initial_order; 1476 } 1477 1478 /* 1479 * True in the initial view when an initial orderby is set via get_sortable_columns() 1480 * and true in the sorted views when the actual $_GET['orderby'] is equal to $orderby. 1481 */ 1482 if ( $current_orderby === $orderby ) { 1483 // The sorted column. The `aria-sort` attribute must be set only on the sorted column. 1484 if ( 'asc' === $current_order ) { 1485 $order = 'desc'; 1486 $aria_sort_attr = ' aria-sort="ascending"'; 1487 } else { 1488 $order = 'asc'; 1489 $aria_sort_attr = ' aria-sort="descending"'; 1490 } 1491 1492 $class[] = 'sorted'; 1493 $class[] = $current_order; 1494 } else { 1495 // The other sortable columns. 1496 $order = strtolower( $desc_first ); 1497 1498 if ( ! in_array( $order, array( 'desc', 'asc' ), true ) ) { 1499 $order = $desc_first ? 'desc' : 'asc'; 1500 } 1501 1502 $class[] = 'sortable'; 1503 $class[] = 'desc' === $order ? 'asc' : 'desc'; 1504 1505 /* translators: Hidden accessibility text. */ 1506 $asc_text = __( 'Sort ascending.' ); 1507 /* translators: Hidden accessibility text. */ 1508 $desc_text = __( 'Sort descending.' ); 1509 $order_text = 'asc' === $order ? $asc_text : $desc_text; 1510 } 1511 1512 if ( '' !== $order_text ) { 1513 $order_text = ' <span class="screen-reader-text">' . $order_text . '</span>'; 1514 } 1515 1516 // Print an 'abbr' attribute if a value is provided via get_sortable_columns(). 1517 $abbr_attr = $abbr ? ' abbr="' . esc_attr( $abbr ) . '"' : ''; 1518 1519 $column_display_name = sprintf( 1520 '<a href="%1$s">' . 1521 '<span>%2$s</span>' . 1522 '<span class="sorting-indicators">' . 1523 '<span class="sorting-indicator asc" aria-hidden="true"></span>' . 1524 '<span class="sorting-indicator desc" aria-hidden="true"></span>' . 1525 '</span>' . 1526 '%3$s' . 1527 '</a>', 1528 esc_url( add_query_arg( compact( 'orderby', 'order' ), $current_url ) ), 1529 $column_display_name, 1530 $order_text 1531 ); 1532 } 1533 1534 $tag = ( 'cb' === $column_key ) ? 'td' : 'th'; 1535 $scope = ( 'th' === $tag ) ? 'scope="col"' : ''; 1536 $id = $with_id ? "id='$column_key'" : ''; 1537 $class_attr = "class='" . implode( ' ', $class ) . "'"; 1538 1539 echo "<$tag $scope $id $class_attr $aria_sort_attr $abbr_attr>$column_display_name</$tag>"; 1540 } 1541 } 1542 1543 /** 1544 * Print a table description with information about current sorting and order. 1545 * 1546 * For the table initial view, information about initial orderby and order 1547 * should be provided via get_sortable_columns(). 1548 * 1549 * @since 6.3.0 1550 */ 1551 public function print_table_description() { 1552 list( $columns, $hidden, $sortable ) = $this->get_column_info(); 1553 1554 if ( empty( $sortable ) ) { 1555 return; 1556 } 1557 1558 // When users click on a column header to sort by other columns. 1559 if ( isset( $_GET['orderby'] ) ) { 1560 $current_orderby = $_GET['orderby']; 1561 // In the initial view there's no orderby parameter. 1562 } else { 1563 $current_orderby = ''; 1564 } 1565 1566 // Not in the initial view and descending order. 1567 if ( isset( $_GET['order'] ) && 'desc' === $_GET['order'] ) { 1568 $current_order = 'desc'; 1569 } else { 1570 // The initial view is not always 'asc', we'll take care of this below. 1571 $current_order = 'asc'; 1572 } 1573 1574 foreach ( array_keys( $columns ) as $column_key ) { 1575 1576 if ( isset( $sortable[ $column_key ] ) ) { 1577 $orderby = $sortable[ $column_key ][0] ?? ''; 1578 $desc_first = $sortable[ $column_key ][1] ?? false; 1579 $abbr = $sortable[ $column_key ][2] ?? ''; 1580 $orderby_text = $sortable[ $column_key ][3] ?? ''; 1581 $initial_order = $sortable[ $column_key ][4] ?? ''; 1582 1583 if ( ! is_string( $orderby_text ) || '' === $orderby_text ) { 1584 return; 1585 } 1586 /* 1587 * We're in the initial view and there's no $_GET['orderby'] then check if the 1588 * initial sorting information is set in the sortable columns and use that. 1589 */ 1590 if ( '' === $current_orderby && $initial_order ) { 1591 // Use the initially sorted column $orderby as current orderby. 1592 $current_orderby = $orderby; 1593 // Use the initially sorted column asc/desc order as initial order. 1594 $current_order = $initial_order; 1595 } 1596 1597 /* 1598 * True in the initial view when an initial orderby is set via get_sortable_columns() 1599 * and true in the sorted views when the actual $_GET['orderby'] is equal to $orderby. 1600 */ 1601 if ( $current_orderby === $orderby ) { 1602 /* translators: Hidden accessibility text. */ 1603 $asc_text = __( 'Ascending.' ); 1604 /* translators: Hidden accessibility text. */ 1605 $desc_text = __( 'Descending.' ); 1606 $order_text = 'asc' === $current_order ? $asc_text : $desc_text; 1607 echo '<caption class="screen-reader-text">' . $orderby_text . ' ' . $order_text . '</caption>'; 1608 1609 return; 1610 } 1611 } 1612 } 1613 } 1614 1615 /** 1616 * Displays the table. 1617 * 1618 * @since 3.1.0 1619 */ 1620 public function display() { 1621 $singular = $this->_args['singular']; 1622 1623 $this->display_tablenav( 'top' ); 1624 1625 $this->screen->render_screen_reader_content( 'heading_list' ); 1626 ?> 1627 <table class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>"> 1628 <?php $this->print_table_description(); ?> 1629 <thead> 1630 <tr> 1631 <?php $this->print_column_headers(); ?> 1632 </tr> 1633 </thead> 1634 1635 <tbody id="the-list" 1636 <?php 1637 if ( $singular ) { 1638 echo " data-wp-lists='list:$singular'"; 1639 } 1640 ?> 1641 > 1642 <?php $this->display_rows_or_placeholder(); ?> 1643 </tbody> 1644 1645 <tfoot> 1646 <tr> 1647 <?php $this->print_column_headers( false ); ?> 1648 </tr> 1649 </tfoot> 1650 1651 </table> 1652 <?php 1653 $this->display_tablenav( 'bottom' ); 1654 } 1655 1656 /** 1657 * Gets a list of CSS classes for the WP_List_Table table tag. 1658 * 1659 * @since 3.1.0 1660 * 1661 * @return string[] Array of CSS classes for the table tag. 1662 */ 1663 protected function get_table_classes() { 1664 $mode = get_user_setting( 'posts_list_mode', 'list' ); 1665 1666 $mode_class = esc_attr( 'table-view-' . $mode ); 1667 1668 return array( 'widefat', 'fixed', 'striped', $mode_class, $this->_args['plural'] ); 1669 } 1670 1671 /** 1672 * Generates the table navigation above or below the table. 1673 * 1674 * @since 3.1.0 1675 * 1676 * @param string $which The location of the navigation: Either 'top' or 'bottom'. 1677 */ 1678 protected function display_tablenav( $which ) { 1679 if ( 'bottom' === $which && ! $this->has_items() ) { 1680 return; 1681 } 1682 if ( 'top' === $which ) { 1683 wp_nonce_field( 'bulk-' . $this->_args['plural'] ); 1684 } 1685 ?> 1686 <div class="tablenav <?php echo esc_attr( $which ); ?>"> 1687 1688 <?php if ( $this->has_items() ) : ?> 1689 <div class="alignleft actions bulkactions"> 1690 <?php $this->bulk_actions( $which ); ?> 1691 </div> 1692 <?php 1693 endif; 1694 $this->extra_tablenav( $which ); 1695 $this->pagination( $which ); 1696 ?> 1697 1698 <br class="clear" /> 1699 </div> 1700 <?php 1701 } 1702 1703 /** 1704 * Displays extra controls between bulk actions and pagination. 1705 * 1706 * @since 3.1.0 1707 * 1708 * @param string $which The location: 'top' or 'bottom'. 1709 */ 1710 protected function extra_tablenav( $which ) {} 1711 1712 /** 1713 * Generates the tbody element for the list table. 1714 * 1715 * @since 3.1.0 1716 */ 1717 public function display_rows_or_placeholder() { 1718 if ( $this->has_items() ) { 1719 $this->display_rows(); 1720 } else { 1721 echo '<tr class="no-items"><td class="colspanchange" colspan="' . $this->get_column_count() . '">'; 1722 $this->no_items(); 1723 echo '</td></tr>'; 1724 } 1725 } 1726 1727 /** 1728 * Generates the list table rows. 1729 * 1730 * @since 3.1.0 1731 */ 1732 public function display_rows() { 1733 foreach ( $this->items as $item ) { 1734 $this->single_row( $item ); 1735 } 1736 } 1737 1738 /** 1739 * Generates content for a single row of the table. 1740 * 1741 * @since 3.1.0 1742 * 1743 * @param object|array $item The current item 1744 */ 1745 public function single_row( $item ) { 1746 echo '<tr>'; 1747 $this->single_row_columns( $item ); 1748 echo '</tr>'; 1749 } 1750 1751 /** 1752 * Handles an unknown column. 1753 * 1754 * @since 4.2.0 1755 * 1756 * @param object|array $item The current item. 1757 * @param string $column_name Name of the column. 1758 */ 1759 protected function column_default( $item, $column_name ) {} 1760 1761 /** 1762 * Handles the checkbox column output. 1763 * 1764 * @since 4.2.0 1765 * 1766 * @param object|array $item The current item. 1767 */ 1768 protected function column_cb( $item ) {} 1769 1770 /** 1771 * Generates the columns for a single row of the table. 1772 * 1773 * @since 3.1.0 1774 * 1775 * @param object|array $item The current item. 1776 */ 1777 protected function single_row_columns( $item ) { 1778 list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info(); 1779 1780 foreach ( $columns as $column_name => $column_display_name ) { 1781 $classes = "$column_name column-$column_name"; 1782 if ( $primary === $column_name ) { 1783 $classes .= ' has-row-actions column-primary'; 1784 } 1785 1786 if ( in_array( $column_name, $hidden, true ) ) { 1787 $classes .= ' hidden'; 1788 } 1789 1790 /* 1791 * Comments column uses HTML in the display name with screen reader text. 1792 * Strip tags to get closer to a user-friendly string. 1793 */ 1794 $data = 'data-colname="' . esc_attr( wp_strip_all_tags( $column_display_name ) ) . '"'; 1795 1796 $attributes = "class='$classes' $data"; 1797 1798 if ( 'cb' === $column_name ) { 1799 echo '<th scope="row" class="check-column">'; 1800 echo $this->column_cb( $item ); 1801 echo '</th>'; 1802 } elseif ( method_exists( $this, '_column_' . $column_name ) ) { 1803 echo call_user_func( 1804 array( $this, '_column_' . $column_name ), 1805 $item, 1806 $classes, 1807 $data, 1808 $primary 1809 ); 1810 } elseif ( method_exists( $this, 'column_' . $column_name ) ) { 1811 echo "<td $attributes>"; 1812 echo call_user_func( array( $this, 'column_' . $column_name ), $item ); 1813 echo $this->handle_row_actions( $item, $column_name, $primary ); 1814 echo '</td>'; 1815 } else { 1816 echo "<td $attributes>"; 1817 echo $this->column_default( $item, $column_name ); 1818 echo $this->handle_row_actions( $item, $column_name, $primary ); 1819 echo '</td>'; 1820 } 1821 } 1822 } 1823 1824 /** 1825 * Generates and display row actions links for the list table. 1826 * 1827 * @since 4.3.0 1828 * 1829 * @param object|array $item The item being acted upon. 1830 * @param string $column_name Current column name. 1831 * @param string $primary Primary column name. 1832 * @return string The row actions HTML, or an empty string 1833 * if the current column is not the primary column. 1834 */ 1835 protected function handle_row_actions( $item, $column_name, $primary ) { 1836 return $column_name === $primary ? '<button type="button" class="toggle-row"><span class="screen-reader-text">' . 1837 /* translators: Hidden accessibility text. */ 1838 __( 'Show more details' ) . 1839 '</span></button>' : ''; 1840 } 1841 1842 /** 1843 * Handles an incoming ajax request (called from admin-ajax.php) 1844 * 1845 * @since 3.1.0 1846 */ 1847 public function ajax_response() { 1848 $this->prepare_items(); 1849 1850 ob_start(); 1851 if ( ! empty( $_REQUEST['no_placeholder'] ) ) { 1852 $this->display_rows(); 1853 } else { 1854 $this->display_rows_or_placeholder(); 1855 } 1856 1857 $rows = ob_get_clean(); 1858 1859 $response = array( 'rows' => $rows ); 1860 1861 if ( isset( $this->_pagination_args['total_items'] ) ) { 1862 $response['total_items_i18n'] = sprintf( 1863 /* translators: Number of items. */ 1864 _n( '%s item', '%s items', $this->_pagination_args['total_items'] ), 1865 number_format_i18n( $this->_pagination_args['total_items'] ) 1866 ); 1867 } 1868 if ( isset( $this->_pagination_args['total_pages'] ) ) { 1869 $response['total_pages'] = $this->_pagination_args['total_pages']; 1870 $response['total_pages_i18n'] = number_format_i18n( $this->_pagination_args['total_pages'] ); 1871 } 1872 1873 die( wp_json_encode( $response ) ); 1874 } 1875 1876 /** 1877 * Sends required variables to JavaScript land. 1878 * 1879 * @since 3.1.0 1880 */ 1881 public function _js_vars() { 1882 $args = array( 1883 'class' => get_class( $this ), 1884 'screen' => array( 1885 'id' => $this->screen->id, 1886 'base' => $this->screen->base, 1887 ), 1888 ); 1889 1890 printf( "<script>list_args = %s;</script>\n", wp_json_encode( $args, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ) ); 1891 } 1892 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Fri Jun 26 08:20:11 2026 | Cross-referenced by PHPXref |