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