PluginProbe
TableKit – WordPress Table Builder for Data Tables, WooCommerce Product Tables & Post Tables / trunk
TableKit – WordPress Table Builder for Data Tables, WooCommerce Product Tables & Post Tables vtrunk
2.2.13 2.2.12 2.2.11 2.2.10 2.2.9 2.2.8 2.2.7 2.2.6 2.2.5 2.2.4 2.2.3 trunk 1.0.0 1.0.1 2.0.0 2.0.1 2.1.0 2.1.1 2.1.2 2.2.0 2.2.1 2.2.2
table-builder-block / includes / Elementor / TablekitWidget.php

TablekitWidget.php in TableKit – WordPress Table Builder for Data Tables, WooCommerce Product Tables & Post Tables trunk, at includes/Elementor/TablekitWidget.php

424 lines 12.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Elementor widget for TableKit tables
4 *
5 * @package TableKit
6 */
7
8 use TableBuilder\Shortcode\ShortcodeUtils;
9
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit;
12 }
13
14 /**
15 * Renders a TableKit table as a native Elementor widget.
16 */
17 class TableKit_Elementor_Widget extends \Elementor\Widget_Base {
18
19 private const ALL_BLOCKS = '';
20
21 /**
22 * Whether the shared global styles have been printed yet this request.
23 *
24 * @var bool
25 */
26 private static bool $global_styles_printed = false;
27
28 /**
29 * Block IDs whose per-instance styles have already been printed.
30 *
31 * @var array
32 */
33 private static array $printed_block_styles = array();
34
35 /**
36 * Block IDs whose per-instance scripts have already been printed.
37 *
38 * @var array
39 */
40 private static array $printed_block_scripts = array();
41
42 /**
43 * Cached table options list, or null if not yet built.
44 *
45 * @var array|null
46 */
47 private static ?array $table_options_cache = null;
48
49 // -------------------------------------------------------------------------
50 // Identity
51 // -------------------------------------------------------------------------
52
53 /**
54 * Elementor widget's internal name/slug.
55 *
56 * @return string
57 */
58 public function get_name(): string {
59 return 'tablekit_table';
60 }
61
62 /**
63 * Elementor widget's display title (shown in the widget panel).
64 *
65 * @return string
66 */
67 public function get_title(): string {
68 return __( 'TableKit', 'table-builder-block' );
69 }
70
71 /**
72 * Elementor widget's icon class.
73 *
74 * @return string
75 */
76 public function get_icon(): string {
77 return 'eicon-table';
78 }
79
80 /**
81 * Elementor category slugs this widget is listed under.
82 *
83 * @return string[]
84 */
85 public function get_categories(): array {
86 return array( 'general' );
87 }
88
89 /**
90 * Search keywords Elementor's widget panel matches against.
91 *
92 * @return string[]
93 */
94 public function get_keywords(): array {
95 return array( 'table', 'tablekit', 'data', 'grid' );
96 }
97
98 /**
99 * Whether Elementor should force a full preview reload when this widget's
100 * settings change (true, since table selection/rendering isn't done via
101 * live-refreshable controls).
102 *
103 * @return bool
104 */
105 public function is_reload_preview_required(): bool {
106 return true;
107 }
108
109 // -------------------------------------------------------------------------
110 // Controls
111 // -------------------------------------------------------------------------
112
113 /**
114 * Registers the widget's Elementor editor controls (table picker, block picker).
115 *
116 * @return void
117 */
118 protected function register_controls(): void {
119 $this->start_controls_section(
120 'tablekit_table_section',
121 array(
122 'label' => __( 'Table', 'table-builder-block' ),
123 'tab' => \Elementor\Controls_Manager::TAB_CONTENT,
124 )
125 );
126
127 $this->add_control(
128 'table_id',
129 array(
130 'label' => __( 'Select Table', 'table-builder-block' ),
131 'type' => \Elementor\Controls_Manager::SELECT2,
132 'options' => $this->get_table_options(),
133 'default' => '',
134 'render_type' => 'template',
135 'label_block' => true,
136 'description' => __( 'Create and manage tables under TableKit → Tables in the WP admin.', 'table-builder-block' ),
137 )
138 );
139
140 $this->add_control(
141 'block_index',
142 array(
143 'label' => __( 'Select Block', 'table-builder-block' ),
144 'type' => \Elementor\Controls_Manager::SELECT,
145 'options' => array( '' => __( '— All Blocks —', 'table-builder-block' ) ),
146 'default' => self::ALL_BLOCKS,
147 'render_type' => 'template',
148 'label_block' => true,
149 'description' => __( 'Choose a specific block to display. Or select — All Blocks — to show all blocks.', 'table-builder-block' ),
150 )
151 );
152
153 $this->end_controls_section();
154 }
155
156 // -------------------------------------------------------------------------
157 // Rendering
158 // -------------------------------------------------------------------------
159
160 /**
161 * Renders the widget on the frontend/editor: resolves the selected table post,
162 * optionally narrow to one block, print shared/per-table CSS, render the
163 * table block(s), and print their view scripts.
164 *
165 * @return void
166 */
167 protected function render(): void {
168 $settings = $this->get_settings_for_display();
169 $raw_id = $settings['table_id'] ?? 0;
170
171 if ( is_array( $raw_id ) ) {
172 $raw_id = reset( $raw_id );
173 }
174
175 $table_id = absint( $raw_id );
176
177 if ( empty( $table_id ) ) {
178 $this->render_placeholder( __( 'Select a table to display.', 'table-builder-block' ) );
179 return;
180 }
181
182 $post = get_post( $table_id );
183
184 if ( ! $post ) {
185 $this->render_placeholder( __( 'Table not found.', 'table-builder-block' ) );
186 return;
187 }
188
189 if ( class_exists( '\\TableBuilder\\Config\\Blocks' ) ) {
190 \TableBuilder\Config\Blocks::instance()->enqueue_block_assets();
191 }
192
193 $all_blocks = parse_blocks( $post->post_content );
194 $table_blocks = $this->filter_table_blocks( $all_blocks );
195 $block_index = $settings['block_index'] ?? self::ALL_BLOCKS;
196 $blocks_to_render = $all_blocks;
197
198 if ( self::ALL_BLOCKS !== $block_index && '' !== $block_index ) {
199 $index = (int) $block_index;
200 if ( isset( $table_blocks[ $index ] ) ) {
201 $blocks_to_render = array( $table_blocks[ $index ] );
202 }
203 }
204
205 $block_names = $this->collect_block_names( $blocks_to_render );
206
207 $this->print_shared_styles( $block_names );
208 $this->print_table_styles( $table_id, $post->post_content );
209
210 echo '<div class="tablekit-elementor-wrap">';
211 foreach ( $blocks_to_render as $block ) {
212 echo ( new WP_Block( $block ) )->render(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- standard core block-rendering output, same as render_block().
213 }
214 echo '</div>';
215
216 $this->print_block_scripts( $block_names );
217
218 if ( class_exists( '\\TableBuilder\\Shortcode\\ShortcodeUtils' ) ) {
219 echo \TableBuilder\Shortcode\ShortcodeUtils::get_elementor_init_script(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static hardcoded <script> string, no user input.
220 }
221 }
222
223 /**
224 * No-op: this widget renders server-side only (render() above), Elementor's
225 * JS live-preview template is intentionally unused.
226 *
227 * @return void
228 */
229 protected function content_template(): void {}
230
231 // -------------------------------------------------------------------------
232 // Private helpers
233 // -------------------------------------------------------------------------
234
235 /**
236 * Renders a simple placeholder message in place of the table (used when no
237 * table is selected, or the selected table post can't be found).
238 *
239 * @param string $message Message to display.
240 * @return void
241 */
242 private function render_placeholder( string $message ): void {
243 echo '<div class="tablekit-el-placeholder" style="padding:1.5em;border:1px dashed #ccc;text-align:center;color:#999;">'
244 . '<span class="eicon-table" style="font-size:2em;display:block;margin-bottom:.5em;opacity:.35;"></span>'
245 . '<p style="margin:0;">' . esc_html( $message ) . '</p>'
246 . '</div>';
247 }
248
249 /**
250 * Narrow a parsed block tree down to just the plugin's table blocks.
251 *
252 * @param array $blocks Parsed block tree (as from parse_blocks()).
253 * @return array Table blocks only.
254 */
255 private function filter_table_blocks( array $blocks ): array {
256 return ShortcodeUtils::filter_table_blocks( $blocks );
257 }
258
259 /**
260 * Prints shared CSS once per page load.
261 *
262 * @param string[] $block_names Block names appearing in this render, used to
263 * additionally print each block's registered style once.
264 * @return void
265 */
266 private function print_shared_styles( array $block_names ): void {
267 $css = '';
268
269 if ( ! self::$global_styles_printed ) {
270 foreach ( array( 'global.css', 'components.css' ) as $filename ) {
271 $path = TABLE_BUILDER_BLOCK_PLUGIN_DIR . 'build/tablebuilder/' . $filename;
272 if ( file_exists( $path ) ) {
273 $css .= file_get_contents($path) . "\n"; // phpcs:ignore
274 }
275 }
276
277 $css .= $this->get_registered_style_css( 'tablebuilder/table-builder' );
278 self::$global_styles_printed = true;
279 self::$printed_block_styles['tablebuilder/table-builder'] = true;
280 }
281
282 foreach ( $block_names as $name ) {
283 if ( isset( self::$printed_block_styles[ $name ] ) ) {
284 continue;
285 }
286 $css .= $this->get_registered_style_css( $name );
287 self::$printed_block_styles[ $name ] = true;
288 }
289
290 if ( '' !== trim( $css ) ) {
291 echo '<style id="tablekit-shared-styles">' . $css . '</style>'; // phpcs:ignore
292 }
293 }
294
295 /**
296 * Prints per-table dynamic CSS. Uses a unique ID per table so multiple widgets coexist.
297 *
298 * @param int $table_id Table post ID, used to build a unique <style> element ID.
299 * @param string $post_content Raw post content to collect block-generated CSS from.
300 * @return void
301 */
302 private function print_table_styles( int $table_id, string $post_content ): void {
303 if ( ! class_exists( '\\TableBuilder\\Shortcode\\Shortcode' ) ) {
304 return;
305 }
306
307 $shortcode = \TableBuilder\Shortcode\Shortcode::instance();
308
309 if ( ! $shortcode || ! method_exists( $shortcode, 'collect_blocks_css_from_content' ) ) {
310 return;
311 }
312
313 $css = $shortcode->collect_blocks_css_from_content( $post_content );
314
315 if ( '' !== trim( $css ) ) {
316 echo '<style id="tablekit-el-css-' . esc_attr((string) $table_id) . '">' . $css . '</style>'; // phpcs:ignore
317 }
318 }
319
320 /**
321 * Enqueues and immediately prints block view-scripts inline.
322 * Required for the editor iframe where wp_footer never fires.
323 *
324 * @param string[] $block_names Block names to print view scripts for, in addition
325 * to the base "tablebuilder/table-builder" script.
326 * @return void
327 */
328 private function print_block_scripts( array $block_names ): void {
329 // FIX 8: only print once per full or partial render cycle.
330 static $printed = array();
331
332 foreach ( array_unique( array_merge( array( 'tablebuilder/table-builder' ), $block_names ) ) as $name ) {
333 $handle = \TableBuilder\Shortcode\ShortcodeUtils::get_block_asset_handle( $name, 'viewScript' );
334 if ( isset( $printed[ $handle ] ) ) {
335 continue;
336 }
337 wp_enqueue_script( $handle );
338 wp_print_scripts( $handle );
339 $printed[ $handle ] = true;
340 }
341 }
342
343 /**
344 * Reads the CSS file of a registered WP stylesheet handle.
345 *
346 * @param string $block_name Block name to resolve the registered "style" asset handle for.
347 * @return string The stylesheet's contents, or an empty string if not found/registered.
348 */
349 private function get_registered_style_css( string $block_name ): string {
350 $handle = \TableBuilder\Shortcode\ShortcodeUtils::get_block_asset_handle( $block_name, 'style' );
351 $style = wp_styles()->query( $handle, 'registered' );
352
353 if ( ! $style || empty( $style->src ) ) {
354 return '';
355 }
356
357 $path = str_replace( content_url(), WP_CONTENT_DIR, (string) $style->src );
358 $path = (string) strtok( $path, '?' );
359
360 if ( ! file_exists( $path ) ) {
361 return '';
362 }
363
364 return file_get_contents($path) . "\n"; // phpcs:ignore
365 }
366
367 /**
368 * Recursively collects unique block names from a block tree.
369 *
370 * @param array $blocks Parsed block tree.
371 * @return string[] Unique block names found in the tree.
372 */
373 private function collect_block_names( array $blocks ): array {
374 $all = ShortcodeUtils::collect_blocks_recursive(
375 $blocks,
376 static fn( array $block ): bool => is_string( $block['blockName'] ?? null ) && '' !== $block['blockName']
377 );
378
379 return array_values( array_unique( array_column( $all, 'blockName' ) ) );
380 }
381
382 /**
383 * Builds the SELECT2 options array for the table picker control (published
384 * tablekit_table posts plus any inline-block source posts), memoized per request.
385 *
386 * @return array<string,string> Options keyed by post ID (as a string), valued by label.
387 */
388 private function get_table_options(): array {
389 if ( is_array( self::$table_options_cache ) ) {
390 return self::$table_options_cache;
391 }
392
393 $options = array( '' => __( '-- Select a Table --', 'table-builder-block' ) );
394
395 $tables = get_posts(
396 array(
397 'post_type' => 'tablekit_table',
398 'post_status' => 'publish',
399 'posts_per_page' => -1,
400 'orderby' => 'title',
401 'order' => 'ASC',
402 )
403 );
404
405 foreach ( $tables as $table ) {
406 $options[ (string) $table->ID ] = esc_html( $table->post_title );
407 }
408
409 if ( class_exists( '\\TableBuilder\\Config\\CPT\\TableCPT' ) ) {
410 $inline = \TableBuilder\Config\CPT\TableCPT::instance()->get_inline_table_source_options();
411 foreach ( $inline as $id => $label ) {
412 $key = (string) $id;
413 if ( ! isset( $options[ $key ] ) ) {
414 $options[ $key ] = esc_html( $label );
415 }
416 }
417 }
418
419 self::$table_options_cache = $options;
420
421 return $options;
422 }
423 }
424