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