PluginProbe
TableKit – WordPress Table Builder for Data Tables, WooCommerce Product Tables & Post Tables / 2.3.0
TableKit – WordPress Table Builder for Data Tables, WooCommerce Product Tables & Post Tables v2.3.0
2.3.0 2.2.14 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
← All changes | includes/Config/CPT/VirtualPostBuilder.php +162 -61 2.2.8 → 2.3.0 View file →
@@ -1,120 +1,208 @@
1 1 <?php
2 +/**
3 + * Builds synthetic "virtual" tablekit_table posts for inline-only table blocks
4 + *
5 + * @package TableKit
6 + */
2 7
3 8 namespace TableBuilder\Config\CPT;
4 9
5 -defined('ABSPATH') || exit;
10 +defined( 'ABSPATH' ) || exit;
6 11
7 12 use WP_Post;
8 13 use WP_Query;
9 14
10 -class VirtualPostBuilder
11 -{
15 +/**
16 + * Creates and renders synthetic WP_Post rows for table blocks that exist
17 + * inline in other posts' content, so they can appear in the Tables admin list.
18 + */
19 +class VirtualPostBuilder {
20 +
12 21 private const VIRTUAL_ID_BASE = -1000000;
13 22
23 + /**
24 + * Scans post content for inline table blocks.
25 + *
26 + * @var InlineTableScanner
27 + */
14 28 private InlineTableScanner $scanner;
15 29
16 - public function __construct(InlineTableScanner $scanner)
17 - {
30 + /**
31 + * Stores the scanner used to locate inline table blocks.
32 + *
33 + * @param InlineTableScanner $scanner Scanner used to locate inline table blocks.
34 + */
35 + public function __construct( InlineTableScanner $scanner ) {
18 36 $this->scanner = $scanner;
37 + add_filter( 'post_row_actions', array( $this, 'strip_incompatible_row_actions' ), 1, 2 );
19 38 }
20 39
21 - public function inject_inline_block_tables(array $posts, WP_Query $query): array
22 - {
23 - if (! $this->is_cpt_admin_list_query($query)) {
40 + /**
41 + * @param array $actions Row action links, keyed by action name.
42 + * @param WP_Post $post The row's post (real or virtual).
43 + * @return array Unmodified row actions (only third-party hook state is changed).
44 + */
45 + public function strip_incompatible_row_actions( array $actions, WP_Post $post ): array {
46 + if ( empty( $post->tablekit_is_virtual ) ) {
47 + return $actions;
48 + }
49 +
50 + global $wp_filter;
51 + if ( ! isset( $wp_filter['post_row_actions'] ) || ! ( $wp_filter['post_row_actions'] instanceof \WP_Hook ) ) {
52 + return $actions;
53 + }
54 +
55 + foreach ( $wp_filter['post_row_actions']->callbacks as $priority => $callbacks ) {
56 + foreach ( $callbacks as $callback ) {
57 + $fn = $callback['function'];
58 + if ( is_string( $fn ) && false !== strpos( $fn, '_collaboration_row_actions' ) ) {
59 + remove_filter( 'post_row_actions', $fn, $priority );
60 + }
61 + }
62 + }
63 +
64 + return $actions;
65 + }
66 +
67 + /**
68 + * Append synthetic "virtual" WP_Post rows to a query's results — one per
69 + * source post containing inline table blocks — so they show up in the
70 + * Tables admin list table alongside real tablekit_table posts.
71 + *
72 + * @param WP_Post[] $posts Posts already matched by the query.
73 + * @param WP_Query $query The query being filtered.
74 + * @return WP_Post[] Posts with virtual entries appended, when applicable.
75 + */
76 + public function inject_inline_block_tables( array $posts, WP_Query $query ): array {
77 + if ( ! $this->is_cpt_admin_list_query( $query ) ) {
24 78 return $posts;
25 79 }
26 80
27 81 $inline_tables = $this->scanner->get_all_inline_block_tables();
28 82
29 - if (empty($inline_tables)) {
83 + if ( empty( $inline_tables ) ) {
30 84 return $posts;
31 85 }
32 86
33 - $grouped = $this->group_inline_tables_by_source($inline_tables);
34 - foreach (array_values($grouped) as $index => $group) {
35 - $posts[] = $this->build_virtual_post($group, $index);
87 + $grouped = $this->group_inline_tables_by_source( $inline_tables );
88 + foreach ( array_values( $grouped ) as $index => $group ) {
89 + $posts[] = $this->build_virtual_post( $group, $index );
36 90 }
37 91
38 92 return $posts;
39 93 }
40 94
41 - public function get_virtual_source_id_map(): array
42 - {
43 - $map = array();
44 - $grouped = $this->group_inline_tables_by_source($this->scanner->get_all_inline_block_tables());
95 + /**
96 + * Map each virtual (negative) post ID back to the real source post ID it
97 + * represents, so bulk actions on virtual rows can be redirected to the
98 + * underlying real post.
99 + *
100 + * @return array<int,int> Virtual post ID => real source post ID.
101 + */
102 + public function get_virtual_source_id_map(): array {
103 + $map = array();
104 + $grouped = $this->group_inline_tables_by_source( $this->scanner->get_all_inline_block_tables() );
45 105
46 - foreach (array_values($grouped) as $index => $group) {
47 - $map[self::VIRTUAL_ID_BASE - $index] = (int) $group['source_id'];
106 + foreach ( array_values( $grouped ) as $index => $group ) {
107 + $map[ self::VIRTUAL_ID_BASE - $index ] = (int) $group['source_id'];
48 108 }
49 109
50 110 return $map;
51 111 }
52 112
53 - public function render_shortcode_input(string $shortcode): void
54 - {
113 + /**
114 + * Renders a read-only shortcode text field with a copy-to-clipboard button.
115 + *
116 + * @param string $shortcode The shortcode text to display, e.g. `[tableKit post_id="12"]`.
117 + * @return void
118 + */
119 + public function render_shortcode_input( string $shortcode ): void {
55 120 echo '<div class="tablekit-shortcode-wrap" style="position:relative;">';
56 121 printf(
57 122 '<input type="text" class="tablekit-shortcode-field" readonly value="%s" style="width:100%%;padding-right:32px;" />',
58 - esc_attr($shortcode)
123 + esc_attr( $shortcode )
59 124 );
60 - $copy_label = esc_attr__('Copy shortcode', 'table-builder-block');
125 + $copy_label = esc_attr__( 'Copy shortcode', 'table-builder-block' );
61 126 printf(
62 127 '<button type="button" class="tablekit-copy-shortcode" data-copy-text="%s" aria-label="%s" title="%s" style="position:absolute;right:6px;top:50%%;transform:translateY(-50%%);border:0;background:transparent;color:#2271b1;cursor:pointer;padding:0;line-height:1;">'
63 128 . '<span class="tablekit-copy-icon" aria-hidden="true"><svg viewBox="0 0 20 20" focusable="false"><rect x="7" y="5" width="9" height="11" rx="2"></rect><path d="M4 12.5V4a2 2 0 0 1 2-2h7.5"></path></svg></span>'
64 129 . '</button>',
65 - esc_attr($shortcode),
66 - $copy_label,
67 - $copy_label
130 + esc_attr( $shortcode ),
131 + $copy_label, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already run through esc_attr__() above.
132 + $copy_label // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already run through esc_attr__() above.
68 133 );
69 134 echo '</div>';
70 135 }
71 136
72 - public function render_child_shortcode_panel(array $blocks, int $post_id, int $source_id): void
73 - {
74 - $panel_id = 'tablekit-children-' . absint($post_id);
75 - $count = count($blocks);
76 - $label_open = esc_js(sprintf(__('&#9654; Child shortcodes (%d)', 'table-builder-block'), $count));
77 - $label_close = esc_js(sprintf(__('&#9660; Child shortcodes (%d)', 'table-builder-block'), $count));
78 - ?>
137 + /**
138 + * Renders a collapsible panel listing shortcodes for each child table
139 + * block found within a source post.
140 + *
141 + * @param array $blocks Child block descriptors (block_name/block_id/shortcode_id).
142 + * @param int $post_id The (real or virtual) post ID this panel belongs to.
143 + * @param int $source_id The real source post ID the shortcodes should target.
144 + * @return void
145 + */
146 + public function render_child_shortcode_panel( array $blocks, int $post_id, int $source_id ): void {
147 + $panel_id = 'tablekit-children-' . absint( $post_id );
148 + $count = count( $blocks );
149 + /* translators: %d: Number of child shortcodes. */
150 + $label_open = esc_js( sprintf( __( '&#9654; Child shortcodes (%d)', 'table-builder-block' ), $count ) );
151 + /* translators: %d: Number of child shortcodes. */
152 + $label_close = esc_js( sprintf( __( '&#9660; Child shortcodes (%d)', 'table-builder-block' ), $count ) );
153 + ?>
79 154 <div class="tablekit-child-shortcode-toggle-wrap">
80 155 <button
81 156 type="button"
82 - onclick="(function(btn){var p=document.getElementById('<?php echo esc_js($panel_id); ?>');var o=p.style.display!=='none';p.style.display=o?'none':'block';btn.innerHTML=o?'<?php echo $label_open; ?>':'<?php echo $label_close; ?>';})(this)"
157 + onclick="(function(btn){var p=document.getElementById('<?php echo esc_js( $panel_id ); ?>');var o=p.style.display!=='none';p.style.display=o?'none':'block';btn.innerHTML=o?'<?php echo $label_open; /* phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- pre-escaped via esc_js() above. */ ?>':'<?php echo $label_close; /* phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- pre-escaped via esc_js() above. */ ?>';})(this)"
83 158 class="tablekit-child-shortcode-toggle">
84 - <?php echo wp_kses_post(sprintf(__('&#9654; Child shortcodes (%d)', 'table-builder-block'), $count)); ?>
159 + <?php
160 + /* translators: %d: Number of child shortcodes. */
161 + echo wp_kses_post( sprintf( __( '&#9654; Child shortcodes (%d)', 'table-builder-block' ), $count ) );
162 + ?>
85 163 </button>
86 164 </div>
87 - <div id="<?php echo esc_attr($panel_id); ?>" class="tablekit-child-shortcode-panel" style="display:none;">
88 - <?php foreach ($blocks as $block) : ?>
165 + <div id="<?php echo esc_attr( $panel_id ); ?>" class="tablekit-child-shortcode-panel" style="display:none;">
166 + <?php foreach ( $blocks as $block ) : ?>
89 167 <div class="tablekit-child-shortcode-item">
90 168 <span class="tablekit-child-shortcode-label">
91 - <?php echo esc_html(TableCPT::get_block_label($block['block_name'])); ?>
169 + <?php echo esc_html( TableCPT::get_block_label( $block['block_name'] ) ); ?>
92 170 </span>
93 - <?php $child_shortcode = $this->build_child_shortcode($block, $source_id); ?>
94 - <?php $this->render_shortcode_input($child_shortcode); ?>
171 + <?php $child_shortcode = $this->build_child_shortcode( $block, $source_id ); ?>
172 + <?php $this->render_shortcode_input( $child_shortcode ); ?>
95 173 </div>
96 174 <?php endforeach; ?>
97 175 </div>
98 -<?php
176 + <?php
99 177 }
100 178
101 - private function is_cpt_admin_list_query(WP_Query $query): bool
102 - {
179 + /**
180 + * Checks whether a query is the main admin list-table query for the Tables CPT.
181 + *
182 + * @param WP_Query $query The query to check.
183 + * @return bool True if this is the Tables admin list query.
184 + */
185 + private function is_cpt_admin_list_query( WP_Query $query ): bool {
103 186 return is_admin()
104 187 && $query->is_main_query()
105 - && TableCPT::POST_TYPE === $query->get('post_type');
188 + && TableCPT::POST_TYPE === $query->get( 'post_type' );
106 189 }
107 190
108 - private function group_inline_tables_by_source(array $inline_tables): array
109 - {
191 + /**
192 + * Group flat inline-table records by their source post ID.
193 + *
194 + * @param array $inline_tables Flat list of inline table records from InlineTableScanner.
195 + * @return array<int,array> Records grouped by source_id, each with a "blocks" sub-array.
196 + */
197 + private function group_inline_tables_by_source( array $inline_tables ): array {
110 198 $grouped = array();
111 199
112 - foreach ($inline_tables as $table) {
200 + foreach ( $inline_tables as $table ) {
113 201 $sid = (int) $table['source_id'];
114 202
115 - if (! isset($grouped[$sid])) {
116 - $grouped[$sid] = array(
203 + if ( ! isset( $grouped[ $sid ] ) ) {
204 + $grouped[ $sid ] = array(
117 205 'source_id' => $sid,
118 206 'source_title' => $table['source_title'],
119 207 'source_type' => $table['source_type'],
120 208 'source_author' => $table['source_author'],
@@ -122,9 +210,9 @@
122 210 'blocks' => array(),
123 211 );
124 212 }
125 213
126 - $grouped[$sid]['blocks'][] = array(
214 + $grouped[ $sid ]['blocks'][] = array(
127 215 'block_name' => $table['block_name'],
128 216 'block_id' => $table['block_id'],
129 217 'shortcode_id' => $table['shortcode_id'],
130 218 );
@@ -132,15 +220,22 @@
132 220
133 221 return $grouped;
134 222 }
135 223
136 - private function build_virtual_post(array $group, int $index): WP_Post
137 - {
138 - $block_count = count($group['blocks']);
224 + /**
225 + * Builds a synthetic (never-saved) WP_Post representing one source post's
226 + * group of inline table blocks, for display in the Tables admin list table.
227 + *
228 + * @param array $group A single group from group_inline_tables_by_source().
229 + * @param int $index Zero-based index used to derive a stable negative virtual ID.
230 + * @return WP_Post The virtual post, with tablekit_* properties attached.
231 + */
232 + private function build_virtual_post( array $group, int $index ): WP_Post {
233 + $block_count = count( $group['blocks'] );
139 234
140 - /* translators: %2$s = source post title. */
141 235 $title = sprintf(
142 - _n('Table in: %2$s', 'Tables in: %2$s', $block_count, 'table-builder-block'),
236 + /* translators: %2$s = source post title. */
237 + _n( 'Table in: %2$s', 'Tables in: %2$s', $block_count, 'table-builder-block' ),
143 238 $block_count,
144 239 $group['source_title']
145 240 );
146 241
@@ -151,14 +246,14 @@
151 246 'post_type' => TableCPT::POST_TYPE,
152 247 'post_status' => 'publish',
153 248 'post_author' => 0,
154 249 'post_date' => $group['source_date'],
155 - 'post_date_gmt' => get_gmt_from_date($group['source_date']),
250 + 'post_date_gmt' => get_gmt_from_date( $group['source_date'] ),
156 251 'post_content' => '',
157 252 'post_excerpt' => '',
158 253 'post_name' => '',
159 254 'post_modified' => $group['source_date'],
160 - 'post_modified_gmt' => get_gmt_from_date($group['source_date']),
255 + 'post_modified_gmt' => get_gmt_from_date( $group['source_date'] ),
161 256 'comment_status' => 'closed',
162 257 'ping_status' => 'closed',
163 258 'guid' => '',
164 259 'menu_order' => 0,
@@ -177,13 +272,19 @@
177 272
178 273 return $virtual_post;
179 274 }
180 275
181 - private function build_child_shortcode(array $block, int $source_id): string
182 - {
183 - if ('' !== $block['shortcode_id']) {
184 - return sprintf('[tableKit id="%s"]', $block['shortcode_id']);
276 + /**
277 + * Builds the [tableKit] shortcode text for a single child block.
278 + *
279 + * @param array $block Block descriptor with a "shortcode_id" key.
280 + * @param int $source_id Real source post ID to fall back to when the block has no shortcode_id.
281 + * @return string The shortcode text.
282 + */
283 + private function build_child_shortcode( array $block, int $source_id ): string {
284 + if ( '' !== $block['shortcode_id'] ) {
285 + return sprintf( '[tableKit id="%s"]', $block['shortcode_id'] );
185 286 }
186 287
187 - return sprintf('[tableKit post_id="%d"]', $source_id);
288 + return sprintf( '[tableKit post_id="%d"]', $source_id );
188 289 }
189 290 }