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