PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.28
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.28
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.28, at classes/helpers/FrmListHelper.php

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