PluginProbe
TableKit – WordPress Table Builder for Data Tables, WooCommerce Product Tables & Post Tables / 2.2.14
TableKit – WordPress Table Builder for Data Tables, WooCommerce Product Tables & Post Tables v2.2.14
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
table-builder-block / includes / Config / CPT / VirtualPostBuilder.php

VirtualPostBuilder.php in TableKit – WordPress Table Builder for Data Tables, WooCommerce Product Tables & Post Tables 2.2.14, at includes/Config/CPT/VirtualPostBuilder.php

291 lines 10.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Builds synthetic "virtual" tablekit_table posts for inline-only table blocks
4 *
5 * @package TableKit
6 */
7
8 namespace TableBuilder\Config\CPT;
9
10 defined( 'ABSPATH' ) || exit;
11
12 use WP_Post;
13 use WP_Query;
14
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
21 private const VIRTUAL_ID_BASE = -1000000;
22
23 /**
24 * Scans post content for inline table blocks.
25 *
26 * @var InlineTableScanner
27 */
28 private InlineTableScanner $scanner;
29
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 ) {
36 $this->scanner = $scanner;
37 add_filter( 'post_row_actions', array( $this, 'strip_incompatible_row_actions' ), 1, 2 );
38 }
39
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 ) ) {
78 return $posts;
79 }
80
81 $inline_tables = $this->scanner->get_all_inline_block_tables();
82
83 if ( empty( $inline_tables ) ) {
84 return $posts;
85 }
86
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 );
90 }
91
92 return $posts;
93 }
94
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() );
105
106 foreach ( array_values( $grouped ) as $index => $group ) {
107 $map[ self::VIRTUAL_ID_BASE - $index ] = (int) $group['source_id'];
108 }
109
110 return $map;
111 }
112
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 {
120 echo '<div class="tablekit-shortcode-wrap" style="position:relative;">';
121 printf(
122 '<input type="text" class="tablekit-shortcode-field" readonly value="%s" style="width:100%%;padding-right:32px;" />',
123 esc_attr( $shortcode )
124 );
125 $copy_label = esc_attr__( 'Copy shortcode', 'table-builder-block' );
126 printf(
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;">'
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>'
129 . '</button>',
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.
133 );
134 echo '</div>';
135 }
136
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 ?>
154 <div class="tablekit-child-shortcode-toggle-wrap">
155 <button
156 type="button"
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)"
158 class="tablekit-child-shortcode-toggle">
159 <?php
160 /* translators: %d: Number of child shortcodes. */
161 echo wp_kses_post( sprintf( __( '&#9654; Child shortcodes (%d)', 'table-builder-block' ), $count ) );
162 ?>
163 </button>
164 </div>
165 <div id="<?php echo esc_attr( $panel_id ); ?>" class="tablekit-child-shortcode-panel" style="display:none;">
166 <?php foreach ( $blocks as $block ) : ?>
167 <div class="tablekit-child-shortcode-item">
168 <span class="tablekit-child-shortcode-label">
169 <?php echo esc_html( TableCPT::get_block_label( $block['block_name'] ) ); ?>
170 </span>
171 <?php $child_shortcode = $this->build_child_shortcode( $block, $source_id ); ?>
172 <?php $this->render_shortcode_input( $child_shortcode ); ?>
173 </div>
174 <?php endforeach; ?>
175 </div>
176 <?php
177 }
178
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 {
186 return is_admin()
187 && $query->is_main_query()
188 && TableCPT::POST_TYPE === $query->get( 'post_type' );
189 }
190
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 {
198 $grouped = array();
199
200 foreach ( $inline_tables as $table ) {
201 $sid = (int) $table['source_id'];
202
203 if ( ! isset( $grouped[ $sid ] ) ) {
204 $grouped[ $sid ] = array(
205 'source_id' => $sid,
206 'source_title' => $table['source_title'],
207 'source_type' => $table['source_type'],
208 'source_author' => $table['source_author'],
209 'source_date' => $table['source_date'],
210 'blocks' => array(),
211 );
212 }
213
214 $grouped[ $sid ]['blocks'][] = array(
215 'block_name' => $table['block_name'],
216 'block_id' => $table['block_id'],
217 'shortcode_id' => $table['shortcode_id'],
218 );
219 }
220
221 return $grouped;
222 }
223
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'] );
234
235 $title = sprintf(
236 /* translators: %2$s = source post title. */
237 _n( 'Table in: %2$s', 'Tables in: %2$s', $block_count, 'table-builder-block' ),
238 $block_count,
239 $group['source_title']
240 );
241
242 $virtual_post = new WP_Post(
243 (object) array(
244 'ID' => self::VIRTUAL_ID_BASE - $index,
245 'post_title' => $title,
246 'post_type' => TableCPT::POST_TYPE,
247 'post_status' => 'publish',
248 'post_author' => 0,
249 'post_date' => $group['source_date'],
250 'post_date_gmt' => get_gmt_from_date( $group['source_date'] ),
251 'post_content' => '',
252 'post_excerpt' => '',
253 'post_name' => '',
254 'post_modified' => $group['source_date'],
255 'post_modified_gmt' => get_gmt_from_date( $group['source_date'] ),
256 'comment_status' => 'closed',
257 'ping_status' => 'closed',
258 'guid' => '',
259 'menu_order' => 0,
260 'post_mime_type' => '',
261 'comment_count' => 0,
262 'filter' => 'raw',
263 )
264 );
265
266 $virtual_post->tablekit_is_virtual = true;
267 $virtual_post->tablekit_source_id = $group['source_id'];
268 $virtual_post->tablekit_source_title = $group['source_title'];
269 $virtual_post->tablekit_source_type = $group['source_type'];
270 $virtual_post->tablekit_source_author = $group['source_author'];
271 $virtual_post->tablekit_blocks = $group['blocks'];
272
273 return $virtual_post;
274 }
275
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'] );
286 }
287
288 return sprintf( '[tableKit post_id="%d"]', $source_id );
289 }
290 }
291