[ 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'] = (int) ceil( $args['total_items'] / $args['per_page'] ); 323 } 324 325 // Redirect if page number is invalid and headers are not already sent. 326 if ( ! headers_sent() && ! wp_doing_ajax() && $args['total_pages'] > 0 && $this->get_pagenum() > $args['total_pages'] ) { 327 wp_redirect( add_query_arg( 'paged', $args['total_pages'] ) ); 328 exit; 329 } 330 331 $this->_pagination_args = $args; 332 } 333 334 /** 335 * Access the pagination args. 336 * 337 * @since 3.1.0 338 * 339 * @param string $key Pagination argument to retrieve. Common values include 'total_items', 340 * 'total_pages', 'per_page', or 'infinite_scroll'. 341 * @return int Number of items that correspond to the given pagination argument. 342 */ 343 public function get_pagination_arg( $key ) { 344 if ( 'page' === $key ) { 345 return $this->get_pagenum(); 346 } 347 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: Either '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 $post_object = get_post( $post_id ); 832 $edit_post_cap = $post_object ? 'edit_post' : 'edit_posts'; 833 834 if ( ! current_user_can( $edit_post_cap, $post_id ) 835 && ( post_password_required( $post_id ) 836 || ! current_user_can( 'read_post', $post_id ) ) 837 ) { 838 // The user has no access to the post and thus cannot see the comments. 839 return false; 840 } 841 842 $approved_comments = get_comments_number(); 843 844 $approved_comments_number = number_format_i18n( $approved_comments ); 845 $pending_comments_number = number_format_i18n( $pending_comments ); 846 847 $approved_only_phrase = sprintf( 848 /* translators: %s: Number of comments. */ 849 _n( '%s comment', '%s comments', $approved_comments ), 850 $approved_comments_number 851 ); 852 853 $approved_phrase = sprintf( 854 /* translators: %s: Number of comments. */ 855 _n( '%s approved comment', '%s approved comments', $approved_comments ), 856 $approved_comments_number 857 ); 858 859 $pending_phrase = sprintf( 860 /* translators: %s: Number of comments. */ 861 _n( '%s pending comment', '%s pending comments', $pending_comments ), 862 $pending_comments_number 863 ); 864 865 if ( ! $approved_comments && ! $pending_comments ) { 866 // No comments at all. 867 printf( 868 '<span aria-hidden="true">—</span>' . 869 '<span class="screen-reader-text">%s</span>', 870 __( 'No comments' ) 871 ); 872 } elseif ( $approved_comments && 'trash' === get_post_status( $post_id ) ) { 873 // Don't link the comment bubble for a trashed post. 874 printf( 875 '<span class="post-com-count post-com-count-approved">' . 876 '<span class="comment-count-approved" aria-hidden="true">%s</span>' . 877 '<span class="screen-reader-text">%s</span>' . 878 '</span>', 879 $approved_comments_number, 880 $pending_comments ? $approved_phrase : $approved_only_phrase 881 ); 882 } elseif ( $approved_comments ) { 883 // Link the comment bubble to approved comments. 884 printf( 885 '<a href="%s" class="post-com-count post-com-count-approved">' . 886 '<span class="comment-count-approved" aria-hidden="true">%s</span>' . 887 '<span class="screen-reader-text">%s</span>' . 888 '</a>', 889 esc_url( 890 add_query_arg( 891 array( 892 'p' => $post_id, 893 'comment_status' => 'approved', 894 ), 895 admin_url( 'edit-comments.php' ) 896 ) 897 ), 898 $approved_comments_number, 899 $pending_comments ? $approved_phrase : $approved_only_phrase 900 ); 901 } else { 902 // Don't link the comment bubble when there are no approved comments. 903 printf( 904 '<span class="post-com-count post-com-count-no-comments">' . 905 '<span class="comment-count comment-count-no-comments" aria-hidden="true">%s</span>' . 906 '<span class="screen-reader-text">%s</span>' . 907 '</span>', 908 $approved_comments_number, 909 $pending_comments ? 910 /* translators: Hidden accessibility text. */ 911 __( 'No approved comments' ) : 912 /* translators: Hidden accessibility text. */ 913 __( 'No comments' ) 914 ); 915 } 916 917 if ( $pending_comments ) { 918 printf( 919 '<a href="%s" class="post-com-count post-com-count-pending">' . 920 '<span class="comment-count-pending" aria-hidden="true">%s</span>' . 921 '<span class="screen-reader-text">%s</span>' . 922 '</a>', 923 esc_url( 924 add_query_arg( 925 array( 926 'p' => $post_id, 927 'comment_status' => 'moderated', 928 ), 929 admin_url( 'edit-comments.php' ) 930 ) 931 ), 932 $pending_comments_number, 933 $pending_phrase 934 ); 935 } else { 936 printf( 937 '<span class="post-com-count post-com-count-pending post-com-count-no-pending">' . 938 '<span class="comment-count comment-count-no-pending" aria-hidden="true">%s</span>' . 939 '<span class="screen-reader-text">%s</span>' . 940 '</span>', 941 $pending_comments_number, 942 $approved_comments ? 943 /* translators: Hidden accessibility text. */ 944 __( 'No pending comments' ) : 945 /* translators: Hidden accessibility text. */ 946 __( 'No comments' ) 947 ); 948 } 949 } 950 951 /** 952 * Gets the current page number. 953 * 954 * @since 3.1.0 955 * 956 * @return int 957 */ 958 public function get_pagenum() { 959 $pagenum = isset( $_REQUEST['paged'] ) ? absint( $_REQUEST['paged'] ) : 0; 960 961 if ( isset( $this->_pagination_args['total_pages'] ) && $pagenum > $this->_pagination_args['total_pages'] ) { 962 $pagenum = $this->_pagination_args['total_pages']; 963 } 964 965 return max( 1, $pagenum ); 966 } 967 968 /** 969 * Gets the number of items to display on a single page. 970 * 971 * @since 3.1.0 972 * 973 * @param string $option User option name. 974 * @param int $default_value Optional. The number of items to display. Default 20. 975 * @return int 976 */ 977 protected function get_items_per_page( $option, $default_value = 20 ) { 978 $per_page = (int) get_user_option( $option ); 979 if ( empty( $per_page ) || $per_page < 1 ) { 980 $per_page = $default_value; 981 } 982 983 /** 984 * Filters the number of items to be displayed on each page of the list table. 985 * 986 * The dynamic hook name, `$option`, refers to the `per_page` option depending 987 * on the type of list table in use. Possible filter names include: 988 * 989 * - `edit_comments_per_page` 990 * - `sites_network_per_page` 991 * - `site_themes_network_per_page` 992 * - `themes_network_per_page'` 993 * - `users_network_per_page` 994 * - `edit_post_per_page` 995 * - `edit_page_per_page'` 996 * - `edit_{$post_type}_per_page` 997 * - `edit_post_tag_per_page` 998 * - `edit_category_per_page` 999 * - `edit_{$taxonomy}_per_page` 1000 * - `site_users_network_per_page` 1001 * - `users_per_page` 1002 * 1003 * @since 2.9.0 1004 * 1005 * @param int $per_page Number of items to be displayed. Default 20. 1006 */ 1007 return (int) apply_filters( "{$option}", $per_page ); 1008 } 1009 1010 /** 1011 * Displays the pagination. 1012 * 1013 * @since 3.1.0 1014 * 1015 * @param string $which The location of the pagination: Either 'top' or 'bottom'. 1016 */ 1017 protected function pagination( $which ) { 1018 if ( empty( $this->_pagination_args ) ) { 1019 return; 1020 } 1021 1022 $total_items = $this->_pagination_args['total_items']; 1023 $total_pages = $this->_pagination_args['total_pages']; 1024 $infinite_scroll = false; 1025 if ( isset( $this->_pagination_args['infinite_scroll'] ) ) { 1026 $infinite_scroll = $this->_pagination_args['infinite_scroll']; 1027 } 1028 1029 if ( 'top' === $which && $total_pages > 1 ) { 1030 $this->screen->render_screen_reader_content( 'heading_pagination' ); 1031 } 1032 1033 $output = '<span class="displaying-num">' . sprintf( 1034 /* translators: %s: Number of items. */ 1035 _n( '%s item', '%s items', $total_items ), 1036 number_format_i18n( $total_items ) 1037 ) . '</span>'; 1038 1039 $current = $this->get_pagenum(); 1040 $removable_query_args = wp_removable_query_args(); 1041 1042 $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); 1043 1044 $current_url = remove_query_arg( $removable_query_args, $current_url ); 1045 1046 $page_links = array(); 1047 1048 $total_pages_before = '<span class="paging-input">'; 1049 $total_pages_after = '</span></span>'; 1050 1051 $disable_first = false; 1052 $disable_last = false; 1053 $disable_prev = false; 1054 $disable_next = false; 1055 1056 if ( 1 == $current ) { 1057 $disable_first = true; 1058 $disable_prev = true; 1059 } 1060 if ( $total_pages == $current ) { 1061 $disable_last = true; 1062 $disable_next = true; 1063 } 1064 1065 if ( $disable_first ) { 1066 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">«</span>'; 1067 } else { 1068 $page_links[] = sprintf( 1069 "<a class='first-page button' href='%s'>" . 1070 "<span class='screen-reader-text'>%s</span>" . 1071 "<span aria-hidden='true'>%s</span>" . 1072 '</a>', 1073 esc_url( remove_query_arg( 'paged', $current_url ) ), 1074 /* translators: Hidden accessibility text. */ 1075 __( 'First page' ), 1076 '«' 1077 ); 1078 } 1079 1080 if ( $disable_prev ) { 1081 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">‹</span>'; 1082 } else { 1083 $page_links[] = sprintf( 1084 "<a class='prev-page button' href='%s'>" . 1085 "<span class='screen-reader-text'>%s</span>" . 1086 "<span aria-hidden='true'>%s</span>" . 1087 '</a>', 1088 esc_url( add_query_arg( 'paged', max( 1, $current - 1 ), $current_url ) ), 1089 /* translators: Hidden accessibility text. */ 1090 __( 'Previous page' ), 1091 '‹' 1092 ); 1093 } 1094 1095 if ( 'bottom' === $which ) { 1096 $html_current_page = $current; 1097 $total_pages_before = sprintf( 1098 '<span class="screen-reader-text">%s</span>' . 1099 '<span id="table-paging" class="paging-input">' . 1100 '<span class="tablenav-paging-text">', 1101 /* translators: Hidden accessibility text. */ 1102 __( 'Current Page' ) 1103 ); 1104 } else { 1105 $html_current_page = sprintf( 1106 '<label for="current-page-selector" class="screen-reader-text">%s</label>' . 1107 "<input class='current-page' id='current-page-selector' type='text' 1108 name='paged' value='%s' size='%d' aria-describedby='table-paging' />" . 1109 "<span class='tablenav-paging-text'>", 1110 /* translators: Hidden accessibility text. */ 1111 __( 'Current Page' ), 1112 $current, 1113 strlen( $total_pages ) 1114 ); 1115 } 1116 1117 $html_total_pages = sprintf( "<span class='total-pages'>%s</span>", number_format_i18n( $total_pages ) ); 1118 1119 $page_links[] = $total_pages_before . sprintf( 1120 /* translators: 1: Current page, 2: Total pages. */ 1121 _x( '%1$s of %2$s', 'paging' ), 1122 $html_current_page, 1123 $html_total_pages 1124 ) . $total_pages_after; 1125 1126 if ( $disable_next ) { 1127 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">›</span>'; 1128 } else { 1129 $page_links[] = sprintf( 1130 "<a class='next-page button' href='%s'>" . 1131 "<span class='screen-reader-text'>%s</span>" . 1132 "<span aria-hidden='true'>%s</span>" . 1133 '</a>', 1134 esc_url( add_query_arg( 'paged', min( $total_pages, $current + 1 ), $current_url ) ), 1135 /* translators: Hidden accessibility text. */ 1136 __( 'Next page' ), 1137 '›' 1138 ); 1139 } 1140 1141 if ( $disable_last ) { 1142 $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">»</span>'; 1143 } else { 1144 $page_links[] = sprintf( 1145 "<a class='last-page button' href='%s'>" . 1146 "<span class='screen-reader-text'>%s</span>" . 1147 "<span aria-hidden='true'>%s</span>" . 1148 '</a>', 1149 esc_url( add_query_arg( 'paged', $total_pages, $current_url ) ), 1150 /* translators: Hidden accessibility text. */ 1151 __( 'Last page' ), 1152 '»' 1153 ); 1154 } 1155 1156 $pagination_links_class = 'pagination-links'; 1157 if ( ! empty( $infinite_scroll ) ) { 1158 $pagination_links_class .= ' hide-if-js'; 1159 } 1160 $output .= "\n<span class='$pagination_links_class'>" . implode( "\n", $page_links ) . '</span>'; 1161 1162 if ( $total_pages ) { 1163 $page_class = $total_pages < 2 ? ' one-page' : ''; 1164 } else { 1165 $page_class = ' no-pages'; 1166 } 1167 $this->_pagination = "<div class='tablenav-pages{$page_class}'>$output</div>"; 1168 1169 echo $this->_pagination; 1170 } 1171 1172 /** 1173 * Gets a list of columns. 1174 * 1175 * The format is: 1176 * - `'internal-name' => 'Title'` 1177 * 1178 * @since 3.1.0 1179 * @abstract 1180 * 1181 * @return array 1182 */ 1183 public function get_columns() { 1184 die( 'function WP_List_Table::get_columns() must be overridden in a subclass.' ); 1185 } 1186 1187 /** 1188 * Gets a list of sortable columns. 1189 * 1190 * The format is: 1191 * - `'internal-name' => 'orderby'` 1192 * - `'internal-name' => array( 'orderby', bool, 'abbr', 'orderby-text', 'initially-sorted-column-order' )` - 1193 * - `'internal-name' => array( 'orderby', 'asc' )` - The second element sets the initial sorting order. 1194 * - `'internal-name' => array( 'orderby', true )` - The second element makes the initial order descending. 1195 * 1196 * In the second format, passing true as second parameter will make the initial 1197 * sorting order be descending. Following parameters add a short column name to 1198 * be used as 'abbr' attribute, a translatable string for the current sorting, 1199 * and the initial order for the initial sorted column, 'asc' or 'desc' (default: false). 1200 * 1201 * @since 3.1.0 1202 * @since 6.3.0 Added 'abbr', 'orderby-text' and 'initially-sorted-column-order'. 1203 * 1204 * @return array 1205 */ 1206 protected function get_sortable_columns() { 1207 return array(); 1208 } 1209 1210 /** 1211 * Gets the name of the default primary column. 1212 * 1213 * @since 4.3.0 1214 * 1215 * @return string Name of the default primary column, in this case, an empty string. 1216 */ 1217 protected function get_default_primary_column_name() { 1218 $columns = $this->get_columns(); 1219 $column = ''; 1220 1221 if ( empty( $columns ) ) { 1222 return $column; 1223 } 1224 1225 /* 1226 * We need a primary defined so responsive views show something, 1227 * so let's fall back to the first non-checkbox column. 1228 */ 1229 foreach ( $columns as $col => $column_name ) { 1230 if ( 'cb' === $col ) { 1231 continue; 1232 } 1233 1234 $column = $col; 1235 break; 1236 } 1237 1238 return $column; 1239 } 1240 1241 /** 1242 * Gets the name of the primary column. 1243 * 1244 * Public wrapper for WP_List_Table::get_default_primary_column_name(). 1245 * 1246 * @since 4.4.0 1247 * 1248 * @return string Name of the default primary column. 1249 */ 1250 public function get_primary_column() { 1251 return $this->get_primary_column_name(); 1252 } 1253 1254 /** 1255 * Gets the name of the primary column. 1256 * 1257 * @since 4.3.0 1258 * 1259 * @return string The name of the primary column. 1260 */ 1261 protected function get_primary_column_name() { 1262 $columns = get_column_headers( $this->screen ); 1263 $default = $this->get_default_primary_column_name(); 1264 1265 /* 1266 * If the primary column doesn't exist, 1267 * fall back to the first non-checkbox column. 1268 */ 1269 if ( ! isset( $columns[ $default ] ) ) { 1270 $default = self::get_default_primary_column_name(); 1271 } 1272 1273 /** 1274 * Filters the name of the primary column for the current list table. 1275 * 1276 * @since 4.3.0 1277 * 1278 * @param string $default Column name default for the specific list table, e.g. 'name'. 1279 * @param string $context Screen ID for specific list table, e.g. 'plugins'. 1280 */ 1281 $column = apply_filters( 'list_table_primary_column', $default, $this->screen->id ); 1282 1283 if ( empty( $column ) || ! isset( $columns[ $column ] ) ) { 1284 $column = $default; 1285 } 1286 1287 return $column; 1288 } 1289 1290 /** 1291 * Gets a list of all, hidden, and sortable columns, with filter applied. 1292 * 1293 * @since 3.1.0 1294 * 1295 * @return array 1296 */ 1297 protected function get_column_info() { 1298 // $_column_headers is already set / cached. 1299 if ( 1300 isset( $this->_column_headers ) && 1301 is_array( $this->_column_headers ) 1302 ) { 1303 /* 1304 * Backward compatibility for `$_column_headers` format prior to WordPress 4.3. 1305 * 1306 * In WordPress 4.3 the primary column name was added as a fourth item in the 1307 * column headers property. This ensures the primary column name is included 1308 * in plugins setting the property directly in the three item format. 1309 */ 1310 if ( 4 === count( $this->_column_headers ) ) { 1311 return $this->_column_headers; 1312 } 1313 1314 $column_headers = array( array(), array(), array(), $this->get_primary_column_name() ); 1315 foreach ( $this->_column_headers as $key => $value ) { 1316 $column_headers[ $key ] = $value; 1317 } 1318 1319 $this->_column_headers = $column_headers; 1320 1321 return $this->_column_headers; 1322 } 1323 1324 $columns = get_column_headers( $this->screen ); 1325 $hidden = get_hidden_columns( $this->screen ); 1326 1327 $sortable_columns = $this->get_sortable_columns(); 1328 /** 1329 * Filters the list table sortable columns for a specific screen. 1330 * 1331 * The dynamic portion of the hook name, `$this->screen->id`, refers 1332 * to the ID of the current screen. 1333 * 1334 * @since 3.1.0 1335 * 1336 * @param array $sortable_columns An array of sortable columns. 1337 */ 1338 $_sortable = apply_filters( "manage_{$this->screen->id}_sortable_columns", $sortable_columns ); 1339 1340 $sortable = array(); 1341 foreach ( $_sortable as $id => $data ) { 1342 if ( empty( $data ) ) { 1343 continue; 1344 } 1345 1346 $data = (array) $data; 1347 // Descending initial sorting. 1348 if ( ! isset( $data[1] ) ) { 1349 $data[1] = false; 1350 } 1351 // Current sorting translatable string. 1352 if ( ! isset( $data[2] ) ) { 1353 $data[2] = ''; 1354 } 1355 // Initial view sorted column and asc/desc order, default: false. 1356 if ( ! isset( $data[3] ) ) { 1357 $data[3] = false; 1358 } 1359 // Initial order for the initial sorted column, default: false. 1360 if ( ! isset( $data[4] ) ) { 1361 $data[4] = false; 1362 } 1363 1364 $sortable[ $id ] = $data; 1365 } 1366 1367 $primary = $this->get_primary_column_name(); 1368 $this->_column_headers = array( $columns, $hidden, $sortable, $primary ); 1369 1370 return $this->_column_headers; 1371 } 1372 1373 /** 1374 * Returns the number of visible columns. 1375 * 1376 * @since 3.1.0 1377 * 1378 * @return int 1379 */ 1380 public function get_column_count() { 1381 list ( $columns, $hidden ) = $this->get_column_info(); 1382 $hidden = array_intersect( array_keys( $columns ), array_filter( $hidden ) ); 1383 return count( $columns ) - count( $hidden ); 1384 } 1385 1386 /** 1387 * Prints column headers, accounting for hidden and sortable columns. 1388 * 1389 * @since 3.1.0 1390 * 1391 * @param bool $with_id Whether to set the ID attribute or not 1392 */ 1393 public function print_column_headers( $with_id = true ) { 1394 list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info(); 1395 1396 $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); 1397 $current_url = remove_query_arg( 'paged', $current_url ); 1398 1399 // When users click on a column header to sort by other columns. 1400 if ( isset( $_GET['orderby'] ) ) { 1401 $current_orderby = $_GET['orderby']; 1402 // In the initial view there's no orderby parameter. 1403 } else { 1404 $current_orderby = ''; 1405 } 1406 1407 // Not in the initial view and descending order. 1408 if ( isset( $_GET['order'] ) && 'desc' === $_GET['order'] ) { 1409 $current_order = 'desc'; 1410 } else { 1411 // The initial view is not always 'asc', we'll take care of this below. 1412 $current_order = 'asc'; 1413 } 1414 1415 if ( ! empty( $columns['cb'] ) ) { 1416 static $cb_counter = 1; 1417 $columns['cb'] = '<input id="cb-select-all-' . $cb_counter . '" type="checkbox" /> 1418 <label for="cb-select-all-' . $cb_counter . '">' . 1419 '<span class="screen-reader-text">' . 1420 /* translators: Hidden accessibility text. */ 1421 __( 'Select All' ) . 1422 '</span>' . 1423 '</label>'; 1424 ++$cb_counter; 1425 } 1426 1427 foreach ( $columns as $column_key => $column_display_name ) { 1428 $class = array( 'manage-column', "column-$column_key" ); 1429 $aria_sort_attr = ''; 1430 $abbr_attr = ''; 1431 $order_text = ''; 1432 1433 if ( in_array( $column_key, $hidden, true ) ) { 1434 $class[] = 'hidden'; 1435 } 1436 1437 if ( 'cb' === $column_key ) { 1438 $class[] = 'check-column'; 1439 } elseif ( in_array( $column_key, array( 'posts', 'comments', 'links' ), true ) ) { 1440 $class[] = 'num'; 1441 } 1442 1443 if ( $column_key === $primary ) { 1444 $class[] = 'column-primary'; 1445 } 1446 1447 if ( isset( $sortable[ $column_key ] ) ) { 1448 $orderby = isset( $sortable[ $column_key ][0] ) ? $sortable[ $column_key ][0] : ''; 1449 $desc_first = isset( $sortable[ $column_key ][1] ) ? $sortable[ $column_key ][1] : false; 1450 $abbr = isset( $sortable[ $column_key ][2] ) ? $sortable[ $column_key ][2] : ''; 1451 $orderby_text = isset( $sortable[ $column_key ][3] ) ? $sortable[ $column_key ][3] : ''; 1452 $initial_order = isset( $sortable[ $column_key ][4] ) ? $sortable[ $column_key ][4] : ''; 1453 1454 /* 1455 * We're in the initial view and there's no $_GET['orderby'] then check if the 1456 * initial sorting information is set in the sortable columns and use that. 1457 */ 1458 if ( '' === $current_orderby && $initial_order ) { 1459 // Use the initially sorted column $orderby as current orderby. 1460 $current_orderby = $orderby; 1461 // Use the initially sorted column asc/desc order as initial order. 1462 $current_order = $initial_order; 1463 } 1464 1465 /* 1466 * True in the initial view when an initial orderby is set via get_sortable_columns() 1467 * and true in the sorted views when the actual $_GET['orderby'] is equal to $orderby. 1468 */ 1469 if ( $current_orderby === $orderby ) { 1470 // The sorted column. The `aria-sort` attribute must be set only on the sorted column. 1471 if ( 'asc' === $current_order ) { 1472 $order = 'desc'; 1473 $aria_sort_attr = ' aria-sort="ascending"'; 1474 } else { 1475 $order = 'asc'; 1476 $aria_sort_attr = ' aria-sort="descending"'; 1477 } 1478 1479 $class[] = 'sorted'; 1480 $class[] = $current_order; 1481 } else { 1482 // The other sortable columns. 1483 $order = strtolower( $desc_first ); 1484 1485 if ( ! in_array( $order, array( 'desc', 'asc' ), true ) ) { 1486 $order = $desc_first ? 'desc' : 'asc'; 1487 } 1488 1489 $class[] = 'sortable'; 1490 $class[] = 'desc' === $order ? 'asc' : 'desc'; 1491 1492 /* translators: Hidden accessibility text. */ 1493 $asc_text = __( 'Sort ascending.' ); 1494 /* translators: Hidden accessibility text. */ 1495 $desc_text = __( 'Sort descending.' ); 1496 $order_text = 'asc' === $order ? $asc_text : $desc_text; 1497 } 1498 1499 if ( '' !== $order_text ) { 1500 $order_text = ' <span class="screen-reader-text">' . $order_text . '</span>'; 1501 } 1502 1503 // Print an 'abbr' attribute if a value is provided via get_sortable_columns(). 1504 $abbr_attr = $abbr ? ' abbr="' . esc_attr( $abbr ) . '"' : ''; 1505 1506 $column_display_name = sprintf( 1507 '<a href="%1$s">' . 1508 '<span>%2$s</span>' . 1509 '<span class="sorting-indicators">' . 1510 '<span class="sorting-indicator asc" aria-hidden="true"></span>' . 1511 '<span class="sorting-indicator desc" aria-hidden="true"></span>' . 1512 '</span>' . 1513 '%3$s' . 1514 '</a>', 1515 esc_url( add_query_arg( compact( 'orderby', 'order' ), $current_url ) ), 1516 $column_display_name, 1517 $order_text 1518 ); 1519 } 1520 1521 $tag = ( 'cb' === $column_key ) ? 'td' : 'th'; 1522 $scope = ( 'th' === $tag ) ? 'scope="col"' : ''; 1523 $id = $with_id ? "id='$column_key'" : ''; 1524 1525 if ( ! empty( $class ) ) { 1526 $class = "class='" . implode( ' ', $class ) . "'"; 1527 } 1528 1529 echo "<$tag $scope $id $class $aria_sort_attr $abbr_attr>$column_display_name</$tag>"; 1530 } 1531 } 1532 1533 /** 1534 * Print a table description with information about current sorting and order. 1535 * 1536 * For the table initial view, information about initial orderby and order 1537 * should be provided via get_sortable_columns(). 1538 * 1539 * @since 6.3.0 1540 * @access public 1541 */ 1542 public function print_table_description() { 1543 list( $columns, $hidden, $sortable ) = $this->get_column_info(); 1544 1545 if ( empty( $sortable ) ) { 1546 return; 1547 } 1548 1549 // When users click on a column header to sort by other columns. 1550 if ( isset( $_GET['orderby'] ) ) { 1551 $current_orderby = $_GET['orderby']; 1552 // In the initial view there's no orderby parameter. 1553 } else { 1554 $current_orderby = ''; 1555 } 1556 1557 // Not in the initial view and descending order. 1558 if ( isset( $_GET['order'] ) && 'desc' === $_GET['order'] ) { 1559 $current_order = 'desc'; 1560 } else { 1561 // The initial view is not always 'asc', we'll take care of this below. 1562 $current_order = 'asc'; 1563 } 1564 1565 foreach ( array_keys( $columns ) as $column_key ) { 1566 1567 if ( isset( $sortable[ $column_key ] ) ) { 1568 $orderby = isset( $sortable[ $column_key ][0] ) ? $sortable[ $column_key ][0] : ''; 1569 $desc_first = isset( $sortable[ $column_key ][1] ) ? $sortable[ $column_key ][1] : false; 1570 $abbr = isset( $sortable[ $column_key ][2] ) ? $sortable[ $column_key ][2] : ''; 1571 $orderby_text = isset( $sortable[ $column_key ][3] ) ? $sortable[ $column_key ][3] : ''; 1572 $initial_order = isset( $sortable[ $column_key ][4] ) ? $sortable[ $column_key ][4] : ''; 1573 1574 if ( ! is_string( $orderby_text ) || '' === $orderby_text ) { 1575 return; 1576 } 1577 /* 1578 * We're in the initial view and there's no $_GET['orderby'] then check if the 1579 * initial sorting information is set in the sortable columns and use that. 1580 */ 1581 if ( '' === $current_orderby && $initial_order ) { 1582 // Use the initially sorted column $orderby as current orderby. 1583 $current_orderby = $orderby; 1584 // Use the initially sorted column asc/desc order as initial order. 1585 $current_order = $initial_order; 1586 } 1587 1588 /* 1589 * True in the initial view when an initial orderby is set via get_sortable_columns() 1590 * and true in the sorted views when the actual $_GET['orderby'] is equal to $orderby. 1591 */ 1592 if ( $current_orderby === $orderby ) { 1593 /* translators: Hidden accessibility text. */ 1594 $asc_text = __( 'Ascending.' ); 1595 /* translators: Hidden accessibility text. */ 1596 $desc_text = __( 'Descending.' ); 1597 $order_text = 'asc' === $current_order ? $asc_text : $desc_text; 1598 echo '<caption class="screen-reader-text">' . $orderby_text . ' ' . $order_text . '</caption>'; 1599 1600 return; 1601 } 1602 } 1603 } 1604 } 1605 1606 /** 1607 * Displays the table. 1608 * 1609 * @since 3.1.0 1610 */ 1611 public function display() { 1612 $singular = $this->_args['singular']; 1613 1614 $this->display_tablenav( 'top' ); 1615 1616 $this->screen->render_screen_reader_content( 'heading_list' ); 1617 ?> 1618 <table class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>"> 1619 <?php $this->print_table_description(); ?> 1620 <thead> 1621 <tr> 1622 <?php $this->print_column_headers(); ?> 1623 </tr> 1624 </thead> 1625 1626 <tbody id="the-list" 1627 <?php 1628 if ( $singular ) { 1629 echo " data-wp-lists='list:$singular'"; 1630 } 1631 ?> 1632 > 1633 <?php $this->display_rows_or_placeholder(); ?> 1634 </tbody> 1635 1636 <tfoot> 1637 <tr> 1638 <?php $this->print_column_headers( false ); ?> 1639 </tr> 1640 </tfoot> 1641 1642 </table> 1643 <?php 1644 $this->display_tablenav( 'bottom' ); 1645 } 1646 1647 /** 1648 * Gets a list of CSS classes for the WP_List_Table table tag. 1649 * 1650 * @since 3.1.0 1651 * 1652 * @return string[] Array of CSS classes for the table tag. 1653 */ 1654 protected function get_table_classes() { 1655 $mode = get_user_setting( 'posts_list_mode', 'list' ); 1656 1657 $mode_class = esc_attr( 'table-view-' . $mode ); 1658 1659 return array( 'widefat', 'fixed', 'striped', $mode_class, $this->_args['plural'] ); 1660 } 1661 1662 /** 1663 * Generates the table navigation above or below the table 1664 * 1665 * @since 3.1.0 1666 * @param string $which The location of the navigation: Either 'top' or 'bottom'. 1667 */ 1668 protected function display_tablenav( $which ) { 1669 if ( 'top' === $which ) { 1670 wp_nonce_field( 'bulk-' . $this->_args['plural'] ); 1671 } 1672 ?> 1673 <div class="tablenav <?php echo esc_attr( $which ); ?>"> 1674 1675 <?php if ( $this->has_items() ) : ?> 1676 <div class="alignleft actions bulkactions"> 1677 <?php $this->bulk_actions( $which ); ?> 1678 </div> 1679 <?php 1680 endif; 1681 $this->extra_tablenav( $which ); 1682 $this->pagination( $which ); 1683 ?> 1684 1685 <br class="clear" /> 1686 </div> 1687 <?php 1688 } 1689 1690 /** 1691 * Displays extra controls between bulk actions and pagination. 1692 * 1693 * @since 3.1.0 1694 * 1695 * @param string $which 1696 */ 1697 protected function extra_tablenav( $which ) {} 1698 1699 /** 1700 * Generates the tbody element for the list table. 1701 * 1702 * @since 3.1.0 1703 */ 1704 public function display_rows_or_placeholder() { 1705 if ( $this->has_items() ) { 1706 $this->display_rows(); 1707 } else { 1708 echo '<tr class="no-items"><td class="colspanchange" colspan="' . $this->get_column_count() . '">'; 1709 $this->no_items(); 1710 echo '</td></tr>'; 1711 } 1712 } 1713 1714 /** 1715 * Generates the table rows. 1716 * 1717 * @since 3.1.0 1718 */ 1719 public function display_rows() { 1720 foreach ( $this->items as $item ) { 1721 $this->single_row( $item ); 1722 } 1723 } 1724 1725 /** 1726 * Generates content for a single row of the table. 1727 * 1728 * @since 3.1.0 1729 * 1730 * @param object|array $item The current item 1731 */ 1732 public function single_row( $item ) { 1733 echo '<tr>'; 1734 $this->single_row_columns( $item ); 1735 echo '</tr>'; 1736 } 1737 1738 /** 1739 * @param object|array $item 1740 * @param string $column_name 1741 */ 1742 protected function column_default( $item, $column_name ) {} 1743 1744 /** 1745 * @param object|array $item 1746 */ 1747 protected function column_cb( $item ) {} 1748 1749 /** 1750 * Generates the columns for a single row of the table. 1751 * 1752 * @since 3.1.0 1753 * 1754 * @param object|array $item The current item. 1755 */ 1756 protected function single_row_columns( $item ) { 1757 list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info(); 1758 1759 foreach ( $columns as $column_name => $column_display_name ) { 1760 $classes = "$column_name column-$column_name"; 1761 if ( $primary === $column_name ) { 1762 $classes .= ' has-row-actions column-primary'; 1763 } 1764 1765 if ( in_array( $column_name, $hidden, true ) ) { 1766 $classes .= ' hidden'; 1767 } 1768 1769 /* 1770 * Comments column uses HTML in the display name with screen reader text. 1771 * Strip tags to get closer to a user-friendly string. 1772 */ 1773 $data = 'data-colname="' . esc_attr( wp_strip_all_tags( $column_display_name ) ) . '"'; 1774 1775 $attributes = "class='$classes' $data"; 1776 1777 if ( 'cb' === $column_name ) { 1778 echo '<th scope="row" class="check-column">'; 1779 echo $this->column_cb( $item ); 1780 echo '</th>'; 1781 } elseif ( method_exists( $this, '_column_' . $column_name ) ) { 1782 echo call_user_func( 1783 array( $this, '_column_' . $column_name ), 1784 $item, 1785 $classes, 1786 $data, 1787 $primary 1788 ); 1789 } elseif ( method_exists( $this, 'column_' . $column_name ) ) { 1790 echo "<td $attributes>"; 1791 echo call_user_func( array( $this, 'column_' . $column_name ), $item ); 1792 echo $this->handle_row_actions( $item, $column_name, $primary ); 1793 echo '</td>'; 1794 } else { 1795 echo "<td $attributes>"; 1796 echo $this->column_default( $item, $column_name ); 1797 echo $this->handle_row_actions( $item, $column_name, $primary ); 1798 echo '</td>'; 1799 } 1800 } 1801 } 1802 1803 /** 1804 * Generates and display row actions links for the list table. 1805 * 1806 * @since 4.3.0 1807 * 1808 * @param object|array $item The item being acted upon. 1809 * @param string $column_name Current column name. 1810 * @param string $primary Primary column name. 1811 * @return string The row actions HTML, or an empty string 1812 * if the current column is not the primary column. 1813 */ 1814 protected function handle_row_actions( $item, $column_name, $primary ) { 1815 return $column_name === $primary ? '<button type="button" class="toggle-row"><span class="screen-reader-text">' . 1816 /* translators: Hidden accessibility text. */ 1817 __( 'Show more details' ) . 1818 '</span></button>' : ''; 1819 } 1820 1821 /** 1822 * Handles an incoming ajax request (called from admin-ajax.php) 1823 * 1824 * @since 3.1.0 1825 */ 1826 public function ajax_response() { 1827 $this->prepare_items(); 1828 1829 ob_start(); 1830 if ( ! empty( $_REQUEST['no_placeholder'] ) ) { 1831 $this->display_rows(); 1832 } else { 1833 $this->display_rows_or_placeholder(); 1834 } 1835 1836 $rows = ob_get_clean(); 1837 1838 $response = array( 'rows' => $rows ); 1839 1840 if ( isset( $this->_pagination_args['total_items'] ) ) { 1841 $response['total_items_i18n'] = sprintf( 1842 /* translators: Number of items. */ 1843 _n( '%s item', '%s items', $this->_pagination_args['total_items'] ), 1844 number_format_i18n( $this->_pagination_args['total_items'] ) 1845 ); 1846 } 1847 if ( isset( $this->_pagination_args['total_pages'] ) ) { 1848 $response['total_pages'] = $this->_pagination_args['total_pages']; 1849 $response['total_pages_i18n'] = number_format_i18n( $this->_pagination_args['total_pages'] ); 1850 } 1851 1852 die( wp_json_encode( $response ) ); 1853 } 1854 1855 /** 1856 * Sends required variables to JavaScript land. 1857 * 1858 * @since 3.1.0 1859 */ 1860 public function _js_vars() { 1861 $args = array( 1862 'class' => get_class( $this ), 1863 'screen' => array( 1864 'id' => $this->screen->id, 1865 'base' => $this->screen->base, 1866 ), 1867 ); 1868 1869 printf( "<script type='text/javascript'>list_args = %s;</script>\n", wp_json_encode( $args ) ); 1870 } 1871 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated : Tue Mar 19 08:20:01 2024 | Cross-referenced by PHPXref |