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