PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.22.3
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.22.3
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
formidable / classes / helpers / FrmListHelper.php

FrmListHelper.php in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More 6.22.3, at classes/helpers/FrmListHelper.php

1,267 lines 32.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 die( 'You are not allowed to call this page directly.' );
4 }
5
6 class FrmListHelper {
7 /**
8 * The current list of items
9 *
10 * @since 2.0.18
11 * @var array
12 */
13 public $items;
14
15 /**
16 * @since 4.07
17 * @var bool|int
18 */
19 public $total_items = false;
20
21 /**
22 * Various information about the current table
23 *
24 * @since 2.0.18
25 * @var array
26 */
27 protected $_args;
28
29 /**
30 * Various information needed for displaying the pagination
31 *
32 * @since 2.0.18
33 * @var array
34 */
35 protected $_pagination_args = array();
36
37 /**
38 * The current screen
39 *
40 * @since 2.0.18
41 *
42 * @var \WP_Screen
43 */
44 protected $screen;
45
46 /**
47 * Cached bulk actions
48 *
49 * @since 2.0.18
50 * @var array
51 */
52 private $_actions;
53
54 /**
55 * Cached pagination output
56 *
57 * @since 2.0.18
58 * @var string
59 */
60 private $_pagination;
61
62 /**
63 * The view switcher modes.
64 *
65 * @since 2.0.18
66 * @var array
67 */
68 protected $modes = array();
69
70 /**
71 * @var array
72 */
73 protected $params;
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(
85 'set_pagination_args',
86 'get_views',
87 'get_bulk_actions',
88 'bulk_actions',
89 'row_actions',
90 'view_switcher',
91 'get_items_per_page',
92 'pagination',
93 'get_sortable_columns',
94 'get_column_info',
95 'get_table_classes',
96 'display_tablenav',
97 'extra_tablenav',
98 'single_row_columns',
99 );
100
101 /**
102 * Construct the table object
103 */
104 public function __construct( $args ) {
105 $args = wp_parse_args(
106 $args,
107 array(
108 'params' => array(),
109 'plural' => '',
110 'singular' => '',
111 'ajax' => false,
112 'screen' => null,
113 )
114 );
115
116 $this->params = $args['params'];
117
118 $this->screen = convert_to_screen( $args['screen'] );
119
120 add_filter( "manage_{$this->screen->id}_columns", array( $this, 'get_columns' ), 0 );
121
122 if ( ! $args['plural'] ) {
123 $args['plural'] = $this->screen->base;
124 }
125
126 $args['plural'] = sanitize_key( $args['plural'] );
127 $args['singular'] = sanitize_key( $args['singular'] );
128
129 $this->_args = $args;
130
131 if ( $args['ajax'] ) {
132 // wp_enqueue_script( 'list-table' );
133 add_action( 'admin_footer', array( $this, '_js_vars' ) );
134 }
135
136 if ( empty( $this->modes ) ) {
137 $this->modes = array(
138 'list' => __( 'List View', 'formidable' ),
139 'excerpt' => __( 'Excerpt View', 'formidable' ),
140 );
141 }
142 }
143
144 public function ajax_user_can() {
145 return current_user_can( 'administrator' );
146 }
147
148 public function get_columns() {
149 return array();
150 }
151
152 public function display_rows() {
153 foreach ( $this->items as $item ) {
154 echo "\n\t", $this->single_row( $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
155 }
156 }
157
158 /**
159 * Prepares the list of items for displaying.
160 *
161 * @uses FrmListHelper::set_pagination_args()
162 *
163 * @since 2.0.18
164 * @abstract
165 */
166 public function prepare_items() {
167 die( 'function FrmListHelper::prepare_items() must be over-ridden in a sub-class.' );
168 }
169
170 /**
171 * @since 3.0
172 */
173 protected function get_param( $args ) {
174 return FrmAppHelper::get_simple_request(
175 array(
176 'param' => $args['param'],
177 'default' => isset( $args['default'] ) ? $args['default'] : '',
178 'sanitize' => isset( $args['sanitize'] ) ? $args['sanitize'] : 'sanitize_title',
179 'type' => 'request',
180 )
181 );
182 }
183
184 /**
185 * An internal method that sets all the necessary pagination arguments
186 *
187 * @param array $args An associative array with information about the pagination.
188 */
189 protected function set_pagination_args( $args ) {
190 $args = wp_parse_args(
191 $args,
192 array(
193 'total_items' => 0,
194 'total_pages' => 0,
195 'per_page' => 0,
196 )
197 );
198
199 if ( ! $args['total_pages'] && $args['per_page'] > 0 ) {
200 $args['total_pages'] = ceil( $args['total_items'] / $args['per_page'] );
201 }
202
203 // Redirect if page number is invalid and headers are not already sent.
204 if ( ! headers_sent() && ! wp_doing_ajax() && $args['total_pages'] > 0 && $this->get_pagenum() > $args['total_pages'] ) {
205 wp_redirect( add_query_arg( 'paged', $args['total_pages'] ) );
206 exit;
207 }
208
209 $this->_pagination_args = $args;
210 }
211
212 /**
213 * Access the pagination args.
214 *
215 * @since 2.0.18
216 *
217 * @param string $key Pagination argument to retrieve. Common values include 'total_items',
218 * 'total_pages', 'per_page', or 'infinite_scroll'.
219 *
220 * @return int Number of items that correspond to the given pagination argument.
221 */
222 public function get_pagination_arg( $key ) {
223 if ( 'page' == $key ) {
224 return $this->get_pagenum();
225 }
226
227 if ( isset( $this->_pagination_args[ $key ] ) ) {
228 return $this->_pagination_args[ $key ];
229 }
230 }
231
232 /**
233 * Whether the table has items to display or not
234 *
235 * @since 2.0.18
236 *
237 * @return bool
238 */
239 public function has_items() {
240 return ! empty( $this->items );
241 }
242
243 /**
244 * Message to be displayed when there are no items
245 *
246 * @since 2.0.18
247 */
248 public function no_items() {
249 esc_html_e( 'No items found.', 'formidable' );
250 }
251
252 /**
253 * Display the search box.
254 *
255 * @since 2.0.18
256 *
257 * @param string $text The search button text.
258 * @param string $input_id The search input id.
259 */
260 public function search_box( $text, $input_id ) {
261 if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) {
262 return;
263 }
264
265 foreach ( array( 'orderby', 'order' ) as $search_params ) {
266 $this->hidden_search_inputs( $search_params );
267 }
268
269 FrmAppHelper::show_search_box( compact( 'text', 'input_id' ) );
270 }
271
272 private function hidden_search_inputs( $param_name ) {
273 if ( ! empty( $_REQUEST[ $param_name ] ) ) {
274 $value = sanitize_text_field( wp_unslash( $_REQUEST[ $param_name ] ) );
275 echo '<input type="hidden" name="' . esc_attr( $param_name ) . '" value="' . esc_attr( $value ) . '" />';
276 }
277 }
278
279 /**
280 * Get an associative array ( id => link ) with the list
281 * of views available on this table.
282 *
283 * @since 2.0.18
284 *
285 * @return array
286 */
287 protected function get_views() {
288 return array();
289 }
290
291 /**
292 * Display the list of views available on this table.
293 *
294 * @since 2.0.18
295 */
296 public function views() {
297 $views = $this->get_views();
298 /**
299 * Filter the list of available list table views.
300 *
301 * The dynamic portion of the hook name, `$this->screen->id`, refers
302 * to the ID of the current screen, usually a string.
303 *
304 * @since 3.5.0
305 *
306 * @param array $views An array of available list table views.
307 */
308 $views = apply_filters( 'views_' . $this->screen->id, $views );
309
310 if ( empty( $views ) ) {
311 return;
312 }
313
314 echo "<ul class='subsubsub'>\n";
315 foreach ( $views as $class => $view ) {
316 $views[ $class ] = "\t" . '<li class="' . esc_attr( $class ) . '">' . $view;
317 }
318 echo implode( " |</li>\n", $views ) . "</li>\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
319 echo '</ul>';
320 }
321
322 /**
323 * Get an associative array ( option_name => option_title ) with the list
324 * of bulk actions available on this table.
325 *
326 * @since 2.0.18
327 *
328 * @return array
329 */
330 protected function get_bulk_actions() {
331 return array();
332 }
333
334 /**
335 * Display the bulk actions dropdown.
336 *
337 * @since 2.0.18
338 *
339 * @param string $which The location of the bulk actions: 'top' or 'bottom'.
340 * This is designated as optional for backwards-compatibility.
341 */
342 protected function bulk_actions( $which = '' ) {
343 if ( is_null( $this->_actions ) ) {
344 $no_new_actions = $this->get_bulk_actions();
345 $this->_actions = $no_new_actions;
346
347 /**
348 * Filter the list table Bulk Actions drop-down.
349 *
350 * The dynamic portion of the hook name, `$this->screen->id`, refers
351 * to the ID of the current screen, usually a string.
352 *
353 * This filter can currently only be used to remove bulk actions.
354 *
355 * @since 3.5.0
356 *
357 * @param array $actions An array of the available bulk actions.
358 */
359 $this->_actions = apply_filters( "bulk_actions-{$this->screen->id}", $this->_actions );
360 $this->_actions = array_intersect_assoc( $this->_actions, $no_new_actions );
361
362 $two = '';
363 } else {
364 $two = '2';
365 }//end if
366
367 if ( empty( $this->_actions ) ) {
368 return;
369 }
370
371 echo "<label for='bulk-action-selector-" . esc_attr( $which ) . "' class='screen-reader-text'>" . esc_html__( 'Select bulk action', 'formidable' ) . '</label>';
372 echo "<select name='action" . esc_attr( $two ) . "' id='bulk-action-selector-" . esc_attr( $which ) . "'>\n";
373 echo "<option value='-1' selected='selected'>" . esc_html__( 'Bulk Actions', 'formidable' ) . "</option>\n";
374
375 foreach ( $this->_actions as $name => $title ) {
376 $params = array(
377 'value' => $name,
378 );
379 if ( 'edit' === $name ) {
380 $params['class'] = 'hide-if-no-js';
381 }
382
383 echo "\t<option ";
384 FrmAppHelper::array_to_html_params( $params, true );
385 echo '>' . esc_html( $title ) . '</option>' . "\n";
386 }
387
388 echo "</select>\n";
389
390 if ( isset( $this->_actions['bulk_delete'] ) ) {
391 $verify = $this->confirm_bulk_delete();
392
393 if ( $verify ) {
394 $confirm_delete_attributes = array(
395 'id' => 'confirm-bulk-delete-' . $which,
396 'class' => 'frm-hidden',
397 'tabindex' => '-1',
398 'aria-hidden' => 'true',
399 'href' => 'confirm-bulk-delete',
400 'data-loaded-from' => $this->loaded_from(),
401 'data-frmverify' => $verify,
402 'data-frmverify-btn' => 'frm-button-red',
403 );
404
405 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
406 echo '<a ' . FrmAppHelper::array_to_html_params( $confirm_delete_attributes ) . '></a>';
407 }
408 }
409
410 submit_button( __( 'Apply', 'formidable' ), 'action', '', false, array( 'id' => "doaction$two" ) );
411 echo "\n";
412 }
413
414 /**
415 * @return string if empty there will be no confirmation pop up
416 */
417 protected function confirm_bulk_delete() {
418 return '';
419 }
420
421 /**
422 * Get the current action selected from the bulk actions dropdown.
423 *
424 * @since 2.0.18
425 *
426 * @return false|string The action name or False if no action was selected
427 */
428 public function current_action() {
429 if ( ! empty( $_REQUEST['filter_action'] ) ) {
430 return false;
431 }
432
433 $action = $this->get_bulk_action( 'action' );
434 if ( $action === false ) {
435 $action = $this->get_bulk_action( 'action2' );
436 }
437
438 return $action;
439 }
440
441 private function get_bulk_action( $action_name ) {
442 $action = false;
443 $action_param = $this->get_param(
444 array(
445 'param' => $action_name,
446 'sanitize' => 'sanitize_text_field',
447 )
448 );
449 if ( $action_param && - 1 != $action_param ) {
450 $action = $action_param;
451 }
452
453 return $action;
454 }
455
456 /**
457 * Generate row actions div
458 *
459 * @since 2.0.18
460 *
461 * @param array $actions The list of actions.
462 * @param bool $always_visible Whether the actions should be always visible.
463 *
464 * @return string
465 */
466 protected function row_actions( $actions, $always_visible = false ) {
467 $action_count = count( $actions );
468
469 $i = 0;
470
471 if ( ! $action_count ) {
472 return '';
473 }
474
475 $out = '<div class="' . ( $always_visible ? 'row-actions visible' : 'row-actions' ) . '">';
476 foreach ( $actions as $action => $link ) {
477 ++$i;
478 $sep = $i == $action_count ? '' : ' | ';
479 $out .= "<span class='$action'>$link$sep</span>";
480 }
481 $out .= '</div>';
482
483 $out .= '<button type="button" class="toggle-row"><span class="screen-reader-text">' . __( 'Show more details', 'formidable' ) . '</span></button>';
484
485 return $out;
486 }
487
488 /**
489 * Display a view switcher
490 *
491 * @since 2.0.18
492 *
493 * @param string $current_mode
494 */
495 protected function view_switcher( $current_mode ) {
496 ?>
497 <input type="hidden" name="mode" value="<?php echo esc_attr( $current_mode ); ?>"/>
498 <div class="view-switch">
499 <?php
500 foreach ( $this->modes as $mode => $title ) {
501 $classes = array( 'view-' . $mode );
502 if ( $current_mode == $mode ) {
503 $classes[] = 'current';
504 }
505
506 printf(
507 '<a href="%s" class="%s" id="view-switch-' . esc_attr( $mode ) . '"><span class="screen-reader-text">%s</span></a>' . "\n",
508 esc_url( add_query_arg( 'mode', $mode ) ),
509 esc_attr( implode( ' ', $classes ) ),
510 esc_html( $title )
511 );
512 }
513 ?>
514 </div>
515 <?php
516 }
517
518 /**
519 * Get the current page number
520 *
521 * @since 2.0.18
522 *
523 * @return int
524 */
525 public function get_pagenum() {
526 $pagenum = isset( $_REQUEST['paged'] ) ? absint( $_REQUEST['paged'] ) : 0;
527
528 if ( isset( $this->_pagination_args['total_pages'] ) && $pagenum > $this->_pagination_args['total_pages'] ) {
529 $pagenum = $this->_pagination_args['total_pages'];
530 }
531
532 return max( 1, $pagenum );
533 }
534
535 /**
536 * Get number of items to display on a single page
537 *
538 * @since 2.0.18
539 *
540 * @param string $option
541 * @param int $default
542 *
543 * @return int
544 */
545 protected function get_items_per_page( $option, $default = 20 ) {
546 $per_page = (int) get_user_option( $option );
547 if ( empty( $per_page ) || $per_page < 1 ) {
548 $per_page = $default;
549 }
550
551 /**
552 * Filter the number of items to be displayed on each page of the list table.
553 *
554 * The dynamic hook name, $option, refers to the `per_page` option depending
555 * on the type of list table in use. Possible values include: 'edit_comments_per_page',
556 * 'sites_network_per_page', 'site_themes_network_per_page', 'themes_network_per_page',
557 * 'users_network_per_page', 'edit_post_per_page', 'edit_page_per_page',
558 * 'edit_{$post_type}_per_page', etc.
559 *
560 * @since 2.9.0
561 *
562 * @param int $per_page Number of items to be displayed. Default 20.
563 */
564 return (int) apply_filters( $option, $per_page );
565 }
566
567 /**
568 * Display the pagination.
569 *
570 * @since 2.0.18
571 *
572 * @param string $which
573 */
574 protected function pagination( $which ) {
575 if ( empty( $this->_pagination_args ) ) {
576 return;
577 }
578
579 $total_items = $this->_pagination_args['total_items'];
580 $total_pages = $this->_pagination_args['total_pages'];
581 $infinite_scroll = false;
582 if ( isset( $this->_pagination_args['infinite_scroll'] ) ) {
583 $infinite_scroll = $this->_pagination_args['infinite_scroll'];
584 }
585
586 /* translators: %s: Number of items */
587 $output = '<span class="displaying-num">' . sprintf( _n( '%s item', '%s items', $total_items, 'formidable' ), number_format_i18n( $total_items ) ) . '</span>';
588
589 $current = $this->get_pagenum();
590
591 $page_links = array();
592
593 $total_pages_before = '<span class="paging-input">';
594 $total_pages_after = '</span>';
595
596 $disable = $this->disabled_pages( $total_pages );
597
598 $page_links[] = $this->add_page_link(
599 array(
600 'page' => 'first',
601 'arrow' => '&laquo;',
602 'number' => '',
603 'disabled' => $disable['first'],
604 )
605 );
606
607 $page_links[] = $this->add_page_link(
608 array(
609 'page' => 'prev',
610 'arrow' => '&lsaquo;',
611 'number' => max( 1, $current - 1 ),
612 'disabled' => $disable['prev'],
613 )
614 );
615
616 if ( 'bottom' == $which ) {
617 $html_current_page = $current;
618 $total_pages_before = '<span class="screen-reader-text">' . __( 'Current Page', 'formidable' ) . '</span><span id="table-paging" class="paging-input">';
619 } else {
620 $html_current_page = sprintf(
621 "%s<input class='current-page' id='current-page-selector' type='text' name='paged' value='%s' size='%d' aria-describedby='table-paging' />",
622 '<label for="current-page-selector" class="screen-reader-text">' . __( 'Current Page', 'formidable' ) . '</label>',
623 $current,
624 strlen( $total_pages )
625 );
626 }
627 $html_total_pages = sprintf( "<span class='total-pages'>%s</span>", number_format_i18n( $total_pages ) );
628
629 /* translators: %1$s: Current page number, %2$s: Total pages */
630 $page_links[] = $total_pages_before . sprintf( _x( '%1$s of %2$s', 'paging', 'formidable' ), $html_current_page, $html_total_pages ) . $total_pages_after;
631
632 $page_links[] = $this->add_page_link(
633 array(
634 'page' => 'next',
635 'arrow' => '&rsaquo;',
636 'number' => min( $total_pages, $current + 1 ),
637 'disabled' => $disable['next'],
638 )
639 );
640
641 $page_links[] = $this->add_page_link(
642 array(
643 'page' => 'last',
644 'arrow' => '&raquo;',
645 'number' => $total_pages,
646 'disabled' => $disable['last'],
647 )
648 );
649
650 $pagination_links_class = 'pagination-links';
651 if ( ! empty( $infinite_scroll ) ) {
652 $pagination_links_class = ' hide-if-js';
653 }
654 $output .= "\n" . '<span class="' . esc_attr( $pagination_links_class ) . '">' . implode( "\n", $page_links ) . '</span>';
655
656 if ( $total_pages ) {
657 $page_class = $total_pages < 2 ? ' one-page' : '';
658 } else {
659 $page_class = ' no-pages';
660 }
661 $this->_pagination = "<div class='tablenav-pages" . esc_attr( $page_class ) . "'>$output</div>";
662
663 echo $this->_pagination; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
664 }
665
666 private function disabled_pages( $total_pages ) {
667 $current = $this->get_pagenum();
668 $disable = array(
669 'first' => false,
670 'last' => false,
671 'prev' => false,
672 'next' => false,
673 );
674
675 if ( $current == 1 ) {
676 $disable['first'] = true;
677 $disable['prev'] = true;
678 } elseif ( $current == 2 ) {
679 $disable['first'] = true;
680 }
681
682 if ( $current == $total_pages ) {
683 $disable['last'] = true;
684 $disable['next'] = true;
685 } elseif ( $current == $total_pages - 1 ) {
686 $disable['last'] = true;
687 }
688
689 return $disable;
690 }
691
692 private function link_label( $link ) {
693 $labels = array(
694 'first' => __( 'First page', 'formidable' ),
695 'last' => __( 'Last page', 'formidable' ),
696 'prev' => __( 'Previous page', 'formidable' ),
697 'next' => __( 'Next page', 'formidable' ),
698 );
699
700 return $labels[ $link ];
701 }
702
703 private function current_url() {
704 $current_url = set_url_scheme( 'http://' . FrmAppHelper::get_server_value( 'HTTP_HOST' ) . FrmAppHelper::get_server_value( 'REQUEST_URI' ) );
705
706 return remove_query_arg( array( 'hotkeys_highlight_last', 'hotkeys_highlight_first' ), $current_url );
707 }
708
709 private function add_page_link( $atts ) {
710 if ( $atts['disabled'] ) {
711 $link = $this->add_disabled_link( $atts['arrow'] );
712 } else {
713 $link = $this->add_active_link( $atts );
714 }
715
716 return $link;
717 }
718
719 private function add_disabled_link( $label ) {
720 return '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">' . $label . '</span>';
721 }
722
723 private function add_active_link( $atts ) {
724 $url = esc_url( add_query_arg( 'paged', $atts['number'], $this->current_url() ) );
725 $label = $this->link_label( $atts['page'] );
726
727 return sprintf(
728 "<a class='button %s-page' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
729 $atts['page'],
730 $url,
731 $label,
732 $atts['arrow']
733 );
734 }
735
736 /**
737 * Get a list of sortable columns. The format is:
738 * 'internal-name' => 'orderby'
739 * or
740 * 'internal-name' => array( 'orderby', true )
741 *
742 * The second format will make the initial sorting order be descending
743 *
744 * @since 2.0.18
745 *
746 * @return array
747 */
748 protected function get_sortable_columns() {
749 return array();
750 }
751
752 /**
753 * Gets the name of the default primary column.
754 *
755 * @since 4.3.0
756 *
757 * @return string Name of the default primary column, in this case, an empty string.
758 */
759 protected function get_default_primary_column_name() {
760 $columns = $this->get_columns();
761 $column = '';
762
763 // We need a primary defined so responsive views show something,
764 // so let's fall back to the first non-checkbox column.
765 foreach ( $columns as $col => $column_name ) {
766 if ( 'cb' === $col ) {
767 continue;
768 }
769
770 $column = $col;
771 break;
772 }
773
774 return $column;
775 }
776
777 /**
778 * Gets the name of the primary column.
779 *
780 * @since 4.3.0
781 *
782 * @return string The name of the primary column.
783 */
784 protected function get_primary_column_name() {
785 $columns = $this->get_columns();
786 $default = $this->get_default_primary_column_name();
787
788 // If the primary column doesn't exist fall back to the
789 // first non-checkbox column.
790 if ( ! isset( $columns[ $default ] ) ) {
791 $default = self::get_default_primary_column_name();
792 }
793
794 /**
795 * Filter the name of the primary column for the current list table.
796 *
797 * @since 4.3.0
798 *
799 * @param string $default Column name default for the specific list table, e.g. 'name'.
800 * @param string $context Screen ID for specific list table, e.g. 'plugins'.
801 */
802 $column = apply_filters( 'list_table_primary_column', $default, $this->screen->id );
803
804 if ( empty( $column ) || ! isset( $columns[ $column ] ) ) {
805 $column = $default;
806 }
807
808 return $column;
809 }
810
811 /**
812 * Get a list of all, hidden and sortable columns, with filter applied
813 *
814 * @since 2.0.18
815 *
816 * @return array
817 */
818 protected function get_column_info() {
819 // $_column_headers is already set / cached
820 if ( is_array( $this->_column_headers ) ) {
821 // Back-compat for list tables that have been manually setting $_column_headers for horse reasons.
822 // In 4.3, we added a fourth argument for primary column.
823 $column_headers = array( array(), array(), array(), $this->get_primary_column_name() );
824 foreach ( $this->_column_headers as $key => $value ) {
825 $column_headers[ $key ] = $value;
826 }
827
828 return $column_headers;
829 }
830
831 $columns = get_column_headers( $this->screen );
832 $hidden = get_hidden_columns( $this->screen );
833
834 $sortable_columns = $this->get_sortable_columns();
835 /**
836 * Filter the list table sortable columns for a specific screen.
837 *
838 * The dynamic portion of the hook name, `$this->screen->id`, refers
839 * to the ID of the current screen, usually a string.
840 *
841 * @since 3.5.0
842 *
843 * @param array $sortable_columns An array of sortable columns.
844 */
845 $_sortable = apply_filters( "manage_{$this->screen->id}_sortable_columns", $sortable_columns );
846
847 $sortable = array();
848 foreach ( $_sortable as $id => $data ) {
849 if ( empty( $data ) ) {
850 continue;
851 }
852
853 $data = (array) $data;
854 if ( ! isset( $data[1] ) ) {
855 $data[1] = false;
856 }
857
858 $sortable[ $id ] = $data;
859 }
860
861 $primary = $this->get_primary_column_name();
862
863 $this->_column_headers = array( $columns, $hidden, $sortable, $primary );
864
865 return $this->_column_headers;
866 }
867
868 /**
869 * Return number of visible columns
870 *
871 * @since 2.0.18
872 *
873 * @return int
874 */
875 public function get_column_count() {
876 list ( $columns, $hidden ) = $this->get_column_info();
877 $hidden = array_intersect( array_keys( $columns ), array_filter( $hidden ) );
878
879 return count( $columns ) - count( $hidden );
880 }
881
882 /**
883 * Print column headers, accounting for hidden and sortable columns.
884 *
885 * @since 2.0.18
886 *
887 * @staticvar int $cb_counter
888 *
889 * @param bool $with_id Whether to set the id attribute or not.
890 * @return void
891 */
892 public function print_column_headers( $with_id = true ) {
893 list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();
894
895 $current_url = set_url_scheme( 'http://' . FrmAppHelper::get_server_value( 'HTTP_HOST' ) . FrmAppHelper::get_server_value( 'REQUEST_URI' ) );
896 $current_url = remove_query_arg( 'paged', $current_url );
897
898 if ( isset( $_GET['orderby'] ) ) {
899 $current_orderby = sanitize_text_field( wp_unslash( $_GET['orderby'] ) );
900 } else {
901 $current_orderby = '';
902 }
903
904 if ( isset( $_GET['order'] ) && 'desc' == $_GET['order'] ) {
905 $current_order = 'desc';
906 } else {
907 $current_order = 'asc';
908 }
909
910 FrmAppController::apply_saved_sort_preference( $current_orderby, $current_order );
911
912 if ( ! empty( $columns['cb'] ) ) {
913 static $cb_counter = 1;
914 $columns['cb'] = '<label class="screen-reader-text" for="cb-select-all-' . $cb_counter . '">' . __( 'Select All', 'formidable' ) . '</label>';
915 $columns['cb'] .= '<input id="cb-select-all-' . esc_attr( $cb_counter ) . '" type="checkbox" />';
916 ++$cb_counter;
917 }
918
919 foreach ( $columns as $column_key => $column_display_name ) {
920 $class = array( 'manage-column', "column-$column_key" );
921 $aria_sort_attr = '';
922 $order_text = '';
923
924 if ( in_array( $column_key, $hidden ) ) {
925 $class[] = 'hidden';
926 }
927
928 if ( 'cb' === $column_key ) {
929 $class[] = 'check-column';
930 } elseif ( in_array( $column_key, array( 'posts', 'comments', 'links' ), true ) ) {
931 $class[] = 'num';
932 }
933
934 if ( $column_key === $primary || $column_key === 'name' ) {
935 $class[] = 'column-primary';
936 }
937
938 if ( isset( $sortable[ $column_key ] ) ) {
939 list( $orderby, $desc_first ) = $sortable[ $column_key ];
940
941 if ( $current_orderby == $orderby ) {
942 // The sorted column. The `aria-sort` attribute must be set only on the sorted column.
943 if ( 'asc' === $current_order ) {
944 $order = 'desc';
945 $aria_sort_attr = ' aria-sort="ascending"';
946 } else {
947 $order = 'asc';
948 $aria_sort_attr = ' aria-sort="descending"';
949 }
950
951 $class[] = 'sorted';
952 $class[] = $current_order;
953 } else {
954 $order = $desc_first ? 'desc' : 'asc';
955 $class[] = 'sortable';
956 $class[] = $desc_first ? 'asc' : 'desc';
957
958 /* translators: Hidden accessibility text. */
959 $asc_text = __( 'Sort ascending.', 'formidable' );
960 /* translators: Hidden accessibility text. */
961 $desc_text = __( 'Sort descending.', 'formidable' );
962 $order_text = 'asc' === $order ? $asc_text : $desc_text;
963 }//end if
964
965 if ( '' !== $order_text ) {
966 $order_text = ' <span class="screen-reader-text">' . $order_text . '</span>';
967 }
968
969 $column_display_name = sprintf(
970 '<a href="%1$s">' .
971 '<span>%2$s</span>' .
972 '<span class="sorting-indicators">' .
973 '<span class="sorting-indicator asc" aria-hidden="true"></span>' .
974 '<span class="sorting-indicator desc" aria-hidden="true"></span>' .
975 '</span>' .
976 '%3$s' .
977 '</a>',
978 esc_url( add_query_arg( compact( 'orderby', 'order' ), $current_url ) ),
979 $column_display_name,
980 $order_text
981 );
982 }//end if
983
984 $tag = 'cb' === $column_key ? 'td' : 'th';
985 $scope = 'th' === $tag ? 'scope="col"' : '';
986 $id = $with_id ? "id='" . esc_attr( $column_key ) . "'" : '';
987
988 if ( ! empty( $class ) ) {
989 $class = "class='" . esc_attr( implode( ' ', $class ) ) . "'";
990 }
991
992 if ( ! $this->has_min_items() && ! $with_id ) {
993 // Hide the labels but show the border.
994 $column_display_name = '';
995 }
996 echo "<$tag $scope $id $class $aria_sort_attr>$column_display_name</$tag>"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
997 }//end foreach
998 }
999
1000 /**
1001 * Display the table
1002 *
1003 * @since 2.0.18
1004 * @param array $args
1005 */
1006 public function display( $args = array() ) {
1007 $singular = $this->_args['singular'];
1008 $tbody_params = array();
1009 if ( $singular ) {
1010 $tbody_params['data-wp-lists'] = 'list:' . $singular;
1011 }
1012 if ( $this->should_display( $args, 'display-top-nav' ) ) {
1013 $this->display_tablenav( 'top' );
1014 }
1015 $this->screen->render_screen_reader_content( 'heading_list' );
1016
1017 ?>
1018 <table class="wp-list-table <?php echo esc_attr( implode( ' ', $this->get_table_classes() ) ); ?>">
1019 <?php if ( $this->has_min_items( 1 ) ) { ?>
1020 <thead>
1021 <tr>
1022 <?php $this->print_column_headers(); ?>
1023 </tr>
1024 </thead>
1025 <?php } ?>
1026
1027 <tbody id="the-list"<?php FrmAppHelper::array_to_html_params( $tbody_params, true ); ?>>
1028 <?php $this->display_rows_or_placeholder(); ?>
1029 </tbody>
1030
1031 <?php if ( $this->has_min_items( 1 ) && $this->should_display( $args, 'display-bottom-headers' ) ) { ?>
1032 <tfoot>
1033 <tr>
1034 <?php $this->print_column_headers( false ); ?>
1035 </tr>
1036 </tfoot>
1037 <?php } ?>
1038 </table>
1039 <?php
1040 if ( $this->should_display( $args, 'display-bottom-nav' ) ) {
1041 $this->display_tablenav( 'bottom' );
1042 }
1043 }
1044
1045 /**
1046 * Determines if a particular feature or element should be displayed.
1047 *
1048 * @param array $args An associative array of arguments.
1049 * @param string $settings The specific setting key to check within the arguments array.
1050 *
1051 * @return bool Returns true if the setting is not set or if it is not false; otherwise, returns false.
1052 */
1053 protected function should_display( $args, $settings ) {
1054 return ! isset( $args[ $settings ] ) || false !== $args[ $settings ];
1055 }
1056
1057 /**
1058 * Get a list of CSS classes for the list table table tag.
1059 *
1060 * @since 2.0.18
1061 *
1062 * @return array List of CSS classes for the table tag.
1063 */
1064 protected function get_table_classes() {
1065 return array( 'widefat', 'fixed', 'striped', $this->_args['plural'] );
1066 }
1067
1068 /**
1069 * Generate the table navigation above or below the table
1070 *
1071 * @since 2.0.18
1072 *
1073 * @param string $which
1074 */
1075 protected function display_tablenav( $which ) {
1076 if ( 'top' === $which ) {
1077 wp_nonce_field( 'bulk-' . $this->_args['plural'], '_wpnonce', false );
1078 if ( ! $this->has_min_items( 1 ) ) {
1079 // Don't show bulk actions if no items.
1080 return;
1081 }
1082 } elseif ( ! $this->has_min_items() ) {
1083 // don't show the bulk actions when there aren't many rows.
1084 return;
1085 }
1086 ?>
1087 <div class="tablenav <?php echo esc_attr( $which ); ?>">
1088
1089 <div class="alignleft actions bulkactions">
1090 <?php $this->bulk_actions( $which ); ?>
1091 </div>
1092 <?php
1093 $this->extra_tablenav( $which );
1094 $this->pagination( $which );
1095 ?>
1096
1097 <br class="clear"/>
1098 </div>
1099 <?php
1100 }
1101
1102 /**
1103 * Use this to exclude the footer labels and bulk items.
1104 * When close together, it feels like duplicates.
1105 *
1106 * @since 4.07
1107 */
1108 protected function has_min_items( $limit = 5 ) {
1109 return $this->has_items() && ( $this->total_items === false || $this->total_items >= $limit );
1110 }
1111
1112 /**
1113 * Extra controls to be displayed between bulk actions and pagination
1114 *
1115 * @since 2.0.18
1116 *
1117 * @param string $which
1118 */
1119 protected function extra_tablenav( $which ) {
1120 }
1121
1122 /**
1123 * Generate the tbody element for the list table.
1124 *
1125 * @since 2.0.18
1126 */
1127 public function display_rows_or_placeholder() {
1128 if ( $this->has_items() ) {
1129 $this->display_rows();
1130 } else {
1131 echo '<tr class="no-items"><td class="colspanchange" colspan="' . esc_attr( $this->get_column_count() ) . '">';
1132 $this->no_items();
1133 echo '</td></tr>';
1134 }
1135 }
1136
1137 /**
1138 * Generates content for a single row of the table
1139 *
1140 * @since 2.0.18
1141 *
1142 * @param object $item The current item.
1143 */
1144 public function single_row( $item ) {
1145 echo '<tr>';
1146 $this->single_row_columns( $item );
1147 echo '</tr>';
1148 }
1149
1150 /**
1151 * Generates the columns for a single row of the table
1152 *
1153 * @since 2.0.18
1154 *
1155 * @param object $item The current item.
1156 */
1157 protected function single_row_columns( $item ) {
1158 list( $columns, $hidden,, $primary ) = $this->get_column_info();
1159
1160 foreach ( $columns as $column_name => $column_display_name ) {
1161 $classes = "$column_name column-$column_name";
1162 if ( $primary === $column_name ) {
1163 $classes .= ' has-row-actions column-primary';
1164 }
1165
1166 if ( in_array( $column_name, $hidden ) ) {
1167 $classes .= ' hidden';
1168 }
1169
1170 $params = array(
1171 'class' => $classes,
1172 // Comments column uses HTML in the display name with screen reader text.
1173 // Instead of using esc_attr(), we strip tags to get closer to a user-friendly string.
1174 'data-colname' => $column_display_name,
1175 );
1176
1177 if ( 'cb' === $column_name ) {
1178 echo '<th scope="row" class="check-column"></th>';
1179 } else {
1180 echo '<td ';
1181 FrmAppHelper::array_to_html_params( $params, true );
1182 echo '>';
1183
1184 if ( method_exists( $this, 'column_' . $column_name ) ) {
1185 echo call_user_func( array( $this, 'column_' . $column_name ), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1186 }
1187
1188 echo $this->handle_row_actions( $item, $column_name, $primary ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1189 echo '</td>';
1190 }
1191 }//end foreach
1192 }
1193
1194 /**
1195 * Generates and display row actions links for the list table.
1196 *
1197 * @since 4.3.0
1198 *
1199 * @param object $item The item being acted upon.
1200 * @param string $column_name Current column name.
1201 * @param string $primary Primary column name.
1202 *
1203 * @return string The row actions output. In this case, an empty string.
1204 */
1205 protected function handle_row_actions( $item, $column_name, $primary ) {
1206 return $column_name == $primary ? '<button type="button" class="toggle-row"><span class="screen-reader-text">' . esc_html__( 'Show more details', 'formidable' ) . '</span></button>' : '';
1207 }
1208
1209 /**
1210 * Handle an incoming ajax request (called from admin-ajax.php)
1211 *
1212 * @since 2.0.18
1213 */
1214 public function ajax_response() {
1215 $this->prepare_items();
1216
1217 ob_start();
1218 if ( ! empty( $_REQUEST['no_placeholder'] ) ) {
1219 $this->display_rows();
1220 } else {
1221 $this->display_rows_or_placeholder();
1222 }
1223
1224 $rows = ob_get_clean();
1225
1226 $response = array( 'rows' => $rows );
1227
1228 if ( isset( $this->_pagination_args['total_items'] ) ) {
1229 $response['total_items_i18n'] = sprintf(
1230 /* translators: %s: Number of items */
1231 _n( '%s item', '%s items', $this->_pagination_args['total_items'], 'formidable' ),
1232 number_format_i18n( $this->_pagination_args['total_items'] )
1233 );
1234 }
1235 if ( isset( $this->_pagination_args['total_pages'] ) ) {
1236 $response['total_pages'] = $this->_pagination_args['total_pages'];
1237 $response['total_pages_i18n'] = number_format_i18n( $this->_pagination_args['total_pages'] );
1238 }
1239
1240 die( wp_json_encode( $response ) );
1241 }
1242
1243 /**
1244 * Send required variables to JavaScript land
1245 *
1246 * @return void
1247 */
1248 public function _js_vars() {
1249 $args = array(
1250 'class' => get_class( $this ),
1251 'screen' => array(
1252 'id' => $this->screen->id,
1253 'base' => $this->screen->base,
1254 ),
1255 );
1256
1257 printf( "<script type='text/javascript'>list_args = %s;</script>\n", wp_json_encode( $args ) );
1258 }
1259
1260 /**
1261 * @return string
1262 */
1263 protected function loaded_from() {
1264 return '';
1265 }
1266 }
1267