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

1,345 lines 34.3 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 return;
310 }
311
312 $value = sanitize_text_field( wp_unslash( $_REQUEST[ $param_name ] ) );
313 echo '<input type="hidden" name="' . esc_attr( $param_name ) . '" value="' . esc_attr( $value ) . '" />';
314 }
315
316 /**
317 * Get an associative array ( id => link ) with the list
318 * of views available on this table.
319 *
320 * @since 2.0.18
321 *
322 * @return array
323 */
324 protected function get_views() {
325 return array();
326 }
327
328 /**
329 * Display the list of views available on this table.
330 *
331 * @since 2.0.18
332 */
333 public function views() {
334 $views = $this->get_views();
335 /**
336 * Filter the list of available list table views.
337 *
338 * The dynamic portion of the hook name, `$this->screen->id`, refers
339 * to the ID of the current screen, usually a string.
340 *
341 * @since 3.5.0
342 *
343 * @param array $views An array of available list table views.
344 */
345 $views = apply_filters( 'views_' . $this->screen->id, $views );
346
347 if ( ! $views ) {
348 return;
349 }
350
351 echo "<ul class='subsubsub'>\n";
352
353 foreach ( $views as $class => $view ) {
354 $views[ $class ] = "\t" . '<li class="' . esc_attr( $class ) . '">' . $view;
355 }
356 echo implode( " |</li>\n", $views ) . "</li>\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
357 echo '</ul>';
358 }
359
360 /**
361 * Get an associative array ( option_name => option_title ) with the list
362 * of bulk actions available on this table.
363 *
364 * @since 2.0.18
365 *
366 * @return array
367 */
368 protected function get_bulk_actions() {
369 return array();
370 }
371
372 /**
373 * Display the bulk actions dropdown.
374 *
375 * @since 2.0.18
376 *
377 * @param string $which The location of the bulk actions: 'top' or 'bottom'.
378 * This is designated as optional for backwards-compatibility.
379 */
380 protected function bulk_actions( $which = '' ) {
381 if ( is_null( $this->_actions ) ) {
382 $no_new_actions = $this->get_bulk_actions();
383 $this->_actions = $no_new_actions;
384
385 /**
386 * Filter the list table Bulk Actions drop-down.
387 *
388 * The dynamic portion of the hook name, `$this->screen->id`, refers
389 * to the ID of the current screen, usually a string.
390 *
391 * This filter can currently only be used to remove bulk actions.
392 *
393 * @since 3.5.0
394 *
395 * @param array $actions An array of the available bulk actions.
396 */
397 $this->_actions = apply_filters( "bulk_actions-{$this->screen->id}", $this->_actions );
398 $this->_actions = array_intersect_assoc( $this->_actions, $no_new_actions );
399
400 $two = '';
401 } else {
402 $two = '2';
403 }//end if
404
405 if ( ! $this->_actions ) {
406 return;
407 }
408
409 echo "<label for='bulk-action-selector-" . esc_attr( $which ) . "' class='screen-reader-text'>" . esc_html__( 'Select bulk action', 'formidable' ) . '</label>';
410 echo "<select name='action" . esc_attr( $two ) . "' id='bulk-action-selector-" . esc_attr( $which ) . "'>\n";
411 echo "<option value='-1' selected='selected'>" . esc_html__( 'Bulk Actions', 'formidable' ) . "</option>\n";
412
413 foreach ( $this->_actions as $name => $title ) {
414 $params = array(
415 'value' => $name,
416 );
417
418 if ( 'edit' === $name ) {
419 $params['class'] = 'hide-if-no-js';
420 }
421
422 echo "\t<option ";
423 FrmAppHelper::array_to_html_params( $params, true );
424 echo '>' . esc_html( $title ) . '</option>' . "\n";
425 }
426
427 echo "</select>\n";
428
429 if ( isset( $this->_actions['bulk_delete'] ) ) {
430 $verify = $this->confirm_bulk_delete();
431
432 if ( $verify ) {
433 $confirm_delete_attributes = array(
434 'id' => 'confirm-bulk-delete-' . $which,
435 'class' => 'frm-hidden',
436 'tabindex' => '-1',
437 'aria-hidden' => 'true',
438 'href' => 'confirm-bulk-delete',
439 'data-loaded-from' => $this->loaded_from(),
440 'data-frmverify' => $verify,
441 'data-frmverify-btn' => 'frm-button-red',
442 );
443
444 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
445 echo '<a ' . FrmAppHelper::array_to_html_params( $confirm_delete_attributes ) . '></a>';
446 }
447 }
448
449 submit_button( __( 'Apply', 'formidable' ), 'action', '', false, array( 'id' => "doaction$two" ) );
450 echo "\n";
451 }
452
453 /**
454 * @return string if empty there will be no confirmation pop up
455 */
456 protected function confirm_bulk_delete() {
457 return '';
458 }
459
460 /**
461 * Get the current action selected from the bulk actions dropdown.
462 *
463 * @since 2.0.18
464 *
465 * @return false|string The action name or False if no action was selected
466 */
467 public function current_action() {
468 if ( ! empty( $_REQUEST['filter_action'] ) ) {
469 return false;
470 }
471
472 $action = $this->get_bulk_action( 'action' );
473
474 return $action === false ? $this->get_bulk_action( 'action2' ) : $action;
475 }
476
477 /**
478 * @param string $action_name
479 *
480 * @return false|string
481 */
482 private function get_bulk_action( $action_name ) {
483 $action = false;
484 $action_param = $this->get_param(
485 array(
486 'param' => $action_name,
487 'sanitize' => 'sanitize_text_field',
488 )
489 );
490
491 // phpcs:ignore Universal.Operators.StrictComparisons
492 if ( $action_param && - 1 != $action_param ) {
493 return $action_param;
494 }
495
496 return $action;
497 }
498
499 /**
500 * Generate row actions div
501 *
502 * @since 2.0.18
503 *
504 * @param array $actions The list of actions.
505 * @param bool $always_visible Whether the actions should be always visible.
506 *
507 * @return string
508 */
509 protected function row_actions( $actions, $always_visible = false ) {
510 $action_count = count( $actions );
511
512 if ( ! $action_count ) {
513 return '';
514 }
515
516 $i = 0;
517 $out = '<div class="' . ( $always_visible ? 'row-actions visible' : 'row-actions' ) . '">';
518
519 foreach ( $actions as $action => $link ) {
520 ++$i;
521 $sep = $i === $action_count ? '' : ' | ';
522 $out .= "<span class='$action'>$link$sep</span>";
523 }
524
525 $out .= '</div>';
526
527 return $out . ( '<button type="button" class="toggle-row"><span class="screen-reader-text">' . esc_html__( 'Show more details', 'formidable' ) . '</span></button>' );
528 }
529
530 /**
531 * Display a view switcher
532 *
533 * @since 2.0.18
534 *
535 * @param string $current_mode
536 */
537 protected function view_switcher( $current_mode ) {
538 // phpcs:disable Generic.WhiteSpace.ScopeIndent
539 ?>
540 <input type="hidden" name="mode" value="<?php echo esc_attr( $current_mode ); ?>"/>
541 <div class="view-switch">
542 <?php
543 foreach ( $this->modes as $mode => $title ) {
544 $classes = array( 'view-' . $mode );
545
546 if ( $current_mode === $mode ) {
547 $classes[] = 'current';
548 }
549
550 printf(
551 '<a href="%s" class="%s" id="view-switch-' . esc_attr( $mode ) . '"><span class="screen-reader-text">%s</span></a>' . "\n",
552 esc_url( add_query_arg( 'mode', $mode ) ),
553 esc_attr( implode( ' ', $classes ) ),
554 esc_html( $title )
555 );
556 }
557 ?>
558 </div>
559 <?php
560 // phpcs:enable Generic.WhiteSpace.ScopeIndent
561 }
562
563 /**
564 * Get the current page number
565 *
566 * @since 2.0.18
567 *
568 * @return int
569 */
570 public function get_pagenum() {
571 $pagenum = isset( $_REQUEST['paged'] ) ? absint( $_REQUEST['paged'] ) : 0;
572
573 if ( isset( $this->_pagination_args['total_pages'] ) && $pagenum > $this->_pagination_args['total_pages'] ) {
574 $pagenum = $this->_pagination_args['total_pages'];
575 }
576
577 return max( 1, $pagenum );
578 }
579
580 /**
581 * Get number of items to display on a single page
582 *
583 * @since 2.0.18
584 *
585 * @param string $option
586 * @param int $default
587 *
588 * @return int
589 */
590 protected function get_items_per_page( $option, $default = 20 ) {
591 $per_page = (int) get_user_option( $option );
592
593 if ( ! $per_page || $per_page < 1 ) {
594 $per_page = $default;
595 }
596
597 /**
598 * Filter the number of items to be displayed on each page of the list table.
599 *
600 * The dynamic hook name, $option, refers to the `per_page` option depending
601 * on the type of list table in use. Possible values include: 'edit_comments_per_page',
602 * 'sites_network_per_page', 'site_themes_network_per_page', 'themes_network_per_page',
603 * 'users_network_per_page', 'edit_post_per_page', 'edit_page_per_page',
604 * 'edit_{$post_type}_per_page', etc.
605 *
606 * @since 2.9.0
607 *
608 * @param int $per_page Number of items to be displayed. Default 20.
609 */
610 return (int) apply_filters( $option, $per_page );
611 }
612
613 /**
614 * Display the pagination.
615 *
616 * @since 2.0.18
617 *
618 * @param string $which
619 */
620 protected function pagination( $which ) {
621 if ( ! $this->_pagination_args ) {
622 return;
623 }
624
625 $total_items = $this->_pagination_args['total_items'];
626 $total_pages = $this->_pagination_args['total_pages'];
627 $infinite_scroll = false;
628
629 if ( isset( $this->_pagination_args['infinite_scroll'] ) ) {
630 $infinite_scroll = $this->_pagination_args['infinite_scroll'];
631 }
632
633 /* translators: %s: Number of items */
634 $output = '<span class="displaying-num">' . sprintf( _n( '%s item', '%s items', $total_items, 'formidable' ), number_format_i18n( $total_items ) ) . '</span>';
635
636 $current = $this->get_pagenum();
637 $page_links = array();
638 $total_pages_before = '<span class="paging-input">';
639 $total_pages_after = '</span>';
640 $disable = $this->disabled_pages( $total_pages );
641
642 $page_links[] = $this->add_page_link(
643 array(
644 'page' => 'first',
645 'arrow' => '&laquo;',
646 'number' => '',
647 'disabled' => $disable['first'],
648 )
649 );
650
651 $page_links[] = $this->add_page_link(
652 array(
653 'page' => 'prev',
654 'arrow' => '&lsaquo;',
655 'number' => max( 1, $current - 1 ),
656 'disabled' => $disable['prev'],
657 )
658 );
659
660 if ( 'bottom' === $which ) {
661 $html_current_page = $current;
662 $total_pages_before = '<span class="screen-reader-text">' . esc_html__( 'Current Page', 'formidable' ) . '</span><span id="table-paging" class="paging-input">';
663 } else {
664 $html_current_page = sprintf(
665 "%s<input class='current-page' id='current-page-selector' type='text' name='paged' value='%s' size='%d' aria-describedby='table-paging' />",
666 '<label for="current-page-selector" class="screen-reader-text">' . esc_html__( 'Current Page', 'formidable' ) . '</label>',
667 $current,
668 strlen( $total_pages )
669 );
670 }
671
672 $html_total_pages = sprintf( "<span class='total-pages'>%s</span>", number_format_i18n( $total_pages ) );
673
674 /* translators: %1$s: Current page number, %2$s: Total pages */
675 $page_links[] = $total_pages_before . sprintf( _x( '%1$s of %2$s', 'paging', 'formidable' ), $html_current_page, $html_total_pages ) . $total_pages_after;
676
677 $page_links[] = $this->add_page_link(
678 array(
679 'page' => 'next',
680 'arrow' => '&rsaquo;',
681 'number' => min( $total_pages, $current + 1 ),
682 'disabled' => $disable['next'],
683 )
684 );
685
686 $page_links[] = $this->add_page_link(
687 array(
688 'page' => 'last',
689 'arrow' => '&raquo;',
690 'number' => $total_pages,
691 'disabled' => $disable['last'],
692 )
693 );
694
695 $pagination_links_class = 'pagination-links';
696
697 if ( $infinite_scroll ) {
698 $pagination_links_class = ' hide-if-js';
699 }
700
701 $output .= "\n" . '<span class="' . esc_attr( $pagination_links_class ) . '">' . implode( "\n", $page_links ) . '</span>';
702
703 if ( $total_pages ) {
704 $page_class = $total_pages < 2 ? ' one-page' : '';
705 } else {
706 $page_class = ' no-pages';
707 }
708 $this->_pagination = "<div class='tablenav-pages" . esc_attr( $page_class ) . "'>$output</div>";
709
710 echo $this->_pagination; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
711 }
712
713 /**
714 * @param int $total_pages
715 *
716 * @return array
717 */
718 private function disabled_pages( $total_pages ) {
719 $current = $this->get_pagenum();
720 $disable = array(
721 'first' => false,
722 'last' => false,
723 'prev' => false,
724 'next' => false,
725 );
726
727 // phpcs:ignore Universal.Operators.StrictComparisons
728 if ( $current == 1 ) {
729 $disable['first'] = true;
730 $disable['prev'] = true;
731 } elseif ( $current == 2 ) { // phpcs:ignore Universal.Operators.StrictComparisons
732 $disable['first'] = true;
733 }
734
735 // phpcs:ignore Universal.Operators.StrictComparisons
736 if ( $current == $total_pages ) {
737 $disable['last'] = true;
738 $disable['next'] = true;
739 } elseif ( $current == $total_pages - 1 ) { // phpcs:ignore Universal.Operators.StrictComparisons
740 $disable['last'] = true;
741 }
742
743 return $disable;
744 }
745
746 /**
747 * @param string $link
748 *
749 * @return string
750 */
751 private function link_label( $link ) {
752 $labels = array(
753 'first' => __( 'First page', 'formidable' ),
754 'last' => __( 'Last page', 'formidable' ),
755 'prev' => __( 'Previous page', 'formidable' ),
756 'next' => __( 'Next page', 'formidable' ),
757 );
758
759 return $labels[ $link ];
760 }
761
762 private function current_url() {
763 $current_url = set_url_scheme( 'http://' . FrmAppHelper::get_server_value( 'HTTP_HOST' ) . FrmAppHelper::get_server_value( 'REQUEST_URI' ) );
764 return remove_query_arg( array( 'hotkeys_highlight_last', 'hotkeys_highlight_first' ), $current_url );
765 }
766
767 /**
768 * @param array $atts
769 *
770 * @return string
771 */
772 private function add_page_link( $atts ) {
773 return $atts['disabled'] ? $this->add_disabled_link( $atts['arrow'] ) : $this->add_active_link( $atts );
774 }
775
776 /**
777 * @param string $label
778 *
779 * @return string
780 */
781 private function add_disabled_link( $label ) {
782 return '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">' . $label . '</span>';
783 }
784
785 /**
786 * @param array $atts
787 *
788 * @return string
789 */
790 private function add_active_link( $atts ) {
791 $url = esc_url( add_query_arg( 'paged', $atts['number'], $this->current_url() ) );
792 $label = $this->link_label( $atts['page'] );
793
794 return sprintf(
795 "<a class='button %s-page' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
796 $atts['page'],
797 $url,
798 $label,
799 $atts['arrow']
800 );
801 }
802
803 /**
804 * Get a list of sortable columns. The format is:
805 * 'internal-name' => 'orderby'
806 * or
807 * 'internal-name' => array( 'orderby', true )
808 *
809 * The second format will make the initial sorting order be descending
810 *
811 * @since 2.0.18
812 *
813 * @return array
814 */
815 protected function get_sortable_columns() {
816 return array();
817 }
818
819 /**
820 * Gets the name of the default primary column.
821 *
822 * @since 4.3.0
823 *
824 * @return string Name of the default primary column, in this case, an empty string.
825 */
826 protected function get_default_primary_column_name() {
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 ( $this->get_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 /**
901 * Filter the list table sortable columns for a specific screen.
902 *
903 * The dynamic portion of the hook name, `$this->screen->id`, refers
904 * to the ID of the current screen, usually a string.
905 *
906 * @since 3.5.0
907 *
908 * @param array $sortable_columns An array of sortable columns.
909 */
910 $_sortable = apply_filters( "manage_{$this->screen->id}_sortable_columns", $this->get_sortable_columns() );
911
912 $sortable = array();
913
914 foreach ( $_sortable as $id => $data ) {
915 if ( ! $data ) {
916 continue;
917 }
918
919 $data = (array) $data;
920
921 if ( ! isset( $data[1] ) ) {
922 $data[1] = false;
923 }
924
925 $sortable[ $id ] = $data;
926 }
927
928 $primary = $this->get_primary_column_name();
929
930 $this->_column_headers = array( $columns, $hidden, $sortable, $primary );
931
932 return $this->_column_headers;
933 }
934
935 /**
936 * Return number of visible columns
937 *
938 * @since 2.0.18
939 *
940 * @return int
941 */
942 public function get_column_count() {
943 list ( $columns, $hidden ) = $this->get_column_info();
944 $hidden = array_intersect( array_keys( $columns ), array_filter( $hidden ) );
945
946 return count( $columns ) - count( $hidden );
947 }
948
949 /**
950 * Print column headers, accounting for hidden and sortable columns.
951 *
952 * @since 2.0.18
953 *
954 * @staticvar int $cb_counter
955 *
956 * @param bool $with_id Whether to set the id attribute or not.
957 *
958 * @return void
959 */
960 public function print_column_headers( $with_id = true ) { // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh, Generic.Metrics.CyclomaticComplexity.MaxExceeded, SlevomatCodingStandard.Files.LineLength.LineTooLong
961 list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();
962
963 $current_url = set_url_scheme( 'http://' . FrmAppHelper::get_server_value( 'HTTP_HOST' ) . FrmAppHelper::get_server_value( 'REQUEST_URI' ) );
964 $current_url = remove_query_arg( 'paged', $current_url );
965 $current_orderby = isset( $_GET['orderby'] ) ? sanitize_text_field( wp_unslash( $_GET['orderby'] ) ) : '';
966 $current_order = isset( $_GET['order'] ) && 'desc' === $_GET['order'] ? 'desc' : 'asc';
967
968 FrmAppController::apply_saved_sort_preference( $current_orderby, $current_order );
969
970 if ( ! empty( $columns['cb'] ) ) {
971 static $cb_counter = 1;
972 $columns['cb'] = '<label class="screen-reader-text" for="cb-select-all-' . $cb_counter . '">' . esc_html__( 'Select All', 'formidable' ) . '</label>';
973 $columns['cb'] .= '<input id="cb-select-all-' . esc_attr( $cb_counter ) . '" type="checkbox" />';
974 ++$cb_counter;
975 }
976
977 foreach ( $columns as $column_key => $column_display_name ) {
978 $class = array( 'manage-column', "column-$column_key" );
979 $aria_sort_attr = '';
980 $order_text = '';
981
982 // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
983 if ( in_array( $column_key, $hidden ) ) {
984 $class[] = 'hidden';
985 }
986
987 if ( 'cb' === $column_key ) {
988 $class[] = 'check-column';
989 } elseif ( in_array( $column_key, array( 'posts', 'comments', 'links' ), true ) ) {
990 $class[] = 'num';
991 }
992
993 if ( $column_key === $primary || $column_key === 'name' ) {
994 $class[] = 'column-primary';
995 }
996
997 if ( isset( $sortable[ $column_key ] ) ) {
998 list( $orderby, $desc_first ) = $sortable[ $column_key ];
999
1000 // phpcs:ignore Universal.Operators.StrictComparisons
1001 if ( $current_orderby == $orderby ) {
1002 // The sorted column. The `aria-sort` attribute must be set only on the sorted column.
1003 if ( 'asc' === $current_order ) {
1004 $order = 'desc';
1005 $aria_sort_attr = ' aria-sort="ascending"';
1006 } else {
1007 $order = 'asc';
1008 $aria_sort_attr = ' aria-sort="descending"';
1009 }
1010
1011 $class[] = 'sorted';
1012 $class[] = $current_order;
1013 } else {
1014 $order = $desc_first ? 'desc' : 'asc';
1015 $class[] = 'sortable';
1016 $class[] = $desc_first ? 'asc' : 'desc';
1017
1018 /* translators: Hidden accessibility text. */
1019 $asc_text = __( 'Sort ascending.', 'formidable' );
1020 /* translators: Hidden accessibility text. */
1021 $desc_text = __( 'Sort descending.', 'formidable' );
1022 $order_text = 'asc' === $order ? $asc_text : $desc_text;
1023 }//end if
1024
1025 if ( '' !== $order_text ) {
1026 $order_text = ' <span class="screen-reader-text">' . $order_text . '</span>';
1027 }
1028
1029 $column_display_name = sprintf(
1030 '<a href="%1$s">' .
1031 '<span>%2$s</span>' .
1032 '<span class="sorting-indicators">' .
1033 '<span class="sorting-indicator asc" aria-hidden="true"></span>' .
1034 '<span class="sorting-indicator desc" aria-hidden="true"></span>' .
1035 '</span>' .
1036 '%3$s' .
1037 '</a>',
1038 esc_url( add_query_arg( compact( 'orderby', 'order' ), $current_url ) ),
1039 $column_display_name,
1040 $order_text
1041 );
1042 }//end if
1043
1044 $tag = 'cb' === $column_key ? 'td' : 'th';
1045 $scope = 'th' === $tag ? 'scope="col"' : '';
1046 $id = $with_id ? "id='" . esc_attr( $column_key ) . "'" : '';
1047
1048 if ( $class ) {
1049 $class = "class='" . esc_attr( implode( ' ', $class ) ) . "'";
1050 }
1051
1052 if ( ! $this->has_min_items() && ! $with_id ) {
1053 // Hide the labels but show the border.
1054 $column_display_name = '';
1055 }
1056 echo "<$tag $scope $id $class $aria_sort_attr>$column_display_name</$tag>"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1057 }//end foreach
1058 }
1059
1060 /**
1061 * Display the table
1062 *
1063 * @since 2.0.18
1064 *
1065 * @param array $args
1066 *
1067 * @return void
1068 */
1069 public function display( $args = array() ) {
1070 $singular = $this->_args['singular'];
1071 $tbody_params = array();
1072
1073 if ( $singular ) {
1074 $tbody_params['data-wp-lists'] = 'list:' . $singular;
1075 }
1076
1077 if ( $this->should_display( $args, 'display-top-nav' ) ) {
1078 $this->display_tablenav( 'top' );
1079 }
1080 $this->screen->render_screen_reader_content( 'heading_list' );
1081
1082 // phpcs:disable Generic.WhiteSpace.ScopeIndent
1083 ?>
1084 <table class="wp-list-table <?php echo esc_attr( implode( ' ', $this->get_table_classes() ) ); ?>">
1085 <?php if ( $this->has_min_items( 1 ) ) { ?>
1086 <thead>
1087 <tr>
1088 <?php $this->print_column_headers(); ?>
1089 </tr>
1090 </thead>
1091 <?php } ?>
1092
1093 <tbody id="the-list"<?php FrmAppHelper::array_to_html_params( $tbody_params, true ); ?>>
1094 <?php $this->display_rows_or_placeholder(); ?>
1095 </tbody>
1096
1097 <?php if ( $this->has_min_items( 1 ) && $this->should_display( $args, 'display-bottom-headers' ) ) { ?>
1098 <tfoot>
1099 <tr>
1100 <?php $this->print_column_headers( false ); ?>
1101 </tr>
1102 </tfoot>
1103 <?php } ?>
1104 </table>
1105 <?php
1106 // phpcs:enable Generic.WhiteSpace.ScopeIndent
1107
1108 if ( $this->should_display( $args, 'display-bottom-nav' ) ) {
1109 $this->display_tablenav( 'bottom' );
1110 }
1111 }
1112
1113 /**
1114 * Determines if a particular feature or element should be displayed.
1115 *
1116 * @param array $args An associative array of arguments.
1117 * @param string $settings The specific setting key to check within the arguments array.
1118 *
1119 * @return bool Returns true if the setting is not set or if it is not false; otherwise, returns false.
1120 */
1121 protected function should_display( $args, $settings ) {
1122 return ! isset( $args[ $settings ] ) || false !== $args[ $settings ];
1123 }
1124
1125 /**
1126 * Get a list of CSS classes for the list table table tag.
1127 *
1128 * @since 2.0.18
1129 *
1130 * @return array List of CSS classes for the table tag.
1131 */
1132 protected function get_table_classes() {
1133 return array( 'widefat', 'fixed', 'striped', $this->_args['plural'] );
1134 }
1135
1136 /**
1137 * Generate the table navigation above or below the table
1138 *
1139 * @since 2.0.18
1140 *
1141 * @param string $which
1142 */
1143 protected function display_tablenav( $which ) {
1144 if ( 'top' === $which ) {
1145 wp_nonce_field( 'bulk-' . $this->_args['plural'], '_wpnonce', false );
1146
1147 if ( ! $this->has_min_items( 1 ) ) {
1148 // Don't show bulk actions if no items.
1149 return;
1150 }
1151 } elseif ( ! $this->has_min_items() ) {
1152 // Don't show the bulk actions when there aren't many rows.
1153 return;
1154 }
1155 // phpcs:disable Generic.WhiteSpace.ScopeIndent
1156 ?>
1157 <div class="tablenav <?php echo esc_attr( $which ); ?>">
1158
1159 <div class="alignleft actions bulkactions">
1160 <?php $this->bulk_actions( $which ); ?>
1161 </div>
1162 <?php
1163 $this->extra_tablenav( $which );
1164 $this->pagination( $which );
1165 ?>
1166
1167 <br class="clear"/>
1168 </div>
1169 <?php
1170 // phpcs:enable Generic.WhiteSpace.ScopeIndent
1171 }
1172
1173 /**
1174 * Use this to exclude the footer labels and bulk items.
1175 * When close together, it feels like duplicates.
1176 *
1177 * @since 4.07
1178 *
1179 * @param int $limit
1180 *
1181 * @return bool
1182 */
1183 protected function has_min_items( $limit = 5 ) {
1184 return $this->has_items() && ( $this->total_items === false || $this->total_items >= $limit );
1185 }
1186
1187 /**
1188 * Extra controls to be displayed between bulk actions and pagination
1189 *
1190 * @since 2.0.18
1191 *
1192 * @param string $which
1193 */
1194 protected function extra_tablenav( $which ) {
1195 }
1196
1197 /**
1198 * Generate the tbody element for the list table.
1199 *
1200 * @since 2.0.18
1201 */
1202 public function display_rows_or_placeholder() {
1203 if ( $this->has_items() ) {
1204 $this->display_rows();
1205 } else {
1206 echo '<tr class="no-items"><td class="colspanchange" colspan="' . esc_attr( $this->get_column_count() ) . '">';
1207 $this->no_items();
1208 echo '</td></tr>';
1209 }
1210 }
1211
1212 /**
1213 * Generates content for a single row of the table
1214 *
1215 * @since 2.0.18
1216 *
1217 * @param stdClass $item The current item.
1218 */
1219 public function single_row( $item ) {
1220 echo '<tr>';
1221 $this->single_row_columns( $item );
1222 echo '</tr>';
1223 }
1224
1225 /**
1226 * Generates the columns for a single row of the table
1227 *
1228 * @since 2.0.18
1229 *
1230 * @param object $item The current item.
1231 */
1232 protected function single_row_columns( $item ) {
1233 list( $columns, $hidden,, $primary ) = $this->get_column_info();
1234
1235 foreach ( $columns as $column_name => $column_display_name ) {
1236 $classes = "$column_name column-$column_name";
1237
1238 if ( $primary === $column_name ) {
1239 $classes .= ' has-row-actions column-primary';
1240 }
1241
1242 // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
1243 if ( in_array( $column_name, $hidden ) ) {
1244 $classes .= ' hidden';
1245 }
1246
1247 $params = array(
1248 'class' => $classes,
1249 // Comments column uses HTML in the display name with screen reader text.
1250 // Instead of using esc_attr(), we strip tags to get closer to a user-friendly string.
1251 'data-colname' => $column_display_name,
1252 );
1253
1254 if ( 'cb' === $column_name ) {
1255 echo '<th scope="row" class="check-column"></th>';
1256 } else {
1257 echo '<td ';
1258 FrmAppHelper::array_to_html_params( $params, true );
1259 echo '>';
1260
1261 if ( method_exists( $this, 'column_' . $column_name ) ) {
1262 echo call_user_func( array( $this, 'column_' . $column_name ), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1263 }
1264
1265 echo $this->handle_row_actions( $item, $column_name, $primary ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1266 echo '</td>';
1267 }
1268 }//end foreach
1269 }
1270
1271 /**
1272 * Generates and display row actions links for the list table.
1273 *
1274 * @since 4.3.0
1275 *
1276 * @param object $item The item being acted upon.
1277 * @param string $column_name Current column name.
1278 * @param string $primary Primary column name.
1279 *
1280 * @return string The row actions output. In this case, an empty string.
1281 */
1282 protected function handle_row_actions( $item, $column_name, $primary ) {
1283 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
1284 }
1285
1286 /**
1287 * Handle an incoming ajax request (called from admin-ajax.php)
1288 *
1289 * @since 2.0.18
1290 */
1291 public function ajax_response() {
1292 $this->prepare_items();
1293
1294 ob_start();
1295
1296 if ( ! empty( $_REQUEST['no_placeholder'] ) ) {
1297 $this->display_rows();
1298 } else {
1299 $this->display_rows_or_placeholder();
1300 }
1301
1302 $rows = ob_get_clean();
1303 $response = array( 'rows' => $rows );
1304
1305 if ( isset( $this->_pagination_args['total_items'] ) ) {
1306 $response['total_items_i18n'] = sprintf(
1307 /* translators: %s: Number of items */
1308 _n( '%s item', '%s items', $this->_pagination_args['total_items'], 'formidable' ),
1309 number_format_i18n( $this->_pagination_args['total_items'] )
1310 );
1311 }
1312
1313 if ( isset( $this->_pagination_args['total_pages'] ) ) {
1314 $response['total_pages'] = $this->_pagination_args['total_pages'];
1315 $response['total_pages_i18n'] = number_format_i18n( $this->_pagination_args['total_pages'] );
1316 }
1317
1318 die( wp_json_encode( $response ) );
1319 }
1320
1321 /**
1322 * Send required variables to JavaScript land
1323 *
1324 * @return void
1325 */
1326 public function _js_vars() {
1327 $args = array(
1328 'class' => get_class( $this ),
1329 'screen' => array(
1330 'id' => $this->screen->id,
1331 'base' => $this->screen->base,
1332 ),
1333 );
1334
1335 printf( "<script type='text/javascript'>list_args = %s;</script>\n", wp_json_encode( $args ) );
1336 }
1337
1338 /**
1339 * @return string
1340 */
1341 protected function loaded_from() {
1342 return '';
1343 }
1344 }
1345