balmet.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

class-wp-list-table.php (41597B)


      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  * @access private
     15  */
     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 	 * Make private properties readable for backward compatibility.
    177 	 *
    178 	 * @since 4.0.0
    179 	 *
    180 	 * @param string $name Property to get.
    181 	 * @return mixed Property.
    182 	 */
    183 	public function __get( $name ) {
    184 		if ( in_array( $name, $this->compat_fields, true ) ) {
    185 			return $this->$name;
    186 		}
    187 	}
    188 
    189 	/**
    190 	 * Make private properties settable for backward compatibility.
    191 	 *
    192 	 * @since 4.0.0
    193 	 *
    194 	 * @param string $name  Property to check if set.
    195 	 * @param mixed  $value Property value.
    196 	 * @return mixed Newly-set property.
    197 	 */
    198 	public function __set( $name, $value ) {
    199 		if ( in_array( $name, $this->compat_fields, true ) ) {
    200 			return $this->$name = $value;
    201 		}
    202 	}
    203 
    204 	/**
    205 	 * Make private properties checkable for backward compatibility.
    206 	 *
    207 	 * @since 4.0.0
    208 	 *
    209 	 * @param string $name Property to check if set.
    210 	 * @return bool Whether the property is set.
    211 	 */
    212 	public function __isset( $name ) {
    213 		if ( in_array( $name, $this->compat_fields, true ) ) {
    214 			return isset( $this->$name );
    215 		}
    216 	}
    217 
    218 	/**
    219 	 * Make private properties un-settable for backward compatibility.
    220 	 *
    221 	 * @since 4.0.0
    222 	 *
    223 	 * @param string $name Property to unset.
    224 	 */
    225 	public function __unset( $name ) {
    226 		if ( in_array( $name, $this->compat_fields, true ) ) {
    227 			unset( $this->$name );
    228 		}
    229 	}
    230 
    231 	/**
    232 	 * Make private/protected methods readable for backward compatibility.
    233 	 *
    234 	 * @since 4.0.0
    235 	 *
    236 	 * @param string $name      Method to call.
    237 	 * @param array  $arguments Arguments to pass when calling.
    238 	 * @return mixed|bool Return value of the callback, false otherwise.
    239 	 */
    240 	public function __call( $name, $arguments ) {
    241 		if ( in_array( $name, $this->compat_methods, true ) ) {
    242 			return $this->$name( ...$arguments );
    243 		}
    244 		return false;
    245 	}
    246 
    247 	/**
    248 	 * Checks the current user's permissions
    249 	 *
    250 	 * @since 3.1.0
    251 	 * @abstract
    252 	 */
    253 	public function ajax_user_can() {
    254 		die( 'function WP_List_Table::ajax_user_can() must be overridden in a subclass.' );
    255 	}
    256 
    257 	/**
    258 	 * Prepares the list of items for displaying.
    259 	 *
    260 	 * @uses WP_List_Table::set_pagination_args()
    261 	 *
    262 	 * @since 3.1.0
    263 	 * @abstract
    264 	 */
    265 	public function prepare_items() {
    266 		die( 'function WP_List_Table::prepare_items() must be overridden in a subclass.' );
    267 	}
    268 
    269 	/**
    270 	 * An internal method that sets all the necessary pagination arguments
    271 	 *
    272 	 * @since 3.1.0
    273 	 *
    274 	 * @param array|string $args Array or string of arguments with information about the pagination.
    275 	 */
    276 	protected function set_pagination_args( $args ) {
    277 		$args = wp_parse_args(
    278 			$args,
    279 			array(
    280 				'total_items' => 0,
    281 				'total_pages' => 0,
    282 				'per_page'    => 0,
    283 			)
    284 		);
    285 
    286 		if ( ! $args['total_pages'] && $args['per_page'] > 0 ) {
    287 			$args['total_pages'] = ceil( $args['total_items'] / $args['per_page'] );
    288 		}
    289 
    290 		// Redirect if page number is invalid and headers are not already sent.
    291 		if ( ! headers_sent() && ! wp_doing_ajax() && $args['total_pages'] > 0 && $this->get_pagenum() > $args['total_pages'] ) {
    292 			wp_redirect( add_query_arg( 'paged', $args['total_pages'] ) );
    293 			exit;
    294 		}
    295 
    296 		$this->_pagination_args = $args;
    297 	}
    298 
    299 	/**
    300 	 * Access the pagination args.
    301 	 *
    302 	 * @since 3.1.0
    303 	 *
    304 	 * @param string $key Pagination argument to retrieve. Common values include 'total_items',
    305 	 *                    'total_pages', 'per_page', or 'infinite_scroll'.
    306 	 * @return int Number of items that correspond to the given pagination argument.
    307 	 */
    308 	public function get_pagination_arg( $key ) {
    309 		if ( 'page' === $key ) {
    310 			return $this->get_pagenum();
    311 		}
    312 
    313 		if ( isset( $this->_pagination_args[ $key ] ) ) {
    314 			return $this->_pagination_args[ $key ];
    315 		}
    316 	}
    317 
    318 	/**
    319 	 * Whether the table has items to display or not
    320 	 *
    321 	 * @since 3.1.0
    322 	 *
    323 	 * @return bool
    324 	 */
    325 	public function has_items() {
    326 		return ! empty( $this->items );
    327 	}
    328 
    329 	/**
    330 	 * Message to be displayed when there are no items
    331 	 *
    332 	 * @since 3.1.0
    333 	 */
    334 	public function no_items() {
    335 		_e( 'No items found.' );
    336 	}
    337 
    338 	/**
    339 	 * Displays the search box.
    340 	 *
    341 	 * @since 3.1.0
    342 	 *
    343 	 * @param string $text     The 'submit' button label.
    344 	 * @param string $input_id ID attribute value for the search input field.
    345 	 */
    346 	public function search_box( $text, $input_id ) {
    347 		if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) {
    348 			return;
    349 		}
    350 
    351 		$input_id = $input_id . '-search-input';
    352 
    353 		if ( ! empty( $_REQUEST['orderby'] ) ) {
    354 			echo '<input type="hidden" name="orderby" value="' . esc_attr( $_REQUEST['orderby'] ) . '" />';
    355 		}
    356 		if ( ! empty( $_REQUEST['order'] ) ) {
    357 			echo '<input type="hidden" name="order" value="' . esc_attr( $_REQUEST['order'] ) . '" />';
    358 		}
    359 		if ( ! empty( $_REQUEST['post_mime_type'] ) ) {
    360 			echo '<input type="hidden" name="post_mime_type" value="' . esc_attr( $_REQUEST['post_mime_type'] ) . '" />';
    361 		}
    362 		if ( ! empty( $_REQUEST['detached'] ) ) {
    363 			echo '<input type="hidden" name="detached" value="' . esc_attr( $_REQUEST['detached'] ) . '" />';
    364 		}
    365 		?>
    366 <p class="search-box">
    367 	<label class="screen-reader-text" for="<?php echo esc_attr( $input_id ); ?>"><?php echo $text; ?>:</label>
    368 	<input type="search" id="<?php echo esc_attr( $input_id ); ?>" name="s" value="<?php _admin_search_query(); ?>" />
    369 		<?php submit_button( $text, '', '', false, array( 'id' => 'search-submit' ) ); ?>
    370 </p>
    371 		<?php
    372 	}
    373 
    374 	/**
    375 	 * Gets the list of views available on this table.
    376 	 *
    377 	 * The format is an associative array:
    378 	 * - `'id' => 'link'`
    379 	 *
    380 	 * @since 3.1.0
    381 	 *
    382 	 * @return array
    383 	 */
    384 	protected function get_views() {
    385 		return array();
    386 	}
    387 
    388 	/**
    389 	 * Displays the list of views available on this table.
    390 	 *
    391 	 * @since 3.1.0
    392 	 */
    393 	public function views() {
    394 		$views = $this->get_views();
    395 		/**
    396 		 * Filters the list of available list table views.
    397 		 *
    398 		 * The dynamic portion of the hook name, `$this->screen->id`, refers
    399 		 * to the ID of the current screen.
    400 		 *
    401 		 * @since 3.1.0
    402 		 *
    403 		 * @param string[] $views An array of available list table views.
    404 		 */
    405 		$views = apply_filters( "views_{$this->screen->id}", $views );
    406 
    407 		if ( empty( $views ) ) {
    408 			return;
    409 		}
    410 
    411 		$this->screen->render_screen_reader_content( 'heading_views' );
    412 
    413 		echo "<ul class='subsubsub'>\n";
    414 		foreach ( $views as $class => $view ) {
    415 			$views[ $class ] = "\t<li class='$class'>$view";
    416 		}
    417 		echo implode( " |</li>\n", $views ) . "</li>\n";
    418 		echo '</ul>';
    419 	}
    420 
    421 	/**
    422 	 * Retrieves the list of bulk actions available for this table.
    423 	 *
    424 	 * The format is an associative array where each element represents either a top level option value and label, or
    425 	 * an array representing an optgroup and its options.
    426 	 *
    427 	 * For a standard option, the array element key is the field value and the array element value is the field label.
    428 	 *
    429 	 * For an optgroup, the array element key is the label and the array element value is an associative array of
    430 	 * options as above.
    431 	 *
    432 	 * Example:
    433 	 *
    434 	 *     [
    435 	 *         'edit'         => 'Edit',
    436 	 *         'delete'       => 'Delete',
    437 	 *         'Change State' => [
    438 	 *             'feature' => 'Featured',
    439 	 *             'sale'    => 'On Sale',
    440 	 *         ]
    441 	 *     ]
    442 	 *
    443 	 * @since 3.1.0
    444 	 * @since 5.6.0 A bulk action can now contain an array of options in order to create an optgroup.
    445 	 *
    446 	 * @return array
    447 	 */
    448 	protected function get_bulk_actions() {
    449 		return array();
    450 	}
    451 
    452 	/**
    453 	 * Displays the bulk actions dropdown.
    454 	 *
    455 	 * @since 3.1.0
    456 	 *
    457 	 * @param string $which The location of the bulk actions: 'top' or 'bottom'.
    458 	 *                      This is designated as optional for backward compatibility.
    459 	 */
    460 	protected function bulk_actions( $which = '' ) {
    461 		if ( is_null( $this->_actions ) ) {
    462 			$this->_actions = $this->get_bulk_actions();
    463 
    464 			/**
    465 			 * Filters the items in the bulk actions menu of the list table.
    466 			 *
    467 			 * The dynamic portion of the hook name, `$this->screen->id`, refers
    468 			 * to the ID of the current screen.
    469 			 *
    470 			 * @since 3.1.0
    471 			 * @since 5.6.0 A bulk action can now contain an array of options in order to create an optgroup.
    472 			 *
    473 			 * @param array $actions An array of the available bulk actions.
    474 			 */
    475 			$this->_actions = apply_filters( "bulk_actions-{$this->screen->id}", $this->_actions ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
    476 
    477 			$two = '';
    478 		} else {
    479 			$two = '2';
    480 		}
    481 
    482 		if ( empty( $this->_actions ) ) {
    483 			return;
    484 		}
    485 
    486 		echo '<label for="bulk-action-selector-' . esc_attr( $which ) . '" class="screen-reader-text">' . __( 'Select bulk action' ) . '</label>';
    487 		echo '<select name="action' . $two . '" id="bulk-action-selector-' . esc_attr( $which ) . "\">\n";
    488 		echo '<option value="-1">' . __( 'Bulk actions' ) . "</option>\n";
    489 
    490 		foreach ( $this->_actions as $key => $value ) {
    491 			if ( is_array( $value ) ) {
    492 				echo "\t" . '<optgroup label="' . esc_attr( $key ) . '">' . "\n";
    493 
    494 				foreach ( $value as $name => $title ) {
    495 					$class = ( 'edit' === $name ) ? ' class="hide-if-no-js"' : '';
    496 
    497 					echo "\t\t" . '<option value="' . esc_attr( $name ) . '"' . $class . '>' . $title . "</option>\n";
    498 				}
    499 				echo "\t" . "</optgroup>\n";
    500 			} else {
    501 				$class = ( 'edit' === $key ) ? ' class="hide-if-no-js"' : '';
    502 
    503 				echo "\t" . '<option value="' . esc_attr( $key ) . '"' . $class . '>' . $value . "</option>\n";
    504 			}
    505 		}
    506 
    507 		echo "</select>\n";
    508 
    509 		submit_button( __( 'Apply' ), 'action', '', false, array( 'id' => "doaction$two" ) );
    510 		echo "\n";
    511 	}
    512 
    513 	/**
    514 	 * Gets the current action selected from the bulk actions dropdown.
    515 	 *
    516 	 * @since 3.1.0
    517 	 *
    518 	 * @return string|false The action name. False if no action was selected.
    519 	 */
    520 	public function current_action() {
    521 		if ( isset( $_REQUEST['filter_action'] ) && ! empty( $_REQUEST['filter_action'] ) ) {
    522 			return false;
    523 		}
    524 
    525 		if ( isset( $_REQUEST['action'] ) && -1 != $_REQUEST['action'] ) {
    526 			return $_REQUEST['action'];
    527 		}
    528 
    529 		return false;
    530 	}
    531 
    532 	/**
    533 	 * Generates the required HTML for a list of row action links.
    534 	 *
    535 	 * @since 3.1.0
    536 	 *
    537 	 * @param string[] $actions        An array of action links.
    538 	 * @param bool     $always_visible Whether the actions should be always visible.
    539 	 * @return string The HTML for the row actions.
    540 	 */
    541 	protected function row_actions( $actions, $always_visible = false ) {
    542 		$action_count = count( $actions );
    543 
    544 		if ( ! $action_count ) {
    545 			return '';
    546 		}
    547 
    548 		$mode = get_user_setting( 'posts_list_mode', 'list' );
    549 
    550 		if ( 'excerpt' === $mode ) {
    551 			$always_visible = true;
    552 		}
    553 
    554 		$out = '<div class="' . ( $always_visible ? 'row-actions visible' : 'row-actions' ) . '">';
    555 
    556 		$i = 0;
    557 
    558 		foreach ( $actions as $action => $link ) {
    559 			++$i;
    560 
    561 			$sep = ( $i < $action_count ) ? ' | ' : '';
    562 
    563 			$out .= "<span class='$action'>$link$sep</span>";
    564 		}
    565 
    566 		$out .= '</div>';
    567 
    568 		$out .= '<button type="button" class="toggle-row"><span class="screen-reader-text">' . __( 'Show more details' ) . '</span></button>';
    569 
    570 		return $out;
    571 	}
    572 
    573 	/**
    574 	 * Displays a dropdown for filtering items in the list table by month.
    575 	 *
    576 	 * @since 3.1.0
    577 	 *
    578 	 * @global wpdb      $wpdb      WordPress database abstraction object.
    579 	 * @global WP_Locale $wp_locale WordPress date and time locale object.
    580 	 *
    581 	 * @param string $post_type The post type.
    582 	 */
    583 	protected function months_dropdown( $post_type ) {
    584 		global $wpdb, $wp_locale;
    585 
    586 		/**
    587 		 * Filters whether to remove the 'Months' drop-down from the post list table.
    588 		 *
    589 		 * @since 4.2.0
    590 		 *
    591 		 * @param bool   $disable   Whether to disable the drop-down. Default false.
    592 		 * @param string $post_type The post type.
    593 		 */
    594 		if ( apply_filters( 'disable_months_dropdown', false, $post_type ) ) {
    595 			return;
    596 		}
    597 
    598 		/**
    599 		 * Filters to short-circuit performing the months dropdown query.
    600 		 *
    601 		 * @since 5.7.0
    602 		 *
    603 		 * @param object[]|false $months   'Months' drop-down results. Default false.
    604 		 * @param string         $post_type The post type.
    605 		 */
    606 		$months = apply_filters( 'pre_months_dropdown_query', false, $post_type );
    607 
    608 		if ( ! is_array( $months ) ) {
    609 			$extra_checks = "AND post_status != 'auto-draft'";
    610 			if ( ! isset( $_GET['post_status'] ) || 'trash' !== $_GET['post_status'] ) {
    611 				$extra_checks .= " AND post_status != 'trash'";
    612 			} elseif ( isset( $_GET['post_status'] ) ) {
    613 				$extra_checks = $wpdb->prepare( ' AND post_status = %s', $_GET['post_status'] );
    614 			}
    615 
    616 			$months = $wpdb->get_results(
    617 				$wpdb->prepare(
    618 					"
    619 				SELECT DISTINCT YEAR( post_date ) AS year, MONTH( post_date ) AS month
    620 				FROM $wpdb->posts
    621 				WHERE post_type = %s
    622 				$extra_checks
    623 				ORDER BY post_date DESC
    624 			",
    625 					$post_type
    626 				)
    627 			);
    628 		}
    629 
    630 		/**
    631 		 * Filters the 'Months' drop-down results.
    632 		 *
    633 		 * @since 3.7.0
    634 		 *
    635 		 * @param object[] $months    Array of the months drop-down query results.
    636 		 * @param string   $post_type The post type.
    637 		 */
    638 		$months = apply_filters( 'months_dropdown_results', $months, $post_type );
    639 
    640 		$month_count = count( $months );
    641 
    642 		if ( ! $month_count || ( 1 == $month_count && 0 == $months[0]->month ) ) {
    643 			return;
    644 		}
    645 
    646 		$m = isset( $_GET['m'] ) ? (int) $_GET['m'] : 0;
    647 		?>
    648 		<label for="filter-by-date" class="screen-reader-text"><?php echo get_post_type_object( $post_type )->labels->filter_by_date; ?></label>
    649 		<select name="m" id="filter-by-date">
    650 			<option<?php selected( $m, 0 ); ?> value="0"><?php _e( 'All dates' ); ?></option>
    651 		<?php
    652 		foreach ( $months as $arc_row ) {
    653 			if ( 0 == $arc_row->year ) {
    654 				continue;
    655 			}
    656 
    657 			$month = zeroise( $arc_row->month, 2 );
    658 			$year  = $arc_row->year;
    659 
    660 			printf(
    661 				"<option %s value='%s'>%s</option>\n",
    662 				selected( $m, $year . $month, false ),
    663 				esc_attr( $arc_row->year . $month ),
    664 				/* translators: 1: Month name, 2: 4-digit year. */
    665 				sprintf( __( '%1$s %2$d' ), $wp_locale->get_month( $month ), $year )
    666 			);
    667 		}
    668 		?>
    669 		</select>
    670 		<?php
    671 	}
    672 
    673 	/**
    674 	 * Displays a view switcher.
    675 	 *
    676 	 * @since 3.1.0
    677 	 *
    678 	 * @param string $current_mode
    679 	 */
    680 	protected function view_switcher( $current_mode ) {
    681 		?>
    682 		<input type="hidden" name="mode" value="<?php echo esc_attr( $current_mode ); ?>" />
    683 		<div class="view-switch">
    684 		<?php
    685 		foreach ( $this->modes as $mode => $title ) {
    686 			$classes      = array( 'view-' . $mode );
    687 			$aria_current = '';
    688 
    689 			if ( $current_mode === $mode ) {
    690 				$classes[]    = 'current';
    691 				$aria_current = ' aria-current="page"';
    692 			}
    693 
    694 			printf(
    695 				"<a href='%s' class='%s' id='view-switch-$mode'$aria_current><span class='screen-reader-text'>%s</span></a>\n",
    696 				esc_url( remove_query_arg( 'attachment-filter', add_query_arg( 'mode', $mode ) ) ),
    697 				implode( ' ', $classes ),
    698 				$title
    699 			);
    700 		}
    701 		?>
    702 		</div>
    703 		<?php
    704 	}
    705 
    706 	/**
    707 	 * Displays a comment count bubble.
    708 	 *
    709 	 * @since 3.1.0
    710 	 *
    711 	 * @param int $post_id          The post ID.
    712 	 * @param int $pending_comments Number of pending comments.
    713 	 */
    714 	protected function comments_bubble( $post_id, $pending_comments ) {
    715 		$approved_comments = get_comments_number();
    716 
    717 		$approved_comments_number = number_format_i18n( $approved_comments );
    718 		$pending_comments_number  = number_format_i18n( $pending_comments );
    719 
    720 		$approved_only_phrase = sprintf(
    721 			/* translators: %s: Number of comments. */
    722 			_n( '%s comment', '%s comments', $approved_comments ),
    723 			$approved_comments_number
    724 		);
    725 
    726 		$approved_phrase = sprintf(
    727 			/* translators: %s: Number of comments. */
    728 			_n( '%s approved comment', '%s approved comments', $approved_comments ),
    729 			$approved_comments_number
    730 		);
    731 
    732 		$pending_phrase = sprintf(
    733 			/* translators: %s: Number of comments. */
    734 			_n( '%s pending comment', '%s pending comments', $pending_comments ),
    735 			$pending_comments_number
    736 		);
    737 
    738 		if ( ! $approved_comments && ! $pending_comments ) {
    739 			// No comments at all.
    740 			printf(
    741 				'<span aria-hidden="true">&#8212;</span><span class="screen-reader-text">%s</span>',
    742 				__( 'No comments' )
    743 			);
    744 		} elseif ( $approved_comments && 'trash' === get_post_status( $post_id ) ) {
    745 			// Don't link the comment bubble for a trashed post.
    746 			printf(
    747 				'<span class="post-com-count post-com-count-approved"><span class="comment-count-approved" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></span>',
    748 				$approved_comments_number,
    749 				$pending_comments ? $approved_phrase : $approved_only_phrase
    750 			);
    751 		} elseif ( $approved_comments ) {
    752 			// Link the comment bubble to approved comments.
    753 			printf(
    754 				'<a href="%s" class="post-com-count post-com-count-approved"><span class="comment-count-approved" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></a>',
    755 				esc_url(
    756 					add_query_arg(
    757 						array(
    758 							'p'              => $post_id,
    759 							'comment_status' => 'approved',
    760 						),
    761 						admin_url( 'edit-comments.php' )
    762 					)
    763 				),
    764 				$approved_comments_number,
    765 				$pending_comments ? $approved_phrase : $approved_only_phrase
    766 			);
    767 		} else {
    768 			// Don't link the comment bubble when there are no approved comments.
    769 			printf(
    770 				'<span class="post-com-count post-com-count-no-comments"><span class="comment-count comment-count-no-comments" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></span>',
    771 				$approved_comments_number,
    772 				$pending_comments ? __( 'No approved comments' ) : __( 'No comments' )
    773 			);
    774 		}
    775 
    776 		if ( $pending_comments ) {
    777 			printf(
    778 				'<a href="%s" class="post-com-count post-com-count-pending"><span class="comment-count-pending" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></a>',
    779 				esc_url(
    780 					add_query_arg(
    781 						array(
    782 							'p'              => $post_id,
    783 							'comment_status' => 'moderated',
    784 						),
    785 						admin_url( 'edit-comments.php' )
    786 					)
    787 				),
    788 				$pending_comments_number,
    789 				$pending_phrase
    790 			);
    791 		} else {
    792 			printf(
    793 				'<span class="post-com-count post-com-count-pending post-com-count-no-pending"><span class="comment-count comment-count-no-pending" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></span>',
    794 				$pending_comments_number,
    795 				$approved_comments ? __( 'No pending comments' ) : __( 'No comments' )
    796 			);
    797 		}
    798 	}
    799 
    800 	/**
    801 	 * Gets the current page number.
    802 	 *
    803 	 * @since 3.1.0
    804 	 *
    805 	 * @return int
    806 	 */
    807 	public function get_pagenum() {
    808 		$pagenum = isset( $_REQUEST['paged'] ) ? absint( $_REQUEST['paged'] ) : 0;
    809 
    810 		if ( isset( $this->_pagination_args['total_pages'] ) && $pagenum > $this->_pagination_args['total_pages'] ) {
    811 			$pagenum = $this->_pagination_args['total_pages'];
    812 		}
    813 
    814 		return max( 1, $pagenum );
    815 	}
    816 
    817 	/**
    818 	 * Gets the number of items to display on a single page.
    819 	 *
    820 	 * @since 3.1.0
    821 	 *
    822 	 * @param string $option
    823 	 * @param int    $default
    824 	 * @return int
    825 	 */
    826 	protected function get_items_per_page( $option, $default = 20 ) {
    827 		$per_page = (int) get_user_option( $option );
    828 		if ( empty( $per_page ) || $per_page < 1 ) {
    829 			$per_page = $default;
    830 		}
    831 
    832 		/**
    833 		 * Filters the number of items to be displayed on each page of the list table.
    834 		 *
    835 		 * The dynamic hook name, `$option`, refers to the `per_page` option depending
    836 		 * on the type of list table in use. Possible filter names include:
    837 		 *
    838 		 *  - `edit_comments_per_page`
    839 		 *  - `sites_network_per_page`
    840 		 *  - `site_themes_network_per_page`
    841 		 *  - `themes_network_per_page'`
    842 		 *  - `users_network_per_page`
    843 		 *  - `edit_post_per_page`
    844 		 *  - `edit_page_per_page'`
    845 		 *  - `edit_{$post_type}_per_page`
    846 		 *  - `edit_post_tag_per_page`
    847 		 *  - `edit_category_per_page`
    848 		 *  - `edit_{$taxonomy}_per_page`
    849 		 *  - `site_users_network_per_page`
    850 		 *  - `users_per_page`
    851 		 *
    852 		 * @since 2.9.0
    853 		 *
    854 		 * @param int $per_page Number of items to be displayed. Default 20.
    855 		 */
    856 		return (int) apply_filters( "{$option}", $per_page );
    857 	}
    858 
    859 	/**
    860 	 * Displays the pagination.
    861 	 *
    862 	 * @since 3.1.0
    863 	 *
    864 	 * @param string $which
    865 	 */
    866 	protected function pagination( $which ) {
    867 		if ( empty( $this->_pagination_args ) ) {
    868 			return;
    869 		}
    870 
    871 		$total_items     = $this->_pagination_args['total_items'];
    872 		$total_pages     = $this->_pagination_args['total_pages'];
    873 		$infinite_scroll = false;
    874 		if ( isset( $this->_pagination_args['infinite_scroll'] ) ) {
    875 			$infinite_scroll = $this->_pagination_args['infinite_scroll'];
    876 		}
    877 
    878 		if ( 'top' === $which && $total_pages > 1 ) {
    879 			$this->screen->render_screen_reader_content( 'heading_pagination' );
    880 		}
    881 
    882 		$output = '<span class="displaying-num">' . sprintf(
    883 			/* translators: %s: Number of items. */
    884 			_n( '%s item', '%s items', $total_items ),
    885 			number_format_i18n( $total_items )
    886 		) . '</span>';
    887 
    888 		$current              = $this->get_pagenum();
    889 		$removable_query_args = wp_removable_query_args();
    890 
    891 		$current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
    892 
    893 		$current_url = remove_query_arg( $removable_query_args, $current_url );
    894 
    895 		$page_links = array();
    896 
    897 		$total_pages_before = '<span class="paging-input">';
    898 		$total_pages_after  = '</span></span>';
    899 
    900 		$disable_first = false;
    901 		$disable_last  = false;
    902 		$disable_prev  = false;
    903 		$disable_next  = false;
    904 
    905 		if ( 1 == $current ) {
    906 			$disable_first = true;
    907 			$disable_prev  = true;
    908 		}
    909 		if ( 2 == $current ) {
    910 			$disable_first = true;
    911 		}
    912 		if ( $total_pages == $current ) {
    913 			$disable_last = true;
    914 			$disable_next = true;
    915 		}
    916 		if ( $total_pages - 1 == $current ) {
    917 			$disable_last = true;
    918 		}
    919 
    920 		if ( $disable_first ) {
    921 			$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&laquo;</span>';
    922 		} else {
    923 			$page_links[] = sprintf(
    924 				"<a class='first-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
    925 				esc_url( remove_query_arg( 'paged', $current_url ) ),
    926 				__( 'First page' ),
    927 				'&laquo;'
    928 			);
    929 		}
    930 
    931 		if ( $disable_prev ) {
    932 			$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&lsaquo;</span>';
    933 		} else {
    934 			$page_links[] = sprintf(
    935 				"<a class='prev-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
    936 				esc_url( add_query_arg( 'paged', max( 1, $current - 1 ), $current_url ) ),
    937 				__( 'Previous page' ),
    938 				'&lsaquo;'
    939 			);
    940 		}
    941 
    942 		if ( 'bottom' === $which ) {
    943 			$html_current_page  = $current;
    944 			$total_pages_before = '<span class="screen-reader-text">' . __( 'Current Page' ) . '</span><span id="table-paging" class="paging-input"><span class="tablenav-paging-text">';
    945 		} else {
    946 			$html_current_page = sprintf(
    947 				"%s<input class='current-page' id='current-page-selector' type='text' name='paged' value='%s' size='%d' aria-describedby='table-paging' /><span class='tablenav-paging-text'>",
    948 				'<label for="current-page-selector" class="screen-reader-text">' . __( 'Current Page' ) . '</label>',
    949 				$current,
    950 				strlen( $total_pages )
    951 			);
    952 		}
    953 		$html_total_pages = sprintf( "<span class='total-pages'>%s</span>", number_format_i18n( $total_pages ) );
    954 		$page_links[]     = $total_pages_before . sprintf(
    955 			/* translators: 1: Current page, 2: Total pages. */
    956 			_x( '%1$s of %2$s', 'paging' ),
    957 			$html_current_page,
    958 			$html_total_pages
    959 		) . $total_pages_after;
    960 
    961 		if ( $disable_next ) {
    962 			$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&rsaquo;</span>';
    963 		} else {
    964 			$page_links[] = sprintf(
    965 				"<a class='next-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
    966 				esc_url( add_query_arg( 'paged', min( $total_pages, $current + 1 ), $current_url ) ),
    967 				__( 'Next page' ),
    968 				'&rsaquo;'
    969 			);
    970 		}
    971 
    972 		if ( $disable_last ) {
    973 			$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&raquo;</span>';
    974 		} else {
    975 			$page_links[] = sprintf(
    976 				"<a class='last-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
    977 				esc_url( add_query_arg( 'paged', $total_pages, $current_url ) ),
    978 				__( 'Last page' ),
    979 				'&raquo;'
    980 			);
    981 		}
    982 
    983 		$pagination_links_class = 'pagination-links';
    984 		if ( ! empty( $infinite_scroll ) ) {
    985 			$pagination_links_class .= ' hide-if-js';
    986 		}
    987 		$output .= "\n<span class='$pagination_links_class'>" . implode( "\n", $page_links ) . '</span>';
    988 
    989 		if ( $total_pages ) {
    990 			$page_class = $total_pages < 2 ? ' one-page' : '';
    991 		} else {
    992 			$page_class = ' no-pages';
    993 		}
    994 		$this->_pagination = "<div class='tablenav-pages{$page_class}'>$output</div>";
    995 
    996 		echo $this->_pagination;
    997 	}
    998 
    999 	/**
   1000 	 * Gets a list of columns.
   1001 	 *
   1002 	 * The format is:
   1003 	 * - `'internal-name' => 'Title'`
   1004 	 *
   1005 	 * @since 3.1.0
   1006 	 * @abstract
   1007 	 *
   1008 	 * @return array
   1009 	 */
   1010 	public function get_columns() {
   1011 		die( 'function WP_List_Table::get_columns() must be overridden in a subclass.' );
   1012 	}
   1013 
   1014 	/**
   1015 	 * Gets a list of sortable columns.
   1016 	 *
   1017 	 * The format is:
   1018 	 * - `'internal-name' => 'orderby'`
   1019 	 * - `'internal-name' => array( 'orderby', 'asc' )` - The second element sets the initial sorting order.
   1020 	 * - `'internal-name' => array( 'orderby', true )`  - The second element makes the initial order descending.
   1021 	 *
   1022 	 * @since 3.1.0
   1023 	 *
   1024 	 * @return array
   1025 	 */
   1026 	protected function get_sortable_columns() {
   1027 		return array();
   1028 	}
   1029 
   1030 	/**
   1031 	 * Gets the name of the default primary column.
   1032 	 *
   1033 	 * @since 4.3.0
   1034 	 *
   1035 	 * @return string Name of the default primary column, in this case, an empty string.
   1036 	 */
   1037 	protected function get_default_primary_column_name() {
   1038 		$columns = $this->get_columns();
   1039 		$column  = '';
   1040 
   1041 		if ( empty( $columns ) ) {
   1042 			return $column;
   1043 		}
   1044 
   1045 		// We need a primary defined so responsive views show something,
   1046 		// so let's fall back to the first non-checkbox column.
   1047 		foreach ( $columns as $col => $column_name ) {
   1048 			if ( 'cb' === $col ) {
   1049 				continue;
   1050 			}
   1051 
   1052 			$column = $col;
   1053 			break;
   1054 		}
   1055 
   1056 		return $column;
   1057 	}
   1058 
   1059 	/**
   1060 	 * Public wrapper for WP_List_Table::get_default_primary_column_name().
   1061 	 *
   1062 	 * @since 4.4.0
   1063 	 *
   1064 	 * @return string Name of the default primary column.
   1065 	 */
   1066 	public function get_primary_column() {
   1067 		return $this->get_primary_column_name();
   1068 	}
   1069 
   1070 	/**
   1071 	 * Gets the name of the primary column.
   1072 	 *
   1073 	 * @since 4.3.0
   1074 	 *
   1075 	 * @return string The name of the primary column.
   1076 	 */
   1077 	protected function get_primary_column_name() {
   1078 		$columns = get_column_headers( $this->screen );
   1079 		$default = $this->get_default_primary_column_name();
   1080 
   1081 		// If the primary column doesn't exist,
   1082 		// fall back to the first non-checkbox column.
   1083 		if ( ! isset( $columns[ $default ] ) ) {
   1084 			$default = self::get_default_primary_column_name();
   1085 		}
   1086 
   1087 		/**
   1088 		 * Filters the name of the primary column for the current list table.
   1089 		 *
   1090 		 * @since 4.3.0
   1091 		 *
   1092 		 * @param string $default Column name default for the specific list table, e.g. 'name'.
   1093 		 * @param string $context Screen ID for specific list table, e.g. 'plugins'.
   1094 		 */
   1095 		$column = apply_filters( 'list_table_primary_column', $default, $this->screen->id );
   1096 
   1097 		if ( empty( $column ) || ! isset( $columns[ $column ] ) ) {
   1098 			$column = $default;
   1099 		}
   1100 
   1101 		return $column;
   1102 	}
   1103 
   1104 	/**
   1105 	 * Gets a list of all, hidden, and sortable columns, with filter applied.
   1106 	 *
   1107 	 * @since 3.1.0
   1108 	 *
   1109 	 * @return array
   1110 	 */
   1111 	protected function get_column_info() {
   1112 		// $_column_headers is already set / cached.
   1113 		if ( isset( $this->_column_headers ) && is_array( $this->_column_headers ) ) {
   1114 			/*
   1115 			 * Backward compatibility for `$_column_headers` format prior to WordPress 4.3.
   1116 			 *
   1117 			 * In WordPress 4.3 the primary column name was added as a fourth item in the
   1118 			 * column headers property. This ensures the primary column name is included
   1119 			 * in plugins setting the property directly in the three item format.
   1120 			 */
   1121 			$column_headers = array( array(), array(), array(), $this->get_primary_column_name() );
   1122 			foreach ( $this->_column_headers as $key => $value ) {
   1123 				$column_headers[ $key ] = $value;
   1124 			}
   1125 
   1126 			return $column_headers;
   1127 		}
   1128 
   1129 		$columns = get_column_headers( $this->screen );
   1130 		$hidden  = get_hidden_columns( $this->screen );
   1131 
   1132 		$sortable_columns = $this->get_sortable_columns();
   1133 		/**
   1134 		 * Filters the list table sortable columns for a specific screen.
   1135 		 *
   1136 		 * The dynamic portion of the hook name, `$this->screen->id`, refers
   1137 		 * to the ID of the current screen.
   1138 		 *
   1139 		 * @since 3.1.0
   1140 		 *
   1141 		 * @param array $sortable_columns An array of sortable columns.
   1142 		 */
   1143 		$_sortable = apply_filters( "manage_{$this->screen->id}_sortable_columns", $sortable_columns );
   1144 
   1145 		$sortable = array();
   1146 		foreach ( $_sortable as $id => $data ) {
   1147 			if ( empty( $data ) ) {
   1148 				continue;
   1149 			}
   1150 
   1151 			$data = (array) $data;
   1152 			if ( ! isset( $data[1] ) ) {
   1153 				$data[1] = false;
   1154 			}
   1155 
   1156 			$sortable[ $id ] = $data;
   1157 		}
   1158 
   1159 		$primary               = $this->get_primary_column_name();
   1160 		$this->_column_headers = array( $columns, $hidden, $sortable, $primary );
   1161 
   1162 		return $this->_column_headers;
   1163 	}
   1164 
   1165 	/**
   1166 	 * Returns the number of visible columns.
   1167 	 *
   1168 	 * @since 3.1.0
   1169 	 *
   1170 	 * @return int
   1171 	 */
   1172 	public function get_column_count() {
   1173 		list ( $columns, $hidden ) = $this->get_column_info();
   1174 		$hidden                    = array_intersect( array_keys( $columns ), array_filter( $hidden ) );
   1175 		return count( $columns ) - count( $hidden );
   1176 	}
   1177 
   1178 	/**
   1179 	 * Prints column headers, accounting for hidden and sortable columns.
   1180 	 *
   1181 	 * @since 3.1.0
   1182 	 *
   1183 	 * @param bool $with_id Whether to set the ID attribute or not
   1184 	 */
   1185 	public function print_column_headers( $with_id = true ) {
   1186 		list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();
   1187 
   1188 		$current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
   1189 		$current_url = remove_query_arg( 'paged', $current_url );
   1190 
   1191 		if ( isset( $_GET['orderby'] ) ) {
   1192 			$current_orderby = $_GET['orderby'];
   1193 		} else {
   1194 			$current_orderby = '';
   1195 		}
   1196 
   1197 		if ( isset( $_GET['order'] ) && 'desc' === $_GET['order'] ) {
   1198 			$current_order = 'desc';
   1199 		} else {
   1200 			$current_order = 'asc';
   1201 		}
   1202 
   1203 		if ( ! empty( $columns['cb'] ) ) {
   1204 			static $cb_counter = 1;
   1205 			$columns['cb']     = '<label class="screen-reader-text" for="cb-select-all-' . $cb_counter . '">' . __( 'Select All' ) . '</label>'
   1206 				. '<input id="cb-select-all-' . $cb_counter . '" type="checkbox" />';
   1207 			$cb_counter++;
   1208 		}
   1209 
   1210 		foreach ( $columns as $column_key => $column_display_name ) {
   1211 			$class = array( 'manage-column', "column-$column_key" );
   1212 
   1213 			if ( in_array( $column_key, $hidden, true ) ) {
   1214 				$class[] = 'hidden';
   1215 			}
   1216 
   1217 			if ( 'cb' === $column_key ) {
   1218 				$class[] = 'check-column';
   1219 			} elseif ( in_array( $column_key, array( 'posts', 'comments', 'links' ), true ) ) {
   1220 				$class[] = 'num';
   1221 			}
   1222 
   1223 			if ( $column_key === $primary ) {
   1224 				$class[] = 'column-primary';
   1225 			}
   1226 
   1227 			if ( isset( $sortable[ $column_key ] ) ) {
   1228 				list( $orderby, $desc_first ) = $sortable[ $column_key ];
   1229 
   1230 				if ( $current_orderby === $orderby ) {
   1231 					$order = 'asc' === $current_order ? 'desc' : 'asc';
   1232 
   1233 					$class[] = 'sorted';
   1234 					$class[] = $current_order;
   1235 				} else {
   1236 					$order = strtolower( $desc_first );
   1237 
   1238 					if ( ! in_array( $order, array( 'desc', 'asc' ), true ) ) {
   1239 						$order = $desc_first ? 'desc' : 'asc';
   1240 					}
   1241 
   1242 					$class[] = 'sortable';
   1243 					$class[] = 'desc' === $order ? 'asc' : 'desc';
   1244 				}
   1245 
   1246 				$column_display_name = sprintf(
   1247 					'<a href="%s"><span>%s</span><span class="sorting-indicator"></span></a>',
   1248 					esc_url( add_query_arg( compact( 'orderby', 'order' ), $current_url ) ),
   1249 					$column_display_name
   1250 				);
   1251 			}
   1252 
   1253 			$tag   = ( 'cb' === $column_key ) ? 'td' : 'th';
   1254 			$scope = ( 'th' === $tag ) ? 'scope="col"' : '';
   1255 			$id    = $with_id ? "id='$column_key'" : '';
   1256 
   1257 			if ( ! empty( $class ) ) {
   1258 				$class = "class='" . implode( ' ', $class ) . "'";
   1259 			}
   1260 
   1261 			echo "<$tag $scope $id $class>$column_display_name</$tag>";
   1262 		}
   1263 	}
   1264 
   1265 	/**
   1266 	 * Displays the table.
   1267 	 *
   1268 	 * @since 3.1.0
   1269 	 */
   1270 	public function display() {
   1271 		$singular = $this->_args['singular'];
   1272 
   1273 		$this->display_tablenav( 'top' );
   1274 
   1275 		$this->screen->render_screen_reader_content( 'heading_list' );
   1276 		?>
   1277 <table class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>">
   1278 	<thead>
   1279 	<tr>
   1280 		<?php $this->print_column_headers(); ?>
   1281 	</tr>
   1282 	</thead>
   1283 
   1284 	<tbody id="the-list"
   1285 		<?php
   1286 		if ( $singular ) {
   1287 			echo " data-wp-lists='list:$singular'";
   1288 		}
   1289 		?>
   1290 		>
   1291 		<?php $this->display_rows_or_placeholder(); ?>
   1292 	</tbody>
   1293 
   1294 	<tfoot>
   1295 	<tr>
   1296 		<?php $this->print_column_headers( false ); ?>
   1297 	</tr>
   1298 	</tfoot>
   1299 
   1300 </table>
   1301 		<?php
   1302 		$this->display_tablenav( 'bottom' );
   1303 	}
   1304 
   1305 	/**
   1306 	 * Gets a list of CSS classes for the WP_List_Table table tag.
   1307 	 *
   1308 	 * @since 3.1.0
   1309 	 *
   1310 	 * @return string[] Array of CSS classes for the table tag.
   1311 	 */
   1312 	protected function get_table_classes() {
   1313 		$mode = get_user_setting( 'posts_list_mode', 'list' );
   1314 
   1315 		$mode_class = esc_attr( 'table-view-' . $mode );
   1316 
   1317 		return array( 'widefat', 'fixed', 'striped', $mode_class, $this->_args['plural'] );
   1318 	}
   1319 
   1320 	/**
   1321 	 * Generates the table navigation above or below the table
   1322 	 *
   1323 	 * @since 3.1.0
   1324 	 * @param string $which
   1325 	 */
   1326 	protected function display_tablenav( $which ) {
   1327 		if ( 'top' === $which ) {
   1328 			wp_nonce_field( 'bulk-' . $this->_args['plural'] );
   1329 		}
   1330 		?>
   1331 	<div class="tablenav <?php echo esc_attr( $which ); ?>">
   1332 
   1333 		<?php if ( $this->has_items() ) : ?>
   1334 		<div class="alignleft actions bulkactions">
   1335 			<?php $this->bulk_actions( $which ); ?>
   1336 		</div>
   1337 			<?php
   1338 		endif;
   1339 		$this->extra_tablenav( $which );
   1340 		$this->pagination( $which );
   1341 		?>
   1342 
   1343 		<br class="clear" />
   1344 	</div>
   1345 		<?php
   1346 	}
   1347 
   1348 	/**
   1349 	 * Extra controls to be displayed between bulk actions and pagination.
   1350 	 *
   1351 	 * @since 3.1.0
   1352 	 *
   1353 	 * @param string $which
   1354 	 */
   1355 	protected function extra_tablenav( $which ) {}
   1356 
   1357 	/**
   1358 	 * Generates the tbody element for the list table.
   1359 	 *
   1360 	 * @since 3.1.0
   1361 	 */
   1362 	public function display_rows_or_placeholder() {
   1363 		if ( $this->has_items() ) {
   1364 			$this->display_rows();
   1365 		} else {
   1366 			echo '<tr class="no-items"><td class="colspanchange" colspan="' . $this->get_column_count() . '">';
   1367 			$this->no_items();
   1368 			echo '</td></tr>';
   1369 		}
   1370 	}
   1371 
   1372 	/**
   1373 	 * Generates the table rows.
   1374 	 *
   1375 	 * @since 3.1.0
   1376 	 */
   1377 	public function display_rows() {
   1378 		foreach ( $this->items as $item ) {
   1379 			$this->single_row( $item );
   1380 		}
   1381 	}
   1382 
   1383 	/**
   1384 	 * Generates content for a single row of the table.
   1385 	 *
   1386 	 * @since 3.1.0
   1387 	 *
   1388 	 * @param object|array $item The current item
   1389 	 */
   1390 	public function single_row( $item ) {
   1391 		echo '<tr>';
   1392 		$this->single_row_columns( $item );
   1393 		echo '</tr>';
   1394 	}
   1395 
   1396 	/**
   1397 	 * @param object|array $item
   1398 	 * @param string $column_name
   1399 	 */
   1400 	protected function column_default( $item, $column_name ) {}
   1401 
   1402 	/**
   1403 	 * @param object|array $item
   1404 	 */
   1405 	protected function column_cb( $item ) {}
   1406 
   1407 	/**
   1408 	 * Generates the columns for a single row of the table.
   1409 	 *
   1410 	 * @since 3.1.0
   1411 	 *
   1412 	 * @param object|array $item The current item.
   1413 	 */
   1414 	protected function single_row_columns( $item ) {
   1415 		list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();
   1416 
   1417 		foreach ( $columns as $column_name => $column_display_name ) {
   1418 			$classes = "$column_name column-$column_name";
   1419 			if ( $primary === $column_name ) {
   1420 				$classes .= ' has-row-actions column-primary';
   1421 			}
   1422 
   1423 			if ( in_array( $column_name, $hidden, true ) ) {
   1424 				$classes .= ' hidden';
   1425 			}
   1426 
   1427 			// Comments column uses HTML in the display name with screen reader text.
   1428 			// Strip tags to get closer to a user-friendly string.
   1429 			$data = 'data-colname="' . esc_attr( wp_strip_all_tags( $column_display_name ) ) . '"';
   1430 
   1431 			$attributes = "class='$classes' $data";
   1432 
   1433 			if ( 'cb' === $column_name ) {
   1434 				echo '<th scope="row" class="check-column">';
   1435 				echo $this->column_cb( $item );
   1436 				echo '</th>';
   1437 			} elseif ( method_exists( $this, '_column_' . $column_name ) ) {
   1438 				echo call_user_func(
   1439 					array( $this, '_column_' . $column_name ),
   1440 					$item,
   1441 					$classes,
   1442 					$data,
   1443 					$primary
   1444 				);
   1445 			} elseif ( method_exists( $this, 'column_' . $column_name ) ) {
   1446 				echo "<td $attributes>";
   1447 				echo call_user_func( array( $this, 'column_' . $column_name ), $item );
   1448 				echo $this->handle_row_actions( $item, $column_name, $primary );
   1449 				echo '</td>';
   1450 			} else {
   1451 				echo "<td $attributes>";
   1452 				echo $this->column_default( $item, $column_name );
   1453 				echo $this->handle_row_actions( $item, $column_name, $primary );
   1454 				echo '</td>';
   1455 			}
   1456 		}
   1457 	}
   1458 
   1459 	/**
   1460 	 * Generates and display row actions links for the list table.
   1461 	 *
   1462 	 * @since 4.3.0
   1463 	 *
   1464 	 * @param object|array $item        The item being acted upon.
   1465 	 * @param string       $column_name Current column name.
   1466 	 * @param string       $primary     Primary column name.
   1467 	 * @return string The row actions HTML, or an empty string
   1468 	 *                if the current column is not the primary column.
   1469 	 */
   1470 	protected function handle_row_actions( $item, $column_name, $primary ) {
   1471 		return $column_name === $primary ? '<button type="button" class="toggle-row"><span class="screen-reader-text">' . __( 'Show more details' ) . '</span></button>' : '';
   1472 	}
   1473 
   1474 	/**
   1475 	 * Handles an incoming ajax request (called from admin-ajax.php)
   1476 	 *
   1477 	 * @since 3.1.0
   1478 	 */
   1479 	public function ajax_response() {
   1480 		$this->prepare_items();
   1481 
   1482 		ob_start();
   1483 		if ( ! empty( $_REQUEST['no_placeholder'] ) ) {
   1484 			$this->display_rows();
   1485 		} else {
   1486 			$this->display_rows_or_placeholder();
   1487 		}
   1488 
   1489 		$rows = ob_get_clean();
   1490 
   1491 		$response = array( 'rows' => $rows );
   1492 
   1493 		if ( isset( $this->_pagination_args['total_items'] ) ) {
   1494 			$response['total_items_i18n'] = sprintf(
   1495 				/* translators: Number of items. */
   1496 				_n( '%s item', '%s items', $this->_pagination_args['total_items'] ),
   1497 				number_format_i18n( $this->_pagination_args['total_items'] )
   1498 			);
   1499 		}
   1500 		if ( isset( $this->_pagination_args['total_pages'] ) ) {
   1501 			$response['total_pages']      = $this->_pagination_args['total_pages'];
   1502 			$response['total_pages_i18n'] = number_format_i18n( $this->_pagination_args['total_pages'] );
   1503 		}
   1504 
   1505 		die( wp_json_encode( $response ) );
   1506 	}
   1507 
   1508 	/**
   1509 	 * Sends required variables to JavaScript land.
   1510 	 *
   1511 	 * @since 3.1.0
   1512 	 */
   1513 	public function _js_vars() {
   1514 		$args = array(
   1515 			'class'  => get_class( $this ),
   1516 			'screen' => array(
   1517 				'id'   => $this->screen->id,
   1518 				'base' => $this->screen->base,
   1519 			),
   1520 		);
   1521 
   1522 		printf( "<script type='text/javascript'>list_args = %s;</script>\n", wp_json_encode( $args ) );
   1523 	}
   1524 }