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 +247 -139 2.43.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() ) {
@@ -314,9 +382,9 @@
314 382 $datatables_strings = array();
315 383 }
316 384 } elseif ( str_ends_with( $language_file, '.json' ) ) {
317 385 $datatables_strings = file_get_contents( $language_file );
318 - $datatables_strings = json_decode( $datatables_strings, true ); // @phpstan-ignore-line
386 + $datatables_strings = json_decode( $datatables_strings, true ); // @phpstan-ignore argument.type
319 387 // Check if JSON could be decoded.
320 388 if ( is_null( $datatables_strings ) ) {
321 389 $datatables_strings = array();
322 390 }
@@ -340,58 +408,56 @@
340 408 * @param string $datatables_locale Current locale/language for the DataTables JS library.
341 409 */
342 410 $datatables_language[ $datatables_locale ] = apply_filters( 'tablepress_datatables_language_strings', $datatables_strings, $datatables_locale );
343 411 }
344 - $parameters['language'] = '"language":DT_language["' . $datatables_locale . '"]';
412 + $parameters['language'] = "language:DT_language['{$datatables_locale}']";
345 413
346 414 // These parameters need to be added for performance gain or to overwrite unwanted default behavior.
347 415 if ( $js_options['datatables_sort'] ) {
348 416 // No initial sort.
349 - $parameters['order'] = '"order":[]';
417 + $parameters['order'] = 'order:[]';
350 418 // Don't add additional classes, to speed up sorting.
351 - $parameters['orderClasses'] = '"orderClasses":false';
419 + $parameters['orderClasses'] = 'orderClasses:false';
352 420 }
353 421
354 - // Alternating row colors is default, so remove them if not wanted with [].
355 - $parameters['stripeClasses'] = '"stripeClasses":' . ( ( $js_options['alternating_row_colors'] ) ? '["even","odd"]' : '[]' );
356 -
357 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.
358 423 if ( ! $js_options['datatables_sort'] ) {
359 - $parameters['ordering'] = '"ordering":false';
424 + $parameters['ordering'] = 'ordering:false';
360 425 }
361 426 if ( $js_options['datatables_paginate'] ) {
362 - $parameters['pagingType'] = '"pagingType":"simple"';
427 + $parameters['pagingType'] = "pagingType:'simple_numbers'";
363 428 if ( $js_options['datatables_lengthchange'] ) {
364 429 $length_menu = array( 10, 25, 50, 100 );
365 430 if ( ! in_array( $js_options['datatables_paginate_entries'], $length_menu, true ) ) {
366 431 $length_menu[] = $js_options['datatables_paginate_entries'];
367 432 sort( $length_menu, SORT_NUMERIC );
368 - $parameters['lengthMenu'] = '"lengthMenu":[' . implode( ',', $length_menu ) . ']';
433 + $parameters['lengthMenu'] = 'lengthMenu:[' . implode( ',', $length_menu ) . ']';
369 434 }
370 435 } else {
371 - $parameters['lengthChange'] = '"lengthChange":false';
436 + $parameters['lengthChange'] = 'lengthChange:false';
372 437 }
373 438 if ( 10 !== $js_options['datatables_paginate_entries'] ) {
374 - $parameters['pageLength'] = '"pageLength":' . $js_options['datatables_paginate_entries'];
439 + $parameters['pageLength'] = "pageLength:{$js_options['datatables_paginate_entries']}";
375 440 }
376 441 } else {
377 - $parameters['paging'] = '"paging":false';
442 + $parameters['paging'] = 'paging:false';
378 443 }
379 444 if ( ! $js_options['datatables_filter'] ) {
380 - $parameters['searching'] = '"searching":false';
445 + $parameters['searching'] = 'searching:false';
381 446 }
382 447 if ( ! $js_options['datatables_info'] ) {
383 - $parameters['info'] = '"info":false';
448 + $parameters['info'] = 'info:false';
384 449 }
385 450 if ( $js_options['datatables_scrollx'] ) {
386 - $parameters['scrollX'] = '"scrollX":true';
451 + $parameters['scrollX'] = 'scrollX:true';
387 452 }
388 453 if ( false !== $js_options['datatables_scrolly'] ) {
389 - $parameters['scrollY'] = '"scrollY":"' . preg_replace( '#[^0-9a-z.%]#', '', $js_options['datatables_scrolly'] ) . '"';
390 - $parameters['scrollCollapse'] = '"scrollCollapse":true';
454 + $parameters['scrollY'] = 'scrollY:"' . preg_replace( '#[^0-9a-z.%]#', '', $js_options['datatables_scrolly'] ) . '"';
455 + $parameters['scrollCollapse'] = 'scrollCollapse:true';
391 456 }
392 - if ( ! empty( $js_options['datatables_custom_commands'] ) ) {
393 - $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.
394 460 }
395 461
396 462 /**
397 463 * Filters the parameters that are passed to the DataTables JavaScript library.
@@ -404,21 +470,22 @@
404 470 * @param array<string, mixed> $js_options The options for the JS library.
405 471 */
406 472 $parameters = apply_filters( 'tablepress_datatables_parameters', $parameters, $table_id, $html_id, $js_options );
407 473
408 - // If an existing parameter (in the from `"parameter":`) is set in the "Custom Commands", remove its default value.
409 - if ( isset( $parameters['custom_commands'] ) ) {
410 - foreach ( array_keys( $parameters ) as $maybe_overwritten_parameter ) {
411 - if ( str_contains( $parameters['custom_commands'], "\"{$maybe_overwritten_parameter}\":" ) ) {
412 - unset( $parameters[ $maybe_overwritten_parameter ] );
413 - }
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 ] );
414 479 }
415 480 }
416 481
482 + $name = substr( $html_id, 11 ); // Remove "tablepress-" from the HTML ID.
483 + $name = "DT_TP['" . str_replace( '-', '_', $name ) . "']";
417 484 $parameters = implode( ',', $parameters );
418 485 $parameters = ( ! empty( $parameters ) ) ? '{' . $parameters . '}' : '';
419 486
420 - $command = "$('#{$html_id}').DataTable({$parameters});";
487 + $command = "{$name} = new DataTable('#{$html_id}',{$parameters});";
421 488 /**
422 489 * Filters the JavaScript command that invokes the DataTables JavaScript library on one table.
423 490 *
424 491 * @since 1.0.0
@@ -427,10 +494,11 @@
427 494 * @param string $html_id The ID of the table HTML element.
428 495 * @param string $parameters The parameters for the DataTables JS library.
429 496 * @param string $table_id The current table ID.
430 497 * @param array<string, mixed> $js_options The options for the JS library.
498 + * @param string $name The name of the DataTable instance.
431 499 */
432 - $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 );
433 501 if ( ! empty( $command ) ) {
434 502 $commands[] = $command;
435 503 }
436 504 } // foreach table instance
@@ -437,14 +505,40 @@
437 505 } // foreach table ID
438 506
439 507 // DataTables language/translation handling.
440 508 if ( ! empty( $datatables_language ) ) {
441 - $datatables_language = wp_json_encode( $datatables_language, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_FORCE_OBJECT );
442 - $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";
443 511 } else {
444 - $datatables_language = '';
512 + $datatables_language_command = '';
445 513 }
446 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 +
447 541 $commands = implode( "\n", $commands );
448 542 /**
449 543 * Filters the JavaScript commands that invoke the DataTables JavaScript library on all tables on the page.
450 544 *
@@ -456,27 +550,26 @@
456 550 if ( '' === $commands ) {
457 551 return;
458 552 }
459 553
460 - $script_type_attr = current_theme_supports( 'html5', 'script' ) ? '' : ' type="text/javascript"';
461 -
462 - $js_wrapper = <<<'JS'
463 -<script%3$s>
464 -jQuery(function($){
465 -%1$s%2$s
466 -});
467 -</script>
468 -JS;
554 + $script_template = <<<'JS'
555 + var DT_TP = {};
556 + jQuery(($)=>{
557 + %1$s%2$s
558 + });
559 + JS;
469 560 /**
470 561 * Filters the script/jQuery wrapper code for the DataTables commands calls.
471 562 *
472 563 * @since 1.14.0
473 564 *
474 - * @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.
475 566 */
476 - $js_wrapper = apply_filters( 'tablepress_all_datatables_commands_wrapper', $js_wrapper );
477 - printf( $js_wrapper, $datatables_language, $commands, $script_type_attr );
567 + $script_template = apply_filters( 'tablepress_all_datatables_commands_wrapper', $script_template );
478 568
569 + $script = sprintf( $script_template, $datatables_pre_commands, $commands );
570 + wp_add_inline_script( 'tablepress-datatables', $script );
571 +
479 572 // Prevent repeated execution (which would lead to DataTables error messages) via a static variable.
480 573 $datatables_calls_printed = true;
481 574 }
482 575
@@ -490,8 +583,10 @@
490 583 */
491 584 public function shortcode_table( /* array|string */ $shortcode_atts ): string {
492 585 $shortcode_atts = (array) $shortcode_atts;
493 586
587 + $this->enqueue_css();
588 +
494 589 $_render = TablePress::load_class( 'TablePress_Render', 'class-render.php', 'classes' );
495 590
496 591 $default_shortcode_atts = $_render->get_default_render_options();
497 592 /**
@@ -594,8 +689,12 @@
594 689 $render_options[ $key ] = $value;
595 690 }
596 691 }
597 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 +
598 697 // Generate unique HTML ID, depending on how often this table has already been shown on this page.
599 698 if ( ! isset( $this->shown_tables[ $table_id ] ) ) {
600 699 $this->shown_tables[ $table_id ] = array(
601 700 'count' => 0,
@@ -630,9 +729,9 @@
630 729 *
631 730 * @param bool $show Whether to show the "Edit" link below the table. Default true.
632 731 * @param string $table_id The current table ID.
633 732 */
634 - 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'] ) ) {
635 734 $render_options['edit_table_url'] = TablePress::url( array( 'action' => 'edit', 'table_id' => $table['id'] ) );
636 735 }
637 736
638 737 /**
@@ -646,12 +745,16 @@
646 745 * @param array<string, mixed> $table The current table.
647 746 */
648 747 $render_options = apply_filters( 'tablepress_table_render_options', $render_options, $table );
649 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 +
650 753 // Check if table output shall and can be loaded from the transient cache, otherwise generate the output.
651 754 if ( $render_options['cache_table_output'] && ! is_user_logged_in() ) {
652 755 // Hash the Render Options array to get a unique cache identifier.
653 - $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
654 757 $transient_name = 'tablepress_' . $table_hash; // Attention: This string must not be longer than 45 characters!
655 758 $output = get_transient( $transient_name );
656 759 if ( false === $output || '' === $output ) {
657 760 // Render/generate the table HTML, as it was not found in the cache.
@@ -688,13 +791,11 @@
688 791 }
689 792
690 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.
691 794 if ( $render_options['use_datatables']
692 - && $render_options['table_head']
693 - && str_contains( $output, '<thead' ) // A `<thead>` tag is required.
694 - && ! str_contains( $output, ' colspan="' ) // `colspan` attributes are forbidden.
695 - && ! str_contains( $output, ' rowspan="' ) // `rowspan` attributes are forbidden.
696 - ) {
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 + ) {
697 798 // Get options for the DataTables JavaScript library from the table's render options.
698 799 $js_options = array();
699 800 foreach ( array(
700 801 'alternating_row_colors',
@@ -724,10 +825,21 @@
724 825 * @param string $table_id The current table ID.
725 826 * @param array<string, mixed> $render_options The render options for the table.
726 827 */
727 828 $js_options = apply_filters( 'tablepress_table_js_options', $js_options, $table_id, $render_options );
829 +
728 830 $this->shown_tables[ $table_id ]['instances'][ (string) $render_options['html_id'] ] = $js_options;
729 - $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 + }
730 842 }
731 843
732 844 // Maybe print a list of used render options.
733 845 if ( $render_options['shortcode_debug'] && is_user_logged_in() ) {
@@ -853,14 +965,10 @@
853 965 break;
854 966 case 'number_rows':
855 967 $output = count( $table['data'] );
856 968 if ( 'raw' !== $format ) {
857 - if ( $table['options']['table_head'] ) {
858 - --$output;
859 - }
860 - if ( $table['options']['table_foot'] ) {
861 - --$output;
862 - }
969 + $output -= $table['options']['table_head'];
970 + $output -= $table['options']['table_foot'];
863 971 }
864 972 break;
865 973 case 'number_columns':
866 974 $output = count( $table['data'][0] );
@@ -912,9 +1020,9 @@
912 1020
913 1021 global $wpdb;
914 1022
915 1023 // Protect against cases where `null` is somehow passed to the filter hook callback.
916 - 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.)
917 1025 return '';
918 1026 }
919 1027
920 1028 if ( ! is_search() || ! is_main_query() ) {
@@ -946,10 +1054,10 @@
946 1054 continue;
947 1055 }
948 1056
949 1057 foreach ( $search_terms as $search_term ) {
950 - if ( ( $table['options']['print_name'] && false !== stripos( $table['name'], $search_term ) )
951 - || ( $table['options']['print_description'] && false !== stripos( $table['description'], $search_term ) ) ) {
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 ) ) ) {
952 1060 // Found the search term in the name or description (and they are shown).
953 1061 $query_result[ $search_term ][] = $table_id; // Add table ID to result list.
954 1062 // No need to continue searching this search term in this table.
955 1063 continue;
@@ -966,9 +1074,9 @@
966 1074 // Column is hidden, so don't search in it.
967 1075 continue;
968 1076 }
969 1077 // @todo Cells are not evaluated here, so math formulas are searched.
970 - if ( false !== stripos( $table_cell, $search_term ) ) {
1078 + if ( false !== stripos( $table_cell, (string) $search_term ) ) {
971 1079 // Found the search term in the cell content.
972 1080 $query_result[ $search_term ][] = $table_id; // Add table ID to result list
973 1081 // No need to continue searching this search term in this table.
974 1082 continue 3;
@@ -985,9 +1093,9 @@
985 1093 $n = ( empty( $exact ) ) ? '%' : '';
986 1094 $search_sql = $wpdb->remove_placeholder_escape( $search_sql );
987 1095 foreach ( $query_result as $search_term => $table_ids ) {
988 1096 $search_term = esc_sql( $wpdb->esc_like( $search_term ) );
989 - $old_or = "OR ({$wpdb->posts}.post_content LIKE '{$n}{$search_term}{$n}')"; // @phpstan-ignore-line (The esc_sql() call above returns a string, as a string is passed.)
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.)
990 1098 $table_ids = implode( '|', $table_ids );
991 1099 $regexp = '\\\\[' . TablePress::$shortcode . ' id=(["\\\']?)(' . $table_ids . ')([\]"\\\' /])'; // ' needs to be single escaped, [ double escaped (with \\) in mySQL
992 1100 $new_or = $old_or . " OR ({$wpdb->posts}.post_content REGEXP '{$regexp}')";
993 1101 $search_sql = str_replace( $old_or, $new_or, $search_sql );