PluginProbe
SheetsPilot – AI Spreadsheet Bulk Edit for Posts, WooCommerce Products & SEO / trunk
SheetsPilot – AI Spreadsheet Bulk Edit for Posts, WooCommerce Products & SEO vtrunk
sheetspilot / inc_php / plugins_jetengine_class.php

plugins_jetengine_class.php in SheetsPilot – AI Spreadsheet Bulk Edit for Posts, WooCommerce Products & SEO trunk, at inc_php/plugins_jetengine_class.php

1,047 lines 28.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package SheetsPilot
4 * @author Unlimited Elements
5 * @copyright (C) 2026 Unlimited Elements, All Rights Reserved.
6 * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
7 **/
8 if ( ! defined( 'ABSPATH' ) ) exit;
9 if (!defined("SHEETSPILOT_INC")) die("restricted access");
10
11 class SheetsPilotPluginsJetEngine
12 {
13 /**
14 * JetEngine layout rows that are not stored meta fields.
15 *
16 * @var string[]
17 */
18 private static $skipObjectTypes = array( 'html', 'tab', 'accordion', 'endpoint' );
19
20 function __construct(){
21 add_filter('sheetspilot_filter_table_columns', array( $this, 'filter_table_columns' ), 10, 2);
22 add_filter('sheetspilot_filter_table_values', array( $this, 'filter_table_values' ), 10, 3);
23 add_filter('sheetspilot_filter_table_fields', array( $this, 'filter_table_fields' ), 10, 3);
24 }
25
26 /**
27 * Register JetEngine meta fields as table columns.
28 *
29 * @param array $columns
30 * @param string $postType
31 * @return array
32 */
33 public function filter_table_columns( $columns, $postType ){
34 foreach ( self::get_fields( $postType ) as $field_data ) {
35 $columns[] = self::get_column_args( $field_data );
36 }
37
38 return $columns;
39 }
40
41 /**
42 * Add JetEngine cell values for one post.
43 *
44 * @param array $values
45 * @param string $postType
46 * @param int $postId
47 * @return array
48 */
49 public function filter_table_values( $values, $postType, $postId ){
50 foreach ( self::get_fields( $postType ) as $field_data ) {
51 $values[][ 'plugins_' . $field_data['name'] ] = self::get_cell_value( $postId, $field_data );
52 }
53
54 return $values;
55 }
56
57 /**
58 * Allow JetEngine column keys through the row field whitelist.
59 *
60 * @param array $values
61 * @param string $postType
62 * @param int $postId
63 * @return array
64 */
65 public function filter_table_fields( $values, $postType, $postId ){
66 foreach ( self::get_fields( $postType ) as $field_data ) {
67 $values[] = 'plugins_' . $field_data['name'];
68 }
69
70 return $values;
71 }
72
73 /**
74 * @param string $column
75 * @return bool
76 */
77 public static function is_column( $column ){
78 return is_string( $column ) && strpos( $column, 'plugins_' ) === 0;
79 }
80
81 /**
82 * Convert a SheetsPilot cell value to the format JetEngine stores, then save.
83 * Non-JetEngine `plugins_` keys pass through unchanged.
84 *
85 * @param int $post_id
86 * @param string $column Column name including the plugins_ prefix.
87 * @param mixed $value
88 * @return bool
89 */
90 public static function save_column_meta( $post_id, $column, $value ){
91 if ( ! self::is_column( $column ) ) {
92 return false;
93 }
94
95 $meta_key = substr( $column, 8 );
96 $post_type = get_post_type( $post_id );
97 $field = $post_type ? self::get_field( $post_type, $meta_key ) : null;
98
99 // Repeater rows are saved from the drawer, not from the table cell.
100 if ( $field && 'repeater' === $field['type'] ) {
101 return false;
102 }
103
104 $value = self::prepare_save_value( $post_id, $meta_key, $value );
105 update_post_meta( $post_id, $meta_key, $value );
106
107 return true;
108 }
109
110 /**
111 * Convert a table cell value into JetEngine post meta.
112 *
113 * @param int $post_id
114 * @param string $meta_key
115 * @param mixed $value
116 * @return mixed
117 */
118 public static function prepare_save_value( $post_id, $meta_key, $value ){
119 $post_type = get_post_type( $post_id );
120 $field = $post_type ? self::get_field( $post_type, $meta_key ) : null;
121
122 if ( ! $field ) {
123 return $value;
124 }
125
126 $type = $field['type'];
127
128 if ( 'switcher' === $type ) {
129 return self::is_switcher_on( $value ) ? 'true' : '';
130 }
131
132 if ( in_array( $type, array( 'radio', 'select' ), true ) ) {
133 $multiple = self::is_multiple_select( $field );
134
135 if ( is_array( $value ) ) {
136 $value = array_values( $value );
137 return $multiple ? $value : ( isset( $value[0] ) ? $value[0] : '' );
138 }
139
140 return $value;
141 }
142
143 if ( 'media' === $type ) {
144 $ids = self::ids_from_save_value( $value );
145 return self::format_media_for_save( isset( $ids[0] ) ? $ids[0] : 0, self::get_value_format( $field ) );
146 }
147
148 if ( 'gallery' === $type ) {
149 return self::format_gallery_for_save( self::ids_from_save_value( $value ), self::get_value_format( $field ) );
150 }
151
152 return $value;
153 }
154
155 /**
156 * Fields registered for a post type, excluding layout-only rows.
157 *
158 * @param string $post_type
159 * @return array
160 */
161 private static function get_fields( $post_type ){
162 if ( ! function_exists( 'jet_engine' ) || ! jet_engine()->meta_boxes ) {
163 return array();
164 }
165
166 $fields = jet_engine()->meta_boxes->get_fields_for_context( 'post_type', $post_type );
167 if ( ! is_array( $fields ) ) {
168 return array();
169 }
170
171 $out = array();
172 foreach ( $fields as $field_data ) {
173 if ( self::is_real_field( $field_data ) ) {
174 $out[] = $field_data;
175 }
176 }
177
178 return $out;
179 }
180
181 /**
182 * @param string $post_type
183 * @param string $field_name
184 * @return array|null
185 */
186 private static function get_field( $post_type, $field_name ){
187 foreach ( self::get_fields( $post_type ) as $field_data ) {
188 if ( $field_data['name'] === $field_name ) {
189 return $field_data;
190 }
191 }
192
193 return null;
194 }
195
196 /**
197 * @param array $field_data
198 * @return bool
199 */
200 private static function is_real_field( $field_data ){
201 if ( empty( $field_data['name'] ) || empty( $field_data['type'] ) ) {
202 return false;
203 }
204
205 $object_type = isset( $field_data['object_type'] ) ? $field_data['object_type'] : 'field';
206 if ( in_array( $object_type, self::$skipObjectTypes, true ) ) {
207 return false;
208 }
209
210 return true;
211 }
212
213 /**
214 * @param array $field_data
215 * @return array
216 */
217 private static function get_column_args( $field_data ){
218 $type = $field_data['type'];
219 $out_type = self::map_column_type( $type );
220 $source = self::get_column_source( $field_data );
221 $column_search = 'text';
222 $readonly = ! empty( $field_data['readonly'] );
223 $bottom_manage = '';
224
225 if ( in_array( $type, array( 'select', 'radio' ), true ) ) {
226 $column_search = 'filter';
227 }
228
229 if ( 'switcher' === $type ) {
230 $column_search = 'filter';
231 $source = array(
232 array( 'id' => 'no', 'name' => __( 'Off', 'sheetspilot' ) ),
233 array( 'id' => 'yes', 'name' => __( 'On', 'sheetspilot' ) ),
234 );
235 }
236
237 if ( 'wysiwyg' === $type ) {
238 $readonly = true;
239 $column_search = 'text';
240 $bottom_manage =
241 '<span class="edit_wysiwyg_field has-tooltip" data-title="' . esc_attr( SheetsPilotGlobals::$editWyswygText ) . '"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#737373" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"></path><path d="m15 5 4 4"></path></svg></span>
242 ';
243 }
244
245 if ( 'repeater' === $type ) {
246 $readonly = true;
247 $column_search = 'text';
248 $bottom_manage =
249 '<span class="new_edit_repeater_field has-tooltip" data-title="' . esc_attr( SheetsPilotGlobals::$editRepeaterText ) . '"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#737373" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"></path><path d="m15 5 4 4"></path></svg></span>
250 <span class="delete_repeater_data has-tooltip" data-title="' . esc_attr( SheetsPilotGlobals::$dropRepeaterText ) . '"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#737373" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2 w-3.5 h-3.5 text-muted-foreground"><path d="M3 6h18"></path><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path><line x1="10" x2="10" y1="11" y2="17"></line><line x1="14" x2="14" y1="11" y2="17"></line></svg></span>
251 ';
252 }
253
254 if ( 'media' === $type ) {
255 $readonly = true;
256 $column_search = 'text';
257 $bottom_manage =
258 SheetsPilotCellEditor::get_image_cell_download_icon_html() . '
259 <span class="inline_edit_image_field has-tooltip" data-title="' . esc_attr( SheetsPilotGlobals::$inlineEditImageText ) . '"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#737373" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"></path><path d="m15 5 4 4"></path></svg></span>
260 <span class="edit_image_field has-tooltip" data-title="' . esc_attr( SheetsPilotGlobals::$addImageText ) . '"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#737373" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-image w-3.5 h-3.5 text-muted-foreground"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"></rect><circle cx="9" cy="9" r="2"></circle><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"></path></svg></span>
261 <span class="has-tooltip delete_image_field" data-title="' . esc_attr( SheetsPilotGlobals::$deleteImageText ) . '"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#737373" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2 w-3.5 h-3.5 text-muted-foreground"><path d="M3 6h18"></path><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path><line x1="10" x2="10" y1="11" y2="17"></line><line x1="14" x2="14" y1="11" y2="17"></line></svg></span>
262 ';
263 }
264
265 if ( 'gallery' === $type ) {
266 $readonly = true;
267 $column_search = 'text';
268 $bottom_manage =
269 '<span class="add_gallery_image has-tooltip" data-title="' . esc_attr( SheetsPilotGlobals::$addGalleryImageText ) . '"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#737373" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"></path><path d="M12 5v14"></path></svg></span>
270 <span class="delete_all_images has-tooltip" data-title="' . esc_attr( SheetsPilotGlobals::$dropGalleryText ) . '"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#737373" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2 w-3.5 h-3.5 text-muted-foreground"><path d="M3 6h18"></path><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path><line x1="10" x2="10" y1="11" y2="17"></line><line x1="14" x2="14" y1="11" y2="17"></line></svg></span>';
271 }
272
273 $title = ! empty( $field_data['title'] ) ? $field_data['title'] : $field_data['name'];
274
275 return array(
276 'title' => $title,
277 'name' => 'plugins_' . $field_data['name'],
278 'width' => 100,
279 'type' => $out_type,
280 'dev_type' => 'meta_field',
281 'readonly' => $readonly,
282 'orderable' => true,
283 'source' => $source,
284 'switchable' => true,
285 'is_acf' => false,
286 'bottom_manage' => $bottom_manage,
287 'column_search' => $column_search,
288 'is_pro' => ( in_array( $out_type, SheetsPilotGlobals::$proFilesList, true ) ? true : false ),
289 );
290 }
291
292 /**
293 * Map a JetEngine field type to a SheetsPilot cell type.
294 *
295 * @param string $type
296 * @return string
297 */
298 private static function map_column_type( $type ){
299 if ( in_array( $type, array( 'select', 'radio' ), true ) ) {
300 return 'acf_select';
301 }
302 if ( in_array( $type, array( 'text', 'textarea' ), true ) ) {
303 return 'textarea';
304 }
305 if ( 'media' === $type ) {
306 return 'image';
307 }
308 if ( 'gallery' === $type ) {
309 return 'acf_gallery';
310 }
311 if ( 'switcher' === $type ) {
312 return 'switch';
313 }
314 if ( 'wysiwyg' === $type ) {
315 return 'wysiwyg';
316 }
317 if ( 'repeater' === $type ) {
318 return 'repeater';
319 }
320
321 return $type;
322 }
323
324 /**
325 * @param array $field_data
326 * @return array
327 */
328 private static function get_column_source( $field_data ){
329 if ( ! in_array( $field_data['type'], array( 'select', 'radio' ), true ) ) {
330 return array();
331 }
332
333 $source = array();
334 $options = isset( $field_data['options'] ) && is_array( $field_data['options'] ) ? $field_data['options'] : array();
335
336 foreach ( $options as $s_option ) {
337 if ( ! is_array( $s_option ) || ! isset( $s_option['key'] ) ) {
338 continue;
339 }
340
341 $source[] = array(
342 'id' => $s_option['key'],
343 'name' => isset( $s_option['value'] ) ? $s_option['value'] : $s_option['key'],
344 );
345 }
346
347 return $source;
348 }
349
350 /**
351 * @param int $post_id
352 * @param array $field_data
353 * @return mixed
354 */
355 private static function get_cell_value( $post_id, $field_data ){
356 $type = $field_data['type'];
357 $meta_key = $field_data['name'];
358 $raw = self::get_jet_meta( $post_id, $meta_key );
359
360 if ( 'media' === $type ) {
361 return self::format_media_cell_html( self::parse_attachment_id( $raw ) );
362 }
363
364 if ( 'gallery' === $type ) {
365 return array(
366 'values' => self::parse_gallery_items( $raw ),
367 );
368 }
369
370 if ( in_array( $type, array( 'select', 'radio' ), true ) ) {
371 return self::format_select_value( $raw, self::is_multiple_select( $field_data ) );
372 }
373
374 if ( 'switcher' === $type ) {
375 return self::is_switcher_on( $raw ) ? 'yes' : 'no';
376 }
377
378 if ( 'wysiwyg' === $type ) {
379 if ( $raw ) {
380 return substr( wp_strip_all_tags( (string) $raw ), 0, 100 );
381 }
382
383 return '';
384 }
385
386 if ( 'repeater' === $type ) {
387 return array(
388 'values' => is_array( $raw ) ? count( $raw ) : 0,
389 );
390 }
391
392 if ( is_array( $raw ) || is_object( $raw ) ) {
393 return '';
394 }
395
396 if ( $raw === false || $raw === null ) {
397 return '';
398 }
399
400 return (string) $raw;
401 }
402
403 /**
404 * Read a JetEngine meta value. Never uses ACF get_field().
405 *
406 * @param int $post_id
407 * @param string $meta_key
408 * @return mixed
409 */
410 private static function get_jet_meta( $post_id, $meta_key ){
411 $post = get_post( $post_id );
412 if ( ! $post ) {
413 return '';
414 }
415
416 if ( function_exists( 'jet_engine' ) ) {
417 $jet = jet_engine();
418 if ( $jet && ! empty( $jet->listings ) && ! empty( $jet->listings->data ) ) {
419 return $jet->listings->data->get_meta( $meta_key, $post );
420 }
421 }
422
423 return get_post_meta( $post_id, $meta_key, true );
424 }
425
426 /**
427 * Shape a radio/select value for the acf_select cell renderer.
428 *
429 * @param mixed $raw
430 * @param bool $multiple
431 * @return array
432 */
433 private static function format_select_value( $raw, $multiple ){
434 if ( $multiple ) {
435 $values = is_array( $raw ) ? array_values( $raw ) : ( ( $raw === '' || $raw === false || $raw === null ) ? array() : array( $raw ) );
436 } else {
437 if ( is_array( $raw ) ) {
438 $raw = isset( $raw[0] ) ? $raw[0] : '';
439 }
440 $values = ( $raw === '' || $raw === false || $raw === null ) ? array() : array( $raw );
441 }
442
443 return array(
444 'values' => $values,
445 'multiple' => $multiple ? 1 : 0,
446 );
447 }
448
449 /**
450 * @param array $field_data
451 * @return bool
452 */
453 private static function is_multiple_select( $field_data ){
454 if ( 'select' !== $field_data['type'] ) {
455 return false;
456 }
457
458 return ! empty( $field_data['is_multiple'] ) && filter_var( $field_data['is_multiple'], FILTER_VALIDATE_BOOLEAN );
459 }
460
461 /**
462 * JetEngine switcher stores true / 'true' / 1 when on.
463 *
464 * @param mixed $value
465 * @return bool
466 */
467 private static function is_switcher_on( $value ){
468 if ( is_bool( $value ) ) {
469 return $value;
470 }
471 if ( is_array( $value ) ) {
472 return false;
473 }
474
475 $value = strtolower( trim( (string) $value ) );
476
477 return in_array( $value, array( 'true', '1', 'yes', 'on' ), true );
478 }
479
480 /**
481 * @param array $field
482 * @return string id|url|both
483 */
484 private static function get_value_format( $field ){
485 $format = isset( $field['value_format'] ) ? $field['value_format'] : 'id';
486 if ( ! in_array( $format, array( 'id', 'url', 'both' ), true ) ) {
487 return 'id';
488 }
489
490 return $format;
491 }
492
493 /**
494 * @param mixed $raw
495 * @return mixed
496 */
497 private static function maybe_decode_media_value( $raw ){
498 if ( ! is_string( $raw ) ) {
499 return $raw;
500 }
501
502 $trim = trim( $raw );
503 if ( $trim === '' ) {
504 return $raw;
505 }
506
507 $first = $trim[0];
508 if ( '{' !== $first && '[' !== $first ) {
509 return $raw;
510 }
511
512 $decoded = json_decode( wp_unslash( $trim ), true );
513 return is_array( $decoded ) ? $decoded : $raw;
514 }
515
516 /**
517 * @param string $url
518 * @return int
519 */
520 private static function attachment_id_from_url( $url ){
521 if ( ! $url || ! is_string( $url ) ) {
522 return 0;
523 }
524
525 if ( class_exists( 'SheetsPilotCellEditor' ) ) {
526 $id = SheetsPilotCellEditor::get_image_id_by_url( $url );
527 if ( $id ) {
528 return (int) $id;
529 }
530 }
531
532 return (int) attachment_url_to_postid( $url );
533 }
534
535 /**
536 * Resolve a JetEngine media value (id, url, or both) to an attachment ID.
537 *
538 * @param mixed $raw
539 * @return int
540 */
541 private static function parse_attachment_id( $raw ){
542 $raw = self::maybe_decode_media_value( $raw );
543
544 if ( $raw === '' || $raw === false || $raw === null ) {
545 return 0;
546 }
547
548 if ( is_array( $raw ) ) {
549 if ( isset( $raw['id'] ) ) {
550 return (int) $raw['id'];
551 }
552 if ( isset( $raw['ID'] ) ) {
553 return (int) $raw['ID'];
554 }
555 if ( ! empty( $raw['url'] ) ) {
556 return self::attachment_id_from_url( $raw['url'] );
557 }
558
559 return 0;
560 }
561
562 if ( is_numeric( $raw ) ) {
563 return (int) $raw;
564 }
565
566 if ( is_string( $raw ) && preg_match( '#^https?://#i', $raw ) ) {
567 return self::attachment_id_from_url( $raw );
568 }
569
570 return 0;
571 }
572
573 /**
574 * @param mixed $raw
575 * @return array<int, array{id:int,url:string}>
576 */
577 private static function parse_gallery_items( $raw ){
578 $raw = self::maybe_decode_media_value( $raw );
579 $items = array();
580
581 if ( $raw === '' || $raw === false || $raw === null ) {
582 return $items;
583 }
584
585 if ( is_string( $raw ) ) {
586 $parts = array_filter( array_map( 'trim', explode( ',', $raw ) ) );
587 $raw = array_values( $parts );
588 }
589
590 if ( ! is_array( $raw ) ) {
591 $raw = array( $raw );
592 }
593
594 foreach ( $raw as $entry ) {
595 $id = 0;
596 $url = '';
597
598 if ( is_array( $entry ) ) {
599 $id = self::parse_attachment_id( $entry );
600 $url = ! empty( $entry['url'] ) ? $entry['url'] : '';
601 } elseif ( is_numeric( $entry ) ) {
602 $id = (int) $entry;
603 } elseif ( is_string( $entry ) && preg_match( '#^https?://#i', $entry ) ) {
604 $id = self::attachment_id_from_url( $entry );
605 $url = $entry;
606 }
607
608 if ( $id <= 0 ) {
609 continue;
610 }
611
612 if ( $url === '' ) {
613 $url = (string) wp_get_attachment_url( $id );
614 }
615
616 $items[] = array(
617 'id' => $id,
618 'url' => $url,
619 );
620 }
621
622 return $items;
623 }
624
625 /**
626 * @param int $image_id
627 * @return string
628 */
629 private static function format_media_cell_html( $image_id ){
630 $image_id = (int) $image_id;
631 $thumbnail = $image_id > 0 ? wp_get_attachment_url( $image_id ) : '';
632 $is_placeholder = false;
633
634 if ( $image_id <= 0 || ! $thumbnail ) {
635 $thumbnail = SheetsPilotGlobals::$urlImagePlaceholder;
636 $is_placeholder = true;
637 $image_id = 0;
638 }
639
640 return '<img src="' . esc_url( $thumbnail ) . '" data-full="' . esc_url( $thumbnail ) . '" data-id="' . $image_id . '"' . SheetsPilotHelper::getAttachmentImagePreviewDataAttrs( $image_id ) . ' class="ubai_featured_image_uploader sp_hover_preview ' . ( $is_placeholder ? 'is_placeholder' : '' ) . '" />';
641 }
642
643 /**
644 * @param mixed $value
645 * @return int[]
646 */
647 private static function ids_from_save_value( $value ){
648 if ( is_array( $value ) ) {
649 $ids = array();
650 foreach ( $value as $item ) {
651 if ( is_array( $item ) ) {
652 $id = self::parse_attachment_id( $item );
653 } else {
654 $id = (int) $item;
655 }
656 if ( $id > 0 ) {
657 $ids[] = $id;
658 }
659 }
660
661 return array_values( array_unique( $ids ) );
662 }
663
664 if ( $value === '' || $value === null || $value === false ) {
665 return array();
666 }
667
668 if ( is_numeric( $value ) ) {
669 $id = (int) $value;
670 return $id > 0 ? array( $id ) : array();
671 }
672
673 if ( is_string( $value ) && strpos( $value, ',' ) !== false ) {
674 return self::ids_from_save_value( explode( ',', $value ) );
675 }
676
677 $id = self::parse_attachment_id( $value );
678 return $id > 0 ? array( $id ) : array();
679 }
680
681 /**
682 * @param int $image_id
683 * @param string $format
684 * @return mixed
685 */
686 private static function format_media_for_save( $image_id, $format ){
687 $image_id = (int) $image_id;
688 if ( $image_id <= 0 ) {
689 return '';
690 }
691
692 $url = (string) wp_get_attachment_url( $image_id );
693
694 if ( 'url' === $format ) {
695 return $url;
696 }
697
698 if ( 'both' === $format ) {
699 return array(
700 'id' => $image_id,
701 'url' => $url,
702 );
703 }
704
705 return $image_id;
706 }
707
708 /**
709 * @param int[] $ids
710 * @param string $format
711 * @return mixed
712 */
713 private static function format_gallery_for_save( $ids, $format ){
714 $ids = array_values( array_filter( array_map( 'intval', (array) $ids ) ) );
715 if ( empty( $ids ) ) {
716 return '';
717 }
718
719 if ( 'url' === $format ) {
720 $urls = array();
721 foreach ( $ids as $id ) {
722 $url = wp_get_attachment_url( $id );
723 if ( $url ) {
724 $urls[] = $url;
725 }
726 }
727
728 return implode( ',', $urls );
729 }
730
731 if ( 'both' === $format ) {
732 $items = array();
733 foreach ( $ids as $id ) {
734 $items[] = array(
735 'id' => $id,
736 'url' => (string) wp_get_attachment_url( $id ),
737 );
738 }
739
740 return $items;
741 }
742
743 return implode( ',', $ids );
744 }
745
746 /**
747 * Structure + values for the shared repeater drawer (ACF-shaped payload).
748 *
749 * @param string $column
750 * @param int $post_id
751 * @return array
752 */
753 public static function get_repeater_drawer_data( $column, $post_id ){
754 $meta_key = self::column_to_meta_key( $column );
755 $post_type = get_post_type( $post_id );
756 $field = ( $meta_key && $post_type ) ? self::get_field( $post_type, $meta_key ) : null;
757
758 $structure = $field
759 ? self::field_to_drawer_structure( $field )
760 : array(
761 'id' => md5( (string) $meta_key ),
762 'key' => $meta_key,
763 'name' => $meta_key,
764 'label' => $meta_key,
765 'type' => 'repeater',
766 'sub_fields' => array(),
767 );
768
769 return array(
770 'structure' => $structure,
771 'values' => self::meta_to_drawer_values( self::get_jet_meta( $post_id, $meta_key ) ),
772 );
773 }
774
775 /**
776 * Save drawer rows as JetEngine item-0 / item-1 meta.
777 *
778 * @param string $column
779 * @param int $post_id
780 * @param array $rows Numeric list of row field maps from the drawer.
781 * @return bool
782 */
783 public static function save_repeater_from_drawer( $column, $post_id, $rows ){
784 if ( ! self::is_column( $column ) ) {
785 return false;
786 }
787
788 $meta_key = self::column_to_meta_key( $column );
789 $meta = self::rows_to_jetengine_meta( is_array( $rows ) ? $rows : array() );
790
791 if ( empty( $meta ) ) {
792 delete_post_meta( $post_id, $meta_key );
793 } else {
794 update_post_meta( $post_id, $meta_key, $meta );
795 }
796
797 return true;
798 }
799
800 /**
801 * @param string $column
802 * @param int $post_id
803 * @return bool
804 */
805 public static function delete_repeater_meta( $column, $post_id ){
806 if ( ! self::is_column( $column ) ) {
807 return false;
808 }
809
810 delete_post_meta( $post_id, self::column_to_meta_key( $column ) );
811
812 return true;
813 }
814
815 /**
816 * @param string $column
817 * @return string
818 */
819 private static function column_to_meta_key( $column ){
820 if ( ! self::is_column( $column ) ) {
821 return '';
822 }
823
824 return substr( $column, 8 );
825 }
826
827 /**
828 * Convert a JetEngine field (and nested repeater-fields) to the drawer schema.
829 *
830 * @param array $field
831 * @param string $parent_path
832 * @return array
833 */
834 private static function field_to_drawer_structure( $field, $parent_path = '' ){
835 $name = isset( $field['name'] ) ? $field['name'] : '';
836 $label = ! empty( $field['title'] ) ? $field['title'] : $name;
837 $type = self::map_repeater_subfield_type( isset( $field['type'] ) ? $field['type'] : 'text' );
838 $path = $parent_path === '' ? $name : $parent_path . '/' . $name;
839
840 $item = array(
841 'id' => md5( $path ),
842 'key' => $name,
843 'name' => $name,
844 'label' => $label,
845 'type' => $type,
846 'sub_fields' => array(),
847 );
848
849 if ( in_array( $type, array( 'select', 'radio', 'checkbox' ), true ) ) {
850 $item['choices'] = self::get_repeater_choices( $field );
851 if ( 'select' === $type ) {
852 $item['multiple'] = self::is_multiple_select( $field );
853 }
854 }
855
856 $sub_fields = isset( $field['repeater-fields'] ) && is_array( $field['repeater-fields'] )
857 ? $field['repeater-fields']
858 : array();
859
860 if ( $sub_fields ) {
861 foreach ( $sub_fields as $sub_field ) {
862 if ( ! self::is_real_field( $sub_field ) ) {
863 continue;
864 }
865 $item['sub_fields'][] = self::field_to_drawer_structure( $sub_field, $path );
866 }
867 }
868
869 return $item;
870 }
871
872 /**
873 * Map JetEngine subfield types to names the drawer already renders.
874 *
875 * @param string $type
876 * @return string
877 */
878 private static function map_repeater_subfield_type( $type ){
879 $map = array(
880 'colorpicker' => 'color_picker',
881 'date' => 'text',
882 'datetime-local' => 'text',
883 'time' => 'text',
884 'switcher' => 'text',
885 );
886
887 return isset( $map[ $type ] ) ? $map[ $type ] : $type;
888 }
889
890 /**
891 * @param array $field
892 * @return array
893 */
894 private static function get_repeater_choices( $field ){
895 $choices = array();
896 $options = isset( $field['options'] ) && is_array( $field['options'] ) ? $field['options'] : array();
897
898 foreach ( $options as $key => $option ) {
899 if ( is_array( $option ) ) {
900 $stored = isset( $option['key'] ) ? $option['key'] : $key;
901 $label = isset( $option['value'] ) ? $option['value'] : $stored;
902 $choices[ $stored ] = $label;
903 continue;
904 }
905
906 $choices[ $key ] = $option;
907 }
908
909 return $choices;
910 }
911
912 /**
913 * Convert stored JetEngine repeater meta to drawer values.
914 *
915 * @param mixed $raw
916 * @return array
917 */
918 private static function meta_to_drawer_values( $raw ){
919 $rows = self::repeater_meta_to_rows( $raw );
920 $result = array();
921
922 foreach ( $rows as $index => $row ) {
923 if ( ! is_array( $row ) ) {
924 continue;
925 }
926
927 $fields = array();
928 foreach ( $row as $key => $value ) {
929 if ( self::is_item_keyed_repeater( $value ) || self::is_numeric_assoc_rows( $value ) ) {
930 $fields[ $key ] = self::meta_to_drawer_values( $value );
931 } else {
932 $fields[ $key ] = $value;
933 }
934 }
935
936 $result[] = array(
937 'row_index' => (int) $index,
938 'fields' => $fields,
939 );
940 }
941
942 return $result;
943 }
944
945 /**
946 * @param mixed $raw
947 * @return array
948 */
949 private static function repeater_meta_to_rows( $raw ){
950 if ( ! is_array( $raw ) || $raw === array() ) {
951 return array();
952 }
953
954 if ( self::is_item_keyed_repeater( $raw ) ) {
955 uksort(
956 $raw,
957 function( $a, $b ) {
958 return (int) substr( $a, 5 ) - (int) substr( $b, 5 );
959 }
960 );
961
962 return array_values( $raw );
963 }
964
965 if ( self::is_numeric_assoc_rows( $raw ) ) {
966 return array_values( $raw );
967 }
968
969 return array();
970 }
971
972 /**
973 * Convert drawer numeric rows back to JetEngine item-N meta.
974 *
975 * @param array $rows
976 * @return array
977 */
978 private static function rows_to_jetengine_meta( $rows ){
979 $out = array();
980 $index = 0;
981
982 foreach ( $rows as $row ) {
983 if ( ! is_array( $row ) ) {
984 continue;
985 }
986
987 $item = array();
988 foreach ( $row as $key => $value ) {
989 if ( self::is_numeric_assoc_rows( $value ) ) {
990 $item[ $key ] = self::rows_to_jetengine_meta( $value );
991 } else {
992 $item[ $key ] = $value;
993 }
994 }
995
996 $out[ 'item-' . $index ] = $item;
997 $index++;
998 }
999
1000 return $out;
1001 }
1002
1003 /**
1004 * @param mixed $value
1005 * @return bool
1006 */
1007 private static function is_item_keyed_repeater( $value ){
1008 if ( ! is_array( $value ) || $value === array() ) {
1009 return false;
1010 }
1011
1012 foreach ( $value as $key => $row ) {
1013 if ( ! is_string( $key ) || ! preg_match( '/^item-\d+$/', $key ) || ! is_array( $row ) ) {
1014 return false;
1015 }
1016 }
1017
1018 return true;
1019 }
1020
1021 /**
1022 * Numeric list of associative row arrays (drawer / nested save shape).
1023 *
1024 * @param mixed $value
1025 * @return bool
1026 */
1027 private static function is_numeric_assoc_rows( $value ){
1028 if ( ! is_array( $value ) || $value === array() ) {
1029 return false;
1030 }
1031
1032 if ( array_keys( $value ) !== range( 0, count( $value ) - 1 ) ) {
1033 return false;
1034 }
1035
1036 foreach ( $value as $row ) {
1037 if ( ! is_array( $row ) ) {
1038 return false;
1039 }
1040 }
1041
1042 return true;
1043 }
1044
1045 }
1046 new SheetsPilotPluginsJetEngine();
1047