PluginProbe
WP Data Access – App Builder for Tables, Forms, Charts, Maps & Dashboards / 5.5.84
WP Data Access – App Builder for Tables, Forms, Charts, Maps & Dashboards v5.5.84
5.5.84 5.5.83 5.5.82 5.5.81 5.5.80 5.5.79 5.5.77 5.5.76 5.5.75 5.5.73 5.5.72 5.5.22 5.5.23 5.5.29 5.5.3 5.5.31 5.5.32 5.5.34 5.5.35 5.5.36 5.5.37 5.5.4 5.5.40 5.5.41 5.5.42 All 160 releases
wp-data-access / WPDataAccess / Wordpress_Original / WP_List_Table.php

WP_List_Table.php in WP Data Access – App Builder for Tables, Forms, Charts, Maps & Dashboards 5.5.84, at WPDataAccess/Wordpress_Original/WP_List_Table.php

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