PluginProbe
TablePress – Tables in WordPress made easy / 3.4
TablePress – Tables in WordPress made easy v3.4
3.4 3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 All 45 releases
← All changes | classes/class-render.php +132 -106 2.4 → 3.4 View file →
@@ -7,8 +7,10 @@
7 7 * @author Tobias Bäthge
8 8 * @since 1.0.0
9 9 */
10 10
11 +declare(strict_types=1);
12 +
11 13 // Prohibit direct script loading.
12 14 defined( 'ABSPATH' ) || die( 'No direct script access allowed!' );
13 15
14 16 /**
@@ -26,9 +28,9 @@
26 28 *
27 29 * @since 1.0.0
28 30 * @var array<string, mixed>
29 31 */
30 - protected $table = array();
32 + protected array $table = array();
31 33
32 34 /**
33 35 * Table options that influence the output result.
34 36 *
@@ -34,9 +36,9 @@
34 36 *
35 37 * @since 1.0.0
36 38 * @var array<string, mixed>
37 39 */
38 - protected $render_options = array();
40 + protected array $render_options = array();
39 41
40 42 /**
41 43 * Rendered HTML code of the table or PHP array.
42 44 *
@@ -50,9 +52,9 @@
50 52 *
51 53 * @since 1.0.0
52 54 * @var array<string, string>
53 55 */
54 - protected $span_trigger = array(
56 + protected array $span_trigger = array(
55 57 'colspan' => '#colspan#',
56 58 'rowspan' => '#rowspan#',
57 59 'span' => '#span#',
58 60 );
@@ -62,9 +64,9 @@
62 64 *
63 65 * @since 1.0.0
64 66 * @var int[]
65 67 */
66 - protected $rowspan = array();
68 + protected array $rowspan = array();
67 69
68 70 /**
69 71 * Buffer to store the counts of colspan per row, initialized in _render_table().
70 72 *
@@ -70,25 +72,30 @@
70 72 *
71 73 * @since 1.0.0
72 74 * @var int[]
73 75 */
74 - protected $colspan = array();
76 + protected array $colspan = array();
75 77
76 78 /**
79 + * Whether the table has connected cells (colspan or rowspan), set in _render_table().
80 + *
81 + * @since 3.0.0
82 + */
83 + protected bool $tbody_has_connected_cells = false;
84 +
85 + /**
77 86 * Index of the last row of the visible data in the table, set in _render_table().
78 87 *
79 88 * @since 1.0.0
80 - * @var int
81 89 */
82 - protected $last_row_idx;
90 + protected int $last_row_idx;
83 91
84 92 /**
85 93 * Index of the last column of the visible data in the table, set in _render_table().
86 94 *
87 95 * @since 1.0.0
88 - * @var int
89 96 */
90 - protected $last_column_idx;
97 + protected int $last_column_idx;
91 98
92 99 /**
93 100 * Class constructor.
94 101 *
@@ -190,10 +197,12 @@
190 197 */
191 198 protected function _prepare_render_data(): void {
192 199 $orig_table = $this->table;
193 200
194 - $num_rows = count( $this->table['data'] );
195 - $num_columns = ( $num_rows > 0 ) ? count( $this->table['data'][0] ) : 0;
201 + $counts = array(
202 + 'rows' => count( $this->table['data'] ),
203 + );
204 + $counts['columns'] = ( $counts['rows'] > 0 ) ? count( $this->table['data'][0] ) : 0;
196 205
197 206 // Evaluate show/hide_rows/columns parameters.
198 207 $actions = array( 'show', 'hide' );
199 208 $elements = array( 'rows', 'columns' );
@@ -198,56 +207,23 @@
198 207 $actions = array( 'show', 'hide' );
199 208 $elements = array( 'rows', 'columns' );
200 209 foreach ( $actions as $action ) {
201 210 foreach ( $elements as $element ) {
202 - if ( empty( $this->render_options[ "{$action}_{$element}" ] ) ) {
203 - $this->render_options[ "{$action}_{$element}" ] = array();
211 + $action_element = "{$action}_{$element}";
212 +
213 + if ( empty( $this->render_options[ $action_element ] ) ) {
214 + $this->render_options[ $action_element ] = array();
204 215 continue;
205 216 }
206 217
207 - // Add all rows/columns to array if "all" value set for one of the four parameters.
208 - if ( 'all' === $this->render_options[ "{$action}_{$element}" ] ) {
209 - $this->render_options[ "{$action}_{$element}" ] = range( 0, ${'num_' . $element} - 1 );
218 + // Add all rows/columns to array if a parameter is set to "all".
219 + if ( 'all' === $this->render_options[ $action_element ] ) {
220 + $this->render_options[ $action_element ] = range( 0, $counts[ $element ] - 1 );
210 221 continue;
211 222 }
212 223
213 - // We have a list of rows/columns (possibly with ranges in it).
214 - $this->render_options[ "{$action}_{$element}" ] = explode( ',', $this->render_options[ "{$action}_{$element}" ] );
215 - // Support for ranges like 3-6 or A-BA.
216 - $range_cells = array();
217 - foreach ( $this->render_options[ "{$action}_{$element}" ] as $key => $value ) {
218 - $range_dash = strpos( $value, '-' );
219 - if ( false !== $range_dash ) {
220 - unset( $this->render_options[ "{$action}_{$element}" ][ $key ] );
221 - $start = trim( substr( $value, 0, $range_dash ) );
222 - if ( ! is_numeric( $start ) ) {
223 - $start = TablePress::letter_to_number( $start );
224 - }
225 - $end = trim( substr( $value, $range_dash + 1 ) );
226 - if ( ! is_numeric( $end ) ) {
227 - $end = TablePress::letter_to_number( $end );
228 - }
229 - $current_range = range( $start, $end );
230 - $range_cells = array_merge( $range_cells, $current_range );
231 - }
232 - }
233 - $this->render_options[ "{$action}_{$element}" ] = array_merge( $this->render_options[ "{$action}_{$element}" ], $range_cells );
234 -
235 - /*
236 - * Parse single letters and change from regular numbering to zero-based numbering,
237 - * as rows/columns are indexed from 0 internally, but from 1 externally.
238 - */
239 - foreach ( $this->render_options[ "{$action}_{$element}" ] as $key => $value ) {
240 - $value = trim( $value );
241 - if ( ! is_numeric( $value ) ) {
242 - $value = TablePress::letter_to_number( $value );
243 - }
244 - $this->render_options[ "{$action}_{$element}" ][ $key ] = (int) $value - 1;
245 - }
246 -
247 - // Remove duplicate entries and sort the array.
248 - $this->render_options[ "{$action}_{$element}" ] = array_unique( $this->render_options[ "{$action}_{$element}" ] );
249 - sort( $this->render_options[ "{$action}_{$element}" ], SORT_NUMERIC );
224 + // Convert the list to a unique, sorted, and zero-based array of integers.
225 + $this->render_options[ $action_element ] = TablePress::convert_row_column_list_to_array( $this->render_options[ $action_element ], 1, $counts[ $element ], array( 'zero-based' => true ) );
250 226 }
251 227 }
252 228
253 229 // Load information about hidden rows and columns.
@@ -350,8 +326,9 @@
350 326 $num_columns = ( $num_rows > 0 ) ? count( $this->table['data'][0] ) : 0;
351 327
352 328 // Check if there are rows and columns in the table (might not be the case after removing hidden rows/columns!).
353 329 if ( 0 === $num_rows || 0 === $num_columns ) {
330 + /* translators: %s: Table ID */
354 331 $this->output = sprintf( __( '<!-- The table with the ID %s is empty! -->', 'tablepress' ), $this->table['id'] );
355 332 return;
356 333 }
357 334
@@ -461,30 +438,46 @@
461 438 if ( $this->render_options['print_description'] && 'above' === $this->render_options['print_description_position'] ) {
462 439 $output .= $print_description_html;
463 440 }
464 441
465 - $thead = '';
466 - $tfoot = '';
442 + $thead = array();
443 + $tfoot = array();
467 444 $tbody = array();
468 445
469 446 $this->last_row_idx = $num_rows - 1;
470 447 $this->last_column_idx = $num_columns - 1;
448 +
471 449 // Loop through rows in reversed order, to search for rowspan trigger keyword.
472 - for ( $row_idx = $this->last_row_idx; $row_idx >= 0; $row_idx-- ) {
473 - // Last row, need to check for footer (but only if at least two rows).
474 - if ( $this->last_row_idx === $row_idx && $this->render_options['table_foot'] && $num_rows > 1 ) {
475 - $tfoot = $this->_render_row( $row_idx, 'th' );
476 - continue;
450 + $row_idx = $this->last_row_idx;
451 +
452 + // Render the table footer rows, if there is at least one extra row.
453 + if ( $this->render_options['table_foot'] > 0 && $num_rows >= $this->render_options['table_head'] + $this->render_options['table_foot'] ) {
454 + $last_tbody_idx = $this->last_row_idx - $this->render_options['table_foot'];
455 + while ( $row_idx > $last_tbody_idx ) {
456 + $tfoot[] = $this->_render_row( $row_idx, 'th' );
457 + --$row_idx;
477 458 }
478 - // First row, need to check for head (but only if at least two rows).
479 - if ( 0 === $row_idx && $this->render_options['table_head'] && $num_rows > 1 ) {
480 - $thead = $this->_render_row( $row_idx, 'th' );
481 - continue;
482 - }
483 - // Neither first nor last row (with respective head/foot enabled), so render as body row.
459 + // Reverse rows because we looped through the rows in reverse order.
460 + $tfoot = array_reverse( $tfoot );
461 + }
462 +
463 + // Render the table body rows.
464 + $last_thead_idx = $this->render_options['table_head'] - 1;
465 + while ( $row_idx > $last_thead_idx ) {
484 466 $tbody[] = $this->_render_row( $row_idx, 'td' );
467 + --$row_idx;
485 468 }
469 + // Reverse rows because we looped through the rows in reverse order.
470 + $tbody = array_reverse( $tbody );
486 471
472 + // Render the table header rows, if rows are left.
473 + while ( $row_idx > -1 ) {
474 + $thead[] = $this->_render_row( $row_idx, 'th' );
475 + --$row_idx;
476 + }
477 + // Reverse rows because we looped through the rows in reverse order.
478 + $thead = array_reverse( $thead );
479 +
487 480 // <caption> tag.
488 481 /**
489 482 * Filters the content for the HTML caption element of the table.
490 483 *
@@ -551,18 +544,36 @@
551 544 if ( ! empty( $colgroup ) ) {
552 545 $colgroup = "<colgroup>\n{$colgroup}</colgroup>\n";
553 546 }
554 547
555 - // <thead>, <tfoot>, and <tbody> tags.
548 + /*
549 + * <thead>, <tfoot>, and <tbody> tags.
550 + */
551 +
556 552 if ( ! empty( $thead ) ) {
557 - $thead = "<thead>\n{$thead}</thead>\n";
553 + $thead = "<thead>\n" . implode( '', $thead ) . "</thead>\n";
554 + } else {
555 + $thead = '';
558 556 }
557 +
559 558 if ( ! empty( $tfoot ) ) {
560 - $tfoot = "<tfoot>\n{$tfoot}</tfoot>\n";
559 + $tfoot = "<tfoot>\n" . implode( '', $tfoot ) . "</tfoot>\n";
560 + } else {
561 + $tfoot = '';
561 562 }
562 - $tbody_class = ( $this->render_options['row_hover'] ) ? ' class="row-hover"' : '';
563 - // Reverse rows because we looped through the rows in reverse order.
564 - $tbody = array_reverse( $tbody );
563 +
564 + $tbody_classes = array();
565 + if ( $this->render_options['alternating_row_colors'] ) {
566 + $tbody_classes[] = 'row-striping';
567 + }
568 + if ( $this->render_options['row_hover'] ) {
569 + $tbody_classes[] = 'row-hover';
570 + }
571 + $tbody_class = implode( ' ', $tbody_classes );
572 + if ( '' !== $tbody_class ) {
573 + $tbody_class = ' class="' . esc_attr( $tbody_class ) . '"';
574 + }
575 +
565 576 $tbody = "<tbody{$tbody_class}>\n" . implode( '', $tbody ) . "</tbody>\n";
566 577
567 578 // Attributes for the table (HTML table element).
568 579 $table_attributes = array();
@@ -572,9 +583,16 @@
572 583 $table_attributes['id'] = $this->render_options['html_id'];
573 584 }
574 585
575 586 // "class" attribute.
576 - $css_classes = array( 'tablepress', "tablepress-id-{$this->table['id']}", $this->render_options['extra_css_classes'] );
587 + $css_classes = array(
588 + 'tablepress',
589 + "tablepress-id-{$this->table['id']}",
590 + $this->render_options['extra_css_classes'],
591 + );
592 + if ( $this->tbody_has_connected_cells ) {
593 + $css_classes[] = 'tbody-has-connected-cells';
594 + }
577 595 /**
578 596 * Filters the CSS classes that are given to the HTML table element.
579 597 *
580 598 * @since 1.0.0
@@ -586,10 +604,11 @@
586 604 // $css_classes might contain several classes in one array entry.
587 605 $css_classes = explode( ' ', implode( ' ', $css_classes ) );
588 606 $css_classes = array_map( array( 'TablePress', 'sanitize_css_class' ), $css_classes );
589 607 $css_classes = array_unique( $css_classes );
590 - $css_classes = trim( implode( ' ', $css_classes ) );
591 - if ( ! empty( $css_classes ) ) {
608 + $css_classes = array_filter( $css_classes ); // Remove empty entries.
609 + $css_classes = implode( ' ', $css_classes );
610 + if ( '' !== $css_classes ) {
592 611 $table_attributes['class'] = $css_classes;
593 612 }
594 613
595 614 // ARIA label attributes.
@@ -619,9 +638,9 @@
619 638
620 639 // Legacy support for attributes that are not encouraged in HTML5.
621 640 foreach ( array( 'cellspacing', 'cellpadding', 'border' ) as $attribute ) {
622 641 if ( false !== $this->render_options[ $attribute ] ) {
623 - $table_attributes[ $attribute ] = (int) $this->render_options[ $attribute ];
642 + $table_attributes[ $attribute ] = (string) absint( $this->render_options[ $attribute ] );
624 643 }
625 644 }
626 645
627 646 /**
@@ -691,10 +710,10 @@
691 710
692 711 if ( $this->span_trigger['rowspan'] === $cell_content ) { // There will be a rowspan.
693 712 if ( ! (
694 713 ( 0 === $row_idx ) // No rowspan inside first row.
695 - || ( 1 === $row_idx && $this->render_options['table_head'] ) // No rowspan into table head.
696 - || ( $this->last_row_idx === $row_idx && $this->render_options['table_foot'] ) // No rowspan out of table foot.
714 + || ( $this->render_options['table_head'] === $row_idx ) // No rowspan into table head.
715 + || ( $this->last_row_idx - $this->render_options['table_foot'] + 1 === $row_idx ) // No rowspan out of table foot.
697 716 ) ) {
698 717 // Increase counter for rowspan in this column.
699 718 ++$this->rowspan[ $col_idx ];
700 719 // Reset counter for colspan in this row, combined col- and rowspan might be happening.
@@ -718,10 +737,10 @@
718 737 $cell_content = '';
719 738 } elseif ( $this->span_trigger['span'] === $cell_content ) { // There will be a combined col- and rowspan.
720 739 if ( ! (
721 740 ( 0 === $row_idx ) // No rowspan inside first row.
722 - || ( 1 === $row_idx && $this->render_options['table_head'] ) // No rowspan into table head.
723 - || ( $this->last_row_idx === $row_idx && $this->render_options['table_foot'] ) // No rowspan out of table foot.
741 + || ( $this->render_options['table_head'] === $row_idx ) // No rowspan into table head.
742 + || ( $this->last_row_idx - $this->render_options['table_foot'] + 1 === $row_idx ) // No rowspan out of table foot.
724 743 ) && ! (
725 744 ( 0 === $col_idx ) // No colspan inside first column.
726 745 || ( 1 === $col_idx && $this->render_options['first_column_th'] ) // No colspan into first column head.
727 746 ) ) {
@@ -736,11 +755,19 @@
736 755
737 756 // "colspan" and "rowspan" attributes.
738 757 if ( $this->colspan[ $row_idx ] > 1 ) { // We have colspaned cells.
739 758 $tag_attributes['colspan'] = (string) $this->colspan[ $row_idx ];
759 + if ( ! $this->tbody_has_connected_cells && $row_idx > $this->render_options['table_head'] - 1 && $row_idx < $this->last_row_idx - $this->render_options['table_foot'] + 1 ) {
760 + // Set flag that there are connected cells in the tbody.
761 + $this->tbody_has_connected_cells = true;
762 + }
740 763 }
741 764 if ( $this->rowspan[ $col_idx ] > 1 ) { // We have rowspaned cells.
742 765 $tag_attributes['rowspan'] = (string) $this->rowspan[ $col_idx ];
766 + if ( ! $this->tbody_has_connected_cells && $row_idx > $this->render_options['table_head'] - 1 && $row_idx < $this->last_row_idx - $this->render_options['table_foot'] + 1 ) {
767 + // Set flag that there are connected cells in the tbody.
768 + $this->tbody_has_connected_cells = true;
769 + }
743 770 }
744 771
745 772 // "class" attribute.
746 773 $cell_class = 'column-' . ( $col_idx + 1 );
@@ -782,13 +809,17 @@
782 809 */
783 810 $tag_attributes = apply_filters( 'tablepress_cell_tag_attributes', $tag_attributes, $this->table['id'], $cell_content, $row_idx + 1, $col_idx + 1, $this->colspan[ $row_idx ], $this->rowspan[ $col_idx ] );
784 811 $tag_attributes = $this->_attributes_array_to_string( $tag_attributes );
785 812
786 - if ( $this->render_options['first_column_th'] && 0 === $col_idx ) {
787 - $tag = 'th';
813 + if ( '' === $cell_content ) {
814 + $cell_tag = 'td'; // For accessibility, empty cells should use `td` and not `th` tags.
815 + } elseif ( $this->render_options['first_column_th'] && 0 === $col_idx ) {
816 + $cell_tag = 'th'; // Non-empty cells in the first column should use `th` tags, if enabled.
817 + } else {
818 + $cell_tag = $tag; // Otherwise, use the tag that was passed in as the default for the row.
788 819 }
789 820
790 - $row_cells[] = "<{$tag}{$tag_attributes}>{$cell_content}</{$tag}>";
821 + $row_cells[] = "<{$cell_tag}{$tag_attributes}>{$cell_content}</{$cell_tag}>";
791 822 $this->colspan[ $row_idx ] = 1; // Reset.
792 823 $this->rowspan[ $col_idx ] = 1; // Reset.
793 824 }
794 825
@@ -796,11 +827,8 @@
796 827 $tr_attributes = array();
797 828
798 829 // "class" attribute.
799 830 $row_classes = 'row-' . ( $row_idx + 1 );
800 - if ( $this->render_options['alternating_row_colors'] ) {
801 - $row_classes .= ( 1 === ( $row_idx % 2 ) ) ? ' even' : ' odd';
802 - }
803 831 /**
804 832 * Filters the CSS classes that are given to a row (HTML tr element) of a table.
805 833 *
806 834 * @since 1.0.0
@@ -852,10 +880,8 @@
852 880
853 881 /**
854 882 * Possibly replace certain HTML entities and replace line breaks with HTML.
855 883 *
856 - * @todo Find a better solution than this function, e.g. something like wpautop().
857 - *
858 884 * @since 1.0.0
859 885 *
860 886 * @param string $text The string to process.
861 887 * @return string Processed string for output.
@@ -898,8 +924,9 @@
898 924 'cellspacing' => false,
899 925 'column_widths' => '',
900 926 'convert_line_breaks' => true,
901 927 'datatables_custom_commands' => null,
928 + 'datatables_datetime' => '',
902 929 'datatables_filter' => null,
903 930 'datatables_info' => null,
904 931 'datatables_lengthchange' => null,
905 932 'datatables_locale' => get_locale(),
@@ -940,30 +967,29 @@
940 967 $tablepress_css = TablePress::load_class( 'TablePress_CSS', 'class-css.php', 'classes' );
941 968 $default_css_minified = $tablepress_css->load_default_css_from_file( $is_rtl );
942 969 if ( false === $default_css_minified ) {
943 970 $default_css_minified = '';
944 - } else {
945 - // Change relative URLs to web font files to absolute URLs, as combining the CSS files and saving to another directory breaks the relative URLs.
946 - $absolute_path = plugins_url( 'css/build/tablepress.', TABLEPRESS__FILE__ );
947 - // Make the absolute URL protocol-relative to prevent mixed content warnings.
948 - $absolute_path = str_replace( array( 'http:', 'https:' ), '', $absolute_path );
949 - $default_css_minified = str_replace( 'url(tablepress.', 'url(' . $absolute_path, $default_css_minified );
950 971 }
951 972
952 973 $rtl_direction = $is_rtl ? "\ndirection: rtl;" : '';
953 974
954 975 return <<<CSS
955 -<style>
956 -/* iframe */
957 -body {
958 - margin: 10px;
959 - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;{$rtl_direction}
960 -}
961 -p {
962 - font-size: 13px;
963 -}
964 -{$default_css_minified}
965 -</style>
966 -CSS;
976 + <style>
977 + /* iframe */
978 + body {
979 + margin: 10px;
980 + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;{$rtl_direction}
981 + }
982 + p {
983 + font-size: 13px;
984 + }
985 + .preview {
986 + overflow-x: auto;
987 + overflow-y: hidden;
988 + scroll-behavior: smooth;
989 + }
990 + {$default_css_minified}
991 + </style>
992 + CSS;
967 993 }
968 994
969 995 } // class TablePress_Render