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