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