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 +576 -302 1.123.0.4 View file →
@@ -12,8 +12,9 @@
12 12 defined( 'ABSPATH' ) || die( 'No direct script access allowed!' );
13 13
14 14 /**
15 15 * Frontend Controller class, extends Base Controller Class
16 + *
16 17 * @package TablePress
17 18 * @subpackage Controllers
18 19 * @author Tobias Bäthge
19 20 * @since 1.0.0
@@ -20,18 +21,47 @@
20 21 */
21 22 class TablePress_Frontend_Controller extends TablePress_Controller {
22 23
23 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 + /**
24 46 * List of tables that are shown for the current request.
25 47 *
26 48 * @since 1.0.0
27 - * @var array
49 + * @var array<string, array{count: int, instances: array<string, array<string, mixed>>}>
28 50 */
29 - protected $shown_tables = array();
51 + protected array $shown_tables = array();
30 52
31 53 /**
32 - * Initiate Frontend functionality.
54 + * List of registered DataTables datetime formats.
33 55 *
56 + * @since 3.0.0
57 + * @var string[]
58 + */
59 + protected array $datatables_datetime_formats = array();
60 +
61 + /**
62 + * Initiates Frontend functionality.
63 + *
34 64 * @since 1.0.0
35 65 */
36 66 public function __construct() {
37 67 parent::__construct();
@@ -36,26 +66,38 @@
36 66 public function __construct() {
37 67 parent::__construct();
38 68
39 69 /**
40 - * Filter 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.
41 71 *
42 72 * @since 1.0.0
43 73 *
44 - * @param bool $use Whether the Default CSS shall be loaded. Default true.
74 + * @param string $parent_page Current admin menu parent page.
45 75 */
46 - 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 ) {
47 90 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_css' ) );
48 91 }
49 92
50 - // Add DataTables invocation calls.
51 - 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()`.
52 94
53 - // Remove WP-Table Reloaded Shortcodes and CSS, and add TablePress Shortcodes.
54 - add_action( 'init', array( $this, 'init_shortcodes' ), 20 ); // run on priority 20 as WP-Table Reloaded Shortcodes are registered at priority 10
95 + // Register TablePress Shortcodes. Priority 20 is kept for backwards-compatibility purposes.
96 + add_action( 'init', array( $this, 'init_shortcodes' ), 20 );
55 97
56 98 /**
57 - * Filter whether the WordPress search shall also search TablePress tables.
99 + * Filters whether the WordPress search shall also search TablePress tables.
58 100 *
59 101 * @since 1.0.0
60 102 *
61 103 * @param bool $search Whether the TablePress tables shall be searched. Default true.
@@ -67,57 +109,114 @@
67 109
68 110 /**
69 111 * Load TablePress Template Tag functions.
70 112 */
71 - require_once TABLEPRESS_ABSPATH . 'controllers/template-tag-functions.php';
113 + TablePress::load_file( 'template-tag-functions.php', 'controllers' );
114 +
115 + /**
116 + * Register the tablepress/table block and its dependencies.
117 + */
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',
127 + array(
128 + 'render_callback' => array( $this, 'table_block_render_callback' ),
129 + ),
130 + );
72 131 }
73 132
74 133 /**
75 - * Register TablePress Shortcodes, after removing WP-Table Reloaded Shortcodes.
134 + * Registers TablePress Shortcodes.
76 135 *
77 136 * @since 1.0.0
78 137 */
79 - public function init_shortcodes() {
80 - // Remove previously registered [table /] Shortcodes (e.g. from WP-Table Reloaded), as these would otherwise be used instead of TablePress's Shortcodes.
81 - remove_shortcode( TablePress::$shortcode );
82 - remove_shortcode( TablePress::$shortcode_info );
83 - // Dequeue WP-Table Relaoded Default CSS, as it can influence TablePress table styling.
84 - if ( isset( $GLOBALS['WP_Table_Reloaded_Frontend'] ) ) {
85 - remove_action( 'wp_head', array( $GLOBALS['WP_Table_Reloaded_Frontend'], 'add_frontend_css' ) );
138 + public function init_shortcodes(): void {
139 + add_shortcode( TablePress::$shortcode, array( $this, 'shortcode_table' ) );
140 + add_shortcode( TablePress::$shortcode_info, array( $this, 'shortcode_table_info' ) );
141 + }
142 +
143 + /**
144 + * Checks if the CSS files for TablePress default CSS and "Custom CSS" should be loaded.
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;
86 154 }
87 155
88 - add_shortcode( TablePress::$shortcode, array( $this, 'shortcode_table' ) );
89 - add_shortcode( TablePress::$shortcode_info, array( $this, 'shortcode_table_info' ) );
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();
90 173 }
91 174
92 175 /**
93 - * Enqueue CSS files for default CSS and "Custom CSS" (if desired).
176 + * Enqueues CSS files for TablePress default CSS and "Custom CSS" (if desired).
94 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 + *
95 181 * @since 1.0.0
96 182 */
97 - public function enqueue_css() {
98 - /** This filter is documented in controllers/controller-frontend.php */
183 + public function enqueue_css(): void {
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 + */
99 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 +
100 200 $custom_css = TablePress::$model_options->get( 'custom_css' );
101 - $use_custom_css = ( TablePress::$model_options->get( 'use_custom_css' ) && '' !== $custom_css );
102 - $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' );
103 203 /**
104 - * Filter the "Custom CSS" version number that is appended to the enqueued CSS files
204 + * Filters the "Custom CSS" version number that is appended to the enqueued CSS files
105 205 *
106 206 * @since 1.0.0
107 207 *
108 208 * @param int $version The "Custom CSS" version.
109 209 */
110 - $custom_css_version = apply_filters( 'tablepress_custom_css_version', TablePress::$model_options->get( 'custom_css_version' ) );
210 + $custom_css_version = (string) apply_filters( 'tablepress_custom_css_version', TablePress::$model_options->get( 'custom_css_version' ) );
111 211
112 212 $tablepress_css = TablePress::load_class( 'TablePress_CSS', 'class-css.php', 'classes' );
113 213
114 214 // Determine Default CSS URL.
115 215 $rtl = ( is_rtl() ) ? '-rtl' : '';
116 - $suffix = SCRIPT_DEBUG ? '' : '.min';
117 - $unfiltered_default_css_url = plugins_url( "css/default{$rtl}{$suffix}.css", TABLEPRESS__FILE__ );
216 + $unfiltered_default_css_url = plugins_url( "css/build/default{$rtl}.css", TABLEPRESS__FILE__ );
118 217 /**
119 - * Filter the URL from which the TablePress Default CSS file is loaded.
218 + * Filters the URL from which the TablePress Default CSS file is loaded.
120 219 *
121 220 * @since 1.0.0
122 221 *
123 222 * @param string $unfiltered_default_css_url URL of the TablePress Default CSS file.
@@ -129,85 +228,75 @@
129 228 if ( $use_custom_css_combined_file ) {
130 229 $custom_css_combined_url = $tablepress_css->get_custom_css_location( 'combined', 'url' );
131 230 // Need to use 'tablepress-default' instead of 'tablepress-combined' to not break existing TablePress Extensions.
132 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 );
133 240 } else {
134 - $custom_css_dependencies = array();
135 - if ( $use_default_css ) {
136 - wp_enqueue_style( 'tablepress-default', $default_css_url, array(), TablePress::version );
137 - // Add dependency to make sure that Custom CSS is printed after Default CSS.
138 - $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' );
139 251 }
252 + return;
253 + }
140 254
141 - $use_custom_css_minified_file = ( $use_custom_css_file && ! SCRIPT_DEBUG && $tablepress_css->load_custom_css_from_file( 'minified' ) );
142 - if ( $use_custom_css_minified_file ) {
143 - $custom_css_minified_url = $tablepress_css->get_custom_css_location( 'minified', 'url' );
144 - wp_enqueue_style( 'tablepress-custom', $custom_css_minified_url, $custom_css_dependencies, $custom_css_version );
145 - 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' );
146 261 }
262 + return;
263 + }
147 264
148 - $use_custom_css_normal_file = ( $use_custom_css_file && $tablepress_css->load_custom_css_from_file( 'normal' ) );
149 - if ( $use_custom_css_normal_file ) {
150 - $custom_css_normal_url = $tablepress_css->get_custom_css_location( 'normal', 'url' );
151 - wp_enqueue_style( 'tablepress-custom', $custom_css_normal_url, $custom_css_dependencies, $custom_css_version );
152 - 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;
153 270 }
154 -
155 - if ( $use_custom_css ) {
156 - // Get "Custom CSS" from options, try minified Custom CSS first,
157 - $custom_css_minified = TablePress::$model_options->get( 'custom_css_minified' );
158 - if ( ! empty( $custom_css_minified ) ) {
159 - $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' );
160 283 }
161 - /**
162 - * Filter the "Custom CSS" code that is to be loaded as inline CSS.
163 - *
164 - * @since 1.0.0
165 - *
166 - * @param string $custom_css The "Custom CSS" code.
167 - */
168 - $custom_css = apply_filters( 'tablepress_custom_css', $custom_css );
169 - if ( ! empty( $custom_css ) ) {
170 - // wp_add_inline_style() requires a loaded CSS file, so we have to work around that if "Default CSS" is disabled,
171 - if ( $use_default_css ) {
172 - // Handle of the file to which the <style> shall be appended.
173 - wp_add_inline_style( 'tablepress-default', $custom_css );
174 - } else {
175 - add_action( 'wp_head', array( $this, '_print_custom_css' ), 8 ); // priority 8 to hook in right after WP_Styles has been processed
176 - }
177 - }
284 + return;
178 285 }
179 286 }
180 287 }
181 288
182 289 /**
183 - * Print "Custom CSS" to "wp_head" inline.
290 + * Enqueues the DataTables JavaScript library and its dependencies.
184 291 *
185 - * This is necessary if "Default CSS" is off, and saving "Custom CSS" to a file is not possible.
186 - *
187 - * @since 1.0.0
292 + * @since 3.0.0
188 293 */
189 - public function _print_custom_css() {
190 - // Get "Custom CSS" from options, try minified Custom CSS first.
191 - $custom_css = TablePress::$model_options->get( 'custom_css_minified' );
192 - if ( empty( $custom_css ) ) {
193 - $custom_css = TablePress::$model_options->get( 'custom_css' );
194 - }
195 - /** This filter is documented in controllers/controller-frontend.php */
196 - $custom_css = apply_filters( 'tablepress_custom_css', $custom_css );
197 - echo "<style type='text/css'>\n{$custom_css}\n</style>\n";
198 - }
199 -
200 - /**
201 - * Enqueue the DataTables JavaScript library and its dependencies.
202 - *
203 - * @since 1.0.0
204 - */
205 - protected function _enqueue_datatables() {
294 + protected function enqueue_datatables_files(): void {
206 295 $js_file = 'js/jquery.datatables.min.js';
207 296 $js_url = plugins_url( $js_file, TABLEPRESS__FILE__ );
208 297 /**
209 - * Filter the URL from which the DataTables JavaScript library file is loaded.
298 + * Filters the URL from which the DataTables JavaScript library file is loaded.
210 299 *
211 300 * @since 1.0.0
212 301 *
213 302 * @param string $js_url URL of the DataTables JS library file.
@@ -213,45 +302,93 @@
213 302 * @param string $js_url URL of the DataTables JS library file.
214 303 * @param string $js_file Path and file name of the DataTables JS library file.
215 304 */
216 305 $js_url = apply_filters( 'tablepress_datatables_js_url', $js_url, $js_file );
217 - wp_enqueue_script( 'tablepress-datatables', $js_url, array( 'jquery' ), 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 );
218 321 }
219 322
220 323 /**
221 - * Add JS code for invocation of DataTables JS library.
324 + * Adds the JavaScript code for the invocation of the DataTables JS library.
222 325 *
223 326 * @since 1.0.0
224 327 */
225 - public function add_datatables_calls() {
328 + public function add_datatables_calls(): void {
329 + // Prevent repeated execution (which would lead to DataTables error messages) via a static variable.
330 + static $datatables_calls_printed = false;
331 + if ( $datatables_calls_printed ) {
332 + return;
333 + }
334 +
335 + // Bail early if there are no TablePress tables on the page.
226 336 if ( empty( $this->shown_tables ) ) {
227 - // There are no tables with activated DataTables on the page that is currently rendered.
228 337 return;
229 338 }
230 339
231 - // Storage for the DataTables languages.
232 - $datatables_languages = array();
340 + /*
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.
343 + */
344 + if ( function_exists( 'get_current_screen' ) ) {
345 + $current_screen = get_current_screen();
346 + if ( ( $current_screen instanceof WP_Screen ) && $current_screen->is_block_editor() ) {
347 + return;
348 + }
349 + }
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 +
366 + // Storage for the DataTables language strings.
367 + $datatables_language = array();
233 368 // Generate the specific JS commands, depending on chosen features on the "Edit" screen and the Shortcode parameters.
234 369 $commands = array();
235 370
236 - foreach ( $this->shown_tables as $table_id => $table_store ) {
237 - if ( empty( $table_store['instances'] ) ) {
238 - continue;
239 - }
371 + foreach ( $shown_tables_with_datatables as $table_id => $table_store ) {
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.
373 +
240 374 foreach ( $table_store['instances'] as $html_id => $js_options ) {
241 375 $parameters = array();
242 376
243 377 // Settle dependencies/conflicts between certain features.
244 - if ( false !== $js_options['datatables_scrolly'] ) { // not necessarily a boolean!
378 + if ( false !== $js_options['datatables_scrolly'] ) { // datatables_scrolly can be a string, so that the explicit `false` check is needed.
245 379 // Vertical scrolling and pagination don't work together.
246 380 $js_options['datatables_paginate'] = false;
247 381 }
248 382 // Sanitize, as it may come from a Shortcode attribute.
249 - $js_options['datatables_paginate_entries'] = intval( $js_options['datatables_paginate_entries'] );
383 + $js_options['datatables_paginate_entries'] = (int) $js_options['datatables_paginate_entries'];
250 384
251 - // DataTables language/translation handling.
385 + /*
386 + * DataTables language/translation handling.
387 + */
388 +
252 389 /**
253 - * Filter the locale/language for the DataTables JavaScript library.
390 + * Filters the locale/language for the DataTables JavaScript library.
254 391 *
255 392 * @since 1.0.0
256 393 *
257 394 * @param string $locale The DataTables JS library locale.
@@ -257,125 +394,198 @@
257 394 * @param string $locale The DataTables JS library locale.
258 395 * @param string $table_id The current table ID.
259 396 */
260 397 $datatables_locale = apply_filters( 'tablepress_datatables_locale', $js_options['datatables_locale'], $table_id );
261 - // Only do the expensive language file checks if they haven't been done yet.
262 - if ( ! isset( $datatables_languages[ $datatables_locale ] ) ) {
263 - $orig_language_file = TABLEPRESS_ABSPATH . "i18n/datatables/lang-{$datatables_locale}.json";
398 +
399 + // Only load each locale's language file once.
400 + if ( ! isset( $datatables_language[ $datatables_locale ] ) ) {
401 + $orig_language_file = TABLEPRESS_ABSPATH . "i18n/datatables/lang-{$datatables_locale}.php";
402 +
264 403 /**
265 - * Filter the language file for the DataTables JavaScript library.
404 + * Filters the language file path for the DataTables JavaScript library.
266 405 *
406 + * PHP files that return an array and JSON files are supported.
407 + * The JSON file method is deprecated and should no longer be used.
408 + *
267 409 * @since 1.0.0
268 410 *
269 - * @param string $orig_language_file Language file for the DataTables JS library.
411 + * @param string $orig_language_file Language file path for the DataTables JS library.
270 412 * @param string $datatables_locale Current locale/language for the DataTables JS library.
271 - * @param string $path Path of the language file.
413 + * @param string $tablepress_abspath Base path of the TablePress plugin.
272 414 */
273 - $language_file = apply_filters( 'tablepress_datatables_language_file', $orig_language_file, $datatables_locale, TABLEPRESS_ABSPATH ); // Make sure to check file_exists( $new_file ) when using this filter!
274 - // Load translation if it's not "en_US" (included as the default in DataTables) and the language file exists, or if the filter was used to change the language file.
275 - if ( ( 'en_US' !== $datatables_locale && file_exists( $language_file ) )
276 - || ( $orig_language_file !== $language_file ) ) {
277 - $datatables_languages[ $datatables_locale ] = $language_file;
415 + $language_file = apply_filters( 'tablepress_datatables_language_file', $orig_language_file, $datatables_locale, TABLEPRESS_ABSPATH );
416 +
417 + /*
418 + * Load translation file if it's not "en_US" (included as the default in DataTables)
419 + * or if the filter was used to change the language file, and the language file exists.
420 + * Otherwise, use an empty en_US placeholder, so that the strings are filterable later.
421 + */
422 + if ( ( 'en_US' !== $datatables_locale || $orig_language_file !== $language_file ) && file_exists( $language_file ) ) {
423 + if ( str_ends_with( $language_file, '.php' ) ) {
424 + $datatables_strings = require $language_file;
425 + if ( ! is_array( $datatables_strings ) ) {
426 + $datatables_strings = array();
427 + }
428 + } elseif ( str_ends_with( $language_file, '.json' ) ) {
429 + $datatables_strings = file_get_contents( $language_file );
430 + $datatables_strings = json_decode( $datatables_strings, true ); // @phpstan-ignore argument.type
431 + // Check if JSON could be decoded.
432 + if ( is_null( $datatables_strings ) ) {
433 + $datatables_strings = array();
434 + }
435 + $datatables_strings = (array) $datatables_strings;
436 + } else {
437 + // The filtered language file exists, but is not a .php or .json file, so don't use it.
438 + $datatables_strings = array();
439 + }
440 + } else {
441 + // If no translation file for the defined locale exists or is needed, use "en_US", as that's built-in.
442 + $datatables_locale = 'en_US';
443 + $datatables_strings = array();
278 444 }
445 +
446 + /**
447 + * Filters the language strings for the DataTables JavaScript library's features.
448 + *
449 + * @since 2.0.0
450 + *
451 + * @param array<string, mixed> $datatables_strings The language strings for DataTables.
452 + * @param string $datatables_locale Current locale/language for the DataTables JS library.
453 + */
454 + $datatables_language[ $datatables_locale ] = apply_filters( 'tablepress_datatables_language_strings', $datatables_strings, $datatables_locale );
279 455 }
280 - // If translation is registered to have its strings added to the JS, add corresponding parameter to DataTables call.
281 - if ( isset( $datatables_languages[ $datatables_locale ] ) ) {
282 - $parameters['language'] = '"language":DataTables_language["' . $datatables_locale . '"]';
283 - }
456 + $parameters['language'] = "language:DT_language['{$datatables_locale}']";
457 +
284 458 // These parameters need to be added for performance gain or to overwrite unwanted default behavior.
285 459 if ( $js_options['datatables_sort'] ) {
286 460 // No initial sort.
287 - $parameters['order'] = '"order":[]';
461 + $parameters['order'] = 'order:[]';
288 462 // Don't add additional classes, to speed up sorting.
289 - $parameters['orderClasses'] = '"orderClasses":false';
463 + $parameters['orderClasses'] = 'orderClasses:false';
290 464 }
291 - // Alternating row colors is default, so remove them if not wanted with [].
292 - $parameters['stripeClasses'] = '"stripeClasses":' . ( ( $js_options['alternating_row_colors'] ) ? '["even","odd"]' : '[]' );
465 +
293 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.
294 467 if ( ! $js_options['datatables_sort'] ) {
295 - $parameters['ordering'] = '"ordering":false';
468 + $parameters['ordering'] = 'ordering:false';
296 469 }
297 470 if ( $js_options['datatables_paginate'] ) {
298 - $parameters['pagingType'] = '"pagingType":"simple"';
471 + $parameters['pagingType'] = "pagingType:'simple_numbers'";
299 472 if ( $js_options['datatables_lengthchange'] ) {
300 473 $length_menu = array( 10, 25, 50, 100 );
301 474 if ( ! in_array( $js_options['datatables_paginate_entries'], $length_menu, true ) ) {
302 475 $length_menu[] = $js_options['datatables_paginate_entries'];
303 476 sort( $length_menu, SORT_NUMERIC );
304 - $parameters['lengthMenu'] = '"lengthMenu":[' . implode( ',', $length_menu ) . ']';
477 + $parameters['lengthMenu'] = 'lengthMenu:[' . implode( ',', $length_menu ) . ']';
305 478 }
306 479 } else {
307 - $parameters['lengthChange'] = '"lengthChange":false';
480 + $parameters['lengthChange'] = 'lengthChange:false';
308 481 }
309 482 if ( 10 !== $js_options['datatables_paginate_entries'] ) {
310 - $parameters['pageLength'] = '"pageLength":' . $js_options['datatables_paginate_entries'];
483 + $parameters['pageLength'] = "pageLength:{$js_options['datatables_paginate_entries']}";
311 484 }
312 485 } else {
313 - $parameters['paging'] = '"paging":false';
486 + $parameters['paging'] = 'paging:false';
314 487 }
315 488 if ( ! $js_options['datatables_filter'] ) {
316 - $parameters['searching'] = '"searching":false';
489 + $parameters['searching'] = 'searching:false';
317 490 }
318 491 if ( ! $js_options['datatables_info'] ) {
319 - $parameters['info'] = '"info":false';
492 + $parameters['info'] = 'info:false';
320 493 }
321 494 if ( $js_options['datatables_scrollx'] ) {
322 - $parameters['scrollX'] = '"scrollX":true';
495 + $parameters['scrollX'] = 'scrollX:true';
323 496 }
324 497 if ( false !== $js_options['datatables_scrolly'] ) {
325 - $parameters['scrollY'] = '"scrollY":"' . preg_replace( '#[^0-9a-z.%]#', '', $js_options['datatables_scrolly'] ) . '"';
326 - $parameters['scrollCollapse'] = '"scrollCollapse":true';
498 + $parameters['scrollY'] = 'scrollY:"' . preg_replace( '#[^0-9a-z.%]#', '', $js_options['datatables_scrolly'] ) . '"';
499 + $parameters['scrollCollapse'] = 'scrollCollapse:true';
327 500 }
328 - if ( ! empty( $js_options['datatables_custom_commands'] ) ) {
329 - $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.
330 504 }
331 505
332 506 /**
333 - * Filter the parameters that are passed to the DataTables JavaScript library.
507 + * Filters the parameters that are passed to the DataTables JavaScript library.
334 508 *
335 509 * @since 1.0.0
336 510 *
337 - * @param array $parameters The parameters for the DataTables JS library.
338 - * @param string $table_id The current table ID.
339 - * @param string $html_id The ID of the table HTML element.
340 - * @param array $js_options The options for the JS library.
511 + * @param array<string, mixed> $parameters The parameters for the DataTables JS library.
512 + * @param string $table_id The current table ID.
513 + * @param string $html_id The ID of the table HTML element.
514 + * @param array<string, mixed> $js_options The options for the JS library.
341 515 */
342 516 $parameters = apply_filters( 'tablepress_datatables_parameters', $parameters, $table_id, $html_id, $js_options );
343 517
344 - // If an existing parameter (in the from `"parameter":`) is set in the "Custom Commands", remove its default value.
345 - if ( isset( $parameters['custom_commands'] ) ) {
346 - foreach ( array_keys( $parameters ) as $maybe_overwritten_parameter ) {
347 - if ( false !== strpos( $parameters['custom_commands'], "\"{$maybe_overwritten_parameter}\":" ) ) {
348 - unset( $parameters[ $maybe_overwritten_parameter ] );
349 - }
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 ] );
350 523 }
351 524 }
352 525
526 + $name = substr( $html_id, 11 ); // Remove "tablepress-" from the HTML ID.
527 + $name = "DT_TP['" . str_replace( '-', '_', $name ) . "']";
353 528 $parameters = implode( ',', $parameters );
354 529 $parameters = ( ! empty( $parameters ) ) ? '{' . $parameters . '}' : '';
355 530
356 - $command = "$('#{$html_id}').dataTable({$parameters});";
531 + $command = "{$name} = new DataTable('#{$html_id}',{$parameters});";
357 532 /**
358 - * Filter the JavaScript command that invokes the DataTables JavaScript library on one table.
533 + * Filters the JavaScript command that invokes the DataTables JavaScript library on one table.
359 534 *
360 535 * @since 1.0.0
361 536 *
362 - * @param string $command The JS command for the DataTables JS library.
363 - * @param string $html_id The ID of the table HTML element.
364 - * @param array $parameters The parameters for the DataTables JS library.
365 - * @param string $table_id The current table ID.
366 - * @param array $js_options The options for the JS library.
537 + * @param string $command The JS command for the DataTables JS library.
538 + * @param string $html_id The ID of the table HTML element.
539 + * @param string $parameters The parameters for the DataTables JS library.
540 + * @param string $table_id The current table ID.
541 + * @param array<string, mixed> $js_options The options for the JS library.
542 + * @param string $name The name of the DataTable instance.
367 543 */
368 - $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 );
369 545 if ( ! empty( $command ) ) {
370 546 $commands[] = $command;
371 547 }
372 - }
548 + } // foreach table instance
549 + } // foreach table ID
550 +
551 + // DataTables language/translation handling.
552 + if ( ! empty( $datatables_language ) ) {
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";
555 + } else {
556 + $datatables_language_command = '';
373 557 }
374 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 +
375 585 $commands = implode( "\n", $commands );
376 586 /**
377 - * Filter the JavaScript commands that invoke the DataTables JavaScript library on all tables on the page.
587 + * Filters the JavaScript commands that invoke the DataTables JavaScript library on all tables on the page.
378 588 *
379 589 * @since 1.0.0
380 590 *
381 591 * @param string $commands The JS commands for the DataTables JS library.
@@ -380,74 +590,75 @@
380 590 *
381 591 * @param string $commands The JS commands for the DataTables JS library.
382 592 */
383 593 $commands = apply_filters( 'tablepress_all_datatables_commands', $commands );
384 - if ( empty( $commands ) ) {
594 + if ( '' === $commands ) {
385 595 return;
386 596 }
387 597
388 - // DataTables language/translation handling.
389 - $datatables_strings = '';
390 - foreach ( $datatables_languages as $locale => $language_file ) {
391 - $strings = file_get_contents( $language_file );
392 - // Remove unnecessary white space.
393 - $strings = str_replace( array( "\n", "\r", "\t" ), '', $strings );
394 - $datatables_strings .= "DataTables_language[\"{$locale}\"]={$strings};\n";
395 - }
396 - if ( ! empty( $datatables_strings ) ) {
397 - $datatables_strings = "var DataTables_language={};\n" . $datatables_strings;
398 - }
598 + $script_template = <<<'JS'
599 + var DT_TP = {};
600 + jQuery(($)=>{
601 + %1$s%2$s
602 + });
603 + JS;
604 + /**
605 + * Filters the script/jQuery wrapper code for the DataTables commands calls.
606 + *
607 + * @since 1.14.0
608 + *
609 + * @param string $script_template Default script/jQuery wrapper code for the DataTables commands calls.
610 + */
611 + $script_template = apply_filters( 'tablepress_all_datatables_commands_wrapper', $script_template );
399 612
400 - // Echo DataTables strings and JS calls.
401 - echo <<<JS
402 -<script type="text/javascript">
403 -jQuery(document).ready(function($){
404 -{$datatables_strings}{$commands}
405 -});
406 -</script>
407 -JS;
613 + $script = sprintf( $script_template, $datatables_pre_commands, $commands );
614 + wp_add_inline_script( 'tablepress-datatables', $script );
615 +
616 + // Prevent repeated execution (which would lead to DataTables error messages) via a static variable.
617 + $datatables_calls_printed = true;
408 618 }
409 619
410 620 /**
411 - * Handle Shortcode [table id=<ID> /] in `the_content()`.
621 + * Handles the Shortcode [table id=<ID> /].
412 622 *
413 623 * @since 1.0.0
414 624 *
415 - * @param array $shortcode_atts List of attributes that where included in the Shortcode.
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 /].
416 626 * @return string Resulting HTML code for the table with the ID <ID>.
417 627 */
418 - public function shortcode_table( $shortcode_atts ) {
419 - // For empty Shortcodes like [table] or [table /], an empty string is passed, see WP Core #26927.
628 + public function shortcode_table( /* array|string */ $shortcode_atts ): string {
420 629 $shortcode_atts = (array) $shortcode_atts;
421 630
631 + $this->maybe_enqueue_css();
632 +
422 633 $_render = TablePress::load_class( 'TablePress_Render', 'class-render.php', 'classes' );
423 634
424 635 $default_shortcode_atts = $_render->get_default_render_options();
425 636 /**
426 - * Filter the available/default attributes for the [table] Shortcode.
637 + * Filters the available/default attributes for the [table] Shortcode.
427 638 *
428 639 * @since 1.0.0
429 640 *
430 - * @param array $default_shortcode_atts The [table] Shortcode default attributes.
641 + * @param array<string, mixed> $default_shortcode_atts The [table] Shortcode default attributes.
431 642 */
432 643 $default_shortcode_atts = apply_filters( 'tablepress_shortcode_table_default_shortcode_atts', $default_shortcode_atts );
433 644 // Parse Shortcode attributes, only allow those that are specified.
434 645 $shortcode_atts = shortcode_atts( $default_shortcode_atts, $shortcode_atts ); // Optional third argument left out on purpose. Use filter in the next line instead.
435 646 /**
436 - * Filter the attributes that were passed to the [table] Shortcode.
647 + * Filters the attributes that were passed to the [table] Shortcode.
437 648 *
438 649 * @since 1.0.0
439 650 *
440 - * @param array $shortcode_atts The attributes passed to the [table] Shortcode.
651 + * @param array<string, mixed> $shortcode_atts The attributes passed to the [table] Shortcode.
441 652 */
442 653 $shortcode_atts = apply_filters( 'tablepress_shortcode_table_shortcode_atts', $shortcode_atts );
443 654
444 655 // Check, if a table with the given ID exists.
445 - $table_id = preg_replace( '/[^a-zA-Z0-9_-]/', '', $shortcode_atts['id'] );
656 + $table_id = (string) preg_replace( '/[^a-zA-Z0-9_-]/', '', $shortcode_atts['id'] );
446 657 if ( ! TablePress::$model_table->table_exists( $table_id ) ) {
447 - $message = "[table &#8220;{$table_id}&#8221; not found /]<br />\n";
658 + $message = "&#91;table “{$table_id}” not found /&#93;<br />\n";
448 659 /**
449 - * Filter the "Table not found" message.
660 + * Filters the "Table not found" message.
450 661 *
451 662 * @since 1.0.0
452 663 *
453 664 * @param string $message The "Table not found" message.
@@ -459,11 +670,11 @@
459 670
460 671 // Load table, with table data, options, and visibility settings.
461 672 $table = TablePress::$model_table->load( $table_id, true, true );
462 673 if ( is_wp_error( $table ) ) {
463 - $message = "[table &#8220;{$table_id}&#8221; could not be loaded /]<br />\n";
674 + $message = "&#91;table “{$table_id}” could not be loaded /&#93;<br />\n";
464 675 /**
465 - * Filter the "Table could not be loaded" message.
676 + * Filters the "Table could not be loaded" message.
466 677 *
467 678 * @since 1.0.0
468 679 *
469 680 * @param string $message The "Table could not be loaded" message.
@@ -473,11 +684,11 @@
473 684 $message = apply_filters( 'tablepress_table_load_error_message', $message, $table_id, $table );
474 685 return $message;
475 686 }
476 687 if ( isset( $table['is_corrupted'] ) && $table['is_corrupted'] ) {
477 - $message = "<div>Attention: The internal data of table &#8220;{$table_id}&#8221; is corrupted!</div>";
688 + $message = "<div>Attention: The internal data of table “{$table_id}” is corrupted!</div>";
478 689 /**
479 - * Filter the "Table data is corrupted" message.
690 + * Filters the "Table data is corrupted" message.
480 691 *
481 692 * @since 1.0.0
482 693 *
483 694 * @param string $message The "Table data is corrupted" message.
@@ -488,9 +699,9 @@
488 699 return $message;
489 700 }
490 701
491 702 /**
492 - * Filter whether the "datatables_custom_commands" Shortcode parameter is disabled.
703 + * Filters whether the "datatables_custom_commands" Shortcode parameter is disabled.
493 704 *
494 705 * By default, the "datatables_custom_commands" Shortcode parameter is disabled for security reasons.
495 706 *
496 707 * @since 1.0.0
@@ -503,20 +714,31 @@
503 714
504 715 // Determine options to use (if set in Shortcode, use those, otherwise use stored options, from the "Edit" screen).
505 716 $render_options = array();
506 717 foreach ( $shortcode_atts as $key => $value ) {
507 - // We have to check this, because strings 'true' or 'false' are not recognized as boolean!
508 - if ( is_string( $value ) && 'true' === strtolower( $value ) ) {
509 - $render_options[ $key ] = true;
510 - } elseif ( is_string( $value ) && 'false' === strtolower( $value ) ) {
511 - $render_options[ $key ] = false;
512 - } elseif ( is_null( $value ) && isset( $table['options'][ $key ] ) ) {
718 + if ( is_null( $value ) && isset( $table['options'][ $key ] ) ) {
719 + // Use the table's stored option value, if the Shortcode parameter was not set.
513 720 $render_options[ $key ] = $table['options'][ $key ];
721 + } elseif ( is_string( $value ) ) {
722 + // Convert strings 'true' or 'false' to boolean, keep others.
723 + $value_lowercase = strtolower( $value );
724 + if ( 'true' === $value_lowercase ) {
725 + $render_options[ $key ] = true;
726 + } elseif ( 'false' === $value_lowercase ) {
727 + $render_options[ $key ] = false;
728 + } else {
729 + $render_options[ $key ] = $value;
730 + }
514 731 } else {
732 + // Keep all other values.
515 733 $render_options[ $key ] = $value;
516 734 }
517 735 }
518 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 +
519 741 // Generate unique HTML ID, depending on how often this table has already been shown on this page.
520 742 if ( ! isset( $this->shown_tables[ $table_id ] ) ) {
521 743 $this->shown_tables[ $table_id ] = array(
522 744 'count' => 0,
@@ -522,9 +744,9 @@
522 744 'count' => 0,
523 745 'instances' => array(),
524 746 );
525 747 }
526 - $this->shown_tables[ $table_id ]['count']++;
748 + ++$this->shown_tables[ $table_id ]['count'];
527 749 $count = $this->shown_tables[ $table_id ]['count'];
528 750 $render_options['html_id'] = "tablepress-{$table_id}";
529 751 if ( $count > 1 ) {
530 752 $render_options['html_id'] .= "-no-{$count}";
@@ -529,15 +751,15 @@
529 751 if ( $count > 1 ) {
530 752 $render_options['html_id'] .= "-no-{$count}";
531 753 }
532 754 /**
533 - * Filter the ID of the table HTML element.
755 + * Filters the ID of the table HTML element.
534 756 *
535 757 * @since 1.0.0
536 758 *
537 759 * @param string $html_id The ID of the table HTML element.
538 760 * @param string $table_id The current table ID.
539 - * @param string $count Number of copies of the table with this table ID on the page.
761 + * @param int $count Number of copies of the table with this table ID on the page.
540 762 */
541 763 $render_options['html_id'] = apply_filters( 'tablepress_html_id', $render_options['html_id'], $table_id, $count );
542 764
543 765 // Generate the "Edit Table" link.
@@ -542,9 +764,9 @@
542 764
543 765 // Generate the "Edit Table" link.
544 766 $render_options['edit_table_url'] = '';
545 767 /**
546 - * Filter whether the "Edit" link below the table shall be shown.
768 + * Filters whether the "Edit" link below the table shall be shown.
547 769 *
548 770 * The "Edit" link is only shown to logged-in users who possess the necessary capability to edit the table.
549 771 *
550 772 * @since 1.0.0
@@ -551,71 +773,38 @@
551 773 *
552 774 * @param bool $show Whether to show the "Edit" link below the table. Default true.
553 775 * @param string $table_id The current table ID.
554 776 */
555 - 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'] ) ) {
556 778 $render_options['edit_table_url'] = TablePress::url( array( 'action' => 'edit', 'table_id' => $table['id'] ) );
557 779 }
558 780
559 781 /**
560 - * Filter the render options for the table.
782 + * Filters the render options for the table.
561 783 *
562 784 * The render options are determined from the settings on a table's "Edit" screen and the Shortcode parameters.
563 785 *
564 786 * @since 1.0.0
565 787 *
566 - * @param array $render_options The render options for the table.
567 - * @param array $table The current table.
788 + * @param array<string, mixed> $render_options The render options for the table.
789 + * @param array<string, mixed> $table The current table.
568 790 */
569 791 $render_options = apply_filters( 'tablepress_table_render_options', $render_options, $table );
570 792
571 - // Eventually add this table to list of tables which have a JS library enabled and thus are to be included in the script's call in the footer.
572 - if ( $render_options['use_datatables'] && $render_options['table_head'] && count( $table['data'] ) > 1 ) {
573 - // Get options for the DataTables JavaScript library from the table's render options.
574 - $js_options = array();
575 - foreach ( array(
576 - 'alternating_row_colors',
577 - 'datatables_sort',
578 - 'datatables_paginate',
579 - 'datatables_paginate',
580 - 'datatables_paginate_entries',
581 - 'datatables_lengthchange',
582 - 'datatables_filter',
583 - 'datatables_info',
584 - 'datatables_scrollx',
585 - 'datatables_scrolly',
586 - 'datatables_locale',
587 - 'datatables_custom_commands',
588 - ) as $option ) {
589 - $js_options[ $option ] = $render_options[ $option ];
590 - }
591 - /**
592 - * Filter the JavaScript options for the table.
593 - *
594 - * The JavaScript options are determined from the settings on a table's "Edit" screen and the Shortcode parameters.
595 - * They are part of the render options and can be overwritten with Shortcode parameters.
596 - *
597 - * @since 1.0.0
598 - *
599 - * @param array $js_options The JavaScript options for the table.
600 - * @param string $table_id The current table ID.
601 - * @param array $render_options The render options for the table.
602 - */
603 - $js_options = apply_filters( 'tablepress_table_js_options', $js_options, $table_id, $render_options );
604 - $this->shown_tables[ $table_id ]['instances'][ $render_options['html_id'] ] = $js_options;
605 - $this->_enqueue_datatables();
606 - }
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'] );
607 796
608 797 // Check if table output shall and can be loaded from the transient cache, otherwise generate the output.
609 798 if ( $render_options['cache_table_output'] && ! is_user_logged_in() ) {
610 799 // Hash the Render Options array to get a unique cache identifier.
611 - $table_hash = md5( wp_json_encode( $render_options, TABLEPRESS_JSON_OPTIONS ) );
800 + $table_hash = md5( wp_json_encode( $render_options, TABLEPRESS_JSON_OPTIONS ) ); // @phpstan-ignore argument.type
612 801 $transient_name = 'tablepress_' . $table_hash; // Attention: This string must not be longer than 45 characters!
613 802 $output = get_transient( $transient_name );
614 803 if ( false === $output || '' === $output ) {
615 804 // Render/generate the table HTML, as it was not found in the cache.
616 805 $_render->set_input( $table, $render_options );
617 - $output = $_render->get_output();
806 + $output = $_render->get_output( 'html' );
618 807 // Save render output in a transient, set cache timeout to 24 hours.
619 808 set_transient( $transient_name, $output, DAY_IN_SECONDS );
620 809 // Update output caches list transient (necessary for cache invalidation upon table saving).
621 810 $caches_list_transient_name = 'tablepress_c_' . md5( $table_id );
@@ -630,9 +819,9 @@
630 819 }
631 820 set_transient( $caches_list_transient_name, wp_json_encode( $caches_list, TABLEPRESS_JSON_OPTIONS ), 2 * DAY_IN_SECONDS );
632 821 } else {
633 822 /**
634 - * Filter the cache hit comment message.
823 + * Filters the cache hit comment message.
635 824 *
636 825 * @since 1.0.0
637 826 *
638 827 * @param string $comment The cache hit comment message.
@@ -641,14 +830,65 @@
641 830 }
642 831 } else {
643 832 // Render/generate the table HTML, as no cache is to be used.
644 833 $_render->set_input( $table, $render_options );
645 - $output = $_render->get_output();
834 + $output = $_render->get_output( 'html' );
646 835 }
647 836
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.
838 + if ( $render_options['use_datatables']
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 + ) {
842 + // Get options for the DataTables JavaScript library from the table's render options.
843 + $js_options = array();
844 + foreach ( array(
845 + 'alternating_row_colors',
846 + 'datatables_sort',
847 + 'datatables_paginate',
848 + 'datatables_paginate',
849 + 'datatables_paginate_entries',
850 + 'datatables_lengthchange',
851 + 'datatables_filter',
852 + 'datatables_info',
853 + 'datatables_scrollx',
854 + 'datatables_scrolly',
855 + 'datatables_locale',
856 + 'datatables_custom_commands',
857 + ) as $option ) {
858 + $js_options[ $option ] = $render_options[ $option ];
859 + }
860 + /**
861 + * Filters the JavaScript options for the table.
862 + *
863 + * The JavaScript options are determined from the settings on a table's "Edit" screen and the Shortcode parameters.
864 + * They are part of the render options and can be overwritten with Shortcode parameters.
865 + *
866 + * @since 1.0.0
867 + *
868 + * @param array<string, mixed> $js_options The JavaScript options for the table.
869 + * @param string $table_id The current table ID.
870 + * @param array<string, mixed> $render_options The render options for the table.
871 + */
872 + $js_options = apply_filters( 'tablepress_table_js_options', $js_options, $table_id, $render_options );
873 +
874 + $this->shown_tables[ $table_id ]['instances'][ (string) $render_options['html_id'] ] = $js_options;
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 + }
886 + }
887 +
648 888 // Maybe print a list of used render options.
649 889 if ( $render_options['shortcode_debug'] && is_user_logged_in() ) {
650 - $output .= '<pre>' . var_export( $render_options, true ) . '</pre>';
890 + $output .= '<pre>' . var_export( $render_options, true ) . '</pre>'; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
651 891 }
652 892
653 893 return $output;
654 894 }
@@ -653,17 +893,16 @@
653 893 return $output;
654 894 }
655 895
656 896 /**
657 - * Handle Shortcode [table-info id=<ID> field=<name> /] in the_content().
897 + * Handles the Shortcode [table-info id=<ID> field=<name> /].
658 898 *
659 899 * @since 1.0.0
660 900 *
661 - * @param array $shortcode_atts List of attributes that where included in the Shortcode.
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 /].
662 902 * @return string Text that replaces the Shortcode (error message or asked-for information).
663 903 */
664 - public function shortcode_table_info( $shortcode_atts ) {
665 - // For empty Shortcodes like [table-info] or [table-info /], an empty string is passed, see Core #26927.
904 + public function shortcode_table_info( /* array|string */ $shortcode_atts ): string {
666 905 $shortcode_atts = (array) $shortcode_atts;
667 906
668 907 // Parse Shortcode attributes, only allow those that are specified.
669 908 $default_shortcode_atts = array(
@@ -671,35 +910,35 @@
671 910 'field' => '',
672 911 'format' => '',
673 912 );
674 913 /**
675 - * Filter the available/default attributes for the [table-info] Shortcode.
914 + * Filters the available/default attributes for the [table-info] Shortcode.
676 915 *
677 916 * @since 1.0.0
678 917 *
679 - * @param array $default_shortcode_atts The [table-info] Shortcode default attributes.
918 + * @param array<string, mixed> $default_shortcode_atts The [table-info] Shortcode default attributes.
680 919 */
681 920 $default_shortcode_atts = apply_filters( 'tablepress_shortcode_table_info_default_shortcode_atts', $default_shortcode_atts );
682 921 $shortcode_atts = shortcode_atts( $default_shortcode_atts, $shortcode_atts ); // Optional third argument left out on purpose. Use filter in the next line instead.
683 922 /**
684 - * Filter the attributes that were passed to the [table-info] Shortcode.
923 + * Filters the attributes that were passed to the [table-info] Shortcode.
685 924 *
686 925 * @since 1.0.0
687 926 *
688 - * @param array $shortcode_atts The attributes passed to the [table-info] Shortcode.
927 + * @param array<string, mixed> $shortcode_atts The attributes passed to the [table-info] Shortcode.
689 928 */
690 929 $shortcode_atts = apply_filters( 'tablepress_shortcode_table_info_shortcode_atts', $shortcode_atts );
691 930
692 931 /**
693 - * Filter whether the output of the [table-info] Shortcode is overwritten/short-circuited.
932 + * Filters whether the output of the [table-info] Shortcode is overwritten/short-circuited.
694 933 *
695 934 * @since 1.0.0
696 935 *
697 - * @param bool|string $overwrite Whether the [table-info] output is overwritten. Return false for the regular content, and a string to overwrite the output.
698 - * @param array $shortcode_atts The attributes passed to the [table-info] Shortcode.
936 + * @param false|string $overwrite Whether the [table-info] output is overwritten. Return false for the regular content, and a string to overwrite the output.
937 + * @param array<string, mixed> $shortcode_atts The attributes passed to the [table-info] Shortcode.
699 938 */
700 939 $overwrite = apply_filters( 'tablepress_shortcode_table_info_overwrite', false, $shortcode_atts );
701 - if ( $overwrite ) {
940 + if ( is_string( $overwrite ) ) {
702 941 return $overwrite;
703 942 }
704 943
705 944 // Check, if a table with the given ID exists.
@@ -704,9 +943,9 @@
704 943
705 944 // Check, if a table with the given ID exists.
706 945 $table_id = preg_replace( '/[^a-zA-Z0-9_-]/', '', $shortcode_atts['id'] );
707 946 if ( ! TablePress::$model_table->table_exists( $table_id ) ) {
708 - $message = "[table &#8220;{$table_id}&#8221; not found /]<br />\n";
947 + $message = "&#91;table “{$table_id}” not found /&#93;<br />\n";
709 948 /** This filter is documented in controllers/controller-frontend.php */
710 949 $message = apply_filters( 'tablepress_table_not_found_message', $message, $table_id );
711 950 return $message;
712 951 }
@@ -713,16 +952,16 @@
713 952
714 953 // Load table, with table data, options, and visibility settings.
715 954 $table = TablePress::$model_table->load( $table_id, true, true );
716 955 if ( is_wp_error( $table ) ) {
717 - $message = "[table &#8220;{$table_id}&#8221; could not be loaded /]<br />\n";
956 + $message = "&#91;table “{$table_id}” could not be loaded /&#93;<br />\n";
718 957 /** This filter is documented in controllers/controller-frontend.php */
719 958 $message = apply_filters( 'tablepress_table_load_error_message', $message, $table_id, $table );
720 959 return $message;
721 960 }
722 961
723 - $field = preg_replace( '/[^a-z_]/', '', strtolower( $shortcode_atts['field'] ) );
724 - $format = preg_replace( '/[^a-z]/', '', strtolower( $shortcode_atts['format'] ) );
962 + $field = (string) preg_replace( '/[^a-z_]/', '', strtolower( $shortcode_atts['field'] ) );
963 + $format = (string) preg_replace( '/[^a-z]/', '', strtolower( $shortcode_atts['format'] ) );
725 964
726 965 // Generate output, depending on what information (field) was asked for.
727 966 switch ( $field ) {
728 967 case 'name':
@@ -731,32 +970,35 @@
731 970 break;
732 971 case 'last_modified':
733 972 switch ( $format ) {
734 973 case 'raw':
974 + case 'mysql':
735 975 $output = $table['last_modified'];
736 976 break;
737 977 case 'human':
738 - $modified_timestamp = strtotime( $table['last_modified'] );
739 - $current_timestamp = current_time( 'timestamp' );
978 + $modified_timestamp = date_create( $table['last_modified'], wp_timezone() );
979 + if ( false === $modified_timestamp ) {
980 + $modified_timestamp = $table['last_modified'];
981 + } else {
982 + $modified_timestamp = $modified_timestamp->getTimestamp();
983 + }
984 + $current_timestamp = time();
740 985 $time_diff = $current_timestamp - $modified_timestamp;
741 - // Time difference is only shown up to one day.
742 - if ( $time_diff >= 0 && $time_diff < DAY_IN_SECONDS ) {
986 + // Time difference is only shown up to one week.
987 + if ( $time_diff >= 0 && $time_diff < WEEK_IN_SECONDS ) {
743 988 $output = sprintf( __( '%s ago', 'default' ), human_time_diff( $modified_timestamp, $current_timestamp ) );
744 989 } else {
745 - $output = TablePress::format_datetime( $table['last_modified'], 'mysql', '<br />' );
990 + $output = TablePress::format_datetime( $table['last_modified'], '<br />' );
746 991 }
747 992 break;
748 993 case 'date':
749 - $modified_timestamp = strtotime( $table['last_modified'] );
750 - $output = date_i18n( get_option( 'date_format' ), $modified_timestamp );
994 + $output = TablePress::format_datetime( $table['last_modified'], get_option( 'date_format' ) );
751 995 break;
752 996 case 'time':
753 - $modified_timestamp = strtotime( $table['last_modified'] );
754 - $output = date_i18n( get_option( 'time_format' ), $modified_timestamp );
997 + $output = TablePress::format_datetime( $table['last_modified'], get_option( 'time_format' ) );
755 998 break;
756 - case 'mysql':
757 999 default:
758 - $output = TablePress::format_datetime( $table['last_modified'], 'mysql', ' ' );
1000 + $output = TablePress::format_datetime( $table['last_modified'] );
759 1001 break;
760 1002 }
761 1003 break;
762 1004 case 'last_editor':
@@ -767,14 +1009,10 @@
767 1009 break;
768 1010 case 'number_rows':
769 1011 $output = count( $table['data'] );
770 1012 if ( 'raw' !== $format ) {
771 - if ( $table['options']['table_head'] ) {
772 - $output = $output - 1;
773 - }
774 - if ( $table['options']['table_foot'] ) {
775 - $output = $output - 1;
776 - }
1013 + $output -= $table['options']['table_head'];
1014 + $output -= $table['options']['table_foot'];
777 1015 }
778 1016 break;
779 1017 case 'number_columns':
780 1018 $output = count( $table['data'][0] );
@@ -779,30 +1017,30 @@
779 1017 case 'number_columns':
780 1018 $output = count( $table['data'][0] );
781 1019 break;
782 1020 default:
783 - $output = "[table-info field &#8220;{$field}&#8221; not found in table &#8220;{$table_id}&#8221; /]<br />\n";
1021 + $output = "&#91;table-info field “{$field}” not found in table “{$table_id}” /&#93;<br />\n";
784 1022 /**
785 - * Filter the "table info field not found" message.
1023 + * Filters the "table info field not found" message.
786 1024 *
787 1025 * @since 1.0.0
788 1026 *
789 - * @param string $output The "table info field not found" message.
790 - * @param array $table The current table ID.
791 - * @param string $field The field that was not found.
792 - * @param string $format The return format for the field.
1027 + * @param string $output The "table info field not found" message.
1028 + * @param array<string, mixed> $table The current table.
1029 + * @param string $field The field that was not found.
1030 + * @param string $format The return format for the field.
793 1031 */
794 1032 $output = apply_filters( 'tablepress_table_info_not_found_message', $output, $table, $field, $format );
795 1033 }
796 1034
797 1035 /**
798 - * Filter the output of the [table-info] Shortcode.
1036 + * Filters the output of the [table-info] Shortcode.
799 1037 *
800 1038 * @since 1.0.0
801 1039 *
802 - * @param string $output The output of the [table-info] Shortcode.
803 - * @param array $table The current table.
804 - * @param array $shortcode_atts The attributes passed to the [table-info] Shortcode.
1040 + * @param string $output The output of the [table-info] Shortcode.
1041 + * @param array<string, mixed> $table The current table.
1042 + * @param array<string, mixed> $shortcode_atts The attributes passed to the [table-info] Shortcode.
805 1043 */
806 1044 $output = apply_filters( 'tablepress_shortcode_table_info_output', $output, $table, $shortcode_atts );
807 1045 return $output;
808 1046 }
@@ -807,9 +1045,9 @@
807 1045 return $output;
808 1046 }
809 1047
810 1048 /**
811 - * 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.
812 1050 *
813 1051 * This is done by looping through all search terms and TablePress tables and searching there for the search term,
814 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
815 1053 * Shortcode for one of these tables in their content.
@@ -820,11 +1058,18 @@
820 1058 *
821 1059 * @param string $search_sql Current part of the "WHERE" clause of the SQL statement used to get posts/pages from the WP database that is related to searching.
822 1060 * @return string Eventually extended SQL "WHERE" clause, to also find posts/pages with Shortcodes in them.
823 1061 */
824 - public function posts_search_filter( $search_sql ) {
1062 + public function posts_search_filter( /* string */ $search_sql ): string {
1063 + // Don't use a type hint in the method declaration as there can be cases where `null` is passed to the filter hook callback somehow.
1064 +
825 1065 global $wpdb;
826 1066
1067 + // Protect against cases where `null` is somehow passed to the filter hook callback.
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.)
1069 + return '';
1070 + }
1071 +
827 1072 if ( ! is_search() || ! is_main_query() ) {
828 1073 return $search_sql;
829 1074 }
830 1075
@@ -842,16 +1087,21 @@
842 1087 foreach ( $table_ids as $table_id ) {
843 1088 // Load table, with table data, options, and visibility settings.
844 1089 $table = TablePress::$model_table->load( $table_id, true, true );
845 1090
1091 + // Skip tables that could not be loaded.
1092 + if ( is_wp_error( $table ) ) {
1093 + continue;
1094 + }
1095 +
1096 + // Do not search in corrupted tables.
846 1097 if ( isset( $table['is_corrupted'] ) && $table['is_corrupted'] ) {
847 - // Do not search in corrupted tables.
848 1098 continue;
849 1099 }
850 1100
851 1101 foreach ( $search_terms as $search_term ) {
852 - if ( ( $table['options']['print_name'] && false !== stripos( $table['name'], $search_term ) )
853 - || ( $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 ) ) ) {
854 1104 // Found the search term in the name or description (and they are shown).
855 1105 $query_result[ $search_term ][] = $table_id; // Add table ID to result list.
856 1106 // No need to continue searching this search term in this table.
857 1107 continue;
@@ -867,11 +1117,11 @@
867 1117 if ( 0 === $table['visibility']['columns'][ $col_idx ] ) {
868 1118 // Column is hidden, so don't search in it.
869 1119 continue;
870 1120 }
871 - // @TODO: Cells are not evaluated here, so math formulas are searched.
872 - if ( false !== stripos( $table_cell, $search_term ) ) {
873 - // Found the search term in the cell content.
1121 + // @todo Cells are not evaluated here, so math formulas are searched.
1122 + if ( false !== stripos( $table_cell, (string) $search_term ) ) {
1123 + // Found the search term in the cell content.
874 1124 $query_result[ $search_term ][] = $table_id; // Add table ID to result list
875 1125 // No need to continue searching this search term in this table.
876 1126 continue 3;
877 1127 }
@@ -887,9 +1137,9 @@
887 1137 $n = ( empty( $exact ) ) ? '%' : '';
888 1138 $search_sql = $wpdb->remove_placeholder_escape( $search_sql );
889 1139 foreach ( $query_result as $search_term => $table_ids ) {
890 1140 $search_term = esc_sql( $wpdb->esc_like( $search_term ) );
891 - $old_or = "OR ({$wpdb->posts}.post_content LIKE '{$n}{$search_term}{$n}')";
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.)
892 1142 $table_ids = implode( '|', $table_ids );
893 1143 $regexp = '\\\\[' . TablePress::$shortcode . ' id=(["\\\']?)(' . $table_ids . ')([\]"\\\' /])'; // ' needs to be single escaped, [ double escaped (with \\) in mySQL
894 1144 $new_or = $old_or . " OR ({$wpdb->posts}.post_content REGEXP '{$regexp}')";
895 1145 $search_sql = str_replace( $old_or, $new_or, $search_sql );
@@ -896,7 +1146,31 @@
896 1146 }
897 1147 $search_sql = $wpdb->add_placeholder_escape( $search_sql );
898 1148
899 1149 return $search_sql;
1150 + }
1151 +
1152 + /**
1153 + * Callback function for rendering the tablepress/table block.
1154 + *
1155 + * @since 2.0.0
1156 + *
1157 + * @param array<string, string> $block_attributes List of attributes that where included in the block settings.
1158 + * @return string Resulting HTML code for the table.
1159 + */
1160 + public function table_block_render_callback( array $block_attributes ): string {
1161 + // Don't return anything if no table was selected.
1162 + if ( '' === $block_attributes['id'] ) {
1163 + return '';
1164 + }
1165 +
1166 + if ( '' !== trim( $block_attributes['parameters'] ) ) {
1167 + $render_attributes = shortcode_parse_atts( $block_attributes['parameters'] );
1168 + } else {
1169 + $render_attributes = array();
1170 + }
1171 + $render_attributes['id'] = $block_attributes['id'];
1172 +
1173 + return $this->shortcode_table( $render_attributes );
900 1174 }
901 1175
902 1176 } // class TablePress_Frontend_Controller