PluginProbe
WP Data Access – App Builder for Tables, Forms, Charts, Maps & Dashboards / 5.5.76
WP Data Access – App Builder for Tables, Forms, Charts, Maps & Dashboards v5.5.76
5.5.85 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 All 161 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.76, at WPDataAccess/Wordpress_Original/WP_List_Table.php

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