[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-admin/includes/ -> class-wp-list-table.php (source)

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


Generated : Wed Nov 4 08:20:38 2026 Cross-referenced by PHPXref