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