WordPress 4.2
[autoinstalls/wordpress.git] / wp-admin / includes / class-wp-list-table.php
1 <?php
2 /**
3  * Base class for displaying a list of items in an ajaxified HTML table.
4  *
5  * @since 3.1.0
6  * @access private
7  *
8  * @package WordPress
9  * @subpackage List_Table
10  */
11 class WP_List_Table {
12
13         /**
14          * The current list of items
15          *
16          * @since 3.1.0
17          * @var array
18          * @access public
19          */
20         public $items;
21
22         /**
23          * Various information about the current table
24          *
25          * @since 3.1.0
26          * @var array
27          * @access protected
28          */
29         protected $_args;
30
31         /**
32          * Various information needed for displaying the pagination
33          *
34          * @since 3.1.0
35          * @var array
36          */
37         protected $_pagination_args = array();
38
39         /**
40          * The current screen
41          *
42          * @since 3.1.0
43          * @var object
44          * @access protected
45          */
46         protected $screen;
47
48         /**
49          * Cached bulk actions
50          *
51          * @since 3.1.0
52          * @var array
53          * @access private
54          */
55         private $_actions;
56
57         /**
58          * Cached pagination output
59          *
60          * @since 3.1.0
61          * @var string
62          * @access private
63          */
64         private $_pagination;
65
66         /**
67          * The view switcher modes.
68          *
69          * @since 4.1.0
70          * @var array
71          * @access protected
72          */
73         protected $modes = array();
74
75         /**
76          * Stores the value returned by ->get_column_info()
77          *
78          * @var array
79          */
80         protected $_column_headers;
81
82         protected $compat_fields = array( '_args', '_pagination_args', 'screen', '_actions', '_pagination' );
83
84         protected $compat_methods = array( 'set_pagination_args', 'get_views', 'get_bulk_actions', 'bulk_actions',
85                 'row_actions', 'months_dropdown', 'view_switcher', 'comments_bubble', 'get_items_per_page', 'pagination',
86                 'get_sortable_columns', 'get_column_info', 'get_table_classes', 'display_tablenav', 'extra_tablenav',
87                 'single_row_columns' );
88
89         /**
90          * Constructor.
91          *
92          * The child class should call this constructor from its own constructor to override
93          * the default $args.
94          *
95          * @since 3.1.0
96          * @access public
97          *
98          * @param array|string $args {
99          *     Array or string of arguments.
100          *
101          *     @type string $plural   Plural value used for labels and the objects being listed.
102          *                            This affects things such as CSS class-names and nonces used
103          *                            in the list table, e.g. 'posts'. Default empty.
104          *     @type string $singular Singular label for an object being listed, e.g. 'post'.
105          *                            Default empty
106          *     @type bool   $ajax     Whether the list table supports AJAX. This includes loading
107          *                            and sorting data, for example. If true, the class will call
108          *                            the {@see _js_vars()} method in the footer to provide variables
109          *                            to any scripts handling AJAX events. Default false.
110          *     @type string $screen   String containing the hook name used to determine the current
111          *                            screen. If left null, the current screen will be automatically set.
112          *                            Default null.
113          * }
114          */
115         public function __construct( $args = array() ) {
116                 $args = wp_parse_args( $args, array(
117                         'plural' => '',
118                         'singular' => '',
119                         'ajax' => false,
120                         'screen' => null,
121                 ) );
122
123                 $this->screen = convert_to_screen( $args['screen'] );
124
125                 add_filter( "manage_{$this->screen->id}_columns", array( $this, 'get_columns' ), 0 );
126
127                 if ( !$args['plural'] )
128                         $args['plural'] = $this->screen->base;
129
130                 $args['plural'] = sanitize_key( $args['plural'] );
131                 $args['singular'] = sanitize_key( $args['singular'] );
132
133                 $this->_args = $args;
134
135                 if ( $args['ajax'] ) {
136                         // wp_enqueue_script( 'list-table' );
137                         add_action( 'admin_footer', array( $this, '_js_vars' ) );
138                 }
139
140                 if ( empty( $this->modes ) ) {
141                         $this->modes = array(
142                                 'list'    => __( 'List View' ),
143                                 'excerpt' => __( 'Excerpt View' )
144                         );
145                 }
146         }
147
148         /**
149          * Make private properties readable for backwards compatibility.
150          *
151          * @since 4.0.0
152          * @access public
153          *
154          * @param string $name Property to get.
155          * @return mixed Property.
156          */
157         public function __get( $name ) {
158                 if ( in_array( $name, $this->compat_fields ) ) {
159                         return $this->$name;
160                 }
161         }
162
163         /**
164          * Make private properties settable for backwards compatibility.
165          *
166          * @since 4.0.0
167          * @access public
168          *
169          * @param string $name  Property to check if set.
170          * @param mixed  $value Property value.
171          * @return mixed Newly-set property.
172          */
173         public function __set( $name, $value ) {
174                 if ( in_array( $name, $this->compat_fields ) ) {
175                         return $this->$name = $value;
176                 }
177         }
178
179         /**
180          * Make private properties checkable for backwards compatibility.
181          *
182          * @since 4.0.0
183          * @access public
184          *
185          * @param string $name Property to check if set.
186          * @return bool Whether the property is set.
187          */
188         public function __isset( $name ) {
189                 if ( in_array( $name, $this->compat_fields ) ) {
190                         return isset( $this->$name );
191                 }
192         }
193
194         /**
195          * Make private properties un-settable for backwards compatibility.
196          *
197          * @since 4.0.0
198          * @access public
199          *
200          * @param string $name Property to unset.
201          */
202         public function __unset( $name ) {
203                 if ( in_array( $name, $this->compat_fields ) ) {
204                         unset( $this->$name );
205                 }
206         }
207
208         /**
209          * Make private/protected methods readable for backwards compatibility.
210          *
211          * @since 4.0.0
212          * @access public
213          *
214          * @param callable $name      Method to call.
215          * @param array    $arguments Arguments to pass when calling.
216          * @return mixed|bool Return value of the callback, false otherwise.
217          */
218         public function __call( $name, $arguments ) {
219                 if ( in_array( $name, $this->compat_methods ) ) {
220                         return call_user_func_array( array( $this, $name ), $arguments );
221                 }
222                 return false;
223         }
224
225         /**
226          * Checks the current user's permissions
227          *
228          * @since 3.1.0
229          * @access public
230          * @abstract
231          */
232         public function ajax_user_can() {
233                 die( 'function WP_List_Table::ajax_user_can() must be over-ridden in a sub-class.' );
234         }
235
236         /**
237          * Prepares the list of items for displaying.
238          * @uses WP_List_Table::set_pagination_args()
239          *
240          * @since 3.1.0
241          * @access public
242          * @abstract
243          */
244         public function prepare_items() {
245                 die( 'function WP_List_Table::prepare_items() must be over-ridden in a sub-class.' );
246         }
247
248         /**
249          * An internal method that sets all the necessary pagination arguments
250          *
251          * @param array $args An associative array with information about the pagination
252          * @access protected
253          */
254         protected function set_pagination_args( $args ) {
255                 $args = wp_parse_args( $args, array(
256                         'total_items' => 0,
257                         'total_pages' => 0,
258                         'per_page' => 0,
259                 ) );
260
261                 if ( !$args['total_pages'] && $args['per_page'] > 0 )
262                         $args['total_pages'] = ceil( $args['total_items'] / $args['per_page'] );
263
264                 // Redirect if page number is invalid and headers are not already sent.
265                 if ( ! headers_sent() && ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) && $args['total_pages'] > 0 && $this->get_pagenum() > $args['total_pages'] ) {
266                         wp_redirect( add_query_arg( 'paged', $args['total_pages'] ) );
267                         exit;
268                 }
269
270                 $this->_pagination_args = $args;
271         }
272
273         /**
274          * Access the pagination args.
275          *
276          * @since 3.1.0
277          * @access public
278          *
279          * @param string $key Pagination argument to retrieve. Common values include 'total_items',
280          *                    'total_pages', 'per_page', or 'infinite_scroll'.
281          * @return int Number of items that correspond to the given pagination argument.
282          */
283         public function get_pagination_arg( $key ) {
284                 if ( 'page' == $key )
285                         return $this->get_pagenum();
286
287                 if ( isset( $this->_pagination_args[$key] ) )
288                         return $this->_pagination_args[$key];
289         }
290
291         /**
292          * Whether the table has items to display or not
293          *
294          * @since 3.1.0
295          * @access public
296          *
297          * @return bool
298          */
299         public function has_items() {
300                 return !empty( $this->items );
301         }
302
303         /**
304          * Message to be displayed when there are no items
305          *
306          * @since 3.1.0
307          * @access public
308          */
309         public function no_items() {
310                 _e( 'No items found.' );
311         }
312
313         /**
314          * Display the search box.
315          *
316          * @since 3.1.0
317          * @access public
318          *
319          * @param string $text The search button text
320          * @param string $input_id The search input id
321          */
322         public function search_box( $text, $input_id ) {
323                 if ( empty( $_REQUEST['s'] ) && !$this->has_items() )
324                         return;
325
326                 $input_id = $input_id . '-search-input';
327
328                 if ( ! empty( $_REQUEST['orderby'] ) )
329                         echo '<input type="hidden" name="orderby" value="' . esc_attr( $_REQUEST['orderby'] ) . '" />';
330                 if ( ! empty( $_REQUEST['order'] ) )
331                         echo '<input type="hidden" name="order" value="' . esc_attr( $_REQUEST['order'] ) . '" />';
332                 if ( ! empty( $_REQUEST['post_mime_type'] ) )
333                         echo '<input type="hidden" name="post_mime_type" value="' . esc_attr( $_REQUEST['post_mime_type'] ) . '" />';
334                 if ( ! empty( $_REQUEST['detached'] ) )
335                         echo '<input type="hidden" name="detached" value="' . esc_attr( $_REQUEST['detached'] ) . '" />';
336 ?>
337 <p class="search-box">
338         <label class="screen-reader-text" for="<?php echo $input_id ?>"><?php echo $text; ?>:</label>
339         <input type="search" id="<?php echo $input_id ?>" name="s" value="<?php _admin_search_query(); ?>" />
340         <?php submit_button( $text, 'button', '', false, array('id' => 'search-submit') ); ?>
341 </p>
342 <?php
343         }
344
345         /**
346          * Get an associative array ( id => link ) with the list
347          * of views available on this table.
348          *
349          * @since 3.1.0
350          * @access protected
351          *
352          * @return array
353          */
354         protected function get_views() {
355                 return array();
356         }
357
358         /**
359          * Display the list of views available on this table.
360          *
361          * @since 3.1.0
362          * @access public
363          */
364         public function views() {
365                 $views = $this->get_views();
366                 /**
367                  * Filter the list of available list table views.
368                  *
369                  * The dynamic portion of the hook name, `$this->screen->id`, refers
370                  * to the ID of the current screen, usually a string.
371                  *
372                  * @since 3.5.0
373                  *
374                  * @param array $views An array of available list table views.
375                  */
376                 $views = apply_filters( "views_{$this->screen->id}", $views );
377
378                 if ( empty( $views ) )
379                         return;
380
381                 echo "<ul class='subsubsub'>\n";
382                 foreach ( $views as $class => $view ) {
383                         $views[ $class ] = "\t<li class='$class'>$view";
384                 }
385                 echo implode( " |</li>\n", $views ) . "</li>\n";
386                 echo "</ul>";
387         }
388
389         /**
390          * Get an associative array ( option_name => option_title ) with the list
391          * of bulk actions available on this table.
392          *
393          * @since 3.1.0
394          * @access protected
395          *
396          * @return array
397          */
398         protected function get_bulk_actions() {
399                 return array();
400         }
401
402         /**
403          * Display the bulk actions dropdown.
404          *
405          * @since 3.1.0
406          * @access protected
407          *
408          * @param string $which The location of the bulk actions: 'top' or 'bottom'.
409          *                      This is designated as optional for backwards-compatibility.
410          */
411         protected function bulk_actions( $which = '' ) {
412                 if ( is_null( $this->_actions ) ) {
413                         $no_new_actions = $this->_actions = $this->get_bulk_actions();
414                         /**
415                          * Filter the list table Bulk Actions drop-down.
416                          *
417                          * The dynamic portion of the hook name, `$this->screen->id`, refers
418                          * to the ID of the current screen, usually a string.
419                          *
420                          * This filter can currently only be used to remove bulk actions.
421                          *
422                          * @since 3.5.0
423                          *
424                          * @param array $actions An array of the available bulk actions.
425                          */
426                         $this->_actions = apply_filters( "bulk_actions-{$this->screen->id}", $this->_actions );
427                         $this->_actions = array_intersect_assoc( $this->_actions, $no_new_actions );
428                         $two = '';
429                 } else {
430                         $two = '2';
431                 }
432
433                 if ( empty( $this->_actions ) )
434                         return;
435
436                 echo "<label for='bulk-action-selector-" . esc_attr( $which ) . "' class='screen-reader-text'>" . __( 'Select bulk action' ) . "</label>";
437                 echo "<select name='action$two' id='bulk-action-selector-" . esc_attr( $which ) . "'>\n";
438                 echo "<option value='-1' selected='selected'>" . __( 'Bulk Actions' ) . "</option>\n";
439
440                 foreach ( $this->_actions as $name => $title ) {
441                         $class = 'edit' == $name ? ' class="hide-if-no-js"' : '';
442
443                         echo "\t<option value='$name'$class>$title</option>\n";
444                 }
445
446                 echo "</select>\n";
447
448                 submit_button( __( 'Apply' ), 'action', '', false, array( 'id' => "doaction$two" ) );
449                 echo "\n";
450         }
451
452         /**
453          * Get the current action selected from the bulk actions dropdown.
454          *
455          * @since 3.1.0
456          * @access public
457          *
458          * @return string|bool The action name or False if no action was selected
459          */
460         public function current_action() {
461                 if ( isset( $_REQUEST['filter_action'] ) && ! empty( $_REQUEST['filter_action'] ) )
462                         return false;
463
464                 if ( isset( $_REQUEST['action'] ) && -1 != $_REQUEST['action'] )
465                         return $_REQUEST['action'];
466
467                 if ( isset( $_REQUEST['action2'] ) && -1 != $_REQUEST['action2'] )
468                         return $_REQUEST['action2'];
469
470                 return false;
471         }
472
473         /**
474          * Generate row actions div
475          *
476          * @since 3.1.0
477          * @access protected
478          *
479          * @param array $actions The list of actions
480          * @param bool $always_visible Whether the actions should be always visible
481          * @return string
482          */
483         protected function row_actions( $actions, $always_visible = false ) {
484                 $action_count = count( $actions );
485                 $i = 0;
486
487                 if ( !$action_count )
488                         return '';
489
490                 $out = '<div class="' . ( $always_visible ? 'row-actions visible' : 'row-actions' ) . '">';
491                 foreach ( $actions as $action => $link ) {
492                         ++$i;
493                         ( $i == $action_count ) ? $sep = '' : $sep = ' | ';
494                         $out .= "<span class='$action'>$link$sep</span>";
495                 }
496                 $out .= '</div>';
497
498                 return $out;
499         }
500
501         /**
502          * Display a monthly dropdown for filtering items
503          *
504          * @since 3.1.0
505          * @access protected
506          *
507          * @param string $post_type
508          */
509         protected function months_dropdown( $post_type ) {
510                 global $wpdb, $wp_locale;
511
512                 /**
513                  * Filter whether to remove the 'Months' drop-down from the post list table.
514                  *
515                  * @since 4.2.0
516                  *
517                  * @param bool   $disable   Whether to disable the drop-down. Default false.
518                  * @param string $post_type The post type.
519                  */
520                 if ( apply_filters( 'disable_months_dropdown', false, $post_type ) ) {
521                         return;
522                 }
523
524                 $months = $wpdb->get_results( $wpdb->prepare( "
525                         SELECT DISTINCT YEAR( post_date ) AS year, MONTH( post_date ) AS month
526                         FROM $wpdb->posts
527                         WHERE post_type = %s
528                         ORDER BY post_date DESC
529                 ", $post_type ) );
530
531                 /**
532                  * Filter the 'Months' drop-down results.
533                  *
534                  * @since 3.7.0
535                  *
536                  * @param object $months    The months drop-down query results.
537                  * @param string $post_type The post type.
538                  */
539                 $months = apply_filters( 'months_dropdown_results', $months, $post_type );
540
541                 $month_count = count( $months );
542
543                 if ( !$month_count || ( 1 == $month_count && 0 == $months[0]->month ) )
544                         return;
545
546                 $m = isset( $_GET['m'] ) ? (int) $_GET['m'] : 0;
547 ?>
548                 <label for="filter-by-date" class="screen-reader-text"><?php _e( 'Filter by date' ); ?></label>
549                 <select name="m" id="filter-by-date">
550                         <option<?php selected( $m, 0 ); ?> value="0"><?php _e( 'All dates' ); ?></option>
551 <?php
552                 foreach ( $months as $arc_row ) {
553                         if ( 0 == $arc_row->year )
554                                 continue;
555
556                         $month = zeroise( $arc_row->month, 2 );
557                         $year = $arc_row->year;
558
559                         printf( "<option %s value='%s'>%s</option>\n",
560                                 selected( $m, $year . $month, false ),
561                                 esc_attr( $arc_row->year . $month ),
562                                 /* translators: 1: month name, 2: 4-digit year */
563                                 sprintf( __( '%1$s %2$d' ), $wp_locale->get_month( $month ), $year )
564                         );
565                 }
566 ?>
567                 </select>
568 <?php
569         }
570
571         /**
572          * Display a view switcher
573          *
574          * @since 3.1.0
575          * @access protected
576          *
577          * @param string $current_mode
578          */
579         protected function view_switcher( $current_mode ) {
580 ?>
581                 <input type="hidden" name="mode" value="<?php echo esc_attr( $current_mode ); ?>" />
582                 <div class="view-switch">
583 <?php
584                         foreach ( $this->modes as $mode => $title ) {
585                                 $classes = array( 'view-' . $mode );
586                                 if ( $current_mode == $mode )
587                                         $classes[] = 'current';
588                                 printf(
589                                         "<a href='%s' class='%s' id='view-switch-$mode'><span class='screen-reader-text'>%s</span></a>\n",
590                                         esc_url( add_query_arg( 'mode', $mode ) ),
591                                         implode( ' ', $classes ),
592                                         $title
593                                 );
594                         }
595                 ?>
596                 </div>
597 <?php
598         }
599
600         /**
601          * Display a comment count bubble
602          *
603          * @since 3.1.0
604          * @access protected
605          *
606          * @param int $post_id          The post ID.
607          * @param int $pending_comments Number of pending comments.
608          */
609         protected function comments_bubble( $post_id, $pending_comments ) {
610                 $pending_phrase = sprintf( __( '%s pending' ), number_format( $pending_comments ) );
611
612                 if ( $pending_comments )
613                         echo '<strong>';
614
615                 echo "<a href='" . esc_url( add_query_arg( 'p', $post_id, admin_url( 'edit-comments.php' ) ) ) . "' title='" . esc_attr( $pending_phrase ) . "' class='post-com-count'><span class='comment-count'>" . number_format_i18n( get_comments_number() ) . "</span></a>";
616
617                 if ( $pending_comments )
618                         echo '</strong>';
619         }
620
621         /**
622          * Get the current page number
623          *
624          * @since 3.1.0
625          * @access public
626          *
627          * @return int
628          */
629         public function get_pagenum() {
630                 $pagenum = isset( $_REQUEST['paged'] ) ? absint( $_REQUEST['paged'] ) : 0;
631
632                 if( isset( $this->_pagination_args['total_pages'] ) && $pagenum > $this->_pagination_args['total_pages'] )
633                         $pagenum = $this->_pagination_args['total_pages'];
634
635                 return max( 1, $pagenum );
636         }
637
638         /**
639          * Get number of items to display on a single page
640          *
641          * @since 3.1.0
642          * @access protected
643          *
644          * @param string $option
645          * @param int    $default
646          * @return int
647          */
648         protected function get_items_per_page( $option, $default = 20 ) {
649                 $per_page = (int) get_user_option( $option );
650                 if ( empty( $per_page ) || $per_page < 1 )
651                         $per_page = $default;
652
653                 /**
654                  * Filter the number of items to be displayed on each page of the list table.
655                  *
656                  * The dynamic hook name, $option, refers to the `per_page` option depending
657                  * on the type of list table in use. Possible values include: 'edit_comments_per_page',
658                  * 'sites_network_per_page', 'site_themes_network_per_page', 'themes_network_per_page',
659                  * 'users_network_per_page', 'edit_post_per_page', 'edit_page_per_page',
660                  * 'edit_{$post_type}_per_page', etc.
661                  *
662                  * @since 2.9.0
663                  *
664                  * @param int $per_page Number of items to be displayed. Default 20.
665                  */
666                 return (int) apply_filters( $option, $per_page );
667         }
668
669         /**
670          * Display the pagination.
671          *
672          * @since 3.1.0
673          * @access protected
674          *
675          * @param string $which
676          */
677         protected function pagination( $which ) {
678                 if ( empty( $this->_pagination_args ) ) {
679                         return;
680                 }
681
682                 $total_items = $this->_pagination_args['total_items'];
683                 $total_pages = $this->_pagination_args['total_pages'];
684                 $infinite_scroll = false;
685                 if ( isset( $this->_pagination_args['infinite_scroll'] ) ) {
686                         $infinite_scroll = $this->_pagination_args['infinite_scroll'];
687                 }
688
689                 $output = '<span class="displaying-num">' . sprintf( _n( '1 item', '%s items', $total_items ), number_format_i18n( $total_items ) ) . '</span>';
690
691                 $current = $this->get_pagenum();
692
693                 $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
694
695                 $current_url = remove_query_arg( array( 'hotkeys_highlight_last', 'hotkeys_highlight_first' ), $current_url );
696
697                 $page_links = array();
698
699                 $disable_first = $disable_last = '';
700                 if ( $current == 1 ) {
701                         $disable_first = ' disabled';
702                 }
703                 if ( $current == $total_pages ) {
704                         $disable_last = ' disabled';
705                 }
706                 $page_links[] = sprintf( "<a class='%s' title='%s' href='%s'>%s</a>",
707                         'first-page' . $disable_first,
708                         esc_attr__( 'Go to the first page' ),
709                         esc_url( remove_query_arg( 'paged', $current_url ) ),
710                         '&laquo;'
711                 );
712
713                 $page_links[] = sprintf( "<a class='%s' title='%s' href='%s'>%s</a>",
714                         'prev-page' . $disable_first,
715                         esc_attr__( 'Go to the previous page' ),
716                         esc_url( add_query_arg( 'paged', max( 1, $current-1 ), $current_url ) ),
717                         '&lsaquo;'
718                 );
719
720                 if ( 'bottom' == $which ) {
721                         $html_current_page = $current;
722                 } else {
723                         $html_current_page = sprintf( "%s<input class='current-page' id='current-page-selector' title='%s' type='text' name='paged' value='%s' size='%d' />",
724                                 '<label for="current-page-selector" class="screen-reader-text">' . __( 'Select Page' ) . '</label>',
725                                 esc_attr__( 'Current page' ),
726                                 $current,
727                                 strlen( $total_pages )
728                         );
729                 }
730                 $html_total_pages = sprintf( "<span class='total-pages'>%s</span>", number_format_i18n( $total_pages ) );
731                 $page_links[] = '<span class="paging-input">' . sprintf( _x( '%1$s of %2$s', 'paging' ), $html_current_page, $html_total_pages ) . '</span>';
732
733                 $page_links[] = sprintf( "<a class='%s' title='%s' href='%s'>%s</a>",
734                         'next-page' . $disable_last,
735                         esc_attr__( 'Go to the next page' ),
736                         esc_url( add_query_arg( 'paged', min( $total_pages, $current+1 ), $current_url ) ),
737                         '&rsaquo;'
738                 );
739
740                 $page_links[] = sprintf( "<a class='%s' title='%s' href='%s'>%s</a>",
741                         'last-page' . $disable_last,
742                         esc_attr__( 'Go to the last page' ),
743                         esc_url( add_query_arg( 'paged', $total_pages, $current_url ) ),
744                         '&raquo;'
745                 );
746
747                 $pagination_links_class = 'pagination-links';
748                 if ( ! empty( $infinite_scroll ) ) {
749                         $pagination_links_class = ' hide-if-js';
750                 }
751                 $output .= "\n<span class='$pagination_links_class'>" . join( "\n", $page_links ) . '</span>';
752
753                 if ( $total_pages ) {
754                         $page_class = $total_pages < 2 ? ' one-page' : '';
755                 } else {
756                         $page_class = ' no-pages';
757                 }
758                 $this->_pagination = "<div class='tablenav-pages{$page_class}'>$output</div>";
759
760                 echo $this->_pagination;
761         }
762
763         /**
764          * Get a list of columns. The format is:
765          * 'internal-name' => 'Title'
766          *
767          * @since 3.1.0
768          * @access public
769          * @abstract
770          *
771          * @return array
772          */
773         public function get_columns() {
774                 die( 'function WP_List_Table::get_columns() must be over-ridden in a sub-class.' );
775         }
776
777         /**
778          * Get a list of sortable columns. The format is:
779          * 'internal-name' => 'orderby'
780          * or
781          * 'internal-name' => array( 'orderby', true )
782          *
783          * The second format will make the initial sorting order be descending
784          *
785          * @since 3.1.0
786          * @access protected
787          *
788          * @return array
789          */
790         protected function get_sortable_columns() {
791                 return array();
792         }
793
794         /**
795          * Get a list of all, hidden and sortable columns, with filter applied
796          *
797          * @since 3.1.0
798          * @access protected
799          *
800          * @return array
801          */
802         protected function get_column_info() {
803                 if ( isset( $this->_column_headers ) )
804                         return $this->_column_headers;
805
806                 $columns = get_column_headers( $this->screen );
807                 $hidden = get_hidden_columns( $this->screen );
808
809                 $sortable_columns = $this->get_sortable_columns();
810                 /**
811                  * Filter the list table sortable columns for a specific screen.
812                  *
813                  * The dynamic portion of the hook name, `$this->screen->id`, refers
814                  * to the ID of the current screen, usually a string.
815                  *
816                  * @since 3.5.0
817                  *
818                  * @param array $sortable_columns An array of sortable columns.
819                  */
820                 $_sortable = apply_filters( "manage_{$this->screen->id}_sortable_columns", $sortable_columns );
821
822                 $sortable = array();
823                 foreach ( $_sortable as $id => $data ) {
824                         if ( empty( $data ) )
825                                 continue;
826
827                         $data = (array) $data;
828                         if ( !isset( $data[1] ) )
829                                 $data[1] = false;
830
831                         $sortable[$id] = $data;
832                 }
833
834                 $this->_column_headers = array( $columns, $hidden, $sortable );
835
836                 return $this->_column_headers;
837         }
838
839         /**
840          * Return number of visible columns
841          *
842          * @since 3.1.0
843          * @access public
844          *
845          * @return int
846          */
847         public function get_column_count() {
848                 list ( $columns, $hidden ) = $this->get_column_info();
849                 $hidden = array_intersect( array_keys( $columns ), array_filter( $hidden ) );
850                 return count( $columns ) - count( $hidden );
851         }
852
853         /**
854          * Print column headers, accounting for hidden and sortable columns.
855          *
856          * @since 3.1.0
857          * @access public
858          *
859          * @param bool $with_id Whether to set the id attribute or not
860          */
861         public function print_column_headers( $with_id = true ) {
862                 list( $columns, $hidden, $sortable ) = $this->get_column_info();
863
864                 $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
865                 $current_url = remove_query_arg( 'paged', $current_url );
866
867                 if ( isset( $_GET['orderby'] ) )
868                         $current_orderby = $_GET['orderby'];
869                 else
870                         $current_orderby = '';
871
872                 if ( isset( $_GET['order'] ) && 'desc' == $_GET['order'] )
873                         $current_order = 'desc';
874                 else
875                         $current_order = 'asc';
876
877                 if ( ! empty( $columns['cb'] ) ) {
878                         static $cb_counter = 1;
879                         $columns['cb'] = '<label class="screen-reader-text" for="cb-select-all-' . $cb_counter . '">' . __( 'Select All' ) . '</label>'
880                                 . '<input id="cb-select-all-' . $cb_counter . '" type="checkbox" />';
881                         $cb_counter++;
882                 }
883
884                 foreach ( $columns as $column_key => $column_display_name ) {
885                         $class = array( 'manage-column', "column-$column_key" );
886
887                         $style = '';
888                         if ( in_array( $column_key, $hidden ) )
889                                 $style = 'display:none;';
890
891                         $style = ' style="' . $style . '"';
892
893                         if ( 'cb' == $column_key )
894                                 $class[] = 'check-column';
895                         elseif ( in_array( $column_key, array( 'posts', 'comments', 'links' ) ) )
896                                 $class[] = 'num';
897
898                         if ( isset( $sortable[$column_key] ) ) {
899                                 list( $orderby, $desc_first ) = $sortable[$column_key];
900
901                                 if ( $current_orderby == $orderby ) {
902                                         $order = 'asc' == $current_order ? 'desc' : 'asc';
903                                         $class[] = 'sorted';
904                                         $class[] = $current_order;
905                                 } else {
906                                         $order = $desc_first ? 'desc' : 'asc';
907                                         $class[] = 'sortable';
908                                         $class[] = $desc_first ? 'asc' : 'desc';
909                                 }
910
911                                 $column_display_name = '<a href="' . esc_url( add_query_arg( compact( 'orderby', 'order' ), $current_url ) ) . '"><span>' . $column_display_name . '</span><span class="sorting-indicator"></span></a>';
912                         }
913
914                         $id = $with_id ? "id='$column_key'" : '';
915
916                         if ( !empty( $class ) )
917                                 $class = "class='" . join( ' ', $class ) . "'";
918
919                         echo "<th scope='col' $id $class $style>$column_display_name</th>";
920                 }
921         }
922
923         /**
924          * Display the table
925          *
926          * @since 3.1.0
927          * @access public
928          */
929         public function display() {
930                 $singular = $this->_args['singular'];
931
932                 $this->display_tablenav( 'top' );
933
934 ?>
935 <table class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>">
936         <thead>
937         <tr>
938                 <?php $this->print_column_headers(); ?>
939         </tr>
940         </thead>
941
942         <tbody id="the-list"<?php
943                 if ( $singular ) {
944                         echo " data-wp-lists='list:$singular'";
945                 } ?>>
946                 <?php $this->display_rows_or_placeholder(); ?>
947         </tbody>
948
949         <tfoot>
950         <tr>
951                 <?php $this->print_column_headers( false ); ?>
952         </tr>
953         </tfoot>
954
955 </table>
956 <?php
957                 $this->display_tablenav( 'bottom' );
958         }
959
960         /**
961          * Get a list of CSS classes for the list table table tag.
962          *
963          * @since 3.1.0
964          * @access protected
965          *
966          * @return array List of CSS classes for the table tag.
967          */
968         protected function get_table_classes() {
969                 return array( 'widefat', 'fixed', 'striped', $this->_args['plural'] );
970         }
971
972         /**
973          * Generate the table navigation above or below the table
974          *
975          * @since 3.1.0
976          * @access protected
977          * @param string $which
978          */
979         protected function display_tablenav( $which ) {
980                 if ( 'top' == $which )
981                         wp_nonce_field( 'bulk-' . $this->_args['plural'] );
982 ?>
983         <div class="tablenav <?php echo esc_attr( $which ); ?>">
984
985                 <div class="alignleft actions bulkactions">
986                         <?php $this->bulk_actions( $which ); ?>
987                 </div>
988 <?php
989                 $this->extra_tablenav( $which );
990                 $this->pagination( $which );
991 ?>
992
993                 <br class="clear" />
994         </div>
995 <?php
996         }
997
998         /**
999          * Extra controls to be displayed between bulk actions and pagination
1000          *
1001          * @since 3.1.0
1002          * @access protected
1003          *
1004          * @param string $which
1005          */
1006         protected function extra_tablenav( $which ) {}
1007
1008         /**
1009          * Generate the tbody element for the list table.
1010          *
1011          * @since 3.1.0
1012          * @access public
1013          */
1014         public function display_rows_or_placeholder() {
1015                 if ( $this->has_items() ) {
1016                         $this->display_rows();
1017                 } else {
1018                         echo '<tr class="no-items"><td class="colspanchange" colspan="' . $this->get_column_count() . '">';
1019                         $this->no_items();
1020                         echo '</td></tr>';
1021                 }
1022         }
1023
1024         /**
1025          * Generate the table rows
1026          *
1027          * @since 3.1.0
1028          * @access public
1029          */
1030         public function display_rows() {
1031                 foreach ( $this->items as $item )
1032                         $this->single_row( $item );
1033         }
1034
1035         /**
1036          * Generates content for a single row of the table
1037          *
1038          * @since 3.1.0
1039          * @access public
1040          *
1041          * @param object $item The current item
1042          */
1043         public function single_row( $item ) {
1044                 echo '<tr>';
1045                 $this->single_row_columns( $item );
1046                 echo '</tr>';
1047         }
1048
1049         protected function column_default( $item, $column_name ) {}
1050
1051         protected function column_cb( $item ) {}
1052
1053         /**
1054          * Generates the columns for a single row of the table
1055          *
1056          * @since 3.1.0
1057          * @access protected
1058          *
1059          * @param object $item The current item
1060          */
1061         protected function single_row_columns( $item ) {
1062                 list( $columns, $hidden ) = $this->get_column_info();
1063
1064                 foreach ( $columns as $column_name => $column_display_name ) {
1065                         $class = "class='$column_name column-$column_name'";
1066
1067                         $style = '';
1068                         if ( in_array( $column_name, $hidden ) )
1069                                 $style = ' style="display:none;"';
1070
1071                         $attributes = "$class$style";
1072
1073                         if ( 'cb' == $column_name ) {
1074                                 echo '<th scope="row" class="check-column">';
1075                                 echo $this->column_cb( $item );
1076                                 echo '</th>';
1077                         }
1078                         elseif ( method_exists( $this, 'column_' . $column_name ) ) {
1079                                 echo "<td $attributes>";
1080                                 echo call_user_func( array( $this, 'column_' . $column_name ), $item );
1081                                 echo "</td>";
1082                         }
1083                         else {
1084                                 echo "<td $attributes>";
1085                                 echo $this->column_default( $item, $column_name );
1086                                 echo "</td>";
1087                         }
1088                 }
1089         }
1090
1091         /**
1092          * Handle an incoming ajax request (called from admin-ajax.php)
1093          *
1094          * @since 3.1.0
1095          * @access public
1096          */
1097         public function ajax_response() {
1098                 $this->prepare_items();
1099
1100                 ob_start();
1101                 if ( ! empty( $_REQUEST['no_placeholder'] ) ) {
1102                         $this->display_rows();
1103                 } else {
1104                         $this->display_rows_or_placeholder();
1105                 }
1106
1107                 $rows = ob_get_clean();
1108
1109                 $response = array( 'rows' => $rows );
1110
1111                 if ( isset( $this->_pagination_args['total_items'] ) ) {
1112                         $response['total_items_i18n'] = sprintf(
1113                                 _n( '1 item', '%s items', $this->_pagination_args['total_items'] ),
1114                                 number_format_i18n( $this->_pagination_args['total_items'] )
1115                         );
1116                 }
1117                 if ( isset( $this->_pagination_args['total_pages'] ) ) {
1118                         $response['total_pages'] = $this->_pagination_args['total_pages'];
1119                         $response['total_pages_i18n'] = number_format_i18n( $this->_pagination_args['total_pages'] );
1120                 }
1121
1122                 die( wp_json_encode( $response ) );
1123         }
1124
1125         /**
1126          * Send required variables to JavaScript land
1127          *
1128          * @access public
1129          */
1130         public function _js_vars() {
1131                 $args = array(
1132                         'class'  => get_class( $this ),
1133                         'screen' => array(
1134                                 'id'   => $this->screen->id,
1135                                 'base' => $this->screen->base,
1136                         )
1137                 );
1138
1139                 printf( "<script type='text/javascript'>list_args = %s;</script>\n", wp_json_encode( $args ) );
1140         }
1141 }