| @@ -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(__('▶ Child shortcodes (%d)', 'table-builder-block'), $count)); | |
| 77 | - $label_close = esc_js(sprintf(__('▼ 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( __( '▶ Child shortcodes (%d)', 'table-builder-block' ), $count ) ); | |
| 151 | + /* translators: %d: Number of child shortcodes. */ | |
| 152 | + $label_close = esc_js( sprintf( __( '▼ 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(__('▶ Child shortcodes (%d)', 'table-builder-block'), $count)); ?> | |
| 159 | + <?php | |
| 160 | + /* translators: %d: Number of child shortcodes. */ | |
| 161 | + echo wp_kses_post( sprintf( __( '▶ 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 | } |