PluginProbe
TablePress – Tables in WordPress made easy / 3.0
TablePress – Tables in WordPress made easy v3.0
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 2.4.4 All 44 releases
← All changes | controllers/controller-frontend.php +270 -151 2.2.33.0 View file →
@@ -21,16 +21,38 @@
21 21 */
22 22 class TablePress_Frontend_Controller extends TablePress_Controller {
23 23
24 24 /**
25 + * File name of the admin screens' parent page in the admin menu.
26 + *
27 + * @since 1.0.0
28 + */
29 + public string $parent_page = 'middle';
30 +
31 + /**
32 + * Whether TablePress admin screens are a top-level menu item in the admin menu.
33 + *
34 + * @since 1.0.0
35 + */
36 + public bool $is_top_level_page = false;
37 +
38 + /**
25 39 * List of tables that are shown for the current request.
26 40 *
27 41 * @since 1.0.0
28 42 * @var array<string, array{count: int, instances: array<string, array<string, mixed>>}>
29 43 */
30 - protected $shown_tables = array();
44 + protected array $shown_tables = array();
31 45
32 46 /**
47 + * List of registered DataTables datetime formats.
48 + *
49 + * @since 3.0.0
50 + * @var string[]
51 + */
52 + protected array $datatables_datetime_formats = array();
53 +
54 + /**
33 55 * Initiate Frontend functionality.
34 56 *
35 57 * @since 1.0.0
36 58 */
@@ -37,20 +59,18 @@
37 59 public function __construct() {
38 60 parent::__construct();
39 61
40 62 /**
41 - * Filters whether the TablePress Default CSS code shall be loaded.
63 + * Filters the admin menu parent page, which is needed for the construction of plugin URLs.
42 64 *
43 65 * @since 1.0.0
44 66 *
45 - * @param bool $use Whether the Default CSS shall be loaded. Default true.
67 + * @param string $parent_page Current admin menu parent page.
46 68 */
47 - if ( apply_filters( 'tablepress_use_default_css', true ) || TablePress::$model_options->get( 'use_custom_css' ) ) {
48 - add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_css' ) );
49 - }
69 + $this->parent_page = apply_filters( 'tablepress_admin_menu_parent_page', TablePress::$model_options->get( 'admin_menu_parent_page' ) );
70 + $this->is_top_level_page = in_array( $this->parent_page, array( 'top', 'middle', 'bottom' ), true );
50 71
51 - // Add DataTables invocation calls.
52 - add_action( 'wp_print_footer_scripts', array( $this, 'add_datatables_calls' ), 11 ); // After inclusion of files.
72 + add_action( 'wp_print_footer_scripts', array( $this, 'add_datatables_calls' ), 9 ); // Priority 9 so that this runs before `_wp_footer_scripts()`.
53 73
54 74 // Register TablePress Shortcodes. Priority 20 is kept for backwards-compatibility purposes.
55 75 add_action( 'init', array( $this, 'init_shortcodes' ), 20 );
56 76
@@ -73,13 +93,20 @@
73 93
74 94 /**
75 95 * Register the tablepress/table block and its dependencies.
76 96 */
77 - register_block_type(
78 - TABLEPRESS_ABSPATH . 'blocks/table/',
97 + if ( function_exists( 'wp_register_block_metadata_collection' ) ) {
98 + // wp_register_block_metadata_collection() is only available since WP 6.7.
99 + wp_register_block_metadata_collection(
100 + TABLEPRESS_ABSPATH . 'blocks',
101 + TABLEPRESS_ABSPATH . 'blocks/blocks-manifest.php',
102 + );
103 + }
104 + register_block_type_from_metadata(
105 + TABLEPRESS_ABSPATH . 'blocks/table/block.json',
79 106 array(
80 107 'render_callback' => array( $this, 'table_block_render_callback' ),
81 - )
108 + ),
82 109 );
83 110 }
84 111
85 112 /**
@@ -92,18 +119,53 @@
92 119 add_shortcode( TablePress::$shortcode_info, array( $this, 'shortcode_table_info' ) );
93 120 }
94 121
95 122 /**
96 - * Enqueue CSS files for default CSS and "Custom CSS" (if desired).
123 + * Enqueues CSS files for TablePress default CSS and "Custom CSS" (if desired).
97 124 *
125 + * This function is only called when a [table /] Shortcode or "TablePress Table" block is evaluated, so that CSS files are only loaded when needed.
126 + *
127 + * If styles have not been printed to the page (in the `<head>`), the TablePress CSS files will be enqueued.
128 + * If styles have already been printed to the page, the TablePress CSS files will be printed right away (likely in the `<body`>).
129 + *
98 130 * @since 1.0.0
99 131 */
100 132 public function enqueue_css(): void {
101 - /** This filter is documented in controllers/controller-frontend.php */
133 + /*
134 + * Bail early if the function is called from some action hook outside of the normal rendering process.
135 + * These are often used by e.g. SEO plugins that render the content in additional contexts, e.g. to get an excerpt via an output buffer.
136 + * In these cases, we don't want to enqueue the CSS, as it would likely not be printed on the page.
137 + */
138 + if ( doing_action( 'wp_head' ) || doing_action( 'wp_footer' ) ) {
139 + return;
140 + }
141 +
142 + // Prevent repeated execution via a static variable.
143 + static $css_enqueued = false;
144 + if ( $css_enqueued && ! doing_action( 'enqueue_block_assets' ) ) {
145 + return;
146 + }
147 + $css_enqueued = true;
148 +
149 + /**
150 + * Filters whether the TablePress Default CSS code shall be loaded.
151 + *
152 + * @since 1.0.0
153 + *
154 + * @param bool $use Whether the Default CSS shall be loaded. Default true.
155 + */
102 156 $use_default_css = apply_filters( 'tablepress_use_default_css', true );
157 + $use_custom_css = TablePress::$model_options->get( 'use_custom_css' );
158 +
159 + if ( ! $use_default_css && ! $use_custom_css ) {
160 + // Register a placeholder dependency, so that the handle is known for other styles.
161 + wp_register_style( 'tablepress-default', false ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
162 + return;
163 + }
164 +
103 165 $custom_css = TablePress::$model_options->get( 'custom_css' );
104 - $use_custom_css = ( TablePress::$model_options->get( 'use_custom_css' ) && '' !== $custom_css );
105 - $use_custom_css_file = ( $use_custom_css && TablePress::$model_options->get( 'use_custom_css_file' ) );
166 + $use_custom_css = $use_custom_css && '' !== $custom_css;
167 + $use_custom_css_file = $use_custom_css && TablePress::$model_options->get( 'use_custom_css_file' );
106 168 /**
107 169 * Filters the "Custom CSS" version number that is appended to the enqueued CSS files
108 170 *
109 171 * @since 1.0.0
@@ -131,81 +193,71 @@
131 193 if ( $use_custom_css_combined_file ) {
132 194 $custom_css_combined_url = $tablepress_css->get_custom_css_location( 'combined', 'url' );
133 195 // Need to use 'tablepress-default' instead of 'tablepress-combined' to not break existing TablePress Extensions.
134 196 wp_enqueue_style( 'tablepress-default', $custom_css_combined_url, array(), $custom_css_version );
197 + if ( did_action( 'wp_print_styles' ) ) {
198 + wp_print_styles( 'tablepress-default' );
199 + }
200 + return;
201 + }
202 +
203 + if ( $use_default_css ) {
204 + wp_enqueue_style( 'tablepress-default', $default_css_url, array(), TablePress::version );
135 205 } else {
136 - $custom_css_dependencies = array();
137 - if ( $use_default_css ) {
138 - wp_enqueue_style( 'tablepress-default', $default_css_url, array(), TablePress::version );
139 - // Add dependency to make sure that Custom CSS is printed after Default CSS.
140 - $custom_css_dependencies[] = 'tablepress-default';
206 + // Register a placeholder dependency, so that the handle is known for other styles.
207 + wp_register_style( 'tablepress-default', false ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
208 + }
209 +
210 + $use_custom_css_minified_file = ( $use_custom_css_file && ! SCRIPT_DEBUG && $tablepress_css->load_custom_css_from_file( 'minified' ) );
211 + if ( $use_custom_css_minified_file ) {
212 + $custom_css_minified_url = $tablepress_css->get_custom_css_location( 'minified', 'url' );
213 + wp_enqueue_style( 'tablepress-custom', $custom_css_minified_url, array( 'tablepress-default' ), $custom_css_version );
214 + if ( did_action( 'wp_print_styles' ) ) {
215 + wp_print_styles( 'tablepress-custom' );
141 216 }
217 + return;
218 + }
142 219
143 - $use_custom_css_minified_file = ( $use_custom_css_file && ! SCRIPT_DEBUG && $tablepress_css->load_custom_css_from_file( 'minified' ) );
144 - if ( $use_custom_css_minified_file ) {
145 - $custom_css_minified_url = $tablepress_css->get_custom_css_location( 'minified', 'url' );
146 - wp_enqueue_style( 'tablepress-custom', $custom_css_minified_url, $custom_css_dependencies, $custom_css_version );
147 - return;
220 + $use_custom_css_normal_file = ( $use_custom_css_file && $tablepress_css->load_custom_css_from_file( 'normal' ) );
221 + if ( $use_custom_css_normal_file ) {
222 + $custom_css_normal_url = $tablepress_css->get_custom_css_location( 'normal', 'url' );
223 + wp_enqueue_style( 'tablepress-custom', $custom_css_normal_url, array( 'tablepress-default' ), $custom_css_version );
224 + if ( did_action( 'wp_print_styles' ) ) {
225 + wp_print_styles( 'tablepress-custom' );
148 226 }
227 + return;
228 + }
149 229
150 - $use_custom_css_normal_file = ( $use_custom_css_file && $tablepress_css->load_custom_css_from_file( 'normal' ) );
151 - if ( $use_custom_css_normal_file ) {
152 - $custom_css_normal_url = $tablepress_css->get_custom_css_location( 'normal', 'url' );
153 - wp_enqueue_style( 'tablepress-custom', $custom_css_normal_url, $custom_css_dependencies, $custom_css_version );
154 - return;
230 + if ( $use_custom_css ) {
231 + // Get "Custom CSS" from options, try minified Custom CSS first.
232 + $custom_css_minified = TablePress::$model_options->get( 'custom_css_minified' );
233 + if ( ! empty( $custom_css_minified ) ) {
234 + $custom_css = $custom_css_minified;
155 235 }
156 -
157 - if ( $use_custom_css ) {
158 - // Get "Custom CSS" from options, try minified Custom CSS first.
159 - $custom_css_minified = TablePress::$model_options->get( 'custom_css_minified' );
160 - if ( ! empty( $custom_css_minified ) ) {
161 - $custom_css = $custom_css_minified;
236 + /**
237 + * Filters the "Custom CSS" code that is to be loaded as inline CSS.
238 + *
239 + * @since 1.0.0
240 + *
241 + * @param string $custom_css The "Custom CSS" code.
242 + */
243 + $custom_css = apply_filters( 'tablepress_custom_css', $custom_css );
244 + if ( ! empty( $custom_css ) ) {
245 + wp_add_inline_style( 'tablepress-default', $custom_css );
246 + if ( did_action( 'wp_print_styles' ) ) {
247 + wp_print_styles( 'tablepress-default' );
162 248 }
163 - /**
164 - * Filters the "Custom CSS" code that is to be loaded as inline CSS.
165 - *
166 - * @since 1.0.0
167 - *
168 - * @param string $custom_css The "Custom CSS" code.
169 - */
170 - $custom_css = apply_filters( 'tablepress_custom_css', $custom_css );
171 - if ( ! empty( $custom_css ) ) {
172 - // wp_add_inline_style() requires a loaded CSS file, so we have to work around that if "Default CSS" is disabled.
173 - if ( $use_default_css ) {
174 - // Handle of the file to which the <style> shall be appended.
175 - wp_add_inline_style( 'tablepress-default', $custom_css );
176 - } else {
177 - add_action( 'wp_head', array( $this, '_print_custom_css' ), 8 ); // Priority 8 to hook in right after WP_Styles has been processed.
178 - }
179 - }
249 + return;
180 250 }
181 251 }
182 252 }
183 253
184 254 /**
185 - * Print "Custom CSS" to "wp_head" inline.
255 + * Enqueues the DataTables JavaScript library and its dependencies.
186 256 *
187 - * This is necessary if "Default CSS" is off, and saving "Custom CSS" to a file is not possible.
188 - *
189 - * @since 1.0.0
257 + * @since 3.0.0
190 258 */
191 - public function _print_custom_css(): void {
192 - // Get "Custom CSS" from options, try minified Custom CSS first.
193 - $custom_css = TablePress::$model_options->get( 'custom_css_minified' );
194 - if ( empty( $custom_css ) ) {
195 - $custom_css = TablePress::$model_options->get( 'custom_css' );
196 - }
197 - /** This filter is documented in controllers/controller-frontend.php */
198 - $custom_css = apply_filters( 'tablepress_custom_css', $custom_css );
199 - echo "<style>\n{$custom_css}\n</style>\n";
200 - }
201 -
202 - /**
203 - * Enqueue the DataTables JavaScript library and its dependencies.
204 - *
205 - * @since 1.0.0
206 - */
207 - protected function _enqueue_datatables(): void {
259 + protected function enqueue_datatables_files(): void {
208 260 $js_file = 'js/jquery.datatables.min.js';
209 261 $js_url = plugins_url( $js_file, TABLEPRESS__FILE__ );
210 262 /**
211 263 * Filters the URL from which the DataTables JavaScript library file is loaded.
@@ -215,9 +267,23 @@
215 267 * @param string $js_url URL of the DataTables JS library file.
216 268 * @param string $js_file Path and file name of the DataTables JS library file.
217 269 */
218 270 $js_url = apply_filters( 'tablepress_datatables_js_url', $js_url, $js_file );
219 - wp_enqueue_script( 'tablepress-datatables', $js_url, array( 'jquery-core' ), TablePress::version, true );
271 +
272 + $dependencies = array( 'jquery-core' );
273 + if ( ! empty( $this->datatables_datetime_formats ) ) {
274 + $dependencies[] = 'moment';
275 + }
276 + /**
277 + * Filters the dependencies for the DataTables JavaScript library.
278 + *
279 + * @since 3.0.0
280 + *
281 + * @param string[] $dependencies The dependencies for the DataTables JS library.
282 + */
283 + $dependencies = apply_filters( 'tablepress_datatables_js_dependencies', $dependencies );
284 +
285 + wp_enqueue_script( 'tablepress-datatables', $js_url, $dependencies, TablePress::version, true );
220 286 }
221 287
222 288 /**
223 289 * Add JS code for invocation of DataTables JS library.
@@ -235,11 +301,13 @@
235 301 // There are no tables with activated DataTables on the page that is currently rendered.
236 302 return;
237 303 }
238 304
305 + $this->enqueue_datatables_files();
306 +
239 307 /*
240 - * Don't add the DataTables function calls in the scope of the block editor.
241 - * Otherwise, this causes a script error in the block editor iframe.
308 + * Don't add the DataTables function calls in the scope of the block editor iframe.
309 + * This is necessary for non-block themes, for others, the repeated execution check above is sufficient.
242 310 */
243 311 if ( function_exists( 'get_current_screen' ) ) {
244 312 $current_screen = get_current_screen();
245 313 if ( ( $current_screen instanceof WP_Screen ) && $current_screen->is_block_editor() ) {
@@ -252,8 +320,10 @@
252 320 // Generate the specific JS commands, depending on chosen features on the "Edit" screen and the Shortcode parameters.
253 321 $commands = array();
254 322
255 323 foreach ( $this->shown_tables as $table_id => $table_store ) {
324 + $table_id = (string) $table_id; // Ensure that the table ID is a string, as it comes from an array key where numeric strings are converted to integers.
325 +
256 326 if ( empty( $table_store['instances'] ) ) {
257 327 continue;
258 328 }
259 329
@@ -312,9 +382,9 @@
312 382 $datatables_strings = array();
313 383 }
314 384 } elseif ( str_ends_with( $language_file, '.json' ) ) {
315 385 $datatables_strings = file_get_contents( $language_file );
316 - $datatables_strings = json_decode( $datatables_strings, true ); // @phpstan-ignore-line
386 + $datatables_strings = json_decode( $datatables_strings, true ); // @phpstan-ignore argument.type
317 387 // Check if JSON could be decoded.
318 388 if ( is_null( $datatables_strings ) ) {
319 389 $datatables_strings = array();
320 390 }
@@ -338,58 +408,56 @@
338 408 * @param string $datatables_locale Current locale/language for the DataTables JS library.
339 409 */
340 410 $datatables_language[ $datatables_locale ] = apply_filters( 'tablepress_datatables_language_strings', $datatables_strings, $datatables_locale );
341 411 }
342 - $parameters['language'] = '"language":DT_language["' . $datatables_locale . '"]';
412 + $parameters['language'] = "language:DT_language['{$datatables_locale}']";
343 413
344 414 // These parameters need to be added for performance gain or to overwrite unwanted default behavior.
345 415 if ( $js_options['datatables_sort'] ) {
346 416 // No initial sort.
347 - $parameters['order'] = '"order":[]';
417 + $parameters['order'] = 'order:[]';
348 418 // Don't add additional classes, to speed up sorting.
349 - $parameters['orderClasses'] = '"orderClasses":false';
419 + $parameters['orderClasses'] = 'orderClasses:false';
350 420 }
351 421
352 - // Alternating row colors is default, so remove them if not wanted with [].
353 - $parameters['stripeClasses'] = '"stripeClasses":' . ( ( $js_options['alternating_row_colors'] ) ? '["even","odd"]' : '[]' );
354 -
355 422 // The following options are activated by default, so we only need to "false" them if we don't want them, but don't need to "true" them if we do.
356 423 if ( ! $js_options['datatables_sort'] ) {
357 - $parameters['ordering'] = '"ordering":false';
424 + $parameters['ordering'] = 'ordering:false';
358 425 }
359 426 if ( $js_options['datatables_paginate'] ) {
360 - $parameters['pagingType'] = '"pagingType":"simple"';
427 + $parameters['pagingType'] = "pagingType:'simple_numbers'";
361 428 if ( $js_options['datatables_lengthchange'] ) {
362 429 $length_menu = array( 10, 25, 50, 100 );
363 430 if ( ! in_array( $js_options['datatables_paginate_entries'], $length_menu, true ) ) {
364 431 $length_menu[] = $js_options['datatables_paginate_entries'];
365 432 sort( $length_menu, SORT_NUMERIC );
366 - $parameters['lengthMenu'] = '"lengthMenu":[' . implode( ',', $length_menu ) . ']';
433 + $parameters['lengthMenu'] = 'lengthMenu:[' . implode( ',', $length_menu ) . ']';
367 434 }
368 435 } else {
369 - $parameters['lengthChange'] = '"lengthChange":false';
436 + $parameters['lengthChange'] = 'lengthChange:false';
370 437 }
371 438 if ( 10 !== $js_options['datatables_paginate_entries'] ) {
372 - $parameters['pageLength'] = '"pageLength":' . $js_options['datatables_paginate_entries'];
439 + $parameters['pageLength'] = "pageLength:{$js_options['datatables_paginate_entries']}";
373 440 }
374 441 } else {
375 - $parameters['paging'] = '"paging":false';
442 + $parameters['paging'] = 'paging:false';
376 443 }
377 444 if ( ! $js_options['datatables_filter'] ) {
378 - $parameters['searching'] = '"searching":false';
445 + $parameters['searching'] = 'searching:false';
379 446 }
380 447 if ( ! $js_options['datatables_info'] ) {
381 - $parameters['info'] = '"info":false';
448 + $parameters['info'] = 'info:false';
382 449 }
383 450 if ( $js_options['datatables_scrollx'] ) {
384 - $parameters['scrollX'] = '"scrollX":true';
451 + $parameters['scrollX'] = 'scrollX:true';
385 452 }
386 453 if ( false !== $js_options['datatables_scrolly'] ) {
387 - $parameters['scrollY'] = '"scrollY":"' . preg_replace( '#[^0-9a-z.%]#', '', $js_options['datatables_scrolly'] ) . '"';
388 - $parameters['scrollCollapse'] = '"scrollCollapse":true';
454 + $parameters['scrollY'] = 'scrollY:"' . preg_replace( '#[^0-9a-z.%]#', '', $js_options['datatables_scrolly'] ) . '"';
455 + $parameters['scrollCollapse'] = 'scrollCollapse:true';
389 456 }
390 - if ( ! empty( $js_options['datatables_custom_commands'] ) ) {
391 - $parameters['custom_commands'] = $js_options['datatables_custom_commands'];
457 + if ( '' !== $js_options['datatables_custom_commands'] ) {
458 + $parameters['custom_commands'] = trim( $js_options['datatables_custom_commands'] ); // Remove leading and trailing whitespace.
459 + $parameters['custom_commands'] = trim( $parameters['custom_commands'], ',' ); // Remove potentially leading and trailing commas to prevent JS script errors.
392 460 }
393 461
394 462 /**
395 463 * Filters the parameters that are passed to the DataTables JavaScript library.
@@ -402,21 +470,22 @@
402 470 * @param array<string, mixed> $js_options The options for the JS library.
403 471 */
404 472 $parameters = apply_filters( 'tablepress_datatables_parameters', $parameters, $table_id, $html_id, $js_options );
405 473
406 - // If an existing parameter (in the from `"parameter":`) is set in the "Custom Commands", remove its default value.
407 - if ( isset( $parameters['custom_commands'] ) ) {
408 - foreach ( array_keys( $parameters ) as $maybe_overwritten_parameter ) {
409 - if ( str_contains( $parameters['custom_commands'], "\"{$maybe_overwritten_parameter}\":" ) ) {
410 - unset( $parameters[ $maybe_overwritten_parameter ] );
411 - }
474 + // If an existing parameter is set as an object key in the "Custom Commands", remove its separate value, to allow for full overrides.
475 + if ( isset( $parameters['custom_commands'] ) && '' !== $parameters['custom_commands'] ) {
476 + $parameters_in_custom_commands = TablePress::extract_keys_from_js_object_string( '{' . $parameters['custom_commands'] . '}' );
477 + foreach ( $parameters_in_custom_commands as $parameter_in_custom_commands ) {
478 + unset( $parameters[ $parameter_in_custom_commands ] );
412 479 }
413 480 }
414 481
482 + $name = substr( $html_id, 11 ); // Remove "tablepress-" from the HTML ID.
483 + $name = "DT_TP['" . str_replace( '-', '_', $name ) . "']";
415 484 $parameters = implode( ',', $parameters );
416 485 $parameters = ( ! empty( $parameters ) ) ? '{' . $parameters . '}' : '';
417 486
418 - $command = "$('#{$html_id}').DataTable({$parameters});";
487 + $command = "{$name} = new DataTable('#{$html_id}',{$parameters});";
419 488 /**
420 489 * Filters the JavaScript command that invokes the DataTables JavaScript library on one table.
421 490 *
422 491 * @since 1.0.0
@@ -425,10 +494,11 @@
425 494 * @param string $html_id The ID of the table HTML element.
426 495 * @param string $parameters The parameters for the DataTables JS library.
427 496 * @param string $table_id The current table ID.
428 497 * @param array<string, mixed> $js_options The options for the JS library.
498 + * @param string $name The name of the DataTable instance.
429 499 */
430 - $command = apply_filters( 'tablepress_datatables_command', $command, $html_id, $parameters, $table_id, $js_options );
500 + $command = apply_filters( 'tablepress_datatables_command', $command, $html_id, $parameters, $table_id, $js_options, $name );
431 501 if ( ! empty( $command ) ) {
432 502 $commands[] = $command;
433 503 }
434 504 } // foreach table instance
@@ -435,14 +505,40 @@
435 505 } // foreach table ID
436 506
437 507 // DataTables language/translation handling.
438 508 if ( ! empty( $datatables_language ) ) {
439 - $datatables_language = wp_json_encode( $datatables_language, JSON_UNESCAPED_UNICODE | JSON_FORCE_OBJECT );
440 - $datatables_language = "var DT_language={$datatables_language};\n";
509 + $datatables_language_command = wp_json_encode( $datatables_language, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_FORCE_OBJECT );
510 + $datatables_language_command = "var DT_language={$datatables_language_command};\n";
441 511 } else {
442 - $datatables_language = '';
512 + $datatables_language_command = '';
443 513 }
444 514
515 + // DataTables datetime format string handling.
516 + if ( ! empty( $this->datatables_datetime_formats ) ) {
517 + // Create a command like `DataTable.datetime('MM/DD/YYYY');DataTable.datetime('DD.MM.YYYY');`.
518 + $datatables_datetime_command = implode(
519 + '',
520 + array_map(
521 + static fn( string $datetime_format ): string => "DataTable.datetime('{$datetime_format}');",
522 + $this->datatables_datetime_formats,
523 + )
524 + ) . "\n";
525 + } else {
526 + $datatables_datetime_command = '';
527 + }
528 +
529 + /**
530 + * Filters the JavaScript code for the DataTables JavaScript library that initializes the automatically detected date/time formats via moment.js.
531 + *
532 + * @since 3.0.0
533 + *
534 + * @param string $datatables_datetime_command The JS code for the DataTables JS library that initializes the date/time formats.
535 + * @param string[] $datatables_datetime_formats The date/time formats for moment.js.
536 + */
537 + $datatables_datetime_command = apply_filters( 'tablepress_datatables_datetime_command', $datatables_datetime_command, $this->datatables_datetime_formats );
538 +
539 + $datatables_pre_commands = $datatables_language_command . $datatables_datetime_command;
540 +
445 541 $commands = implode( "\n", $commands );
446 542 /**
447 543 * Filters the JavaScript commands that invoke the DataTables JavaScript library on all tables on the page.
448 544 *
@@ -454,27 +550,26 @@
454 550 if ( '' === $commands ) {
455 551 return;
456 552 }
457 553
458 - $script_type_attr = current_theme_supports( 'html5', 'script' ) ? '' : ' type="text/javascript"';
459 -
460 - $js_wrapper = <<<'JS'
461 -<script%3$s>
462 -jQuery(function($){
463 -%1$s%2$s
464 -});
465 -</script>
466 -JS;
554 + $script_template = <<<'JS'
555 + var DT_TP = {};
556 + jQuery(($)=>{
557 + %1$s%2$s
558 + });
559 + JS;
467 560 /**
468 561 * Filters the script/jQuery wrapper code for the DataTables commands calls.
469 562 *
470 563 * @since 1.14.0
471 564 *
472 - * @param string $js_wrapper Default script/jQuery wrapper code for the DataTables commands calls.
565 + * @param string $script_template Default script/jQuery wrapper code for the DataTables commands calls.
473 566 */
474 - $js_wrapper = apply_filters( 'tablepress_all_datatables_commands_wrapper', $js_wrapper );
475 - printf( $js_wrapper, $datatables_language, $commands, $script_type_attr );
567 + $script_template = apply_filters( 'tablepress_all_datatables_commands_wrapper', $script_template );
476 568
569 + $script = sprintf( $script_template, $datatables_pre_commands, $commands );
570 + wp_add_inline_script( 'tablepress-datatables', $script );
571 +
477 572 // Prevent repeated execution (which would lead to DataTables error messages) via a static variable.
478 573 $datatables_calls_printed = true;
479 574 }
480 575
@@ -488,8 +583,10 @@
488 583 */
489 584 public function shortcode_table( /* array|string */ $shortcode_atts ): string {
490 585 $shortcode_atts = (array) $shortcode_atts;
491 586
587 + $this->enqueue_css();
588 +
492 589 $_render = TablePress::load_class( 'TablePress_Render', 'class-render.php', 'classes' );
493 590
494 591 $default_shortcode_atts = $_render->get_default_render_options();
495 592 /**
@@ -513,9 +610,9 @@
513 610
514 611 // Check, if a table with the given ID exists.
515 612 $table_id = (string) preg_replace( '/[^a-zA-Z0-9_-]/', '', $shortcode_atts['id'] );
516 613 if ( ! TablePress::$model_table->table_exists( $table_id ) ) {
517 - $message = "[table &#8220;{$table_id}&#8221; not found /]<br />\n";
614 + $message = "&#91;table “{$table_id}” not found /&#93;<br />\n";
518 615 /**
519 616 * Filters the "Table not found" message.
520 617 *
521 618 * @since 1.0.0
@@ -529,9 +626,9 @@
529 626
530 627 // Load table, with table data, options, and visibility settings.
531 628 $table = TablePress::$model_table->load( $table_id, true, true );
532 629 if ( is_wp_error( $table ) ) {
533 - $message = "[table &#8220;{$table_id}&#8221; could not be loaded /]<br />\n";
630 + $message = "&#91;table “{$table_id}” could not be loaded /&#93;<br />\n";
534 631 /**
535 632 * Filters the "Table could not be loaded" message.
536 633 *
537 634 * @since 1.0.0
@@ -543,9 +640,9 @@
543 640 $message = apply_filters( 'tablepress_table_load_error_message', $message, $table_id, $table );
544 641 return $message;
545 642 }
546 643 if ( isset( $table['is_corrupted'] ) && $table['is_corrupted'] ) {
547 - $message = "<div>Attention: The internal data of table &#8220;{$table_id}&#8221; is corrupted!</div>";
644 + $message = "<div>Attention: The internal data of table “{$table_id}” is corrupted!</div>";
548 645 /**
549 646 * Filters the "Table data is corrupted" message.
550 647 *
551 648 * @since 1.0.0
@@ -592,8 +689,12 @@
592 689 $render_options[ $key ] = $value;
593 690 }
594 691 }
595 692
693 + // Backward compatibility: Convert boolean or numeric string "table_head" and "table_foot" options to integer.
694 + $render_options['table_head'] = absint( $render_options['table_head'] );
695 + $render_options['table_foot'] = absint( $render_options['table_foot'] );
696 +
596 697 // Generate unique HTML ID, depending on how often this table has already been shown on this page.
597 698 if ( ! isset( $this->shown_tables[ $table_id ] ) ) {
598 699 $this->shown_tables[ $table_id ] = array(
599 700 'count' => 0,
@@ -628,9 +729,9 @@
628 729 *
629 730 * @param bool $show Whether to show the "Edit" link below the table. Default true.
630 731 * @param string $table_id The current table ID.
631 732 */
632 - if ( is_user_logged_in() && apply_filters( 'tablepress_edit_link_below_table', true, $table['id'] ) && current_user_can( 'tablepress_edit_table', $table['id'] ) ) {
733 + if ( is_user_logged_in() && ! $render_options['block_preview'] && apply_filters( 'tablepress_edit_link_below_table', true, $table['id'] ) && current_user_can( 'tablepress_edit_table', $table['id'] ) ) {
633 734 $render_options['edit_table_url'] = TablePress::url( array( 'action' => 'edit', 'table_id' => $table['id'] ) );
634 735 }
635 736
636 737 /**
@@ -644,18 +745,22 @@
644 745 * @param array<string, mixed> $table The current table.
645 746 */
646 747 $render_options = apply_filters( 'tablepress_table_render_options', $render_options, $table );
647 748
749 + // Backward compatibility: Convert boolean "table_head" and "table_foot" options to integer, in case they were overwritten via the filter hook.
750 + $render_options['table_head'] = absint( $render_options['table_head'] );
751 + $render_options['table_foot'] = absint( $render_options['table_foot'] );
752 +
648 753 // Check if table output shall and can be loaded from the transient cache, otherwise generate the output.
649 754 if ( $render_options['cache_table_output'] && ! is_user_logged_in() ) {
650 755 // Hash the Render Options array to get a unique cache identifier.
651 - $table_hash = md5( wp_json_encode( $render_options, TABLEPRESS_JSON_OPTIONS ) ); // @phpstan-ignore-line
756 + $table_hash = md5( wp_json_encode( $render_options, TABLEPRESS_JSON_OPTIONS ) ); // @phpstan-ignore argument.type
652 757 $transient_name = 'tablepress_' . $table_hash; // Attention: This string must not be longer than 45 characters!
653 758 $output = get_transient( $transient_name );
654 759 if ( false === $output || '' === $output ) {
655 760 // Render/generate the table HTML, as it was not found in the cache.
656 761 $_render->set_input( $table, $render_options );
657 - $output = $_render->get_output();
762 + $output = $_render->get_output( 'html' );
658 763 // Save render output in a transient, set cache timeout to 24 hours.
659 764 set_transient( $transient_name, $output, DAY_IN_SECONDS );
660 765 // Update output caches list transient (necessary for cache invalidation upon table saving).
661 766 $caches_list_transient_name = 'tablepress_c_' . md5( $table_id );
@@ -681,18 +786,16 @@
681 786 }
682 787 } else {
683 788 // Render/generate the table HTML, as no cache is to be used.
684 789 $_render->set_input( $table, $render_options );
685 - $output = $_render->get_output();
790 + $output = $_render->get_output( 'html' );
686 791 }
687 792
688 793 // If DataTables is to be and can be used with this instance of a table, process its parameters and register the call for inclusion in the footer.
689 794 if ( $render_options['use_datatables']
690 - && $render_options['table_head']
691 - && str_contains( $output, '<thead' ) // A `<thead>` tag is required.
692 - && ! str_contains( $output, ' colspan="' ) // `colspan` attributes are forbidden.
693 - && ! str_contains( $output, ' rowspan="' ) // `rowspan` attributes are forbidden.
694 - ) {
795 + && 0 < $render_options['table_head']
796 + && ! str_contains( $output, 'tbody-has-connected-cells' ) // The Render class adds this CSS class to the `<table>` element if the table has connected cells in the `<tbody>`.
797 + ) {
695 798 // Get options for the DataTables JavaScript library from the table's render options.
696 799 $js_options = array();
697 800 foreach ( array(
698 801 'alternating_row_colors',
@@ -722,10 +825,21 @@
722 825 * @param string $table_id The current table ID.
723 826 * @param array<string, mixed> $render_options The render options for the table.
724 827 */
725 828 $js_options = apply_filters( 'tablepress_table_js_options', $js_options, $table_id, $render_options );
829 +
726 830 $this->shown_tables[ $table_id ]['instances'][ (string) $render_options['html_id'] ] = $js_options;
727 - $this->_enqueue_datatables();
831 +
832 + // DataTables datetime format string handling.
833 + if ( '' !== $render_options['datatables_datetime'] ) {
834 + $render_options['datatables_datetime'] = explode( '|', $render_options['datatables_datetime'] );
835 + foreach ( $render_options['datatables_datetime'] as $datetime_format ) {
836 + $datetime_format = trim( $datetime_format );
837 + if ( '' !== $datetime_format && ! in_array( $datetime_format, $this->datatables_datetime_formats, true ) ) {
838 + $this->datatables_datetime_formats[] = $datetime_format;
839 + }
840 + }
841 + }
728 842 }
729 843
730 844 // Maybe print a list of used render options.
731 845 if ( $render_options['shortcode_debug'] && is_user_logged_in() ) {
@@ -785,9 +899,9 @@
785 899
786 900 // Check, if a table with the given ID exists.
787 901 $table_id = preg_replace( '/[^a-zA-Z0-9_-]/', '', $shortcode_atts['id'] );
788 902 if ( ! TablePress::$model_table->table_exists( $table_id ) ) {
789 - $message = "[table &#8220;{$table_id}&#8221; not found /]<br />\n";
903 + $message = "&#91;table “{$table_id}” not found /&#93;<br />\n";
790 904 /** This filter is documented in controllers/controller-frontend.php */
791 905 $message = apply_filters( 'tablepress_table_not_found_message', $message, $table_id );
792 906 return $message;
793 907 }
@@ -794,9 +908,9 @@
794 908
795 909 // Load table, with table data, options, and visibility settings.
796 910 $table = TablePress::$model_table->load( $table_id, true, true );
797 911 if ( is_wp_error( $table ) ) {
798 - $message = "[table &#8220;{$table_id}&#8221; could not be loaded /]<br />\n";
912 + $message = "&#91;table “{$table_id}” could not be loaded /&#93;<br />\n";
799 913 /** This filter is documented in controllers/controller-frontend.php */
800 914 $message = apply_filters( 'tablepress_table_load_error_message', $message, $table_id, $table );
801 915 return $message;
802 916 }
@@ -817,9 +931,13 @@
817 931 $output = $table['last_modified'];
818 932 break;
819 933 case 'human':
820 934 $modified_timestamp = date_create( $table['last_modified'], wp_timezone() );
821 - $modified_timestamp = $modified_timestamp->getTimestamp(); // @phpstan-ignore-line
935 + if ( false === $modified_timestamp ) {
936 + $modified_timestamp = $table['last_modified'];
937 + } else {
938 + $modified_timestamp = $modified_timestamp->getTimestamp();
939 + }
822 940 $current_timestamp = time();
823 941 $time_diff = $current_timestamp - $modified_timestamp;
824 942 // Time difference is only shown up to one week.
825 943 if ( $time_diff >= 0 && $time_diff < WEEK_IN_SECONDS ) {
@@ -847,14 +965,10 @@
847 965 break;
848 966 case 'number_rows':
849 967 $output = count( $table['data'] );
850 968 if ( 'raw' !== $format ) {
851 - if ( $table['options']['table_head'] ) {
852 - --$output;
853 - }
854 - if ( $table['options']['table_foot'] ) {
855 - --$output;
856 - }
969 + $output -= $table['options']['table_head'];
970 + $output -= $table['options']['table_foot'];
857 971 }
858 972 break;
859 973 case 'number_columns':
860 974 $output = count( $table['data'][0] );
@@ -859,9 +973,9 @@
859 973 case 'number_columns':
860 974 $output = count( $table['data'][0] );
861 975 break;
862 976 default:
863 - $output = "[table-info field &#8220;{$field}&#8221; not found in table &#8220;{$table_id}&#8221; /]<br />\n";
977 + $output = "&#91;table-info field “{$field}” not found in table “{$table_id}” /&#93;<br />\n";
864 978 /**
865 979 * Filters the "table info field not found" message.
866 980 *
867 981 * @since 1.0.0
@@ -906,9 +1020,9 @@
906 1020
907 1021 global $wpdb;
908 1022
909 1023 // Protect against cases where `null` is somehow passed to the filter hook callback.
910 - if ( ! is_string( $search_sql ) ) {
1024 + if ( ! is_string( $search_sql ) ) { // @phpstan-ignore function.alreadyNarrowedType (The `is_string()` check is needed as the input is coming from a filter hook.)
911 1025 return '';
912 1026 }
913 1027
914 1028 if ( ! is_search() || ! is_main_query() ) {
@@ -929,16 +1043,21 @@
929 1043 foreach ( $table_ids as $table_id ) {
930 1044 // Load table, with table data, options, and visibility settings.
931 1045 $table = TablePress::$model_table->load( $table_id, true, true );
932 1046
1047 + // Skip tables that could not be loaded.
1048 + if ( is_wp_error( $table ) ) {
1049 + continue;
1050 + }
1051 +
1052 + // Do not search in corrupted tables.
933 1053 if ( isset( $table['is_corrupted'] ) && $table['is_corrupted'] ) {
934 - // Do not search in corrupted tables.
935 1054 continue;
936 1055 }
937 1056
938 1057 foreach ( $search_terms as $search_term ) {
939 - if ( ( $table['options']['print_name'] && false !== stripos( $table['name'], $search_term ) ) // @phpstan-ignore-line
940 - || ( $table['options']['print_description'] && false !== stripos( $table['description'], $search_term ) ) ) { // @phpstan-ignore-line
1058 + if ( ( $table['options']['print_name'] && false !== stripos( $table['name'], (string) $search_term ) )
1059 + || ( $table['options']['print_description'] && false !== stripos( $table['description'], (string) $search_term ) ) ) {
941 1060 // Found the search term in the name or description (and they are shown).
942 1061 $query_result[ $search_term ][] = $table_id; // Add table ID to result list.
943 1062 // No need to continue searching this search term in this table.
944 1063 continue;
@@ -944,9 +1063,9 @@
944 1063 continue;
945 1064 }
946 1065
947 1066 // Search search term in visible table cells (without taking Shortcode parameters into account!).
948 - foreach ( $table['data'] as $row_idx => $table_row ) { // @phpstan-ignore-line
1067 + foreach ( $table['data'] as $row_idx => $table_row ) {
949 1068 if ( 0 === $table['visibility']['rows'][ $row_idx ] ) {
950 1069 // Row is hidden, so don't search in it.
951 1070 continue;
952 1071 }
@@ -955,9 +1074,9 @@
955 1074 // Column is hidden, so don't search in it.
956 1075 continue;
957 1076 }
958 1077 // @todo Cells are not evaluated here, so math formulas are searched.
959 - if ( false !== stripos( $table_cell, $search_term ) ) {
1078 + if ( false !== stripos( $table_cell, (string) $search_term ) ) {
960 1079 // Found the search term in the cell content.
961 1080 $query_result[ $search_term ][] = $table_id; // Add table ID to result list
962 1081 // No need to continue searching this search term in this table.
963 1082 continue 3;
@@ -974,9 +1093,9 @@
974 1093 $n = ( empty( $exact ) ) ? '%' : '';
975 1094 $search_sql = $wpdb->remove_placeholder_escape( $search_sql );
976 1095 foreach ( $query_result as $search_term => $table_ids ) {
977 1096 $search_term = esc_sql( $wpdb->esc_like( $search_term ) );
978 - $old_or = "OR ({$wpdb->posts}.post_content LIKE '{$n}{$search_term}{$n}')"; // @phpstan-ignore-line
1097 + $old_or = "OR ({$wpdb->posts}.post_content LIKE '{$n}{$search_term}{$n}')"; // @phpstan-ignore encapsedStringPart.nonString (The esc_sql() call above returns a string, as a string is passed.)
979 1098 $table_ids = implode( '|', $table_ids );
980 1099 $regexp = '\\\\[' . TablePress::$shortcode . ' id=(["\\\']?)(' . $table_ids . ')([\]"\\\' /])'; // ' needs to be single escaped, [ double escaped (with \\) in mySQL
981 1100 $new_or = $old_or . " OR ({$wpdb->posts}.post_content REGEXP '{$regexp}')";
982 1101 $search_sql = str_replace( $old_or, $new_or, $search_sql );
@@ -1004,9 +1123,9 @@
1004 1123 $render_attributes = shortcode_parse_atts( $block_attributes['parameters'] );
1005 1124 } else {
1006 1125 $render_attributes = array();
1007 1126 }
1008 - $render_attributes['id'] = $block_attributes['id']; // @phpstan-ignore-line
1127 + $render_attributes['id'] = $block_attributes['id'];
1009 1128
1010 1129 return $this->shortcode_table( $render_attributes );
1011 1130 }
1012 1131