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