PluginProbe
TablePress – Tables in WordPress made easy / 3.4
TablePress – Tables in WordPress made easy v3.4
3.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 All 45 releases
← All changes | classes/class-view.php +339 -137 1.9.2 → 3.4 View file →
@@ -7,13 +7,16 @@
7 7 * @author Tobias Bäthge
8 8 * @since 1.0.0
9 9 */
10 10
11 +declare(strict_types=1);
12 +
11 13 // Prohibit direct script loading.
12 14 defined( 'ABSPATH' ) || die( 'No direct script access allowed!' );
13 15
14 16 /**
15 17 * TablePress Base View class
18 + *
16 19 * @package TablePress
17 20 * @subpackage Views
18 21 * @author Tobias Bäthge
19 22 * @since 1.0.0
@@ -23,51 +26,41 @@
23 26 /**
24 27 * Data for the view.
25 28 *
26 29 * @since 1.0.0
27 - * @var array
30 + * @var array<string, mixed>
28 31 */
29 - protected $data = array();
32 + protected array $data = array();
30 33
31 34 /**
32 35 * Number of screen columns for post boxes.
33 36 *
34 37 * @since 1.0.0
35 - * @var int
36 38 */
37 - protected $screen_columns = 0;
39 + protected int $screen_columns = 0;
38 40
39 41 /**
40 42 * User action for this screen.
41 43 *
42 44 * @since 1.0.0
43 - * @var string
44 45 */
45 - protected $action = '';
46 + protected string $action = '';
46 47
47 48 /**
48 - * Instance of the Admin Page Helper Class, with necessary functions.
49 - *
50 - * @since 1.0.0
51 - * @var TablePress_Admin_Page
52 - */
53 - protected $admin_page;
54 -
55 - /**
56 49 * List of text boxes (similar to post boxes, but just with text and without extra functionality).
57 50 *
58 51 * @since 1.0.0
59 - * @var array
52 + * @var array<string, array<string, array<string, mixed>>>
60 53 */
61 - protected $textboxes = array();
54 + protected array $textboxes = array();
62 55
63 56 /**
64 57 * List of messages that are to be displayed as boxes below the page title.
65 58 *
66 59 * @since 1.0.0
67 - * @var array
60 + * @var string[]
68 61 */
69 - protected $header_messages = array();
62 + protected array $header_messages = array();
70 63
71 64 /**
72 65 * Whether there are post boxes registered for this screen,
73 66 * is automatically set to true, when a meta box is added.
@@ -72,22 +65,21 @@
72 65 * Whether there are post boxes registered for this screen,
73 66 * is automatically set to true, when a meta box is added.
74 67 *
75 68 * @since 1.0.0
76 - * @var bool
77 69 */
78 - protected $has_meta_boxes = false;
70 + protected bool $has_meta_boxes = false;
79 71
80 72 /**
81 73 * List of WP feature pointers for this view.
82 74 *
83 75 * @since 1.0.0
84 - * @var array
76 + * @var string[]
85 77 */
86 - protected $wp_pointers = array();
78 + protected array $wp_pointers = array();
87 79
88 80 /**
89 - * Initialize the View class, by setting the correct screen columns and adding help texts.
81 + * Initializes the View class, by setting the correct screen columns and adding help texts.
90 82 *
91 83 * @since 1.0.0
92 84 */
93 85 public function __construct() {
@@ -92,77 +84,92 @@
92 84 */
93 85 public function __construct() {
94 86 $screen = get_current_screen();
95 87 if ( 0 !== $this->screen_columns ) {
96 - $screen->add_option( 'layout_columns', array( 'max' => $this->screen_columns ) );
88 + $screen->add_option( 'layout_columns', array( 'max' => $this->screen_columns ) ); // @phpstan-ignore method.nonObject
97 89 }
98 90 // Enable two column layout.
99 - add_filter( "get_user_option_screen_layout_{$screen->id}", array( $this, 'set_current_screen_layout_columns' ) );
91 + add_filter( "get_user_option_screen_layout_{$screen->id}", array( $this, 'set_current_screen_layout_columns' ) ); // @phpstan-ignore property.nonObject
100 92
101 - $screen->add_help_tab( array(
93 + /* translators: %1$s: URL to TablePress website, %2$s: URL to WordPress Plugin Directory */
94 + $common_content = '<p>' . sprintf( __( 'More information about TablePress can be found on the <a href="%1$s">plugin website</a> or on its page in the <a href="%2$s">WordPress Plugin Directory</a>.', 'tablepress' ), 'https://tablepress.org/', 'https://wordpress.org/plugins/tablepress/' ) . '</p>';
95 + /* translators: %s: URL to Documentation page */
96 + $common_content .= '<p>' . sprintf( __( 'For technical information, please see the <a href="%s">Documentation</a>.', 'tablepress' ), 'https://tablepress.org/documentation/' ) . ' ';
97 + /* translators: %s: URL to FAQ page */
98 + $common_content .= sprintf( __( 'Common questions are answered in the <a href="%s">FAQ</a>.', 'tablepress' ), 'https://tablepress.org/faq/' ) . '</p>';
99 +
100 + if ( tb_tp_fs()->is_free_plan() ) {
101 + $common_content .= '<p>'
102 + . sprintf( __( '<a href="%1$s">Support</a> is provided through the <a href="%2$s">WordPress Support Forums</a>.', 'tablepress' ), 'https://tablepress.org/support/', 'https://wordpress.org/support/plugin/tablepress/' )
103 + . ' '
104 + . sprintf( __( 'Before asking for support, please carefully read the <a href="%s">Frequently Asked Questions</a>, where you will find answers to the most common questions, and search through the forums.', 'tablepress' ), 'https://tablepress.org/faq/' )
105 + . '</p>';
106 + $common_content .= '<p><strong>' . sprintf( __( 'More great features for you and your site’s visitors and priority email support are available with a Premium license plan of TablePress. <a href="%s">Go check them out!</a>', 'tablepress' ), 'https://tablepress.org/premium/?utm_source=plugin&utm_medium=textlink&utm_content=help-tab' ) . '</strong></p>';
107 + }
108 +
109 + $screen->add_help_tab( array( // @phpstan-ignore method.nonObject
102 110 'id' => 'tablepress-help', // This should be unique for the screen.
103 111 'title' => __( 'TablePress Help', 'tablepress' ),
104 - 'content' => '<p>' . $this->help_tab_content() . '</p>'
105 - . '<p>' . sprintf( __( 'More information about TablePress can be found on the <a href="%1$s">plugin&#8217;s website</a> or on its page in the <a href="%2$s">WordPress Plugin Directory</a>.', 'tablepress' ), 'https://tablepress.org/', 'https://wordpress.org/plugins/tablepress/' ) . ' '
106 - . sprintf( __( 'For technical information, please see the <a href="%s">documentation</a>.', 'tablepress' ), 'https://tablepress.org/documentation/' ) . ' '
107 - . sprintf( __( '<a href="%1$s">Support</a> is provided through the <a href="%2$s">WordPress Support Forums</a>.', 'tablepress' ), 'https://tablepress.org/support/', 'https://wordpress.org/tags/tablepress' ) . ' '
108 - . sprintf( __( 'Before asking for support, please carefully read the <a href="%s">Frequently Asked Questions</a>, where you will find answers to the most common questions, and search through the forums.', 'tablepress' ), 'https://tablepress.org/faq/' ) . '<br />'
109 - . sprintf( __( 'If you like the plugin, <a href="%1$s"><strong>a donation</strong></a> is recommended.', 'tablepress' ), 'https://tablepress.org/donate/' ) . '</p>',
112 + 'content' => '<p>' . $this->help_tab_content() . '</p>' . $common_content,
110 113 ) );
111 - // "sidebar" in the help tab.
112 - $screen->set_help_sidebar( '<p><strong>' . __( 'For more information:', 'tablepress' ) . '</strong></p><p><a href="https://tablepress.org/" target="_blank">TablePress Website</a></p><p><a href="https://tablepress.org/faq/" target="_blank">TablePress FAQ</a></p><p><a href="https://tablepress.org/documentation/" target="_blank">TablePress Documentation</a></p><p><a href="https://tablepress.org/support/" target="_blank">TablePress Support</a></p>' );
114 + // "Sidebar" in the help tab.
115 + $screen->set_help_sidebar( // @phpstan-ignore method.nonObject
116 + '<p><strong>' . __( 'For more information:', 'tablepress' ) . '</strong></p>'
117 + . '<p><a href="https://tablepress.org/">TablePress Website</a></p>'
118 + . '<p><a href="https://tablepress.org/faq/">TablePress FAQ</a></p>'
119 + . '<p><a href="https://tablepress.org/documentation/">TablePress Documentation</a></p>'
120 + . '<p><a href="https://tablepress.org/support/">TablePress Support</a></p>'
121 + );
113 122 }
114 123
115 124 /**
116 - * Change the value of the user option "screen_layout_{$screen->id}" through a filter.
125 + * Changes the value of the user option "screen_layout_{$screen->id}" through a filter.
117 126 *
118 127 * @since 1.0.0
119 128 *
120 - * @param int|bool Current value of the user option.
129 + * @param string|int|false $result Current value of the user option.
121 130 * @return int New value for the user option.
122 131 */
123 - public function set_current_screen_layout_columns( $result ) {
132 + public function set_current_screen_layout_columns( /* string|int|false */ $result ): int {
124 133 if ( false === $result ) {
125 134 // The user option does not yet exist.
126 - $result = $this->screen_columns;
127 - } elseif ( $result > $this->screen_columns ) {
135 + return $this->screen_columns;
136 + }
137 +
138 + $result = (int) $result;
139 + if ( $result > $this->screen_columns ) {
128 140 // The value of the user option is bigger than what is possible on this screen (e.g. because the number of columns was reduced in an update).
129 141 $result = $this->screen_columns;
130 142 }
143 +
131 144 return $result;
132 145 }
133 146
134 147 /**
135 - * Set up the view with data and do things that are necessary for all views.
148 + * Sets up the view with data and do things that are necessary for all views.
136 149 *
137 150 * @since 1.0.0
138 151 *
139 - * @param string $action Action for this view.
140 - * @param array $data Data for this view.
152 + * @param string $action Action for this view.
153 + * @param array<string, mixed> $data Data for this view.
141 154 */
142 - public function setup( $action, array $data ) {
155 + public function setup( /* string */ $action, array $data ) /* : void */ {
156 + // Don't use type hints (except array $data) in method declaration, as the method is extended in some TablePress Extensions which are no longer updated.
157 +
143 158 $this->action = $action;
144 159 $this->data = $data;
145 160
146 161 // Set page title.
147 - $GLOBALS['title'] = sprintf( __( '%1$s &lsaquo; %2$s', 'tablepress' ), $this->data['view_actions'][ $this->action ]['page_title'], 'TablePress' );
162 + $GLOBALS['title'] = sprintf( __( '%1$s &lsaquo; %2$s', 'tablepress' ), $this->data['view_actions'][ $this->action ]['page_title'], 'TablePress' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
148 163
149 - // Admin page helpers, like script/style loading, could be moved to view.
150 - $this->admin_page = TablePress::load_class( 'TablePress_Admin_Page', 'class-admin-page-helper.php', 'classes' );
151 - $this->admin_page->enqueue_style( 'common' );
164 + add_filter( 'admin_footer_text', array( $this, 'add_admin_footer_text' ) );
165 +
166 + TablePress::enqueue_style( 'common', array( 'wp-components' ) );
152 167 // RTL styles for the admin interface.
153 168 if ( is_rtl() ) {
154 - $this->admin_page->enqueue_style( 'common-rtl', array( 'tablepress-common' ) );
169 + TablePress::enqueue_style( 'common-rtl', array( 'tablepress-common' ) );
155 170 }
156 - $this->admin_page->enqueue_script( 'common', array( 'jquery-core', 'postbox' ), array(
157 - 'common' => array(
158 - 'ays_delete_single_table' => _n( 'Do you really want to delete this table?', 'Do you really want to delete these tables?', 1, 'tablepress' ),
159 - 'ays_delete_multiple_tables' => _n( 'Do you really want to delete this table?', 'Do you really want to delete these tables?', 2, 'tablepress' ),
160 - )
161 - ) );
162 171
163 - $this->admin_page->add_admin_footer_text();
164 -
165 172 // Initialize WP feature pointers for TablePress.
166 173 $this->_init_wp_pointers();
167 174
168 175 // Necessary fields for all views.
@@ -171,47 +178,60 @@
171 178 $this->add_text_box( 'action_field', array( $this, 'action_field' ), 'header', false );
172 179 }
173 180
174 181 /**
175 - * Register a header message for the view.
182 + * Registers a header message for the view.
176 183 *
177 184 * @since 1.0.0
178 185 *
179 - * @param string $text Text for the header message.
180 - * @param string $class Optional. Additional CSS class for the header message.
186 + * @param string $text Text for the header message.
187 + * @param string $css_class Optional. Additional CSS class for the header message.
188 + * @param string $title Optional. Text for the header title.
181 189 */
182 - protected function add_header_message( $text, $class = 'notice-success' ) {
183 - if ( ! stripos( $class, 'not-dismissible' ) ) {
184 - $class .= ' is-dismissible';
190 + protected function add_header_message( string $text, string $css_class = 'is-success notice-success', string $title = '' ): void {
191 + if ( ! str_contains( $css_class, 'not-dismissible' ) ) {
192 + $css_class .= ' is-dismissible';
185 193 }
186 - $this->header_messages[] = "<div class=\"notice {$class}\"><p>{$text}</p></div>\n";
194 + if ( '' !== $title ) {
195 + $title = "<h3>{$title}</h3>";
196 + }
197 + // Wrap the message text in HTML <p> tags if it does not already start with one (potentially with attributes), indicating custom message HTML.
198 + if ( '' !== $text && ! str_starts_with( $text, '<p' ) ) {
199 + $text = "<p>{$text}</p>";
200 + }
201 + $this->header_messages[] = "<div class=\"notice components-notice {$css_class}\"><div class=\"components-notice__content\">{$title}{$text}</div></div>\n";
187 202 }
188 203
189 204 /**
190 - * Process header action messages, i.e. check if a message should be added to the page.
205 + * Processes header action messages, i.e. check if a message should be added to the page.
191 206 *
192 207 * @since 1.0.0
193 208 *
194 - * @param array $action_messages Action messages for the screen.
209 + * @param array<string, string> $action_messages Action messages for the screen.
195 210 */
196 - protected function process_action_messages( array $action_messages ) {
211 + protected function process_action_messages( array $action_messages ): void {
197 212 if ( $this->data['message'] && isset( $action_messages[ $this->data['message'] ] ) ) {
198 - $class = ( 'error' === substr( $this->data['message'], 0, 5 ) ) ? 'notice-error' : 'notice-success';
199 - $this->add_header_message( "<strong>{$action_messages[ $this->data['message'] ]}</strong>", $class );
213 + $class = ( str_starts_with( $this->data['message'], 'error' ) ) ? 'is-error notice-error' : 'is-success notice-success';
214 +
215 + if ( '' !== $this->data['error_details'] ) {
216 + $this->data['error_details'] = '</p><p>' . sprintf( __( 'Error code: %s', 'tablepress' ), '<code>' . esc_html( $this->data['error_details'] ) . '</code>' );
217 + }
218 +
219 + $this->add_header_message( "<strong>{$action_messages[ $this->data['message'] ]}</strong>{$this->data['error_details']}", $class );
200 220 }
201 221 }
202 222
203 223 /**
204 - * Register a text box for the view.
224 + * Registers a text box for the view.
205 225 *
206 226 * @since 1.0.0
207 227 *
208 228 * @param string $id Unique HTML ID for the text box container (only visible with $wrap = true).
209 - * @param callback $callback Callback that prints the contents of the text box.
229 + * @param callable $callback Callback that prints the contents of the text box.
210 230 * @param string $context Optional. Context/position of the text box (normal, side, additional, header, submit).
211 - * @param bool $ wrap Whether the content of the text box shall be wrapped in a <div> container.
231 + * @param bool $wrap Whether the content of the text box shall be wrapped in a <div> container.
212 232 */
213 - protected function add_text_box( $id, $callback, $context = 'normal', $wrap = false ) {
233 + protected function add_text_box( string $id, callable $callback, string $context = 'normal', bool $wrap = false ): void {
214 234 if ( ! isset( $this->textboxes[ $context ] ) ) {
215 235 $this->textboxes[ $context ] = array();
216 236 }
217 237
@@ -224,32 +244,32 @@
224 244 );
225 245 }
226 246
227 247 /**
228 - * Register a post meta box for the view, that is drag/droppable with WordPress functionality.
248 + * Registers a post meta box for the view, that is drag/droppable with WordPress functionality.
229 249 *
230 250 * @since 1.0.0
231 251 *
232 - * @param string $id Unique ID for the meta box.
233 - * @param string $title Title for the meta box.
234 - * @param callback $callback Callback that prints the contents of the post meta box.
235 - * @param string $context Optional. Context/position of the post meta box (normal, side, additional).
236 - * @param string $priority Optional. Order of the post meta box for the $context position (high, default, low).
237 - * @param bool $callback_args Optional. Additional data for the callback function (e.g. useful when in different class).
252 + * @param string $id Unique ID for the meta box.
253 + * @param string $title Title for the meta box.
254 + * @param callable $callback Callback that prints the contents of the post meta box.
255 + * @param 'normal'|'side'|'additional' $context Optional. Context/position of the post meta box (normal, side, additional).
256 + * @param 'core'|'default'|'high'|'low' $priority Optional. Order of the post meta box for the $context position (high, default, low).
257 + * @param mixed[]|null $callback_args Optional. Additional data for the callback function (e.g. useful when in different class).
238 258 */
239 - protected function add_meta_box( $id, $title, $callback, $context = 'normal', $priority = 'default', $callback_args = null ) {
259 + protected function add_meta_box( string $id, string $title, callable $callback, string $context = 'normal', string $priority = 'default', ?array $callback_args = null ): void {
240 260 $this->has_meta_boxes = true;
241 261 add_meta_box( "tablepress_{$this->action}-{$id}", $title, $callback, null, $context, $priority, $callback_args );
242 262 }
243 263
244 264 /**
245 - * Render all text boxes for the given context.
265 + * Renders all text boxes for the given context.
246 266 *
247 267 * @since 1.0.0
248 268 *
249 269 * @param string $context Context (normal, side, additional, header, submit) for which registered text boxes shall be rendered.
250 270 */
251 - protected function do_text_boxes( $context ) {
271 + protected function do_text_boxes( string $context ): void {
252 272 if ( empty( $this->textboxes[ $context ] ) ) {
253 273 return;
254 274 }
255 275
@@ -264,32 +284,31 @@
264 284 }
265 285 }
266 286
267 287 /**
268 - * Render all post meta boxes for the given context, if there are post meta boxes.
288 + * Renders all post meta boxes for the given context, if there are post meta boxes.
269 289 *
270 290 * @since 1.0.0
271 291 *
272 292 * @param string $context Context (normal, side, additional) for which registered post meta boxes shall be rendered.
273 293 */
274 - protected function do_meta_boxes( $context ) {
275 - if ( ! $this->has_meta_boxes ) {
276 - return;
294 + protected function do_meta_boxes( string $context ): void {
295 + if ( $this->has_meta_boxes ) {
296 + do_meta_boxes( get_current_screen(), $context, $this->data ); // @phpstan-ignore argument.type
277 297 }
278 - do_meta_boxes( null, $context, $this->data );
279 298 }
280 299
281 300 /**
282 - * Print hidden fields with nonces for post meta box AJAX handling, if there are post meta boxes on the screen.
301 + * Prints hidden fields with nonces for post meta box AJAX handling, if there are post meta boxes on the screen.
283 302 *
284 303 * The check is possible as this function is executed after post meta boxes have to be registered.
285 304 *
286 305 * @since 1.0.0
287 306 *
288 - * @param array $data Data for this screen.
289 - * @param array $box Information about the text box.
307 + * @param array<string, mixed> $data Data for this screen.
308 + * @param array<string, mixed> $box Information about the text box.
290 309 */
291 - protected function default_nonce_fields( array $data, array $box ) {
310 + protected function default_nonce_fields( array $data, array $box ): void {
292 311 if ( ! $this->has_meta_boxes ) {
293 312 return;
294 313 }
295 314 wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', false );
@@ -298,54 +317,60 @@
298 317 echo "\n";
299 318 }
300 319
301 320 /**
302 - * Print hidden field with a nonce for the screen's action, to be transmitted in HTTP requests.
321 + * Prints hidden field with a nonce for the screen's action, to be transmitted in HTTP requests.
303 322 *
304 323 * @since 1.0.0
305 324 *
306 - * @param array $data Data for this screen.
307 - * @param array $box Information about the text box.
325 + * @param array<string, mixed> $data Data for this screen.
326 + * @param array<string, mixed> $box Information about the text box.
308 327 */
309 - protected function action_nonce_field( array $data, array $box ) {
328 + protected function action_nonce_field( array $data, array $box ): void {
310 329 wp_nonce_field( TablePress::nonce( $this->action ) );
311 330 echo "\n";
312 331 }
313 332
314 333 /**
315 - * Print hidden field with the screen action.
334 + * Prints hidden field with the screen action.
316 335 *
317 336 * @since 1.0.0
318 337 *
319 - * @param array $data Data for this screen.
320 - * @param array $box Information about the text box.
338 + * @param array<string, mixed> $data Data for this screen.
339 + * @param array<string, mixed> $box Information about the text box.
321 340 */
322 - protected function action_field( array $data, array $box ) {
323 - echo "<input type=\"hidden\" name=\"action\" value=\"tablepress_{$this->action}\" />\n";
341 + protected function action_field( array $data, array $box ): void {
342 + echo "<input type=\"hidden\" name=\"action\" value=\"tablepress_{$this->action}\">\n";
324 343 }
325 344
326 345 /**
327 - * Render the current view.
346 + * Renders the current view.
328 347 *
329 348 * @since 1.0.0
330 349 */
331 - public function render() {
350 + public function render(): void {
332 351 ?>
333 352 <div id="tablepress-page" class="wrap">
334 353 <?php
335 354 $this->print_nav_tab_menu();
336 - // Print all header messages.
337 - foreach ( $this->header_messages as $message ) {
338 - echo $message;
339 - }
340 - // "Import" screen has file upload.
341 - $enctype = ( 'import' === $this->action ) ? ' enctype="multipart/form-data"' : '';
342 355 ?>
343 - <form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post"<?php echo $enctype; ?>>
356 + <div id="tablepress-body">
357 + <hr class="wp-header-end">
358 + <script>document.querySelectorAll('.notice:has(~#tablepress-page)').forEach(e=>document.querySelector('.wp-header-end').after(e));</script>
359 + <?php
360 + // Print all header messages.
361 + foreach ( $this->header_messages as $message ) {
362 + echo $message;
363 + }
364 + // "Import" screen has file upload.
365 + $enctype = ( 'import' === $this->action ) ? ' enctype="multipart/form-data"' : '';
366 + ?>
367 + <form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post"<?php echo $enctype; ?> id="tablepress-page-form">
344 368 <?php
345 - $this->do_text_boxes( 'header' );
369 + $this->do_text_boxes( 'header' );
370 + $hide_if_no_js = ( in_array( $this->action, array( 'export', 'import' ), true ) ) ? ' class="hide-if-no-js"' : '';
346 371 ?>
347 - <div id="poststuff">
372 + <div id="poststuff"<?php echo $hide_if_no_js; ?>>
348 373 <div id="post-body" class="metabox-holder columns-<?php echo ( isset( $GLOBALS['screen_layout_columns'] ) && ( 2 === $GLOBALS['screen_layout_columns'] ) ) ? '2' : '1'; ?>">
349 374 <div id="postbox-container-2" class="postbox-container">
350 375 <?php
351 376 $this->do_text_boxes( 'normal' );
@@ -365,24 +390,52 @@
365 390 $this->do_meta_boxes( 'side' );
366 391 ?>
367 392 </div>
368 393 </div>
369 - <br class="clear" />
394 + <br class="clear">
370 395 </div>
371 396 </form>
372 397 </div>
398 + </div>
373 399 <?php
374 400 }
375 401
376 402 /**
377 - * Render the navigation menu with links to the possible actions, highlighting the current one.
403 + * Renders the navigation menu with links to the possible actions, highlighting the current one.
378 404 *
379 405 * @since 1.0.0
380 406 */
381 - protected function print_nav_tab_menu() {
407 + protected function print_nav_tab_menu(): void {
408 + $milestone_period_ended = ( strtotime( '2026-10-10' ) < strtotime( 'today' ) );
409 + $name = __( 'TablePress', 'tablepress' );
410 + $filename = 'admin/img/tablepress.svg';
382 411 ?>
383 - <div id="tablepress-nav" class="nav-tab-wrapper">
384 - <h1 class="wp-heading-inline"><?php _e( 'TablePress', 'tablepress' ); ?></h1>
412 + <div id="tablepress-header" class="header">
413 + <h1 class="name">
414 + <img src="<?php echo plugins_url( $filename, TABLEPRESS__FILE__ ); ?>" alt="<?php esc_attr_e( 'TablePress plugin logo', 'tablepress' ); ?>">
415 + <span class="screen-reader-text"><?php echo $name; ?></span>
416 + </h1>
417 + <?php if ( 'list' !== $this->action || ! $this->data['messages']['plugin_update'] || $milestone_period_ended ) : ?>
418 + <?php if ( ! TABLEPRESS_IS_PLAYGROUND_PREVIEW && tb_tp_fs()->is_free_plan() ) : ?>
419 + <div class="buttons">
420 + <a href="<?php echo $milestone_period_ended ? esc_url( tb_tp_fs()->pricing_url( WP_FS__PERIOD_ANNUALLY, false ) ) : 'https://tablepress.org/premium/?utm_source=plugin&utm_medium=header&utm_campaign=upgrade-message340&utm_content=button'; ?>" class="tablepress-button">
421 + <span>
422 + <?php
423 + if ( $milestone_period_ended ) {
424 + _e( 'Upgrade to Premium', 'tablepress' );
425 + } else {
426 + echo in_array( get_user_locale(), array( 'de_DE', 'de_AT', 'de_CH' ), true ) ? 'Premium: Spare jetzt 25&#8239;%' : 'Premium: Get 25% off now';
427 + }
428 + ?>
429 + </span>
430 + <span class="dashicons dashicons-arrow-right-alt"></span>
431 + </a>
432 + </div>
433 + <?php endif; ?>
434 + <?php endif; ?>
435 + </div>
436 + <nav id="tablepress-nav">
437 + <ul class="nav-menu">
385 438 <?php
386 439 foreach ( $this->data['view_actions'] as $action => $entry ) {
387 440 if ( '' === $entry['nav_tab_title'] ) {
388 441 continue;
@@ -391,50 +444,146 @@
391 444 continue;
392 445 }
393 446
394 447 $url = esc_url( TablePress::url( array( 'action' => $action ) ) );
395 - $active = ( $action === $this->action ) ? ' nav-tab-active' : '';
396 - $separator = ( 'options' === $action ) ? ' nav-tab-separator' : ''; // Make the "Plugin Options" entry a separator, for some spacing.
397 - echo "<a class=\"nav-tab{$active}{$separator}\" href=\"{$url}\">{$entry['nav_tab_title']}</a>";
448 + $active = ( $action === $this->action ) ? ' active' : '';
449 + $separator = ( 'export' === $action ) ? ' separator' : ''; // Make the "Export" entry a separator, for some spacing.
450 + echo "<li class=\"nav-item\"><a id=\"tablepress-nav-item-{$action}\" class=\"nav-link{$active}{$separator}\" href=\"{$url}\">{$entry['nav_tab_title']}</a></li>";
398 451 }
399 452 ?>
400 - </div><hr class="wp-header-end" />
453 + </ul>
454 + </nav>
401 455 <?php
402 456 }
403 457
404 458 /**
405 - * Print a submit button (only done when function is used as a callback for a text box).
459 + * Prints a notification about JavaScript not being activated in the browser.
406 460 *
461 + * @since 2.0.0
462 + *
463 + * @param array<string, mixed> $data Data for this screen.
464 + * @param array<string, mixed> $box Information about the text box.
465 + */
466 + public function textbox_no_javascript( array $data, array $box ): void {
467 + ?>
468 + <div class="notice components-notice is-error notice-error hide-if-js">
469 + <div class="components-notice__content">
470 + <h3><em>
471 + <?php _e( 'Attention: Unfortunately, there is a problem!', 'tablepress' ); ?>
472 + </em></h3>
473 + <p style="font-size:14px">
474 + <strong><?php _e( 'This screen requires JavaScript. Please enable JavaScript in your browser settings.', 'tablepress' ); ?></strong><br>
475 + <?php _e( 'For help, please follow <a href="https://www.enable-javascript.com/">the instructions on how to enable JavaScript in your browser</a>.', 'tablepress' ); ?>
476 + </p>
477 + <p>
478 + <?php echo '<a href="' . esc_url( TablePress::url( array( 'action' => 'list' ) ) ) . '">' . __( 'Back to the List of Tables', 'tablepress' ) . '</a>'; ?>
479 + </p>
480 + </div>
481 + </div>
482 + <?php
483 + }
484 +
485 + /**
486 + * Prints a submit button (only done when function is used as a callback for a text box).
487 + *
488 + * This method is soft-deprecated. It's no longer used in TablePress, but e.g. in the "TablePress Debug Extension".
489 + *
407 490 * @since 1.0.0
408 491 *
409 - * @param array $data Data for this screen.
410 - * @param array $box Information about the text box.
492 + * @param array<string, mixed> $data Data for this screen.
493 + * @param array<string, mixed> $box Information about the text box.
411 494 */
412 - protected function textbox_submit_button( array $data, array $box ) {
413 - $caption = isset( $data['submit_button_caption'] ) ? $data['submit_button_caption'] : __( 'Save Changes', 'tablepress' );
495 + protected function textbox_submit_button( array $data, array $box ): void {
414 496 ?>
415 - <p class="submit"><input type="submit" value="<?php echo esc_attr( $caption ); ?>" class="button button-primary button-large" name="submit" /></p>
497 + <p class="submit"><input type="submit" class="components-button is-primary button-save-changes" value="<?php esc_attr_e( 'Save Changes', 'tablepress' ); ?>"></p>
416 498 <?php
417 499 }
418 500
419 501 /**
420 - * Return the content for the help tab for this screen.
502 + * Returns a safe JSON representation of a variable for printing inside of JavaScript code.
421 503 *
504 + * @since 2.0.0
505 + *
506 + * @param mixed $data Variable to convert to JSON.
507 + * @return string Safe JSON representation of a variable for printing inside of JavaScript code.
508 + */
509 + public function convert_to_json_parse_output( /* string|array|bool|int|float|null */ $data ): string {
510 + $json = wp_json_encode( $data, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES );
511 + if ( false === $json ) {
512 + // JSON encoding failed, return an error object. Use a prefixed "_error" key to avoid conflicts with intentionally added "error" keys.
513 + $json = '{ "_error": "The data could not be encoded to JSON!" }';
514 + }
515 + // Print the JSON data inside a `JSON.parse()` call in JS for speed gains, with necessary escaping of `\` and `'`.
516 + $json = str_replace( array( '\\', "'" ), array( '\\\\', "\'" ), $json );
517 + return "JSON.parse( '{$json}' )";
518 + }
519 +
520 + /**
521 + * Prints JavaScript variables for the screen.
522 + *
523 + * @since 3.1.0
524 + *
525 + * @param string $variable Name of the JavaScript variable.
526 + * @param array<string, mixed> $data Information about the text box.
527 + */
528 + protected function print_script_data_json( string $variable, array $data ): void {
529 + echo "<script>\n";
530 + echo "window.tp = window.tp || {};\n";
531 + echo "tp.{$variable} = {\n";
532 + foreach ( $data as $key => $value ) {
533 + $value = $this->convert_to_json_parse_output( $value );
534 + echo "\t{$key}: {$value},\n";
535 + }
536 + echo "};\n";
537 + echo "</script>\n";
538 + }
539 +
540 + /**
541 + * Returns the content for the help tab for this screen.
542 + *
422 543 * Has to be implemented for every view that is visible in the WP Dashboard!
423 544 *
424 545 * @since 1.0.0
546 + *
547 + * @return string Help tab content for the view.
425 548 */
426 - protected function help_tab_content() {
549 + protected function help_tab_content(): string {
427 550 // Has to be implemented for every view that is visible in the WP Dashboard!
428 551 return '';
429 552 }
430 553
431 554 /**
432 - * Initialize the WP feature pointers for TablePress.
555 + * Adds a TablePress "Thank You" message to the admin footer content.
433 556 *
434 557 * @since 1.0.0
558 + * @since 3.3.0 This method was moved from the now-removed `TablePress_Admin_Page` class.
559 + *
560 + * @param string $content Current admin footer content.
561 + * @return string New admin footer content.
435 562 */
436 - protected function _init_wp_pointers() {
563 + public function add_admin_footer_text( /* string */ $content ): string {
564 + // Don't use a type hint in the method declaration as many WordPress plugins use the `admin_footer_text` filter without returning a string.
565 +
566 + // Protect against other plugins not returning a string in their filter callbacks.
567 + if ( ! is_string( $content ) ) { // @phpstan-ignore function.alreadyNarrowedType (The `is_string()` check is needed as the input is coming from a filter hook.)
568 + $content = '';
569 + }
570 +
571 + /* translators: %s: URL to TablePress website */
572 + $content .= ' &bull; ' . sprintf( __( 'Thank you for using <a href="%s">TablePress</a>.', 'tablepress' ), 'https://tablepress.org/' );
573 + if ( tb_tp_fs()->is_free_plan() ) {
574 + /* translators: %s: URL to TablePress premium features */
575 + $content .= ' ' . sprintf( __( 'Take a look at the <a href="%s">Premium features</a>!', 'tablepress' ), 'https://tablepress.org/premium/?utm_source=plugin&utm_medium=textlink&utm_content=admin-footer' );
576 + }
577 + return $content;
578 + }
579 +
580 + /**
581 + * Initializes the WP feature pointers for TablePress.
582 + *
583 + * @since 1.0.0
584 + */
585 + protected function _init_wp_pointers(): void {
437 586 // Check if there are WP pointers for this view.
438 587 if ( empty( $this->wp_pointers ) ) {
439 588 return;
440 589 }
@@ -441,21 +590,74 @@
441 590
442 591 // Get dismissed pointers.
443 592 $dismissed = explode( ',', (string) get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true ) );
444 593
445 - $got_pointers = false;
594 + $pointers_on_page = false;
446 595 foreach ( array_diff( $this->wp_pointers, $dismissed ) as $pointer ) {
447 596 // Bind pointer print function.
448 - add_action( "admin_footer-{$GLOBALS['hook_suffix']}", array( $this, 'wp_pointer_' . $pointer ) );
449 - $got_pointers = true;
597 + add_action( "admin_footer-{$GLOBALS['hook_suffix']}", array( $this, 'wp_pointer_' . $pointer ) ); // @phpstan-ignore argument.type
598 + $pointers_on_page = true;
450 599 }
451 600
452 - if ( ! $got_pointers ) {
601 + if ( $pointers_on_page ) {
602 + wp_enqueue_style( 'wp-pointer' );
603 + wp_enqueue_script( 'wp-pointer' );
604 + }
605 + }
606 +
607 + /**
608 + * Prints the JavaScript code for a WP feature pointer.
609 + *
610 + * @since 1.0.0
611 + * @since 3.3.0 This method was moved from the now-removed `TablePress_Admin_Page` class.
612 + *
613 + * @param string $pointer_id The pointer ID.
614 + * @param string $selector The HTML elements, on which the pointer should be attached.
615 + * @param array<string, mixed> $args Arguments to be passed to the pointer JS (see wp-pointer.js).
616 + */
617 + public function print_wp_pointer_js( string $pointer_id, string $selector, array $args ): void {
618 + if ( empty( $pointer_id ) || empty( $selector ) || empty( $args['content'] ) ) {
453 619 return;
454 620 }
455 621
456 - // Add pointers script and style to queue.
457 - wp_enqueue_style( 'wp-pointer' );
458 - wp_enqueue_script( 'wp-pointer' );
622 + $keyboard_shortcut = '';
623 + if ( 'tp33_edit_quick_navigation' === $pointer_id ) {
624 + $keyboard_shortcut = <<<JS
625 + content: options.content.replace( /%metaKey%/g, window?.navigator?.platform?.includes( 'Mac' ) ? wp.i18n._x( '⌘', 'keyboard shortcut modifier key on a Mac keyboard', 'tablepress' ) : wp.i18n._x( 'Ctrl+', 'keyboard shortcut modifier key on a non-Mac keyboard', 'tablepress' ) ),\n
626 + JS;
627 + }
628 +
629 + /*
630 + * Print JS code for the feature pointers, extended with event handling for opened/closed "Screen Options", so that pointers can
631 + * be repositioned. 210 ms is slightly slower than jQuery's "fast" value, to allow all elements to reach their original position.
632 + */
633 + ?>
634 +<script>
635 +( ( $ ) => {
636 + let options = <?php echo wp_json_encode( $args, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ); ?>;
637 + if ( ! options ) {
638 + return;
639 + }
640 +
641 + options = {
642 + ...options,
643 + <?php echo $keyboard_shortcut; ?>
644 + close() {
645 + $.post( ajaxurl, {
646 + pointer: '<?php echo $pointer_id; ?>',
647 + action: 'dismiss-wp-pointer'
648 + } );
649 + $( this ).pointer( { 'disabled': true } );
650 + },
651 + };
652 +
653 + $( () => setTimeout( () => $( '<?php echo $selector; ?>' ).pointer( options ).pointer( 'open' ), 1 ) );
654 +
655 + $( document ).on( 'screen:options:open screen:options:close', () => {
656 + setTimeout( () => $( '<?php echo $selector; ?>' ).pointer( 'reposition' ), 210 );
657 + } );
658 +} )( jQuery );
659 +</script>
660 + <?php
459 661 }
460 662
461 663 } // class TablePress_View