PluginProbe
TablePress – Tables in WordPress made easy / 3.0.4
TablePress – Tables in WordPress made easy v3.0.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 2.4.4 All 44 releases
← All changes | controllers/controller-frontend.php +306 -150 2.3.23.0.4 View file →
@@ -21,18 +21,47 @@
21 21 */
22 22 class TablePress_Frontend_Controller extends TablePress_Controller {
23 23
24 24 /**
25 + * Whether to use the legacy CSS loading method of enqueuing all CSS files on all pages.
26 + *
27 + * @since 3.0.1
28 + */
29 + public bool $use_legacy_css_loading = false;
30 +
31 + /**
32 + * File name of the admin screens' parent page in the admin menu.
33 + *
34 + * @since 1.0.0
35 + */
36 + public string $parent_page = 'middle';
37 +
38 + /**
39 + * Whether TablePress admin screens are a top-level menu item in the admin menu.
40 + *
41 + * @since 1.0.0
42 + */
43 + public bool $is_top_level_page = false;
44 +
45 + /**
25 46 * List of tables that are shown for the current request.
26 47 *
27 48 * @since 1.0.0
28 49 * @var array<string, array{count: int, instances: array<string, array<string, mixed>>}>
29 50 */
30 - protected $shown_tables = array();
51 + protected array $shown_tables = array();
31 52
32 53 /**
33 - * Initiate Frontend functionality.
54 + * List of registered DataTables datetime formats.
34 55 *
56 + * @since 3.0.0
57 + * @var string[]
58 + */
59 + protected array $datatables_datetime_formats = array();
60 +
61 + /**
62 + * Initiates Frontend functionality.
63 + *
35 64 * @since 1.0.0
36 65 */
37 66 public function __construct() {
38 67 parent::__construct();
@@ -37,20 +66,32 @@
37 66 public function __construct() {
38 67 parent::__construct();
39 68
40 69 /**
41 - * Filters whether the TablePress Default CSS code shall be loaded.
70 + * Filters the admin menu parent page, which is needed for the construction of plugin URLs.
42 71 *
43 72 * @since 1.0.0
44 73 *
45 - * @param bool $use Whether the Default CSS shall be loaded. Default true.
74 + * @param string $parent_page Current admin menu parent page.
46 75 */
47 - if ( apply_filters( 'tablepress_use_default_css', true ) || TablePress::$model_options->get( 'use_custom_css' ) ) {
76 + $this->parent_page = apply_filters( 'tablepress_admin_menu_parent_page', TablePress::$model_options->get( 'admin_menu_parent_page' ) );
77 + $this->is_top_level_page = in_array( $this->parent_page, array( 'top', 'middle', 'bottom' ), true );
78 +
79 + /**
80 + * Filters whether TablePress should load its frontend CSS files on all pages.
81 + * For block themes, the default behavior is to only load the CSS files when a table is encountered on the page.
82 + *
83 + * @since 3.0.1
84 + *
85 + * @param bool $use_legacy_css_loading Whether TablePress should load its frontend CSS files on all pages.
86 + */
87 + $this->use_legacy_css_loading = apply_filters( 'tablepress_frontend_legacy_css_loading', ! wp_is_block_theme() );
88 +
89 + if ( $this->use_legacy_css_loading ) {
48 90 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_css' ) );
49 91 }
50 92
51 - // Add DataTables invocation calls.
52 - add_action( 'wp_print_footer_scripts', array( $this, 'add_datatables_calls' ), 11 ); // After inclusion of files.
93 + add_action( 'wp_print_footer_scripts', array( $this, 'add_datatables_calls' ), 9 ); // Priority 9 so that this runs before `_wp_footer_scripts()`.
53 94
54 95 // Register TablePress Shortcodes. Priority 20 is kept for backwards-compatibility purposes.
55 96 add_action( 'init', array( $this, 'init_shortcodes' ), 20 );
56 97
@@ -73,18 +114,25 @@
73 114
74 115 /**
75 116 * Register the tablepress/table block and its dependencies.
76 117 */
77 - register_block_type(
78 - TABLEPRESS_ABSPATH . 'blocks/table/',
118 + if ( function_exists( 'wp_register_block_metadata_collection' ) ) {
119 + // wp_register_block_metadata_collection() is only available since WP 6.7.
120 + wp_register_block_metadata_collection(
121 + TABLEPRESS_ABSPATH . 'blocks',
122 + TABLEPRESS_ABSPATH . 'blocks/blocks-manifest.php',
123 + );
124 + }
125 + register_block_type_from_metadata(
126 + TABLEPRESS_ABSPATH . 'blocks/table/block.json',
79 127 array(
80 128 'render_callback' => array( $this, 'table_block_render_callback' ),
81 - )
129 + ),
82 130 );
83 131 }
84 132
85 133 /**
86 - * Register TablePress Shortcodes.
134 + * Registers TablePress Shortcodes.
87 135 *
88 136 * @since 1.0.0
89 137 */
90 138 public function init_shortcodes(): void {
@@ -92,18 +140,67 @@
92 140 add_shortcode( TablePress::$shortcode_info, array( $this, 'shortcode_table_info' ) );
93 141 }
94 142
95 143 /**
96 - * Enqueue CSS files for default CSS and "Custom CSS" (if desired).
144 + * Checks if the CSS files for TablePress default CSS and "Custom CSS" should be loaded.
97 145 *
146 + * This function is only called when a [table /] Shortcode or "TablePress Table" block is evaluated, so that CSS files are only loaded when needed.
147 + *
148 + * @since 3.0.0
149 + */
150 + public function maybe_enqueue_css(): void {
151 + // Bail early if the legacy CSS loading mechanism is used, as the files will then have been enqueued already.
152 + if ( $this->use_legacy_css_loading && ! doing_action( 'enqueue_block_assets' ) ) {
153 + return;
154 + }
155 +
156 + /*
157 + * Bail early if the function is called from some action hook outside of the normal rendering process.
158 + * 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.
159 + * In these cases, we don't want to enqueue the CSS, as it would likely not be printed on the page.
160 + */
161 + if ( doing_action( 'wp_head' ) || doing_action( 'wp_footer' ) ) {
162 + return;
163 + }
164 +
165 + // Prevent repeated execution via a static variable.
166 + static $css_enqueued = false;
167 + if ( $css_enqueued && ! doing_action( 'enqueue_block_assets' ) ) {
168 + return;
169 + }
170 + $css_enqueued = true;
171 +
172 + $this->enqueue_css();
173 + }
174 +
175 + /**
176 + * Enqueues CSS files for TablePress default CSS and "Custom CSS" (if desired).
177 + *
178 + * If styles have not been printed to the page (in the `<head>`), the TablePress CSS files will be enqueued.
179 + * If styles have already been printed to the page, the TablePress CSS files will be printed right away (likely in the `<body`>).
180 + *
98 181 * @since 1.0.0
99 182 */
100 183 public function enqueue_css(): void {
101 - /** This filter is documented in controllers/controller-frontend.php */
184 + /**
185 + * Filters whether the TablePress Default CSS code shall be loaded.
186 + *
187 + * @since 1.0.0
188 + *
189 + * @param bool $use Whether the Default CSS shall be loaded. Default true.
190 + */
102 191 $use_default_css = apply_filters( 'tablepress_use_default_css', true );
192 + $use_custom_css = TablePress::$model_options->get( 'use_custom_css' );
193 +
194 + if ( ! $use_default_css && ! $use_custom_css ) {
195 + // Register a placeholder dependency, so that the handle is known for other styles.
196 + wp_register_style( 'tablepress-default', false ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
197 + return;
198 + }
199 +
103 200 $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' ) );
201 + $use_custom_css = $use_custom_css && '' !== $custom_css;
202 + $use_custom_css_file = $use_custom_css && TablePress::$model_options->get( 'use_custom_css_file' );
106 203 /**
107 204 * Filters the "Custom CSS" version number that is appended to the enqueued CSS files
108 205 *
109 206 * @since 1.0.0
@@ -131,81 +228,71 @@
131 228 if ( $use_custom_css_combined_file ) {
132 229 $custom_css_combined_url = $tablepress_css->get_custom_css_location( 'combined', 'url' );
133 230 // Need to use 'tablepress-default' instead of 'tablepress-combined' to not break existing TablePress Extensions.
134 231 wp_enqueue_style( 'tablepress-default', $custom_css_combined_url, array(), $custom_css_version );
232 + if ( did_action( 'wp_print_styles' ) ) {
233 + wp_print_styles( 'tablepress-default' );
234 + }
235 + return;
236 + }
237 +
238 + if ( $use_default_css ) {
239 + wp_enqueue_style( 'tablepress-default', $default_css_url, array(), TablePress::version );
135 240 } 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';
241 + // Register a placeholder dependency, so that the handle is known for other styles.
242 + wp_register_style( 'tablepress-default', false ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
243 + }
244 +
245 + $use_custom_css_minified_file = ( $use_custom_css_file && ! SCRIPT_DEBUG && $tablepress_css->load_custom_css_from_file( 'minified' ) );
246 + if ( $use_custom_css_minified_file ) {
247 + $custom_css_minified_url = $tablepress_css->get_custom_css_location( 'minified', 'url' );
248 + wp_enqueue_style( 'tablepress-custom', $custom_css_minified_url, array( 'tablepress-default' ), $custom_css_version );
249 + if ( did_action( 'wp_print_styles' ) ) {
250 + wp_print_styles( 'tablepress-custom' );
141 251 }
252 + return;
253 + }
142 254
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;
255 + $use_custom_css_normal_file = ( $use_custom_css_file && $tablepress_css->load_custom_css_from_file( 'normal' ) );
256 + if ( $use_custom_css_normal_file ) {
257 + $custom_css_normal_url = $tablepress_css->get_custom_css_location( 'normal', 'url' );
258 + wp_enqueue_style( 'tablepress-custom', $custom_css_normal_url, array( 'tablepress-default' ), $custom_css_version );
259 + if ( did_action( 'wp_print_styles' ) ) {
260 + wp_print_styles( 'tablepress-custom' );
148 261 }
262 + return;
263 + }
149 264
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;
265 + if ( $use_custom_css ) {
266 + // Get "Custom CSS" from options, try minified Custom CSS first.
267 + $custom_css_minified = TablePress::$model_options->get( 'custom_css_minified' );
268 + if ( ! empty( $custom_css_minified ) ) {
269 + $custom_css = $custom_css_minified;
155 270 }
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;
271 + /**
272 + * Filters the "Custom CSS" code that is to be loaded as inline CSS.
273 + *
274 + * @since 1.0.0
275 + *
276 + * @param string $custom_css The "Custom CSS" code.
277 + */
278 + $custom_css = apply_filters( 'tablepress_custom_css', $custom_css );
279 + if ( ! empty( $custom_css ) ) {
280 + wp_add_inline_style( 'tablepress-default', $custom_css );
281 + if ( did_action( 'wp_print_styles' ) ) {
282 + wp_print_styles( 'tablepress-default' );
162 283 }
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 - }
284 + return;
180 285 }
181 286 }
182 287 }
183 288
184 289 /**
185 - * Print "Custom CSS" to "wp_head" inline.
290 + * Enqueues the DataTables JavaScript library and its dependencies.
186 291 *
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
292 + * @since 3.0.0
190 293 */
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 {
294 + protected function enqueue_datatables_files(): void {
208 295 $js_file = 'js/jquery.datatables.min.js';
209 296 $js_url = plugins_url( $js_file, TABLEPRESS__FILE__ );
210 297 /**
211 298 * Filters the URL from which the DataTables JavaScript library file is loaded.
@@ -215,13 +302,27 @@
215 302 * @param string $js_url URL of the DataTables JS library file.
216 303 * @param string $js_file Path and file name of the DataTables JS library file.
217 304 */
218 305 $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 );
306 +
307 + $dependencies = array( 'jquery-core' );
308 + if ( ! empty( $this->datatables_datetime_formats ) ) {
309 + $dependencies[] = 'moment';
310 + }
311 + /**
312 + * Filters the dependencies for the DataTables JavaScript library.
313 + *
314 + * @since 3.0.0
315 + *
316 + * @param string[] $dependencies The dependencies for the DataTables JS library.
317 + */
318 + $dependencies = apply_filters( 'tablepress_datatables_js_dependencies', $dependencies );
319 +
320 + wp_enqueue_script( 'tablepress-datatables', $js_url, $dependencies, TablePress::version, true );
220 321 }
221 322
222 323 /**
223 - * Add JS code for invocation of DataTables JS library.
324 + * Adds the JavaScript code for the invocation of the DataTables JS library.
224 325 *
225 326 * @since 1.0.0
226 327 */
227 328 public function add_datatables_calls(): void {
@@ -230,16 +331,16 @@
230 331 if ( $datatables_calls_printed ) {
231 332 return;
232 333 }
233 334
335 + // Bail early if there are no TablePress tables on the page.
234 336 if ( empty( $this->shown_tables ) ) {
235 - // There are no tables with activated DataTables on the page that is currently rendered.
236 337 return;
237 338 }
238 339
239 340 /*
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.
341 + * Don't add the DataTables function calls in the scope of the block editor iframe.
342 + * This is necessary for non-block themes, for others, the repeated execution check above is sufficient.
242 343 */
243 344 if ( function_exists( 'get_current_screen' ) ) {
244 345 $current_screen = get_current_screen();
245 346 if ( ( $current_screen instanceof WP_Screen ) && $current_screen->is_block_editor() ) {
@@ -246,20 +347,31 @@
246 347 return;
247 348 }
248 349 }
249 350
351 + // Filter out all tables that use DataTables.
352 + $shown_tables_with_datatables = array();
353 + foreach ( $this->shown_tables as $table_id => $table_store ) {
354 + if ( ! empty( $table_store['instances'] ) ) {
355 + $shown_tables_with_datatables[ (string) $table_id ] = $table_store;
356 + }
357 + }
358 +
359 + // Bail early if there are no tables with activated DataTables on the page.
360 + if ( empty( $shown_tables_with_datatables ) ) {
361 + return;
362 + }
363 +
364 + $this->enqueue_datatables_files();
365 +
250 366 // Storage for the DataTables language strings.
251 367 $datatables_language = array();
252 368 // Generate the specific JS commands, depending on chosen features on the "Edit" screen and the Shortcode parameters.
253 369 $commands = array();
254 370
255 - foreach ( $this->shown_tables as $table_id => $table_store ) {
371 + foreach ( $shown_tables_with_datatables as $table_id => $table_store ) {
256 372 $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.
257 373
258 - if ( empty( $table_store['instances'] ) ) {
259 - continue;
260 - }
261 -
262 374 foreach ( $table_store['instances'] as $html_id => $js_options ) {
263 375 $parameters = array();
264 376
265 377 // Settle dependencies/conflicts between certain features.
@@ -314,9 +426,9 @@
314 426 $datatables_strings = array();
315 427 }
316 428 } elseif ( str_ends_with( $language_file, '.json' ) ) {
317 429 $datatables_strings = file_get_contents( $language_file );
318 - $datatables_strings = json_decode( $datatables_strings, true ); // @phpstan-ignore-line
430 + $datatables_strings = json_decode( $datatables_strings, true ); // @phpstan-ignore argument.type
319 431 // Check if JSON could be decoded.
320 432 if ( is_null( $datatables_strings ) ) {
321 433 $datatables_strings = array();
322 434 }
@@ -340,58 +452,56 @@
340 452 * @param string $datatables_locale Current locale/language for the DataTables JS library.
341 453 */
342 454 $datatables_language[ $datatables_locale ] = apply_filters( 'tablepress_datatables_language_strings', $datatables_strings, $datatables_locale );
343 455 }
344 - $parameters['language'] = '"language":DT_language["' . $datatables_locale . '"]';
456 + $parameters['language'] = "language:DT_language['{$datatables_locale}']";
345 457
346 458 // These parameters need to be added for performance gain or to overwrite unwanted default behavior.
347 459 if ( $js_options['datatables_sort'] ) {
348 460 // No initial sort.
349 - $parameters['order'] = '"order":[]';
461 + $parameters['order'] = 'order:[]';
350 462 // Don't add additional classes, to speed up sorting.
351 - $parameters['orderClasses'] = '"orderClasses":false';
463 + $parameters['orderClasses'] = 'orderClasses:false';
352 464 }
353 465
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 466 // 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 467 if ( ! $js_options['datatables_sort'] ) {
359 - $parameters['ordering'] = '"ordering":false';
468 + $parameters['ordering'] = 'ordering:false';
360 469 }
361 470 if ( $js_options['datatables_paginate'] ) {
362 - $parameters['pagingType'] = '"pagingType":"simple"';
471 + $parameters['pagingType'] = "pagingType:'simple_numbers'";
363 472 if ( $js_options['datatables_lengthchange'] ) {
364 473 $length_menu = array( 10, 25, 50, 100 );
365 474 if ( ! in_array( $js_options['datatables_paginate_entries'], $length_menu, true ) ) {
366 475 $length_menu[] = $js_options['datatables_paginate_entries'];
367 476 sort( $length_menu, SORT_NUMERIC );
368 - $parameters['lengthMenu'] = '"lengthMenu":[' . implode( ',', $length_menu ) . ']';
477 + $parameters['lengthMenu'] = 'lengthMenu:[' . implode( ',', $length_menu ) . ']';
369 478 }
370 479 } else {
371 - $parameters['lengthChange'] = '"lengthChange":false';
480 + $parameters['lengthChange'] = 'lengthChange:false';
372 481 }
373 482 if ( 10 !== $js_options['datatables_paginate_entries'] ) {
374 - $parameters['pageLength'] = '"pageLength":' . $js_options['datatables_paginate_entries'];
483 + $parameters['pageLength'] = "pageLength:{$js_options['datatables_paginate_entries']}";
375 484 }
376 485 } else {
377 - $parameters['paging'] = '"paging":false';
486 + $parameters['paging'] = 'paging:false';
378 487 }
379 488 if ( ! $js_options['datatables_filter'] ) {
380 - $parameters['searching'] = '"searching":false';
489 + $parameters['searching'] = 'searching:false';
381 490 }
382 491 if ( ! $js_options['datatables_info'] ) {
383 - $parameters['info'] = '"info":false';
492 + $parameters['info'] = 'info:false';
384 493 }
385 494 if ( $js_options['datatables_scrollx'] ) {
386 - $parameters['scrollX'] = '"scrollX":true';
495 + $parameters['scrollX'] = 'scrollX:true';
387 496 }
388 497 if ( false !== $js_options['datatables_scrolly'] ) {
389 - $parameters['scrollY'] = '"scrollY":"' . preg_replace( '#[^0-9a-z.%]#', '', $js_options['datatables_scrolly'] ) . '"';
390 - $parameters['scrollCollapse'] = '"scrollCollapse":true';
498 + $parameters['scrollY'] = 'scrollY:"' . preg_replace( '#[^0-9a-z.%]#', '', $js_options['datatables_scrolly'] ) . '"';
499 + $parameters['scrollCollapse'] = 'scrollCollapse:true';
391 500 }
392 - if ( ! empty( $js_options['datatables_custom_commands'] ) ) {
393 - $parameters['custom_commands'] = $js_options['datatables_custom_commands'];
501 + if ( '' !== $js_options['datatables_custom_commands'] ) {
502 + $parameters['custom_commands'] = trim( $js_options['datatables_custom_commands'] ); // Remove leading and trailing whitespace.
503 + $parameters['custom_commands'] = trim( $parameters['custom_commands'], ',' ); // Remove potentially leading and trailing commas to prevent JS script errors.
394 504 }
395 505
396 506 /**
397 507 * Filters the parameters that are passed to the DataTables JavaScript library.
@@ -404,21 +514,22 @@
404 514 * @param array<string, mixed> $js_options The options for the JS library.
405 515 */
406 516 $parameters = apply_filters( 'tablepress_datatables_parameters', $parameters, $table_id, $html_id, $js_options );
407 517
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 - }
518 + // If an existing parameter is set as an object key in the "Custom Commands", remove its separate value, to allow for full overrides.
519 + if ( isset( $parameters['custom_commands'] ) && '' !== $parameters['custom_commands'] ) {
520 + $parameters_in_custom_commands = TablePress::extract_keys_from_js_object_string( '{' . $parameters['custom_commands'] . '}' );
521 + foreach ( $parameters_in_custom_commands as $parameter_in_custom_commands ) {
522 + unset( $parameters[ $parameter_in_custom_commands ] );
414 523 }
415 524 }
416 525
526 + $name = substr( $html_id, 11 ); // Remove "tablepress-" from the HTML ID.
527 + $name = "DT_TP['" . str_replace( '-', '_', $name ) . "']";
417 528 $parameters = implode( ',', $parameters );
418 529 $parameters = ( ! empty( $parameters ) ) ? '{' . $parameters . '}' : '';
419 530
420 - $command = "$('#{$html_id}').DataTable({$parameters});";
531 + $command = "{$name} = new DataTable('#{$html_id}',{$parameters});";
421 532 /**
422 533 * Filters the JavaScript command that invokes the DataTables JavaScript library on one table.
423 534 *
424 535 * @since 1.0.0
@@ -427,10 +538,11 @@
427 538 * @param string $html_id The ID of the table HTML element.
428 539 * @param string $parameters The parameters for the DataTables JS library.
429 540 * @param string $table_id The current table ID.
430 541 * @param array<string, mixed> $js_options The options for the JS library.
542 + * @param string $name The name of the DataTable instance.
431 543 */
432 - $command = apply_filters( 'tablepress_datatables_command', $command, $html_id, $parameters, $table_id, $js_options );
544 + $command = apply_filters( 'tablepress_datatables_command', $command, $html_id, $parameters, $table_id, $js_options, $name );
433 545 if ( ! empty( $command ) ) {
434 546 $commands[] = $command;
435 547 }
436 548 } // foreach table instance
@@ -437,14 +549,40 @@
437 549 } // foreach table ID
438 550
439 551 // DataTables language/translation handling.
440 552 if ( ! empty( $datatables_language ) ) {
441 - $datatables_language = wp_json_encode( $datatables_language, JSON_UNESCAPED_UNICODE | JSON_FORCE_OBJECT );
442 - $datatables_language = "var DT_language={$datatables_language};\n";
553 + $datatables_language_command = wp_json_encode( $datatables_language, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_FORCE_OBJECT );
554 + $datatables_language_command = "var DT_language={$datatables_language_command};\n";
443 555 } else {
444 - $datatables_language = '';
556 + $datatables_language_command = '';
445 557 }
446 558
559 + // DataTables datetime format string handling.
560 + if ( ! empty( $this->datatables_datetime_formats ) ) {
561 + // Create a command like `DataTable.datetime('MM/DD/YYYY');DataTable.datetime('DD.MM.YYYY');`.
562 + $datatables_datetime_command = implode(
563 + '',
564 + array_map(
565 + static fn( string $datetime_format ): string => "DataTable.datetime('{$datetime_format}');",
566 + $this->datatables_datetime_formats,
567 + )
568 + ) . "\n";
569 + } else {
570 + $datatables_datetime_command = '';
571 + }
572 +
573 + /**
574 + * Filters the JavaScript code for the DataTables JavaScript library that initializes the automatically detected date/time formats via moment.js.
575 + *
576 + * @since 3.0.0
577 + *
578 + * @param string $datatables_datetime_command The JS code for the DataTables JS library that initializes the date/time formats.
579 + * @param string[] $datatables_datetime_formats The date/time formats for moment.js.
580 + */
581 + $datatables_datetime_command = apply_filters( 'tablepress_datatables_datetime_command', $datatables_datetime_command, $this->datatables_datetime_formats );
582 +
583 + $datatables_pre_commands = $datatables_language_command . $datatables_datetime_command;
584 +
447 585 $commands = implode( "\n", $commands );
448 586 /**
449 587 * Filters the JavaScript commands that invoke the DataTables JavaScript library on all tables on the page.
450 588 *
@@ -456,33 +594,32 @@
456 594 if ( '' === $commands ) {
457 595 return;
458 596 }
459 597
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;
598 + $script_template = <<<'JS'
599 + var DT_TP = {};
600 + jQuery(($)=>{
601 + %1$s%2$s
602 + });
603 + JS;
469 604 /**
470 605 * Filters the script/jQuery wrapper code for the DataTables commands calls.
471 606 *
472 607 * @since 1.14.0
473 608 *
474 - * @param string $js_wrapper Default script/jQuery wrapper code for the DataTables commands calls.
609 + * @param string $script_template Default script/jQuery wrapper code for the DataTables commands calls.
475 610 */
476 - $js_wrapper = apply_filters( 'tablepress_all_datatables_commands_wrapper', $js_wrapper );
477 - printf( $js_wrapper, $datatables_language, $commands, $script_type_attr );
611 + $script_template = apply_filters( 'tablepress_all_datatables_commands_wrapper', $script_template );
478 612
613 + $script = sprintf( $script_template, $datatables_pre_commands, $commands );
614 + wp_add_inline_script( 'tablepress-datatables', $script );
615 +
479 616 // Prevent repeated execution (which would lead to DataTables error messages) via a static variable.
480 617 $datatables_calls_printed = true;
481 618 }
482 619
483 620 /**
484 - * Handle Shortcode [table id=<ID> /].
621 + * Handles the Shortcode [table id=<ID> /].
485 622 *
486 623 * @since 1.0.0
487 624 *
488 625 * @param array<string, mixed>|string $shortcode_atts List of attributes that where included in the Shortcode. An empty string for empty Shortcodes like [table] or [table /].
@@ -490,8 +627,10 @@
490 627 */
491 628 public function shortcode_table( /* array|string */ $shortcode_atts ): string {
492 629 $shortcode_atts = (array) $shortcode_atts;
493 630
631 + $this->maybe_enqueue_css();
632 +
494 633 $_render = TablePress::load_class( 'TablePress_Render', 'class-render.php', 'classes' );
495 634
496 635 $default_shortcode_atts = $_render->get_default_render_options();
497 636 /**
@@ -594,8 +733,12 @@
594 733 $render_options[ $key ] = $value;
595 734 }
596 735 }
597 736
737 + // Backward compatibility: Convert boolean or numeric string "table_head" and "table_foot" options to integer.
738 + $render_options['table_head'] = absint( $render_options['table_head'] );
739 + $render_options['table_foot'] = absint( $render_options['table_foot'] );
740 +
598 741 // Generate unique HTML ID, depending on how often this table has already been shown on this page.
599 742 if ( ! isset( $this->shown_tables[ $table_id ] ) ) {
600 743 $this->shown_tables[ $table_id ] = array(
601 744 'count' => 0,
@@ -630,9 +773,9 @@
630 773 *
631 774 * @param bool $show Whether to show the "Edit" link below the table. Default true.
632 775 * @param string $table_id The current table ID.
633 776 */
634 - if ( is_user_logged_in() && apply_filters( 'tablepress_edit_link_below_table', true, $table['id'] ) && current_user_can( 'tablepress_edit_table', $table['id'] ) ) {
777 + 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 778 $render_options['edit_table_url'] = TablePress::url( array( 'action' => 'edit', 'table_id' => $table['id'] ) );
636 779 }
637 780
638 781 /**
@@ -646,12 +789,16 @@
646 789 * @param array<string, mixed> $table The current table.
647 790 */
648 791 $render_options = apply_filters( 'tablepress_table_render_options', $render_options, $table );
649 792
793 + // Backward compatibility: Convert boolean "table_head" and "table_foot" options to integer, in case they were overwritten via the filter hook.
794 + $render_options['table_head'] = absint( $render_options['table_head'] );
795 + $render_options['table_foot'] = absint( $render_options['table_foot'] );
796 +
650 797 // Check if table output shall and can be loaded from the transient cache, otherwise generate the output.
651 798 if ( $render_options['cache_table_output'] && ! is_user_logged_in() ) {
652 799 // 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
800 + $table_hash = md5( wp_json_encode( $render_options, TABLEPRESS_JSON_OPTIONS ) ); // @phpstan-ignore argument.type
654 801 $transient_name = 'tablepress_' . $table_hash; // Attention: This string must not be longer than 45 characters!
655 802 $output = get_transient( $transient_name );
656 803 if ( false === $output || '' === $output ) {
657 804 // Render/generate the table HTML, as it was not found in the cache.
@@ -688,13 +835,11 @@
688 835 }
689 836
690 837 // 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 838 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 - ) {
839 + && 0 < $render_options['table_head']
840 + && ! 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>`.
841 + ) {
697 842 // Get options for the DataTables JavaScript library from the table's render options.
698 843 $js_options = array();
699 844 foreach ( array(
700 845 'alternating_row_colors',
@@ -724,10 +869,21 @@
724 869 * @param string $table_id The current table ID.
725 870 * @param array<string, mixed> $render_options The render options for the table.
726 871 */
727 872 $js_options = apply_filters( 'tablepress_table_js_options', $js_options, $table_id, $render_options );
873 +
728 874 $this->shown_tables[ $table_id ]['instances'][ (string) $render_options['html_id'] ] = $js_options;
729 - $this->_enqueue_datatables();
875 +
876 + // DataTables datetime format string handling.
877 + if ( '' !== $render_options['datatables_datetime'] ) {
878 + $render_options['datatables_datetime'] = explode( '|', $render_options['datatables_datetime'] );
879 + foreach ( $render_options['datatables_datetime'] as $datetime_format ) {
880 + $datetime_format = trim( $datetime_format );
881 + if ( '' !== $datetime_format && ! in_array( $datetime_format, $this->datatables_datetime_formats, true ) ) {
882 + $this->datatables_datetime_formats[] = $datetime_format;
883 + }
884 + }
885 + }
730 886 }
731 887
732 888 // Maybe print a list of used render options.
733 889 if ( $render_options['shortcode_debug'] && is_user_logged_in() ) {
@@ -737,9 +893,9 @@
737 893 return $output;
738 894 }
739 895
740 896 /**
741 - * Handle Shortcode [table-info id=<ID> field=<name> /].
897 + * Handles the Shortcode [table-info id=<ID> field=<name> /].
742 898 *
743 899 * @since 1.0.0
744 900 *
745 901 * @param array<string, mixed>|string $shortcode_atts List of attributes that where included in the Shortcode. An empty string for empty Shortcodes like [table] or [table /].
@@ -819,9 +975,13 @@
819 975 $output = $table['last_modified'];
820 976 break;
821 977 case 'human':
822 978 $modified_timestamp = date_create( $table['last_modified'], wp_timezone() );
823 - $modified_timestamp = $modified_timestamp->getTimestamp(); // @phpstan-ignore-line
979 + if ( false === $modified_timestamp ) {
980 + $modified_timestamp = $table['last_modified'];
981 + } else {
982 + $modified_timestamp = $modified_timestamp->getTimestamp();
983 + }
824 984 $current_timestamp = time();
825 985 $time_diff = $current_timestamp - $modified_timestamp;
826 986 // Time difference is only shown up to one week.
827 987 if ( $time_diff >= 0 && $time_diff < WEEK_IN_SECONDS ) {
@@ -849,14 +1009,10 @@
849 1009 break;
850 1010 case 'number_rows':
851 1011 $output = count( $table['data'] );
852 1012 if ( 'raw' !== $format ) {
853 - if ( $table['options']['table_head'] ) {
854 - --$output;
855 - }
856 - if ( $table['options']['table_foot'] ) {
857 - --$output;
858 - }
1013 + $output -= $table['options']['table_head'];
1014 + $output -= $table['options']['table_foot'];
859 1015 }
860 1016 break;
861 1017 case 'number_columns':
862 1018 $output = count( $table['data'][0] );
@@ -889,9 +1045,9 @@
889 1045 return $output;
890 1046 }
891 1047
892 1048 /**
893 - * Expand WP Search to also find posts and pages that have a search term in a table that is shown in them.
1049 + * Expands the WP Search to also find posts and pages that have a search term in a table that is shown in them.
894 1050 *
895 1051 * This is done by looping through all search terms and TablePress tables and searching there for the search term,
896 1052 * saving all tables's IDs that have a search term and then expanding the WP query to search for posts or pages that have the
897 1053 * Shortcode for one of these tables in their content.
@@ -908,9 +1064,9 @@
908 1064
909 1065 global $wpdb;
910 1066
911 1067 // Protect against cases where `null` is somehow passed to the filter hook callback.
912 - if ( ! is_string( $search_sql ) ) {
1068 + if ( ! is_string( $search_sql ) ) { // @phpstan-ignore function.alreadyNarrowedType (The `is_string()` check is needed as the input is coming from a filter hook.)
913 1069 return '';
914 1070 }
915 1071
916 1072 if ( ! is_search() || ! is_main_query() ) {
@@ -942,10 +1098,10 @@
942 1098 continue;
943 1099 }
944 1100
945 1101 foreach ( $search_terms as $search_term ) {
946 - if ( ( $table['options']['print_name'] && false !== stripos( $table['name'], $search_term ) )
947 - || ( $table['options']['print_description'] && false !== stripos( $table['description'], $search_term ) ) ) {
1102 + if ( ( $table['options']['print_name'] && false !== stripos( $table['name'], (string) $search_term ) )
1103 + || ( $table['options']['print_description'] && false !== stripos( $table['description'], (string) $search_term ) ) ) {
948 1104 // Found the search term in the name or description (and they are shown).
949 1105 $query_result[ $search_term ][] = $table_id; // Add table ID to result list.
950 1106 // No need to continue searching this search term in this table.
951 1107 continue;
@@ -962,9 +1118,9 @@
962 1118 // Column is hidden, so don't search in it.
963 1119 continue;
964 1120 }
965 1121 // @todo Cells are not evaluated here, so math formulas are searched.
966 - if ( false !== stripos( $table_cell, $search_term ) ) {
1122 + if ( false !== stripos( $table_cell, (string) $search_term ) ) {
967 1123 // Found the search term in the cell content.
968 1124 $query_result[ $search_term ][] = $table_id; // Add table ID to result list
969 1125 // No need to continue searching this search term in this table.
970 1126 continue 3;
@@ -981,9 +1137,9 @@
981 1137 $n = ( empty( $exact ) ) ? '%' : '';
982 1138 $search_sql = $wpdb->remove_placeholder_escape( $search_sql );
983 1139 foreach ( $query_result as $search_term => $table_ids ) {
984 1140 $search_term = esc_sql( $wpdb->esc_like( $search_term ) );
985 - $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.)
1141 + $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.)
986 1142 $table_ids = implode( '|', $table_ids );
987 1143 $regexp = '\\\\[' . TablePress::$shortcode . ' id=(["\\\']?)(' . $table_ids . ')([\]"\\\' /])'; // ' needs to be single escaped, [ double escaped (with \\) in mySQL
988 1144 $new_or = $old_or . " OR ({$wpdb->posts}.post_content REGEXP '{$regexp}')";
989 1145 $search_sql = str_replace( $old_or, $new_or, $search_sql );