PluginProbe ʕ •ᴥ•ʔ
Responsive Lightbox & Gallery / 2.7.5
Responsive Lightbox & Gallery v2.7.5
2.7.8 trunk 1.0.0 1.0.1 1.0.1.1 1.0.2 1.0.3 1.0.4 1.1.0 1.1.1 1.1.2 1.2.0 1.2.1 1.2.2 1.2.3 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.4.0 1.4.0.1 1.4.1 1.4.11 1.4.12 1.4.13 1.4.14 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.6.0 1.6.1 1.6.10 1.6.11 1.6.12 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 2.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.1 2.2.0 2.2.1 2.2.2 2.2.3 2.2.3.1 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.3.5 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.4.7 2.4.8 2.4.9 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.6.0 2.6.1 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7
responsive-lightbox / includes / galleries / class-gallery-api.php
responsive-lightbox / includes / galleries Last commit date
class-gallery-api.php 4 months ago class-gallery-base.php 3 months ago class-gallery-config.php 4 months ago class-gallery-design.php 4 months ago class-gallery-field-provider.php 4 months ago class-gallery-images.php 4 months ago class-gallery-lightbox.php 4 months ago class-gallery-misc.php 4 months ago class-gallery-paging.php 4 months ago trait-gallery-ajax.php 3 months ago trait-gallery-duplicate.php 4 months ago trait-gallery-image-methods.php 3 months ago trait-gallery-preview.php 4 months ago trait-gallery-sanitize.php 4 months ago
class-gallery-api.php
1016 lines
1 <?php
2 /**
3 * Responsive Lightbox Gallery Settings API
4 *
5 * Core class for integrating Settings API patterns with gallery post meta.
6 * Handles rendering, validation, and storage for gallery settings tabs.
7 *
8 * @package Responsive_Lightbox
9 */
10
11 // exit if accessed directly
12 if ( ! defined( 'ABSPATH' ) )
13 exit;
14
15 /**
16 * Responsive_Lightbox_Gallery_API class.
17 *
18 * Provides Settings API-like interface for gallery post meta settings.
19 * Manages tab registration, rendering, and data flow.
20 *
21 * @class Responsive_Lightbox_Gallery_API
22 */
23 class Responsive_Lightbox_Gallery_API {
24
25 /**
26 * Registered gallery tabs.
27 *
28 * @var array
29 */
30 private $tabs = [];
31
32 /**
33 * Adapter-owned tab metadata (labels and descriptions).
34 *
35 * @var array
36 */
37 private $tab_meta = [
38 'images' => [
39 'label' => 'Images',
40 'description' => 'The settings below adjust the contents of the gallery.'
41 ],
42 'config' => [
43 'label' => 'Config',
44 'description' => 'The settings below allow you to select a gallery type and adjust the gallery options.'
45 ],
46 'design' => [
47 'label' => 'Design',
48 'description' => 'The settings below adjust the gallery design options.'
49 ],
50 'paging' => [
51 'label' => 'Paging',
52 'description' => 'The settings below adjust the gallery pagination options.'
53 ],
54 'lightbox' => [
55 'label' => 'Lightbox',
56 'description' => 'The settings below adjust the lightbox options.'
57 ],
58 'misc' => [
59 'label' => 'Misc',
60 'description' => 'The settings below adjust miscellaneous options.'
61 ]
62 ];
63
64 /**
65 * Class constructor.
66 *
67 * Initializes the gallery settings system.
68 *
69 * @return void
70 */
71 public function __construct() {
72 // Load gallery settings classes
73 $this->load_gallery_settings_classes();
74
75 // Hook into gallery rendering - use higher priority to run after default
76 add_action( 'add_meta_boxes_rl_gallery', [ $this, 'replace_meta_boxes' ], 20 );
77
78 // Enqueue assets for gallery settings
79 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
80 }
81
82 /**
83 * Get normalized Images menu items from adapter.
84 *
85 * Returns menu items with labels, disabled states, and reasons from the Images tab class.
86 * Single source of truth for Images menu metadata in the adapter.
87 *
88 * @return array Normalized menu items array.
89 */
90 public function get_images_menu_items() {
91 if ( isset( $this->tabs['images'] ) && method_exists( $this->tabs['images'], 'get_menu_items' ) ) {
92 return $this->tabs['images']->get_menu_items();
93 }
94 return [];
95 }
96
97 /**
98 * Load gallery settings classes.
99 *
100 * Includes all gallery settings tab classes and registers them.
101 *
102 * @return void
103 */
104 private function load_gallery_settings_classes() {
105 $classes = [
106 'includes/settings/class-settings-base.php',
107 'includes/galleries/class-gallery-field-provider.php',
108 'includes/galleries/class-gallery-base.php',
109 'includes/galleries/class-gallery-images.php',
110 'includes/galleries/class-gallery-config.php',
111 'includes/galleries/class-gallery-design.php',
112 'includes/galleries/class-gallery-paging.php',
113 'includes/galleries/class-gallery-lightbox.php',
114 'includes/galleries/class-gallery-misc.php',
115 ];
116
117 foreach ( $classes as $class_file ) {
118 $file_path = RESPONSIVE_LIGHTBOX_PATH . $class_file;
119 if ( file_exists( $file_path ) ) {
120 include_once( $file_path );
121 }
122 }
123
124 // Register tab classes
125 $this->tabs = [
126 'images' => new Responsive_Lightbox_Gallery_Images(),
127 'paging' => new Responsive_Lightbox_Gallery_Paging(),
128 'misc' => new Responsive_Lightbox_Gallery_Misc(),
129 'design' => new Responsive_Lightbox_Gallery_Design(),
130 'lightbox' => new Responsive_Lightbox_Gallery_Lightbox(),
131 'config' => new Responsive_Lightbox_Gallery_Config(),
132 ];
133
134 // Allow filtering for extensions
135 $this->tabs = apply_filters( 'rl_gallery_settings_tabs', $this->tabs );
136 }
137
138 /**
139 * Replace default meta boxes with Settings API-based rendering.
140 *
141 * @param WP_Post $post Post object.
142 * @return void
143 */
144 public function replace_meta_boxes( $post ) {
145 // Determine active tab for visibility classes
146 $active_tab = isset( $_GET['rl_active_tab'] ) ? sanitize_key( $_GET['rl_active_tab'] ) : '';
147 $galleries = Responsive_Lightbox()->galleries;
148 $all_tabs = $galleries ? (array) $galleries->get_data( 'tabs' ) : [];
149 $active_tab = ! empty( $active_tab ) && array_key_exists( $active_tab, $all_tabs ) ? $active_tab : 'images';
150
151 // Use registered adapter tabs as the enabled set (deterministic, no rollout filter)
152 foreach ( $this->tabs as $tab_id => $tab_class ) {
153 $tab_data = $this->build_tab_data( $tab_id, $tab_class );
154
155 // Remove the specific default meta box for this tab
156 remove_meta_box( 'responsive-gallery-' . $tab_id, 'rl_gallery', 'responsive_lightbox_metaboxes' );
157
158 // Add wrapper class filter
159 add_filter( 'postbox_classes_rl_gallery_responsive-gallery-' . $tab_id, [ $galleries, 'add_settings_wrapper_class' ] );
160
161 // Add visibility class filter based on active tab
162 if ( $active_tab === $tab_id ) {
163 add_filter( 'postbox_classes_rl_gallery_responsive-gallery-' . $tab_id, [ $galleries, 'display_metabox' ] );
164 } else {
165 add_filter( 'postbox_classes_rl_gallery_responsive-gallery-' . $tab_id, [ $galleries, 'hide_metabox' ] );
166 }
167
168 // Add new Settings API-based meta box
169 add_meta_box(
170 'responsive-gallery-' . $tab_id,
171 $tab_data['label'],
172 [ $this, 'render_meta_box' ],
173 'rl_gallery',
174 'responsive_lightbox_metaboxes',
175 'high',
176 [ 'tab_id' => $tab_id, 'tab_data' => $tab_data ]
177 );
178 }
179 }
180
181 /**
182 * Build adapter-owned tab metadata for rendering.
183 *
184 * @param string $tab_id Tab ID.
185 * @param Responsive_Lightbox_Gallery_Base $tab_class Tab class instance.
186 * @return array Tab metadata.
187 */
188 private function build_tab_data( $tab_id, $tab_class ) {
189 $meta = isset( $this->tab_meta[$tab_id] ) ? $this->tab_meta[$tab_id] : [];
190 $label = $meta['label'] ?? $tab_id;
191 if ( method_exists( $tab_class, 'get_tab_label' ) ) {
192 $label = $tab_class->get_tab_label();
193 } else {
194 $label = __( $label, 'responsive-lightbox' );
195 }
196 $description = '';
197 if ( ! empty( $meta['description'] ) ) {
198 $description = __( $meta['description'], 'responsive-lightbox' );
199 }
200
201 $tab_data = [
202 'label' => $label,
203 'description' => $description
204 ];
205
206 if ( $tab_id === 'images' ) {
207 $images_menu_items = $this->get_images_menu_items();
208 $menu_items = [];
209 foreach ( $images_menu_items as $menu_key => $menu_data ) {
210 $menu_items[$menu_key] = $menu_data['label'] ?? $menu_key;
211 }
212 if ( empty( $menu_items ) ) {
213 $menu_items = [ 'media' => __( 'Media Library', 'responsive-lightbox' ) ];
214 }
215 $tab_data['menu_items'] = $menu_items;
216 }
217
218 if ( method_exists( $tab_class, 'get_tab_data' ) ) {
219 $tab_fields = $tab_class->get_tab_data();
220 if ( isset( $tab_fields['menu_items'] ) && is_array( $tab_fields['menu_items'] ) ) {
221 $tab_data['menu_items'] = $tab_fields['menu_items'];
222 }
223 }
224
225 return $tab_data;
226 }
227
228 /**
229 * Render meta box content.
230 *
231 * @param WP_Post $post Post object.
232 * @param array $args Callback args.
233 * @return void
234 */
235 public function render_meta_box( $post, $args ) {
236 $tab_id = $args['args']['tab_id'];
237 $tab_data = $args['args']['tab_data'];
238
239 if ( ! isset( $this->tabs[$tab_id] ) ) {
240 return;
241 }
242
243 $tab_class = $this->tabs[$tab_id];
244 $saved_data = $this->get_saved_data( $post->ID, $tab_id );
245
246 // Wrap with PicoCSS settings wrapper
247 echo '<div class="rl-settings-form" data-settings-prefix="rl">';
248
249 // Preserve legacy hidden input used by admin-galleries.js helpers.
250 if ( $tab_id === 'images' ) {
251 $active_tab = isset( $_GET['rl_active_tab'] ) ? sanitize_key( $_GET['rl_active_tab'] ) : '';
252 if ( $active_tab === '' ) {
253 $active_tab = 'images';
254 }
255
256 echo '<input type="hidden" name="rl_active_tab" value="' . esc_attr( $active_tab ) . '" />';
257 }
258
259 // Render tab description
260 if ( ! empty( $tab_data['description'] ) ) {
261 echo '<p class="description">' . esc_html( $tab_data['description'] ) . '</p>';
262 }
263
264 // Handle menu items (sub-navigation)
265 if ( ! empty( $tab_data['menu_items'] ) ) {
266 $this->render_menu_navigation( $tab_id, $tab_data, $saved_data, $post->ID );
267 } else {
268 $this->render_tab_fields( $tab_class, $saved_data, $post->ID, $tab_id );
269 }
270
271 echo '</div>';
272 }
273
274 /**
275 * Render menu navigation specifically for Images tab using adapter-owned metadata.
276 *
277 * @param array $menu_items Menu items from adapter.
278 * @param string $current_menu_item Current active menu item.
279 * @param array $saved_data Saved data.
280 * @param int $post_id Post ID.
281 * @return void
282 */
283 private function render_images_menu_navigation( $menu_items, $current_menu_item, $saved_data, $post_id ) {
284 // Menu navigation - use radio buttons for images tab
285 // CRITICAL: rl-gallery-tab-menu-images is required by admin-galleries.js for menu item detection
286 // rl-gallery-menu-nav provides base styling (margin/border)
287 echo '<div class="rl-gallery-tab-menu rl-gallery-tab-menu-images rl-gallery-menu-nav rl-gallery-menu-nav-radio">';
288
289 foreach ( $menu_items as $menu_item => $item_data ) {
290 $label = $item_data['label'] ?? $menu_item;
291 $checked = ( $menu_item === $current_menu_item ) ? ' checked' : '';
292 $disabled = $item_data['disabled'] ?? false;
293 $title = $disabled ? ( $item_data['disabled_reason'] ?? '' ) : '';
294
295 $id = 'rl-menu-images-' . $menu_item;
296 $disabled_attr = $disabled ? ' disabled="disabled"' : '';
297 $title_attr = $title && $disabled ? ' title="' . esc_attr( $title ) . '"' : '';
298 $label_class_attr = $disabled ? ' class="rl-disabled"' : '';
299
300 echo '<input type="radio" class="rl-gallery-tab-menu-item" name="rl_gallery[images][menu_item]" value="' . esc_attr( $menu_item ) . '"' . $checked . $disabled_attr . ' id="' . esc_attr( $id ) . '" />';
301 echo '<label for="' . esc_attr( $id ) . '"' . $label_class_attr . $title_attr . '>' . esc_html( $label ) . '</label>';
302 }
303
304 // Add spinner for Images tab AJAX loading
305 if ( $this->should_show_spinner( 'images' ) ) {
306 echo '<span class="spinner" style="display: none;"></span>';
307 }
308
309 echo '</div>';
310
311 // Render content for current menu item
312 if ( isset( $this->tabs['images'] ) ) {
313 echo '<div class="rl-gallery-tab-content">';
314
315 $content_class = 'rl-gallery-tab-inside rl-gallery-tab-inside-images-' . esc_attr( $current_menu_item );
316
317 // Add loading class if menu item is disabled
318 $menu_item_state = $this->get_menu_item_state( 'images', $current_menu_item );
319 if ( $menu_item_state['disabled'] ) {
320 $content_class .= ' rl-loading-content';
321 }
322
323 echo '<div class="' . esc_attr( $content_class ) . '">';
324 $this->render_tab_fields( $this->tabs['images'], $saved_data, $post_id, 'images', $current_menu_item );
325 echo '</div>';
326
327 echo '</div>'; // .rl-gallery-tab-content
328 }
329 }
330
331 /**
332 * Get menu item disabled state and reason for a tab.
333 *
334 * @param string $tab_id Tab ID.
335 * @param string $menu_item Menu item ID.
336 * @return array Array with 'disabled' and 'disabled_reason' keys.
337 */
338 private function get_menu_item_state( $tab_id, $menu_item ) {
339 $disabled = false;
340 $disabled_reason = '';
341
342 // For images tab, delegate to images class for adapter-owned logic
343 if ( $tab_id === 'images' && isset( $this->tabs['images'] ) ) {
344 $disabled = $this->tabs['images']->is_menu_item_disabled( $menu_item );
345 $disabled_reason = $this->tabs['images']->get_menu_item_disabled_reason( $menu_item );
346 }
347
348 return [ 'disabled' => $disabled, 'disabled_reason' => $disabled_reason ];
349 }
350
351 /**
352 * Get menu item title for display (used for config tab default labels).
353 *
354 * @param string $tab_id Tab ID.
355 * @param string $menu_item Menu item ID.
356 * @param array $tab_data Tab configuration.
357 * @return string Title string.
358 */
359 private function get_menu_item_title( $tab_id, $menu_item, $tab_data ) {
360 $title = '';
361
362 // Config tab: add default gallery type label
363 if ( $tab_id === 'config' && $menu_item === 'default' ) {
364 $rl = Responsive_Lightbox();
365 $builder_gallery = isset( $rl->options['settings']['builder_gallery'] ) ? $rl->options['settings']['builder_gallery'] : 'basicgrid';
366 if ( isset( $tab_data['menu_items'][$builder_gallery] ) ) {
367 $title = ' (' . esc_html( $tab_data['menu_items'][$builder_gallery] ) . ')';
368 }
369 }
370
371 return $title;
372 }
373
374 /**
375 * Check if a tab should show a spinner (AJAX loading indicator).
376 *
377 * @param string $tab_id Tab ID.
378 * @return bool Whether to show spinner.
379 */
380 private function should_show_spinner( $tab_id ) {
381 return $tab_id === 'images';
382 }
383
384 /**
385 * Render menu navigation for tabs with sub-items.
386 *
387 * @param string $tab_id Tab ID.
388 * @param array $tab_data Tab configuration.
389 * @param array $saved_data Saved data.
390 * @param int $post_id Post ID.
391 * @return void
392 */
393 private function render_menu_navigation( $tab_id, $tab_data, $saved_data, $post_id ) {
394 // For Images tab, use adapter-owned menu metadata exclusively
395 if ( $tab_id === 'images' ) {
396 $images_menu_items = $this->get_images_menu_items();
397 if ( empty( $images_menu_items ) ) {
398 echo '<div class="notice notice-warning"><p>' . esc_html__( 'Images tab menu metadata is unavailable. Please refresh the page or check the adapter configuration.', 'responsive-lightbox' ) . '</p></div>';
399 return;
400 }
401 $current_menu_item = isset( $_GET['rl_menu_item'] ) ? sanitize_key( $_GET['rl_menu_item'] ) : ( $saved_data['menu_item'] ?? key( $images_menu_items ) );
402
403 // Normalize current menu item using Images class method
404 if ( isset( $this->tabs[$tab_id] ) && method_exists( $this->tabs[$tab_id], 'normalize_menu_item' ) ) {
405 $current_menu_item = $this->tabs[$tab_id]->normalize_menu_item( $current_menu_item );
406 }
407
408 $this->render_images_menu_navigation( $images_menu_items, $current_menu_item, $saved_data, $post_id );
409 return;
410 }
411
412 // For other tabs, use legacy tab_data approach
413 $current_menu_item = isset( $_GET['rl_menu_item'] ) ? sanitize_key( $_GET['rl_menu_item'] ) : ( $saved_data['menu_item'] ?? key( $tab_data['menu_items'] ) );
414
415 // Ensure the current menu item exists in the available menu items
416 if ( ! isset( $tab_data['menu_items'][$current_menu_item] ) ) {
417 $current_menu_item = key( $tab_data['menu_items'] );
418 }
419
420 // Menu navigation - use radio buttons for config tab
421 $nav_class = 'rl-gallery-tab-menu rl-gallery-tab-menu-' . sanitize_html_class( $tab_id );
422 if ( $tab_id === 'config' ) {
423 $nav_class .= ' rl-gallery-menu-nav-radio';
424 echo '<div class="' . esc_attr( $nav_class ) . '">';
425
426 $rl = Responsive_Lightbox();
427
428 foreach ( $tab_data['menu_items'] as $menu_item => $label ) {
429 $checked = ( $menu_item === $current_menu_item ) ? ' checked' : '';
430 $menu_item_state = $this->get_menu_item_state( $tab_id, $menu_item );
431 $disabled = $menu_item_state['disabled'];
432 $title = $this->get_menu_item_title( $tab_id, $menu_item, $tab_data );
433
434 // Override title with disabled reason if disabled
435 if ( $disabled ) {
436 $title = $menu_item_state['disabled_reason'];
437 }
438
439 $id = 'rl-menu-' . $tab_id . '-' . $menu_item;
440 $disabled_attr = $disabled ? ' disabled="disabled"' : '';
441 $title_attr = $title && $disabled ? ' title="' . esc_attr( $title ) . '"' : '';
442
443 echo '<input type="radio" class="rl-gallery-tab-menu-item" name="rl_gallery[' . esc_attr( $tab_id ) . '][menu_item]" value="' . esc_attr( $menu_item ) . '"' . $checked . $disabled_attr . ' id="' . esc_attr( $id ) . '" />';
444 // For disabled items, title is tooltip only; for config default, title is visual label suffix
445 echo '<label for="' . esc_attr( $id ) . '"' . $title_attr . '>' . esc_html( $label ) . ( ! $disabled ? $title : '' ) . '</label>';
446 }
447
448 echo '</div>';
449 } else {
450 echo '<div class="' . esc_attr( $nav_class ) . '">';
451 foreach ( $tab_data['menu_items'] as $menu_item => $label ) {
452 $active_class = ( $menu_item === $current_menu_item ) ? ' nav-tab-active' : '';
453 $url = add_query_arg( [ 'rl_active_tab' => $tab_id, 'rl_menu_item' => $menu_item ] );
454 echo '<a href="' . esc_url( $url ) . '" class="nav-tab' . esc_attr( $active_class ) . '">' . esc_html( $label ) . '</a>';
455 }
456 echo '</div>';
457 }
458
459 // Render fields for current menu item
460 if ( isset( $this->tabs[$tab_id] ) ) {
461 $tab_class = $this->tabs[$tab_id];
462 if ( $tab_id === 'config' ) {
463 // For config tab, render all menu items' content for radio switching
464 foreach ( $tab_data['menu_items'] as $menu_item => $label ) {
465 $display_style = ( $menu_item === $current_menu_item ) ? '' : ' style="display: none;"';
466 echo '<div class="rl-gallery-tab-inside rl-gallery-tab-inside-' . esc_attr( $tab_id ) . '-' . esc_attr( $menu_item ) . '"' . $display_style . '>';
467 $this->render_tab_fields( $tab_class, $saved_data, $post_id, $tab_id, $menu_item );
468 echo '</div>';
469 }
470 } else {
471 // For other tabs, wrap content with .rl-gallery-tab-content for AJAX target
472 echo '<div class="rl-gallery-tab-content">';
473
474 $content_class = 'rl-gallery-tab-inside rl-gallery-tab-inside-' . esc_attr( $tab_id ) . '-' . esc_attr( $current_menu_item );
475
476 echo '<div class="' . esc_attr( $content_class ) . '">';
477 $this->render_tab_fields( $tab_class, $saved_data, $post_id, $tab_id, $current_menu_item );
478 echo '</div>';
479
480 echo '</div>'; // .rl-gallery-tab-content
481 }
482 }
483 }
484
485 /**
486 * Render tab fields.
487 *
488 * @param Responsive_Lightbox_Gallery_Base $tab_class Tab class instance.
489 * @param array $saved_data Saved data.
490 * @param int $post_id Post ID.
491 * @param string $tab_id Tab ID.
492 * @param string $menu_item Menu item ID.
493 * @return void
494 */
495 private function render_tab_fields( $tab_class, $saved_data, $post_id, $tab_id, $menu_item = '' ) {
496 // Get tab data, passing menu_item for tabs that support it
497 $tab_data = $tab_class->get_tab_data( $menu_item );
498
499 $fields = $tab_data['options'] ?? $tab_data['fields'] ?? $tab_data;
500
501 if ( $tab_id === 'images' ) {
502 if ( method_exists( $tab_class, 'normalize_menu_item' ) ) {
503 $menu_item = $tab_class->normalize_menu_item( $menu_item );
504 } elseif ( $menu_item === '' ) {
505 $menu_item = 'media';
506 }
507 }
508
509 // Default menu_item to 'options' for legacy compatibility
510 if ( empty( $menu_item ) ) {
511 $menu_item = 'options';
512 }
513
514 // For config tab, fields are already flattened by gallery type
515 // For other tabs, check if fields are nested under menu_item
516 if ( $tab_id !== 'config' && isset( $fields[$menu_item] ) ) {
517 $fields = $fields[$menu_item];
518 }
519
520 // Defensive: Ensure filter result is valid array
521 if ( ! is_array( $fields ) || empty( $fields ) ) {
522 return;
523 }
524
525 // Start output buffering to capture HTML for legacy content filter
526 ob_start();
527
528 echo '<table class="form-table rl-galleries-table">';
529
530 // Render hidden menu_item input for legacy save compatibility (skip for config tab as radios provide the value)
531 if ( $menu_item && $tab_id !== 'config' ) {
532 $menu_item_name = sprintf( 'rl_gallery[%s][menu_item]', $tab_id );
533 echo '<input type="hidden" name="' . esc_attr( $menu_item_name ) . '" value="' . esc_attr( $menu_item ) . '" />';
534 }
535
536 foreach ( $fields as $field_key => $field ) {
537 // Defensive: Skip malformed field entries from filtered data BEFORE any array access
538 if ( ! is_array( $field ) || ! isset( $field['type'] ) ) {
539 continue;
540 }
541
542 $value = $this->resolve_field_value( $field_key, $field, $saved_data, $tab_id, $menu_item );
543
544 // For multiple fields, build resolved value map for subfields
545 if ( $field['type'] === 'multiple' && ! empty( $field['fields'] ) ) {
546 $resolved_values = [];
547 $global_settings = $this->get_context_global_settings( $tab_id, $menu_item );
548 $legacy_defaults = $this->get_context_defaults( $tab_id, $menu_item );
549
550 foreach ( $field['fields'] as $subfield_key => $subfield ) {
551 // Precedence: global locked -> saved data -> field default -> legacy defaults
552 if ( isset( $subfield['is_global_locked'] ) && $subfield['is_global_locked'] ) {
553 $resolved_values[$subfield_key] = $global_settings[$subfield_key] ?? $subfield['default'] ?? $legacy_defaults[$subfield_key] ?? '';
554 } else {
555 $resolved_values[$subfield_key] = $this->resolve_field_value( $subfield_key, $subfield, $saved_data, $tab_id, $menu_item );
556 }
557 }
558 $value = $resolved_values;
559 } else {
560 // For disabled fields in config tab, use global settings value
561 if ( $tab_id === 'config' && isset( $field['disabled'] ) && $field['disabled'] ) {
562 $rl = Responsive_Lightbox();
563 $default_gallery_type = isset( $rl->options['settings']['builder_gallery'] ) ? $rl->options['settings']['builder_gallery'] : 'basicgrid';
564 $default_gallery_key = $default_gallery_type . '_gallery';
565 $global_settings = $rl->options[$default_gallery_key] ?? [];
566 $value = $global_settings[$field_key] ?? $value;
567 }
568 }
569
570 $this->render_field_row( $field_key, $field, $value, $post_id, $tab_id, $menu_item );
571 }
572
573 echo '</table>';
574
575 // Capture rendered HTML for legacy content filter
576 $html = ob_get_clean();
577
578 echo $html;
579 }
580
581 /**
582 * Resolve field value with fallback chain.
583 *
584 * @param string $field_key Field key.
585 * @param array $field Field configuration.
586 * @param array $saved_data Saved gallery data.
587 * @param string $tab_id Tab ID.
588 * @param string $menu_item Menu item ID.
589 * @return mixed Resolved value.
590 */
591 private function resolve_field_value( $field_key, $field, $saved_data, $tab_id, $menu_item ) {
592 $legacy_defaults = $this->get_context_defaults( $tab_id, $menu_item );
593 $global_settings = $this->get_context_global_settings( $tab_id, $menu_item );
594
595 // For config tab, check menu_item subarray first
596 if ( $tab_id === 'config' && $menu_item && isset( $saved_data[$menu_item] ) ) {
597 $value = $saved_data[$menu_item][$field_key] ?? null;
598 if ( $value !== null ) {
599 return $value;
600 }
601 }
602
603 // Check menu_item-specific saved data (used by Images tab and legacy-compatible structures).
604 if ( $menu_item && isset( $saved_data[$menu_item] ) && is_array( $saved_data[$menu_item] ) ) {
605 $value = $saved_data[$menu_item][$field_key] ?? null;
606 if ( $value !== null ) {
607 return $value;
608 }
609 }
610
611 // Check saved options
612 $value = $saved_data['options'][$field_key] ?? null;
613 if ( $value !== null ) {
614 return $value;
615 }
616
617 // Check field default
618 if ( isset( $field['default'] ) ) {
619 return $field['default'];
620 }
621
622 // Check legacy defaults
623 if ( isset( $legacy_defaults[$field_key] ) ) {
624 return $legacy_defaults[$field_key];
625 }
626
627 // For disabled fields in config tab, use global settings
628 if ( $tab_id === 'config' && isset( $field['disabled'] ) && $field['disabled'] ) {
629 return $global_settings[$field_key] ?? '';
630 }
631
632 return '';
633 }
634
635 /**
636 * Get context-aware defaults for a tab and menu item.
637 *
638 * @param string $tab_id Tab ID.
639 * @param string $menu_item Menu item ID.
640 * @return array Defaults array.
641 */
642 private function get_context_defaults( $tab_id, $menu_item ) {
643 $rl = Responsive_Lightbox();
644
645 if ( $tab_id === 'config' ) {
646 if ( $menu_item === 'default' ) {
647 // For Global config, use builder gallery defaults
648 $default_gallery_type = isset( $rl->options['settings']['builder_gallery'] ) ? $rl->options['settings']['builder_gallery'] : 'basicgrid';
649 $default_gallery_key = $default_gallery_type . '_gallery';
650 return $rl->defaults[$default_gallery_key] ?? [];
651 } else {
652 // For specific gallery types, use their own defaults
653 $gallery_key = $menu_item . '_gallery';
654 return $rl->defaults[$gallery_key] ?? [];
655 }
656 }
657
658 // For other tabs, use builder gallery defaults as fallback
659 $default_gallery_type = isset( $rl->options['settings']['builder_gallery'] ) ? $rl->options['settings']['builder_gallery'] : 'basicgrid';
660 $default_gallery_key = $default_gallery_type . '_gallery';
661 return $rl->defaults[$default_gallery_key] ?? [];
662 }
663
664 /**
665 * Get context-aware global settings for a tab and menu item.
666 *
667 * @param string $tab_id Tab ID.
668 * @param string $menu_item Menu item ID.
669 * @return array Global settings array.
670 */
671 private function get_context_global_settings( $tab_id, $menu_item ) {
672 $rl = Responsive_Lightbox();
673
674 if ( $tab_id === 'config' ) {
675 // Always use builder gallery global settings for config tab
676 $default_gallery_type = isset( $rl->options['settings']['builder_gallery'] ) ? $rl->options['settings']['builder_gallery'] : 'basicgrid';
677 $default_gallery_key = $default_gallery_type . '_gallery';
678 return $rl->options[$default_gallery_key] ?? [];
679 }
680
681 // For other tabs, use builder gallery global settings
682 $default_gallery_type = isset( $rl->options['settings']['builder_gallery'] ) ? $rl->options['settings']['builder_gallery'] : 'basicgrid';
683 $default_gallery_key = $default_gallery_type . '_gallery';
684 return $rl->options[$default_gallery_key] ?? [];
685 }
686
687 /**
688 * Render individual field row.
689 *
690 * @param string $field_key Field key.
691 * @param array $field Field configuration.
692 * @param mixed $value Current value.
693 * @param int $post_id Post ID.
694 * @param string $tab_id Tab ID.
695 * @param string $menu_item Menu item ID.
696 * @return void
697 */
698 private function render_field_row( $field_key, $field, $value, $post_id, $tab_id, $menu_item ) {
699 // Skip hidden fields
700 if ( isset( $field['hidden'] ) && $field['hidden'] ) {
701 return;
702 }
703
704 $name = $this->get_field_name( $tab_id, $menu_item, $field_key );
705 $id = sanitize_key( $name );
706 $field_type = isset( $field['type'] ) ? $field['type'] : '';
707 $is_images_full_width = ( $tab_id === 'images' && in_array( $field_type, [ 'media_library', 'media_preview' ], true ) );
708
709 // For notice fields, use full-width row without label
710 if ( isset( $field['type'] ) && $field['type'] === 'notice' ) {
711 echo '<tr class="rl-gallery-field-disabled-notice">';
712 echo '<td colspan="2">';
713 } else {
714 $row_classes = [];
715 $row_classes[] = 'rl-gallery-field-' . sanitize_html_class( $tab_id . '-' . $menu_item . '-' . $field_key );
716 $row_classes[] = 'rl-gallery-field-' . sanitize_html_class( $field_type );
717 if ( ! empty( $field['disabled'] ) ) {
718 $row_classes[] = 'rl-disabled';
719 $row_classes[] = 'rl-gallery-field-disabled';
720 }
721
722 $row_attrs = ' class="' . esc_attr( implode( ' ', array_filter( $row_classes ) ) ) . '"';
723 $row_attrs .= ' data-field_type="' . esc_attr( $field_type ) . '"';
724 $row_attrs .= ' data-field_name="' . esc_attr( $field_key ) . '"';
725
726 echo '<tr' . $row_attrs . '>';
727 if ( $is_images_full_width ) {
728 echo '<td colspan="2" class="rl-colspan">';
729 } else {
730 echo '<th scope="row"><label for="' . esc_attr( $id ) . '">' . esc_html( $field['title'] ?? $field_key ) . '</label></th>';
731 echo '<td>';
732 }
733 }
734
735 $use_api_wrapper = ! $is_images_full_width && (bool) apply_filters( 'rl_gallery_settings_use_api_wrapper', true, $tab_id, $field_key, $field );
736 if ( $use_api_wrapper ) {
737 $wrapper_attrs = $this->get_field_wrapper_attrs( $field, $tab_id, $menu_item, $field_key );
738 echo '<div' . $wrapper_attrs . '>';
739 }
740
741 // Use tab class to render field.
742 $tab_class = isset( $this->tabs[$tab_id] ) ? $this->tabs[$tab_id] : null;
743 if ( $tab_class ) {
744
745 if ( $field['type'] === 'multiple' && ! empty( $field['fields'] ) ) {
746 $resolved_values = is_array( $value ) ? $value : [];
747
748 foreach ( $field['fields'] as $subfield_key => $subfield ) {
749 $subvalue = $resolved_values[$subfield_key] ?? $subfield['default'] ?? '';
750
751 // Apply disabled flag to subfield if parent is locked
752 $subfield_for_render = $subfield;
753 if ( isset( $field['is_global_locked'] ) && $field['is_global_locked'] ) {
754 $subfield_for_render['disabled'] = true;
755 }
756
757 echo '<div class="rl-gallery-subfield">';
758 echo $tab_class->render_field_html( $subfield_key, $subfield_for_render, $subvalue, $post_id, $tab_id, $menu_item );
759 echo '</div> ';
760 }
761 } else {
762 echo $tab_class->render_field_html( $field_key, $field, $value, $post_id, $tab_id, $menu_item );
763 }
764 }
765
766 // Field description
767 if ( ! empty( $field['description'] ) && ! $is_images_full_width ) {
768 echo '<p class="description">' . esc_html( $field['description'] ) . '</p>';
769 }
770
771 if ( $use_api_wrapper ) {
772 echo '</div>';
773 }
774
775 if ( isset( $field['type'] ) && $field['type'] === 'notice' ) {
776 echo '</td>';
777 } else {
778 echo '</td>';
779 }
780 echo '</tr>';
781 }
782
783 /**
784 * Build Settings API-like wrapper attributes for a field.
785 *
786 * @param array $field Field configuration.
787 * @param string $tab_id Tab ID.
788 * @param string $menu_item Menu item ID.
789 * @param string $field_key Field key.
790 * @return string Wrapper attributes string.
791 */
792 private function get_field_wrapper_attrs( $field, $tab_id, $menu_item, $field_key ) {
793 $wrapper_classes = [ 'rl-field', 'rl-field-type-' . sanitize_html_class( $field['type'] ) ];
794
795 if ( $field['type'] === 'color_picker' ) {
796 $wrapper_classes[] = 'rl-field-type-color';
797 }
798
799 if ( ! empty( $field['class'] ) ) {
800 $wrapper_classes[] = $field['class'];
801 }
802
803 if ( ! empty( $field['disabled'] ) ) {
804 $wrapper_classes[] = 'rl-disabled';
805 }
806
807 $wrapper_id = 'rl-gallery-' . $tab_id . '-' . ( $menu_item ?: 'options' ) . '-' . $field_key . '-setting';
808 $wrapper_id = sanitize_html_class( str_replace( '_', '-', $wrapper_id ) );
809
810 return ' id="' . esc_attr( $wrapper_id ) . '" class="' . esc_attr( implode( ' ', $wrapper_classes ) ) . '"';
811 }
812
813 /**
814 * Get field name for form input.
815 *
816 * @param string $tab_id Tab ID.
817 * @param string $menu_item Menu item ID.
818 * @param string $field_key Field key.
819 * @return string Field name.
820 */
821 private function get_field_name( $tab_id, $menu_item, $field_key ) {
822 if ( $menu_item ) {
823 return sprintf( 'rl_gallery[%s][%s][%s]', $tab_id, $menu_item, $field_key );
824 }
825 return sprintf( 'rl_gallery[%s][%s]', $tab_id, $field_key );
826 }
827
828 /**
829 * Get saved data for a tab.
830 *
831 * @param int $post_id Post ID.
832 * @param string $tab_id Tab ID.
833 * @return array Saved data.
834 */
835 private function get_saved_data( $post_id, $tab_id ) {
836 $data = get_post_meta( $post_id, '_rl_' . $tab_id, true );
837 return is_array( $data ) ? $data : [];
838 }
839
840 /**
841 * Enqueue assets for gallery settings.
842 *
843 * @return void
844 */
845 public function enqueue_assets() {
846 $screen = get_current_screen();
847 if ( ! $screen || $screen->post_type !== 'rl_gallery' || $screen->base !== 'post' ) {
848 return;
849 }
850
851 // Enqueue admin-galleries styles (includes merged admin-theme styles)
852 wp_enqueue_style(
853 'responsive-lightbox-admin-galleries',
854 RESPONSIVE_LIGHTBOX_URL . '/css/admin-galleries.css',
855 [],
856 Responsive_Lightbox()->defaults['version']
857 );
858 }
859
860 /**
861 * Check if a tab is managed by the adapter.
862 *
863 * @param string $tab_id Tab ID.
864 * @return bool True if managed by adapter.
865 */
866 public function is_managed_tab( $tab_id ) {
867 return isset( $this->tabs[$tab_id] );
868 }
869
870 /**
871 * Render menu content for AJAX requests.
872 *
873 * @param int $post_id Post ID.
874 * @param string $tab_id Tab ID.
875 * @param string $menu_item Menu item ID.
876 * @return string Rendered HTML.
877 */
878 public function render_menu_content( $post_id, $tab_id, $menu_item ) {
879 if ( ! isset( $this->tabs[$tab_id] ) ) {
880 return '';
881 }
882
883 $tab_class = $this->tabs[$tab_id];
884 $saved_data = $this->get_saved_data( $post_id, $tab_id );
885
886 // For Images tab, normalize menu_item using adapter-owned logic.
887 if ( $tab_id === 'images' ) {
888 // Use adapter-owned normalize_menu_item method.
889 if ( isset( $this->tabs[$tab_id] ) && method_exists( $this->tabs[$tab_id], 'normalize_menu_item' ) ) {
890 $menu_item = $this->tabs[$tab_id]->normalize_menu_item( $menu_item );
891 } else {
892 // Fallback only if adapter class unavailable (should not happen in normal operation).
893 $menu_item = 'media';
894 }
895 }
896
897 // Start output buffering
898 ob_start();
899
900 // Render only the inner content for .rl-gallery-tab-content replacement
901 // Contract: return only <div class="rl-gallery-tab-inside rl-gallery-tab-inside-{tab_id}-{menu_item}">...</div>
902 echo '<div class="rl-gallery-tab-inside rl-gallery-tab-inside-' . esc_attr( $tab_id ) . '-' . esc_attr( $menu_item ) . '">';
903 $this->render_tab_fields( $tab_class, $saved_data, $post_id, $tab_id, $menu_item );
904 echo '</div>';
905
906 // Get the rendered HTML (filter already applied in render_tab_fields)
907 return ob_get_clean();
908 }
909
910 /**
911 * Get tab field definitions for compatibility with $rl->galleries->get_data('fields').
912 *
913 * Returns the full field structure for a tab in the same format as legacy $this->fields,
914 * enabling seamless access via get_data('fields') from frontend code.
915 *
916 * @since 2.7.1
917 * @param string $tab_id Tab ID.
918 * @return array Field definitions in legacy format.
919 */
920 public function get_tab_definition( $tab_id ) {
921 if ( ! isset( $this->tabs[$tab_id] ) ) {
922 return [];
923 }
924
925 $tab_class = $this->tabs[$tab_id];
926 $tab_data = $tab_class->get_tab_data();
927
928 // Images tab returns legacy menu-item field structure directly.
929 if ( $tab_id === 'images' && is_array( $tab_data ) ) {
930 return $tab_data;
931 }
932
933 // Return in legacy format: [ 'options' => [ field_key => field_def, ... ] ]
934 // Tab classes may use 'sections' wrapper or direct 'options' key.
935 if ( isset( $tab_data['sections'] ) && is_array( $tab_data['sections'] ) ) {
936 // Find the primary section (usually 'options' or first section)
937 $primary_section = isset( $tab_data['sections']['options'] ) ? 'options' : key( $tab_data['sections'] );
938
939 if ( isset( $tab_data['sections'][$primary_section]['fields'] ) ) {
940 return [ $primary_section => $tab_data['sections'][$primary_section]['fields'] ];
941 }
942 }
943
944 // Direct format: tab data has 'options' key with field definitions
945 if ( isset( $tab_data['options'] ) && is_array( $tab_data['options'] ) ) {
946 return [ 'options' => $tab_data['options'] ];
947 }
948
949 return [];
950 }
951
952 /**
953 * Get tab fields for save pipeline.
954 *
955 * @param string $tab_id Tab ID.
956 * @param string $menu_item Menu item ID.
957 * @return array
958 */
959 public function get_tab_fields_for_save( $tab_id, $menu_item = '' ) {
960 if ( ! isset( $this->tabs[$tab_id] ) ) {
961 return [];
962 }
963
964 $tab_class = $this->tabs[$tab_id];
965 $tab_data = $tab_class->get_tab_data( $menu_item );
966 $fields = $tab_data['options'] ?? $tab_data['fields'] ?? $tab_data;
967
968 if ( $tab_id === 'images' ) {
969 if ( method_exists( $tab_class, 'normalize_menu_item' ) ) {
970 $menu_item = $tab_class->normalize_menu_item( $menu_item );
971 } elseif ( $menu_item === '' ) {
972 $menu_item = 'media';
973 }
974 }
975
976 if ( empty( $menu_item ) ) {
977 $menu_item = 'options';
978 }
979
980 if ( $tab_id !== 'config' && isset( $fields[$menu_item] ) ) {
981 $fields = $fields[$menu_item];
982 }
983
984 return is_array( $fields ) ? $fields : [];
985 }
986
987 /**
988 * Validate sanitized data for a tab.
989 *
990 * @param string $tab_id Tab ID.
991 * @param string $menu_item Menu item ID.
992 * @param array $input Sanitized data for the menu item.
993 * @param int $post_id Post ID.
994 * @return array
995 */
996 public function validate_tab_data( $tab_id, $menu_item, $input, $post_id = 0 ) {
997 if ( ! isset( $this->tabs[$tab_id] ) ) {
998 return is_array( $input ) ? $input : [];
999 }
1000
1001 $tab_class = $this->tabs[$tab_id];
1002 if ( method_exists( $tab_class, 'validate_tab' ) ) {
1003 $input = $tab_class->validate_tab( $input, $tab_id, $menu_item, $post_id );
1004 } elseif ( method_exists( $tab_class, 'validate' ) ) {
1005 $input = $tab_class->validate( $input );
1006 }
1007
1008 if ( ! is_array( $input ) ) {
1009 $input = [];
1010 }
1011
1012 return apply_filters( 'rl_gallery_validate_tab_data', $input, $tab_id, $menu_item, $post_id );
1013 }
1014
1015 }
1016