PluginProbe ʕ •ᴥ•ʔ
Responsive Lightbox & Gallery / 2.7.0
Responsive Lightbox & Gallery v2.7.0
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 / class-settings-api.php
responsive-lightbox / includes Last commit date
providers 5 months ago settings 5 months ago class-fast-image.php 2 years ago class-folders.php 5 months ago class-frontend.php 5 months ago class-galleries.php 5 months ago class-multilang.php 2 years ago class-remote-library-api.php 5 months ago class-remote-library.php 5 months ago class-settings-api.php 5 months ago class-settings-data.php 5 months ago class-settings-pages.php 5 months ago class-settings.php 5 months ago class-tour.php 5 months ago class-welcome.php 2 years ago class-widgets.php 2 years ago functions.php 3 years ago
class-settings-api.php
1589 lines
1 <?php
2 // exit if accessed directly
3 if ( ! defined( 'ABSPATH' ) )
4 exit;
5
6 /**
7 * Responsive_Lightbox_Settings_API class.
8 *
9 * Settings API handler adapted from Post Views Counter.
10 * Provides standardized settings registration, rendering, and validation.
11 *
12 * @class Responsive_Lightbox_Settings_API
13 */
14 class Responsive_Lightbox_Settings_API {
15
16 private $settings = [];
17 private $input_settings = [];
18 private $validated_settings = [];
19 private $pages = [];
20 private $page_types = [];
21 private $pages_ready = false;
22 private $prefix = '';
23 private $slug = '';
24 private $domain = '';
25 private $plugin = '';
26 private $plugin_url = '';
27 private $object;
28 private $nested = false;
29
30 /**
31 * Class constructor.
32 *
33 * @param array $args Configuration arguments.
34 * @return void
35 */
36 public function __construct( $args ) {
37 // set initial data
38 $this->prefix = $args['prefix'];
39 $this->domain = $args['domain'];
40 $this->nested = isset( $args['nested'] ) ? (bool) $args['nested'] : false;
41
42 // empty slug?
43 if ( empty( $args['slug'] ) )
44 $this->slug = $args['domain'];
45 else
46 $this->slug = $args['slug'];
47
48 $this->object = $args['object'];
49 $this->plugin = $args['plugin'];
50 $this->plugin_url = $args['plugin_url'];
51
52 // skip hooks if running in bridge mode (menus handled by legacy system)
53 $skip_hooks = isset( $args['skip_hooks'] ) ? (bool) $args['skip_hooks'] : false;
54
55 if ( ! $skip_hooks ) {
56 // actions
57 add_action( 'admin_menu', [ $this, 'admin_menu_options' ], 11 );
58 add_action( 'admin_init', [ $this, 'register_settings' ], 11 );
59 add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] );
60 } else {
61 // in bridge mode, only register settings (menus handled externally)
62 add_action( 'admin_init', [ $this, 'register_settings' ], 11 );
63 add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] );
64 }
65 }
66
67 /**
68 * Get prefix.
69 *
70 * @return string
71 */
72 public function get_prefix() {
73 return $this->prefix;
74 }
75
76 /**
77 * Get pages.
78 *
79 * @return array
80 */
81 public function get_pages() {
82 $this->prepare_pages();
83
84 return $this->pages;
85 }
86
87 /**
88 * Prepare Settings API pages and types.
89 *
90 * @param bool $register_menus Whether to register menu pages.
91 * @return void
92 */
93 public function prepare_pages( $register_menus = true ) {
94 if ( $this->pages_ready )
95 return;
96
97 $this->pages = apply_filters( $this->prefix . '_settings_pages', [] );
98
99 $types = [
100 'page' => [],
101 'subpage' => [],
102 'settings_page' => []
103 ];
104
105 foreach ( $this->pages as $page => $data ) {
106 // skip invalid page types
107 if ( empty( $data['type'] ) || ! array_key_exists( $data['type'], $types ) )
108 continue;
109
110 if ( $data['type'] === 'page' ) {
111 if ( $register_menus ) {
112 add_menu_page(
113 $data['page_title'],
114 $data['menu_title'],
115 $data['capability'],
116 $data['menu_slug'],
117 [ $this, 'options_page' ],
118 ! empty( $data['icon'] ) ? $data['icon'] : '',
119 ! empty( $data['position'] ) ? $data['position'] : null
120 );
121
122 // Phase 8: Register visible submenus for each tab (compatibility shim)
123 if ( ! empty( $data['tabs'] ) ) {
124 foreach ( $data['tabs'] as $tab_key => $tab_data ) {
125 add_submenu_page(
126 $data['menu_slug'],
127 $tab_data['label'],
128 $tab_data['label'],
129 $data['capability'],
130 $data['menu_slug'] . '&tab=' . $tab_key,
131 [ $this, 'options_page' ]
132 );
133 }
134
135 // Remove first duplicate submenu entry
136 remove_submenu_page( $data['menu_slug'], $data['menu_slug'] );
137 }
138 }
139
140 // add page type
141 $types['page'][$data['menu_slug']] = $page;
142 // menu subpage?
143 } elseif ( $data['type'] === 'subpage' ) {
144 if ( $register_menus ) {
145 add_submenu_page(
146 $data['parent_slug'],
147 $data['page_title'],
148 $data['menu_title'],
149 $data['capability'],
150 $data['menu_slug'],
151 [ $this, 'options_page' ]
152 );
153 }
154
155 // add subpage type
156 $types['subpage'][$data['menu_slug']] = $page;
157 // menu settings page?
158 } elseif ( $data['type'] === 'settings_page' ) {
159 if ( $register_menus ) {
160 add_options_page(
161 $data['page_title'],
162 $data['menu_title'],
163 $data['capability'],
164 $data['menu_slug'],
165 [ $this, 'options_page' ]
166 );
167 }
168
169 // add settings type
170 $types['settings_page'][$data['menu_slug']] = $page;
171 }
172 }
173
174 // set page types
175 $this->page_types = $types;
176 $this->pages_ready = true;
177 }
178
179 /**
180 * Load default scripts and styles.
181 *
182 * @return void
183 */
184 public function admin_enqueue_scripts() {
185 $handler = $this->prefix . '-settings-api-style';
186
187 wp_register_style( $handler, false );
188 wp_enqueue_style( $handler );
189
190 wp_add_inline_style( $handler, '
191 .nav-tab-wrapper span.nav-span-disabled {
192 cursor: not-allowed;
193 float: left;
194 }
195 body.rtl .nav-tab-wrapper span.nav-span-disabled {
196 float: right;
197 }
198 .nav-tab-wrapper a.nav-tab.nav-tab-disabled {
199 pointer-events: none;
200 }
201 .nav-tab-wrapper a.nav-tab.nav-tab-disabled:hover {
202 cursor: not-allowed;
203 }
204 ' );
205 }
206
207 /**
208 * Add menu pages.
209 *
210 * @return void
211 */
212 public function admin_menu_options() {
213 $this->prepare_pages();
214 }
215
216 /**
217 * Render settings page.
218 *
219 * @global string $pagenow
220 *
221 * @return void
222 */
223 public function options_page() {
224 global $pagenow;
225
226 $valid_page = false;
227 $page_args = [];
228 $page_slug = '';
229 $matched_slug = '';
230
231 // get current screen
232 $screen = get_current_screen();
233
234 $page_raw = isset( $_GET['page'] ) ? wp_unslash( $_GET['page'] ) : '';
235 $page_parts = $page_raw !== '' ? explode( '&', $page_raw, 2 ) : [ '' ];
236 $page_slug = $page_parts[0] !== '' ? sanitize_key( $page_parts[0] ) : '';
237
238 if ( ! empty( $page_parts[1] ) )
239 parse_str( $page_parts[1], $page_args );
240
241 // fallback for menu slugs with query args (legacy menus with tab routing)
242 if ( ! $valid_page && $pagenow === 'admin.php' && $page_slug !== '' ) {
243 if ( isset( $this->page_types['page'][$page_slug] ) ) {
244 $valid_page = true;
245 $page_type = 'page';
246 $url_page = 'admin.php';
247 $matched_slug = $page_slug;
248 } elseif ( isset( $this->page_types['subpage'][$page_slug] ) ) {
249 $valid_page = true;
250 $page_type = 'subpage';
251 $url_page = 'admin.php';
252 $matched_slug = $page_slug;
253 }
254 }
255
256 // display top level settings page?
257 if ( ! $valid_page && $pagenow === 'admin.php' && preg_match( '/^toplevel_page_(' . implode( '|', $this->page_types['page'] ) . ')$/', $screen->base, $matches ) === 1 && ! empty( $matches[1] ) ) {
258 $valid_page = true;
259 $page_type = 'page';
260 $url_page = 'admin.php';
261 $matched_slug = $matches[1];
262 }
263
264 // display subpage?
265 if ( ! $valid_page && $pagenow === 'admin.php' && ! empty( $this->page_types['subpage'] ) ) {
266 foreach ( $this->page_types['subpage'] as $menu_slug => $page_key ) {
267 if ( preg_match( '/^lightbox_page_(' . preg_quote( $menu_slug, '/' ) . ')$/', $screen->base, $matches ) === 1 ) {
268 $valid_page = true;
269 $page_type = 'subpage';
270 $url_page = 'admin.php';
271 $matched_slug = $matches[1];
272 break;
273 }
274 }
275 }
276
277 // display settings page?
278 if ( ! $valid_page && $pagenow === 'options-general.php' && preg_match( '/^(?:settings_page_)(' . implode( '|', array_keys( $this->page_types['settings_page'] ) ) . ')$/', $screen->base, $matches ) === 1 ) {
279 $valid_page = true;
280 $page_type = 'settings_page';
281 $url_page = 'options-general.php';
282 $matched_slug = $matches[1];
283 }
284
285 // skip invalid pages
286 if ( ! $valid_page )
287 return;
288
289 $page_key = isset( $this->page_types[$page_type][$matched_slug] ) ? $this->page_types[$page_type][$matched_slug] : '';
290
291 if ( empty( $page_key ) || empty( $this->pages[$page_key] ) || ! is_array( $this->pages[$page_key] ) )
292 return;
293 $tab_key = '';
294 $section_key = '';
295 $tabs = [];
296 $sections = [];
297
298 // any tabs?
299 if ( array_key_exists( 'tabs', $this->pages[$page_key] ) && is_array( $this->pages[$page_key]['tabs'] ) ) {
300 // get tabs
301 $tabs = $this->pages[$page_key]['tabs'];
302
303 // reset tabs
304 reset( $tabs );
305
306 // get first default tab
307 $first_tab = key( $tabs );
308
309 // get current tab
310 $tab_key = ! empty( $_GET['tab'] ) && array_key_exists( $_GET['tab'], $tabs ) ? $_GET['tab'] : ( ! empty( $page_args['tab'] ) && array_key_exists( $page_args['tab'], $tabs ) ? $page_args['tab'] : $first_tab );
311
312 // check current tab
313 if ( ! empty( $_GET['tab'] ) )
314 $tab_key = sanitize_key( $_GET['tab'] );
315 elseif ( ! empty( $page_args['tab'] ) )
316 $tab_key = sanitize_key( $page_args['tab'] );
317
318 // invalid tab?
319 if ( ! array_key_exists( $tab_key, $tabs ) )
320 $tab_key = $first_tab;
321
322 $tab_label = ! empty( $tabs[$tab_key]['label'] ) ? $tabs[$tab_key]['label'] : '';
323 $tab_heading = ! empty( $tabs[$tab_key]['heading'] ) ? $tabs[$tab_key]['heading'] : '';
324
325 // check for subpages (dynamic sections like lightbox scripts or gallery types)
326 if ( ! empty( $tabs[$tab_key]['subpages'] ) ) {
327 $sections = $tabs[$tab_key]['subpages'];
328
329 // reset sections
330 reset( $sections );
331
332 // get first section as fallback
333 $first_section = key( $sections );
334
335 // use default_subpage if defined and valid, otherwise first section
336 $default_section = ! empty( $tabs[$tab_key]['default_subpage'] ) && array_key_exists( $tabs[$tab_key]['default_subpage'], $sections )
337 ? $tabs[$tab_key]['default_subpage']
338 : $first_section;
339
340 // get current section from URL, fallback to default section
341 $section_key = ! empty( $_GET['section'] ) ? sanitize_key( $_GET['section'] ) : ( ! empty( $page_args['section'] ) ? sanitize_key( $page_args['section'] ) : $default_section );
342
343 // invalid section?
344 if ( ! array_key_exists( $section_key, $sections ) )
345 $section_key = $default_section;
346 }
347 } else {
348 $tab_label = '';
349 }
350
351 if ( empty( $tabs ) )
352 $tab_heading = '';
353
354 $heading = $this->plugin !== '' ? __( $this->plugin, $this->domain ) : '';
355
356 echo '
357 <div class="wrap ' . esc_attr( $this->prefix ) . '-settings-wrapper responsive-lightbox-settings" data-settings-prefix="' . esc_attr( $this->prefix ) . '">';
358
359 // render header with breadcrumbs
360 // $this->render_header( $heading, $page_key, $tab_key, $section_key, $sections );
361
362 // render tabs navigation
363 if ( ! empty( $tabs ) ) {
364 echo '
365 <nav class="nav-tab-wrapper">';
366
367 foreach ( $tabs as $key => $tab ) {
368 if ( ! empty( $tab['disabled'] ) )
369 $url = '';
370 else
371 $url = admin_url( $url_page . '?page=' . $matched_slug . '&tab=' . $key );
372
373 if ( ! empty( $tab['disabled'] ) )
374 echo '<span class="nav-span-disabled">';
375
376 echo '
377 <a class="nav-tab' . ( $tab_key === $key ? ' nav-tab-active' : '' ) . ( ! empty( $tab['disabled'] ) ? ' nav-tab-disabled' : '' ) . ( ! empty( $tab['class'] ) ? ' ' . esc_attr( $tab['class'] ) : '' ) . '" href="' . ( $url !== '' ? esc_url( $url ) : '#' ) . '">' . esc_html( $tab['label'] ) . '</a>';
378
379 if ( ! empty( $tab['disabled'] ) )
380 echo '</span>';
381 }
382
383 echo '
384 </nav>';
385 }
386
387 // render subpage/section navigation (for Lightboxes/Galleries)
388 if ( ! empty( $sections ) ) {
389 echo '
390 <div class="nav-sub-wrapper">
391 <ul class="subsubsub">';
392
393 $section_count = count( $sections );
394 $i = 0;
395
396 foreach ( $sections as $key => $section ) {
397 $url = admin_url( $url_page . '?page=' . $matched_slug . '&tab=' . $tab_key . '&section=' . $key );
398
399 echo '<li><a href="' . esc_url( $url ) . '"' . ( $section_key === $key ? ' class="current"' : '' ) . '>' . esc_html( $section['label'] ) . '</a></li>';
400 }
401
402 echo '
403 </ul>
404 </div>
405 <div class="clear"></div>';
406 }
407
408 echo '
409 <div class="content-wrapper">
410 <h1 class="screen-reader-text">' . esc_html( $heading ) . '</h1>';
411
412 // skip for internal options page
413 if ( $page_type !== 'settings_page' )
414 settings_errors();
415
416 // get settings page classes
417 $settings_class = apply_filters( $this->prefix . '_settings_page_class', [ $this->slug . '-settings', $tab_key . '-settings', $this->prefix . '-settings' ] );
418
419 // sanitize settings page classes
420 $settings_class = array_unique( array_filter( array_map( 'sanitize_html_class', $settings_class ) ) );
421
422 // resolve setting group for sidebar/form
423 if ( ! empty( $section_key ) && ! empty( $tabs[$tab_key]['subpages'][$section_key]['option_name'] ) ) {
424 // subpage has its own option name
425 $setting = $tabs[$tab_key]['subpages'][$section_key]['option_name'];
426 } elseif ( ! empty( $tab_key ) ) {
427 if ( ! empty( $tabs[$tab_key]['option_name'] ) ) {
428 $setting = $tabs[$tab_key]['option_name'];
429 } else {
430 $setting = $this->prefix . '_' . $tab_key . '_settings';
431 }
432 } else {
433 $setting = $this->prefix . '_' . $page_key . '_settings';
434 }
435
436 // capture sidebar output
437 ob_start();
438 do_action( $this->prefix . '_settings_sidebar', $setting, $page_type, $url_page, $tab_key, $section_key );
439 $sidebar_html = trim( ob_get_clean() );
440
441 // add has-sidebar class if sidebar has content
442 if ( ! empty( $sidebar_html ) )
443 $settings_class[] = 'has-sidebar';
444
445 echo '
446 <div class="' . implode( ' ', array_map( 'esc_attr', $settings_class ) ) . '">';
447
448 $display_form = true;
449
450 // determine the settings lookup key for form settings
451 // for gallery tab, use section_key (e.g., basicgrid_gallery); for other tabs, use tab_key
452 $form_lookup_key = $tab_key;
453 if ( $tab_key === 'gallery' && ! empty( $section_key ) && isset( $this->settings[$section_key] ) ) {
454 $form_lookup_key = $section_key;
455 }
456
457 // check form attribute
458 if ( ! empty( $tab_key ) && ! empty( $tabs[$tab_key]['form'] ) ) {
459 $form = $tabs[$tab_key]['form'];
460
461 if ( isset( $form['buttons'] ) && ! $form['buttons'] )
462 $display_form = false;
463 } elseif ( ! empty( $form_lookup_key ) && isset( $this->settings[$form_lookup_key]['form'] ) ) {
464 $form = $this->settings[$form_lookup_key]['form'];
465
466 if ( isset( $form['buttons'] ) && ! $form['buttons'] )
467 $display_form = false;
468 } elseif ( $page_key !== '' && isset( $this->settings[$page_key]['form'] ) ) {
469 $form = $this->settings[$page_key]['form'];
470
471 if ( isset( $form['buttons'] ) && ! $form['buttons'] )
472 $display_form = false;
473 }
474
475 if ( $display_form ) {
476 echo '
477 <form action="options.php" method="post" novalidate class="' . esc_attr( $this->prefix ) . '-settings-form">';
478 }
479
480 settings_fields( $setting );
481
482 if ( $display_form )
483 do_action( $this->prefix . '_settings_form', $setting, $page_type, $url_page, $tab_key, $section_key );
484
485 // filter sections by tab and subpage if present
486 global $wp_settings_sections;
487
488 $original_sections = $wp_settings_sections;
489
490 // determine the settings key - for gallery tab, use section_key (e.g., basicgrid_gallery)
491 // for other tabs, use tab_key directly
492 $settings_lookup_key = $tab_key;
493 if ( $tab_key === 'gallery' && ! empty( $section_key ) && isset( $this->settings[$section_key] ) ) {
494 $settings_lookup_key = $section_key;
495 }
496
497 if ( ! empty( $settings_lookup_key ) && isset( $this->settings[$settings_lookup_key]['sections'] ) ) {
498 $filtered_sections = [];
499
500 foreach ( $this->settings[$settings_lookup_key]['sections'] as $section_id => $section ) {
501 // check tab match
502 $tab_match = empty( $section['tab'] ) || $section['tab'] === $tab_key;
503
504 // check subpage/section match
505 $section_match = empty( $section['subpage'] ) || $section['subpage'] === $section_key;
506
507 // include sections matching both criteria
508 if ( $tab_match && $section_match ) {
509 if ( isset( $wp_settings_sections[$setting][$section_id] ) ) {
510 $filtered_sections[$section_id] = $wp_settings_sections[$setting][$section_id];
511 }
512 }
513 }
514
515 // replace with filtered sections
516 if ( isset( $wp_settings_sections[$setting] ) ) {
517 $wp_settings_sections[$setting] = $filtered_sections;
518 }
519 }
520
521 do_settings_sections( $setting );
522
523 // restore original sections
524 $wp_settings_sections = $original_sections;
525
526 if ( $display_form ) {
527 $setting_hyphenated = str_replace( '_', '-', $setting );
528
529 echo '
530 <p class="submit">';
531
532 submit_button( '', 'primary save-' . esc_attr( $setting_hyphenated ), 'save_' . $setting, false, [ 'id' => 'save-' . esc_attr( $setting_hyphenated ) ] );
533
534 submit_button( __( 'Reset to defaults', 'responsive-lightbox' ), 'outline reset-' . esc_attr( $setting_hyphenated ), 'reset_' . $setting, false, [ 'id' => 'reset-' . esc_attr( $setting_hyphenated ) ] );
535
536 echo '
537 </p>
538 </form>';
539 }
540
541 // output sidebar if it has content
542 if ( ! empty( $sidebar_html ) ) {
543 echo '
544 <div class="' . esc_attr( $this->prefix ) . '-sidebar">' . $sidebar_html . '</div>';
545 }
546
547 echo '
548 </div>
549 </div>';
550
551 echo '
552 <div class="clear"></div>
553 </div>';
554 }
555
556 /**
557 * Render header with breadcrumbs.
558 *
559 * @param string $heading Main heading text.
560 * @param string $page_key Current page key.
561 * @param string $tab_key Current tab key.
562 * @param string $section_key Current section key.
563 * @param array $sections Available sections.
564 * @return void
565 */
566 private function render_header( $heading, $page_key, $tab_key, $section_key, $sections ) {
567 echo '
568 <div class="header-wrapper">
569 <span class="header-title">' . esc_html( $heading ) . '</span>';
570
571 // render breadcrumbs if we have context
572 if ( ! empty( $tab_key ) || ! empty( $section_key ) ) {
573 echo '
574 <div class="' . esc_attr( $this->prefix ) . '-breadcrumbs-container">';
575
576 $breadcrumbs = [];
577
578 // add tab to breadcrumbs
579 if ( ! empty( $tab_key ) && ! empty( $this->pages[$page_key]['tabs'][$tab_key]['label'] ) ) {
580 $breadcrumbs[] = $this->pages[$page_key]['tabs'][$tab_key]['label'];
581 }
582
583 // add section to breadcrumbs
584 if ( ! empty( $section_key ) && ! empty( $sections[$section_key]['label'] ) ) {
585 $section_label = $sections[$section_key]['label'];
586 if ( $tab_key === 'gallery' ) {
587 $default_suffix = ' ' . __( '(default)', 'responsive-lightbox' );
588 if ( substr( $section_label, -strlen( $default_suffix ) ) === $default_suffix )
589 $section_label = substr( $section_label, 0, -strlen( $default_suffix ) );
590 }
591 $breadcrumbs[] = $section_label;
592 }
593
594 if ( ! empty( $breadcrumbs ) ) {
595 echo implode( ' <span class="rl-breadcrumb-separator">&rsaquo;</span> ', array_map( 'esc_html', $breadcrumbs ) );
596 }
597
598 echo '
599 </div>';
600 }
601
602 echo '
603 </div>';
604 }
605
606 /**
607 * Register settings.
608 *
609 * @return void
610 */
611 public function register_settings() {
612 $this->settings = apply_filters( $this->prefix . '_settings_data', [] );
613
614 // check settings
615 foreach ( $this->settings as $setting_id => $setting ) {
616 // tabs?
617 if ( is_array( $setting['option_name'] ) ) {
618 foreach ( $setting['option_name'] as $tab => $option_name ) {
619 $this->register_setting_fields( $tab, $setting, $option_name );
620 }
621 } else {
622 $this->register_setting_fields( $setting_id, $setting );
623 }
624 }
625 }
626
627 /**
628 * Register setting with sections and fields.
629 *
630 * @param string $setting_id Setting identifier.
631 * @param array $setting Setting configuration.
632 * @param string $option_name Option name override.
633 * @return void
634 */
635 public function register_setting_fields( $setting_id, $setting, $option_name = '' ) {
636 if ( empty( $option_name ) )
637 $option_name = $setting['option_name'];
638
639 // add capability filter for option page (matches legacy behavior)
640 add_filter( 'option_page_capability_' . $option_name, [ $this, 'manage_options_capability' ] );
641
642 // register setting
643 register_setting( $option_name, $option_name, ! empty( $setting['validate'] ) ? $setting['validate'] : [ $this, 'validate_settings' ] );
644
645 // register setting sections
646 if ( ! empty( $setting['sections'] ) ) {
647 foreach ( $setting['sections'] as $section_id => $section ) {
648 // skip unwanted sections
649 if ( ! empty( $section['tab'] ) && $section['tab'] !== $setting_id )
650 continue;
651
652 // auto-generate section classes and wrapper
653 $base_slug = sanitize_html_class( str_replace( '_', '-', $section_id ) );
654 $section_prefix = apply_filters( $this->prefix . '_settings_section_prefix', $this->prefix );
655 $section_prefix = sanitize_html_class( $section_prefix );
656 $section_classes = $section_prefix . '-section ' . $section_prefix . '-section-' . $base_slug;
657
658 $section_args = [
659 'section_class' => $section_classes,
660 'before_section' => '<section id="' . $section_prefix . '-section-' . $base_slug . '" class="%s">',
661 'after_section' => '</section>',
662 ];
663
664 add_settings_section(
665 $section_id,
666 ! empty( $section['title'] ) ? esc_html( $section['title'] ) : '',
667 ! empty( $section['callback'] ) ? $section['callback'] : null,
668 ! empty( $section['page'] ) ? $section['page'] : $option_name,
669 $section_args
670 );
671 }
672 }
673
674 // register setting fields - check both top-level and section-nested fields
675 $all_fields = [];
676
677 // collect fields from top-level 'fields' array
678 if ( ! empty( $setting['fields'] ) ) {
679 foreach ( $setting['fields'] as $field_key => $field ) {
680 $all_fields[$field_key] = $field;
681 }
682 }
683
684 // collect fields from sections (PVC-style nested structure)
685 if ( ! empty( $setting['sections'] ) ) {
686 foreach ( $setting['sections'] as $section_id => $section ) {
687 if ( ! empty( $section['fields'] ) ) {
688 foreach ( $section['fields'] as $field_key => $field ) {
689 // auto-assign section if not specified
690 if ( empty( $field['section'] ) )
691 $field['section'] = $section_id;
692
693 $all_fields[$field_key] = $field;
694 }
695 }
696 }
697 }
698
699 // register all collected fields
700 if ( ! empty( $all_fields ) ) {
701 foreach ( $all_fields as $field_key => $field ) {
702 // skip unwanted fields
703 if ( ! empty( $field['tab'] ) && $field['tab'] !== $setting_id )
704 continue;
705
706 // set field ID
707 $field_id = implode( '_', [ $this->prefix, $setting_id, $field_key ] );
708
709 // skip rendering this field?
710 if ( ! empty( $field['skip_rendering'] ) )
711 continue;
712
713 // prepare field args
714 $args = array_merge( $this->prepare_field_args( $field, $field_id, $field_key, $setting_id, $option_name ), $field );
715 $args['setting_id'] = $setting_id;
716 $class = sanitize_html_class( str_replace( '_', '-', $field_id ) );
717 $classes = [ $class ];
718
719 if ( ! empty( $args['class'] ) ) {
720 $extra_classes = preg_split( '/\s+/', trim( $args['class'] ) );
721 $extra_classes = array_filter( $extra_classes );
722 $extra_classes = array_map( 'sanitize_html_class', $extra_classes );
723 $classes = array_merge( $classes, $extra_classes );
724 }
725
726 $classes = array_values( array_unique( array_filter( $classes ) ) );
727
728 $field_class = implode( ' ', $classes );
729 $wrapper_class = $class !== '' ? $class . '-row' : '';
730
731 // preserve original field class for button/special types (before adding to wrapper)
732 $args['original_class'] = ! empty( $field['class'] ) ? $field['class'] : '';
733
734 // preserve user classes in wrapper - but not for button type (those are for the button element only)
735 if ( ! empty( $field['class'] ) && ( ! isset( $field['type'] ) || $field['type'] !== 'button' ) )
736 $wrapper_class .= ' ' . $field['class'];
737
738 $args['class'] = $wrapper_class;
739 $args['field_class'] = $field_class;
740 $args['css_id'] = $class;
741
742 add_settings_field(
743 $field_id,
744 ! empty( $field['title'] ) ? esc_html( $field['title'] ) : '',
745 [ $this, 'render_field' ],
746 $option_name,
747 ! empty( $field['section'] ) ? esc_attr( $field['section'] ) : '',
748 $args
749 );
750 }
751 }
752 }
753
754 /**
755 * Prepare field arguments.
756 *
757 * @param array $field Field configuration.
758 * @param string $field_id Field identifier.
759 * @param string $field_key Field key.
760 * @param string $setting_id Setting identifier.
761 * @param string $setting_name Setting name/option name.
762 * @return array Prepared field arguments.
763 */
764 public function prepare_field_args( $field, $field_id, $field_key, $setting_id, $setting_name ) {
765 // get field type
766 $field_type = ! empty( $field['type'] ) ? $field['type'] : '';
767
768 // default lookup path
769 $value = null;
770 $default = null;
771 $name = $setting_name . '[' . $field_key . ']';
772
773 // check for parent (nested fields like configuration[glightbox][loop])
774 if ( ! empty( $field['parent'] ) ) {
775 $name = $setting_name . '[' . $field['parent'] . '][' . $field_key . ']';
776
777 // try with setting_id first
778 if ( isset( $this->object->options[$setting_id][$field['parent']][$field_key] ) ) {
779 $value = $this->object->options[$setting_id][$field['parent']][$field_key];
780 } elseif ( isset( $this->object->options[$field['parent']][$field_key] ) ) {
781 // try without setting_id
782 $value = $this->object->options[$field['parent']][$field_key];
783 }
784
785 // defaults
786 if ( isset( $this->object->defaults[$setting_id][$field['parent']][$field_key] ) ) {
787 $default = $this->object->defaults[$setting_id][$field['parent']][$field_key];
788 } elseif ( isset( $this->object->defaults[$field['parent']][$field_key] ) ) {
789 $default = $this->object->defaults[$field['parent']][$field_key];
790 }
791 } else {
792 // nested mode?
793 if ( $this->nested ) {
794 $name = $setting_name . '[' . $setting_id . '][' . $field_key . ']';
795
796 if ( isset( $this->object->options[$setting_id][$field_key] ) )
797 $value = $this->object->options[$setting_id][$field_key];
798
799 if ( isset( $this->object->defaults[$setting_id][$field_key] ) )
800 $default = $this->object->defaults[$setting_id][$field_key];
801 } else {
802 // flat structure
803 if ( isset( $this->object->options[$setting_id][$field_key] ) ) {
804 $value = $this->object->options[$setting_id][$field_key];
805 } elseif ( isset( $this->object->options[$field_key] ) ) {
806 $value = $this->object->options[$field_key];
807 }
808
809 // defaults
810 if ( isset( $this->object->defaults[$setting_id][$field_key] ) ) {
811 $default = $this->object->defaults[$setting_id][$field_key];
812 } elseif ( isset( $this->object->defaults[$field_key] ) ) {
813 $default = $this->object->defaults[$field_key];
814 }
815 }
816 }
817
818 // use field-provided default if no default found in core defaults
819 // (allows add-ons to specify defaults in field definitions)
820 if ( $default === null && isset( $field['default'] ) )
821 $default = $field['default'];
822
823 if ( $field_type === 'custom' ) {
824 $value = null;
825 $default = null;
826 }
827
828 // for radio/select, ensure a usable value is always set
829 if ( in_array( $field_type, [ 'radio', 'select' ], true ) ) {
830 $options = ! empty( $field['options'] ) && is_array( $field['options'] ) ? $field['options'] : [];
831 $value_in_options = ! empty( $options ) && ( array_key_exists( $value, $options ) || array_key_exists( (string) $value, $options ) );
832 $needs_value = $value === null || $value === '' || $value === false || is_array( $value ) || ! $value_in_options;
833 } else {
834 $needs_value = false;
835 }
836
837 if ( $needs_value ) {
838 $has_default = $default !== null && $default !== '' && ! is_array( $default );
839 if ( $has_default ) {
840 $value = $default;
841 }
842
843 $value_in_options = ! empty( $options ) && ( array_key_exists( $value, $options ) || array_key_exists( (string) $value, $options ) );
844
845 if ( $value === null || $value === '' || $value === false || is_array( $value ) || ! $value_in_options ) {
846 if ( ! empty( $options ) ) {
847 reset( $options );
848 $value = key( $options );
849 }
850 }
851 }
852
853 return [
854 'id' => $field_id,
855 'html_id' => sanitize_html_class( str_replace( '_', '-', $field_id ) ),
856 'name' => $name,
857 'class' => ! empty( $field['class'] ) ? $field['class'] : '',
858 'type' => $field_type,
859 'label' => ! empty( $field['label'] ) ? $field['label'] : '',
860 'description' => ! empty( $field['description'] ) ? $field['description'] : '',
861 'text' => ! empty( $field['text'] ) ? $field['text'] : '',
862 'min' => isset( $field['min'] ) ? (int) $field['min'] : 0,
863 'max' => isset( $field['max'] ) ? (int) $field['max'] : 0,
864 'options' => ! empty( $field['options'] ) ? $field['options'] : [],
865 'callback' => ! empty( $field['callback'] ) ? $field['callback'] : null,
866 'validate' => ! empty( $field['validate'] ) ? $field['validate'] : null,
867 'callback_args' => ! empty( $field['callback_args'] ) ? $field['callback_args'] : [],
868 'default' => $default,
869 'value' => $value,
870 'setting_id' => $setting_id,
871 'parent' => ! empty( $field['parent'] ) ? $field['parent'] : '',
872 'subpage' => ! empty( $field['subpage'] ) ? $field['subpage'] : '',
873 'animation' => ! empty( $field['animation'] ) ? $field['animation'] : '',
874 'logic' => ! empty( $field['logic'] ) ? $field['logic'] : null,
875 'fallback_option' => ! empty( $field['fallback_option'] ) ? sanitize_key( $field['fallback_option'] ) : '',
876 'classname' => ! empty( $field['classname'] ) ? $field['classname'] : '',
877 'url' => ! empty( $field['url'] ) ? $field['url'] : '',
878 'prepend' => ! empty( $field['prepend'] ) ? $field['prepend'] : '',
879 'append' => ! empty( $field['append'] ) ? $field['append'] : '',
880 'fields' => ! empty( $field['fields'] ) ? $field['fields'] : []
881 ];
882 }
883
884 /**
885 * Render settings field.
886 *
887 * @param array $args Field arguments.
888 * @return void|string
889 */
890 public function render_field( $args ) {
891 if ( empty( $args ) || ! is_array( $args ) )
892 return;
893
894 // build wrapper classes
895 $wrapper_classes = [ $this->prefix . '-field', $this->prefix . '-field-type-' . $args['type'] ];
896
897 if ( $args['type'] === 'color_picker' )
898 $wrapper_classes[] = $this->prefix . '-field-type-color';
899
900 if ( ! empty( $args['class'] ) )
901 $wrapper_classes[] = $args['class'];
902
903 // add disabled class if field is disabled
904 if ( ! empty( $args['disabled'] ) && $args['disabled'] === true && empty( $args['available'] ) )
905 $wrapper_classes[] = $this->prefix . '-disabled';
906
907 // build wrapper attributes
908 $wrapper_attrs = ' id="' . esc_attr( str_replace( '_', '-', $args['id'] ) ) . '-setting" class="' . esc_attr( implode( ' ', $wrapper_classes ) ) . '"';
909
910 // add conditional data attributes
911 $data_attrs = '';
912 $conditions = [];
913 $fallback_option = ! empty( $args['fallback_option'] ) ? $args['fallback_option'] : '';
914
915 if ( ! empty( $args['logic'] ) && is_array( $args['logic'] ) ) {
916 if ( isset( $args['logic']['field'] ) ) {
917 $conditions = [ $args['logic'] ];
918 } else {
919 $conditions = $args['logic'];
920 }
921 }
922
923 if ( ! empty( $conditions ) ) {
924 $data_attr_prefix = sanitize_html_class( $this->prefix );
925 $normalized_conditions = [];
926
927 foreach ( $conditions as $condition ) {
928 if ( empty( $condition['field'] ) || empty( $condition['operator'] ) )
929 continue;
930
931 $field = $condition['field'];
932
933 if ( strpos( $field, '-' ) === false && ! empty( $args['setting_id'] ) ) {
934 $field_id = implode( '_', [ $this->prefix, $args['setting_id'], $field ] );
935 $field = sanitize_html_class( str_replace( '_', '-', $field_id ) );
936 }
937
938 $normalized_conditions[] = [
939 'field' => $field,
940 'operator' => $condition['operator'],
941 'value' => isset( $condition['value'] ) ? $condition['value'] : '',
942 'scope' => ! empty( $condition['scope'] ) ? sanitize_key( $condition['scope'] ) : '',
943 'action' => ! empty( $condition['action'] ) ? sanitize_key( $condition['action'] ) : '',
944 'target' => ! empty( $condition['target'] ) ? sanitize_text_field( $condition['target'] ) : '',
945 'container' => ! empty( $condition['container'] ) ? sanitize_text_field( $condition['container'] ) : '',
946 ];
947 }
948
949 if ( ! empty( $normalized_conditions ) && $data_attr_prefix !== '' ) {
950 if ( ! empty( $args['animation'] ) && in_array( $args['animation'], [ 'fade', 'slide' ], true ) ) {
951 $data_attrs .= ' data-' . $data_attr_prefix . '-animation="' . esc_attr( $args['animation'] ) . '"';
952 }
953 $data_attrs .= ' data-' . $data_attr_prefix . '-logic="' . esc_attr( wp_json_encode( $normalized_conditions ) ) . '"';
954 }
955 }
956
957 if ( $fallback_option !== '' )
958 $data_attrs .= ' data-' . $this->prefix . '-fallback-option="' . esc_attr( $fallback_option ) . '"';
959
960 $wrapper_attrs .= $data_attrs;
961
962 $html = '<div' . $wrapper_attrs . '>';
963
964 if ( ! empty( $args['before_field'] ) )
965 $html .= $args['before_field'];
966
967 switch ( $args['type'] ) {
968 case 'boolean':
969 if ( empty( $args['disabled'] ) )
970 $html .= '<input type="hidden" name="' . esc_attr( $args['name'] ) . '" value="false" />';
971
972 $html .= '<label><input id="' . esc_attr( $args['html_id'] ) . '" type="checkbox" role="switch" name="' . esc_attr( $args['name'] ) . '" value="true" ' . checked( (bool) $args['value'], true, false ) . ' ' . disabled( empty( $args['disabled'] ), false, false ) . ' />' . wp_kses_post( $args['label'] ) . '</label>';
973 break;
974
975 case 'button':
976 // Use original_class (preserved before wrapper overwrites class)
977 $button_class = 'button';
978 if ( ! empty( $args['original_class'] ) ) {
979 $button_class .= ' ' . $args['original_class'];
980 } elseif ( ! empty( $args['classname'] ) ) {
981 $button_class .= ' ' . $args['classname'];
982 } else {
983 $button_class .= ' button-secondary';
984 }
985
986 $button_id = ! empty( $args['button_id'] ) ? $args['button_id'] : $args['html_id'];
987
988 // prepend
989 if ( ! empty( $args['prepend'] ) )
990 $html .= '<span>' . esc_html( $args['prepend'] ) . '</span> ';
991
992 // link button (legacy pattern) or actual button
993 if ( ! empty( $args['url'] ) ) {
994 $html .= '<a href="' . esc_url( $args['url'] ) . '" id="' . esc_attr( $button_id ) . '" class="' . esc_attr( $button_class ) . '">' . esc_html( $args['label'] ) . '</a>';
995 } else {
996 $html .= '<button type="button" id="' . esc_attr( $button_id ) . '" class="' . esc_attr( $button_class ) . '"' . disabled( ! empty( $args['disabled'] ), true, false ) . '>' . esc_html( $args['label'] ) . '</button>';
997 }
998
999 // append
1000 if ( ! empty( $args['append'] ) )
1001 $html .= ' <span>' . esc_html( $args['append'] ) . '</span>';
1002
1003 break;
1004
1005 case 'radio':
1006 if ( empty( $args['options'] ) || ! is_array( $args['options'] ) )
1007 break;
1008
1009 $display_type = ! empty( $args['display_type'] ) && in_array( $args['display_type'], [ 'horizontal', 'vertical' ], true ) ? $args['display_type'] : 'vertical';
1010 $disabled_keys = ( isset( $args['disabled'] ) && is_array( $args['disabled'] ) ) ? $args['disabled'] : [];
1011 $selected_key = $args['value'];
1012 $selected_valid = array_key_exists( $selected_key, $args['options'] ) || array_key_exists( (string) $selected_key, $args['options'] );
1013
1014 if ( $selected_valid && ! empty( $disabled_keys ) && in_array( $selected_key, $disabled_keys, true ) )
1015 $selected_valid = false;
1016
1017 if ( ! $selected_valid ) {
1018 $selected_key = null;
1019 foreach ( $args['options'] as $key => $name ) {
1020 if ( empty( $disabled_keys ) || ! in_array( $key, $disabled_keys, true ) ) {
1021 $selected_key = $key;
1022 break;
1023 }
1024 }
1025 }
1026
1027 if ( count( $args['options'] ) > 1 )
1028 $html .= '<div class="' . esc_attr( $this->prefix ) . '-field-group ' . esc_attr( $this->prefix ) . '-radio-group ' . $display_type . '">';
1029
1030 foreach ( $args['options'] as $key => $name ) {
1031 $is_disabled = ! empty( $args['disabled'] ) && ( is_array( $args['disabled'] ) && in_array( $key, $args['disabled'], true ) || $args['disabled'] === true );
1032 $label_classes = [];
1033
1034 if ( $is_disabled && is_array( $args['disabled'] ) )
1035 $label_classes[] = $this->prefix . '-disabled';
1036
1037 $label_class = ! empty( $label_classes ) ? ' class="' . implode( ' ', $label_classes ) . '"' : '';
1038 $display_name = esc_html( $name );
1039
1040 $html .= '<label for="' . esc_attr( $args['html_id'] . '-' . $key ) . '"' . $label_class . '><input id="' . esc_attr( $args['html_id'] . '-' . $key ) . '" type="radio" name="' . esc_attr( $args['name'] ) . '" value="' . esc_attr( $key ) . '" ' . checked( $key, $selected_key, false ) . ' ' . disabled( $is_disabled, true, false ) . ' />' . $display_name . '</label> ';
1041 }
1042
1043 if ( count( $args['options'] ) > 1 )
1044 $html .= '</div>';
1045 break;
1046
1047 case 'checkbox':
1048 // possible "empty" value
1049 if ( $args['value'] === 'empty' )
1050 $args['value'] = [];
1051
1052 // ensure value is an array
1053 if ( ! is_array( $args['value'] ) )
1054 $args['value'] = [];
1055
1056 $display_type = ! empty( $args['display_type'] ) && in_array( $args['display_type'], [ 'horizontal', 'vertical' ], true ) ? $args['display_type'] : 'vertical';
1057
1058 $html .= '<input type="hidden" name="' . esc_attr( $args['name'] ) . '" value="empty" />';
1059
1060 if ( empty( $args['options'] ) || ! is_array( $args['options'] ) )
1061 break;
1062
1063 if ( count( $args['options'] ) > 1 )
1064 $html .= '<div class="' . esc_attr( $this->prefix ) . '-field-group ' . esc_attr( $this->prefix ) . '-checkbox-group ' . $display_type . '">';
1065
1066 foreach ( $args['options'] as $key => $name ) {
1067 $is_disabled = ! empty( $args['disabled'] ) && ( is_array( $args['disabled'] ) && in_array( $key, $args['disabled'], true ) || $args['disabled'] === true );
1068 $label_classes = [];
1069
1070 if ( $is_disabled && is_array( $args['disabled'] ) )
1071 $label_classes[] = $this->prefix . '-disabled';
1072
1073 $label_class = ! empty( $label_classes ) ? ' class="' . implode( ' ', $label_classes ) . '"' : '';
1074 $display_name = esc_html( $name );
1075
1076 $html .= '<label' . $label_class . '><input id="' . esc_attr( $args['html_id'] . '-' . $key ) . '" type="checkbox" name="' . esc_attr( $args['name'] ) . '[]" value="' . esc_attr( $key ) . '" ' . checked( in_array( $key, $args['value'] ), true, false ) . ' ' . disabled( $is_disabled, true, false ) . ' />' . $display_name . '</label>';
1077 }
1078
1079 if ( count( $args['options'] ) > 1 )
1080 $html .= '</div>';
1081 break;
1082
1083 case 'select':
1084 $html .= '<select id="' . esc_attr( $args['html_id'] ) . '" name="' . esc_attr( $args['name'] ) . '" ' . disabled( empty( $args['disabled'] ), false, false ) . '>';
1085
1086 foreach ( $args['options'] as $key => $name ) {
1087 $html .= '<option value="' . esc_attr( $key ) . '" ' . selected( $args['value'], $key, false ) . '>' . esc_html( $name ) . '</option>';
1088 }
1089
1090 $html .= '</select>';
1091 break;
1092
1093 case 'multiple':
1094 if ( ! empty( $args['fields'] ) && is_array( $args['fields'] ) ) {
1095 $html .= '<fieldset>';
1096
1097 $count = 1;
1098 $count_fields = count( $args['fields'] );
1099
1100 foreach ( $args['fields'] as $subfield_id => $subfield ) {
1101 // check if subfield has parent (e.g., configuration[lightcase][transition])
1102 $subfield_parent = ! empty( $subfield['parent'] ) ? $subfield['parent'] : null;
1103
1104 // resolve default and value with parent awareness
1105 $subfield_default = null;
1106 $subfield_value = null;
1107
1108 if ( $subfield_parent ) {
1109 // nested with parent: defaults[configuration][lightcase][transition]
1110 if ( isset( $this->object->defaults[$args['setting_id']][$subfield_parent][$subfield_id] ) )
1111 $subfield_default = $this->object->defaults[$args['setting_id']][$subfield_parent][$subfield_id];
1112
1113 if ( isset( $this->object->options[$args['setting_id']][$subfield_parent][$subfield_id] ) )
1114 $subfield_value = $this->object->options[$args['setting_id']][$subfield_parent][$subfield_id];
1115 } else {
1116 // flat: defaults[configuration][transition]
1117 if ( isset( $this->object->defaults[$args['setting_id']][$subfield_id] ) )
1118 $subfield_default = $this->object->defaults[$args['setting_id']][$subfield_id];
1119
1120 if ( isset( $this->object->options[$args['setting_id']][$subfield_id] ) )
1121 $subfield_value = $this->object->options[$args['setting_id']][$subfield_id];
1122 }
1123
1124 // use field-level default if available
1125 if ( $subfield_default === null && isset( $subfield['default'] ) )
1126 $subfield_default = $subfield['default'];
1127
1128 // for radio/select in multiple fields, ensure value is set (safety fallback)
1129 $subfield_type = ! empty( $subfield['type'] ) ? $subfield['type'] : 'text';
1130 $subfield_options = ! empty( $subfield['options'] ) && is_array( $subfield['options'] ) ? $subfield['options'] : [];
1131 $subfield_value_in_options = ! empty( $subfield_options ) && ( array_key_exists( $subfield_value, $subfield_options ) || array_key_exists( (string) $subfield_value, $subfield_options ) );
1132
1133 if ( in_array( $subfield_type, [ 'radio', 'select' ], true ) && ( $subfield_value === null || $subfield_value === '' || $subfield_value === false || is_array( $subfield_value ) || ! $subfield_value_in_options ) ) {
1134 $has_sub_default = $subfield_default !== null && $subfield_default !== '' && ! is_array( $subfield_default );
1135 if ( $has_sub_default ) {
1136 $subfield_value = $subfield_default;
1137 }
1138
1139 if ( $subfield_value === null || $subfield_value === '' || $subfield_value === false || is_array( $subfield_value ) || ! $subfield_value_in_options ) {
1140 if ( ! empty( $subfield_options ) ) {
1141 reset( $subfield_options );
1142 $subfield_value = key( $subfield_options );
1143 }
1144 }
1145 }
1146
1147 $base_name = preg_replace( '/\[[^\]]+\]$/', '', $args['name'] );
1148 $subfield_name = $subfield_parent ? $base_name . '[' . $subfield_parent . '][' . $subfield_id . ']' : $base_name . '[' . $subfield_id . ']';
1149
1150 // prepare subfield args
1151 $subfield_args = [
1152 'id' => $args['id'] . '-' . $subfield_id,
1153 'html_id' => $args['html_id'] . '-' . sanitize_html_class( str_replace( '_', '-', $subfield_id ) ),
1154 'name' => $subfield_name,
1155 'type' => $subfield_type,
1156 'label' => ! empty( $subfield['label'] ) ? $subfield['label'] : '',
1157 'description' => ! empty( $subfield['description'] ) ? $subfield['description'] : '',
1158 'class' => ! empty( $subfield['class'] ) ? $subfield['class'] : '',
1159 'disabled' => ! empty( $subfield['disabled'] ),
1160 'options' => $subfield_options,
1161 'default' => $subfield_default,
1162 'value' => $subfield_value,
1163 'setting_id' => $args['setting_id'],
1164 'parent' => $subfield_parent ? $subfield_parent : '',
1165 'return' => true
1166 ];
1167
1168 // pass callback/callback_args for custom subfields (enables Conditional Logic, etc.)
1169 if ( ! empty( $subfield['callback'] ) )
1170 $subfield_args['callback'] = $subfield['callback'];
1171 if ( ! empty( $subfield['callback_args'] ) )
1172 $subfield_args['callback_args'] = $subfield['callback_args'];
1173
1174 // pass logic and animation for conditional visibility
1175 if ( ! empty( $subfield['logic'] ) )
1176 $subfield_args['logic'] = $subfield['logic'];
1177 if ( ! empty( $subfield['animation'] ) )
1178 $subfield_args['animation'] = $subfield['animation'];
1179
1180 $html .= $this->render_field( $subfield_args );
1181
1182 if ( $count < $count_fields )
1183 $html .= '<br />';
1184
1185 $count++;
1186 }
1187
1188 $html .= '</fieldset>';
1189 }
1190 break;
1191
1192 case 'number':
1193 $html .= ( ! empty( $args['prepend'] ) ? wp_kses_post( $args['prepend'] ) : '' );
1194 $html .= '<input id="' . esc_attr( $args['html_id'] ) . '" type="text" value="' . esc_attr( $args['value'] ) . '" name="' . esc_attr( $args['name'] ) . '" class="small-text" />';
1195 $html .= ( ! empty( $args['append'] ) ? wp_kses_post( $args['append'] ) : '' );
1196 break;
1197
1198 case 'range':
1199 $range_attrs = '';
1200
1201 if ( isset( $args['min'] ) && $args['min'] !== 0 )
1202 $range_attrs .= ' min="' . esc_attr( (int) $args['min'] ) . '"';
1203
1204 if ( isset( $args['max'] ) && $args['max'] !== 0 )
1205 $range_attrs .= ' max="' . esc_attr( (int) $args['max'] ) . '"';
1206
1207 $html .= '<div class="' . $this->prefix . '-range-control">';
1208 $html .= '<input id="' . esc_attr( $args['html_id'] ) . '" type="range" value="' . esc_attr( $args['value'] ) . '" name="' . esc_attr( $args['name'] ) . '"' . $range_attrs . ' ' . disabled( empty( $args['disabled'] ), false, false ) . ' />';
1209 $html .= '</div>';
1210 break;
1211
1212 case 'color':
1213 case 'color_picker':
1214 $color_value = esc_attr( $args['value'] );
1215 $color_name = esc_attr( $args['name'] );
1216 $input_id = esc_attr( $args['html_id'] );
1217 $input_class = $this->prefix . '-color-input';
1218
1219 if ( ! empty( $args['subclass'] ) )
1220 $input_class .= ' ' . esc_attr( $args['subclass'] );
1221
1222 $swatch_style = ' style="background-color: ' . $color_value . ';"';
1223
1224 $html .= '<div class="' . $this->prefix . '-color-control">';
1225 $html .= '<input id="' . $input_id . '" type="text" name="' . $color_name . '" value="' . $color_value . '" class="small-text ' . $input_class . '" />';
1226 $html .= '<button type="button" class="' . $this->prefix . '-color-swatch"' . $swatch_style . ' aria-label="' . esc_attr__( 'Open color picker', 'responsive-lightbox' ) . '" aria-expanded="false"></button>';
1227 $html .= '<div class="' . $this->prefix . '-color-popover" aria-hidden="true"><hex-color-picker class="' . $this->prefix . '-hex-color-picker" color="' . $color_value . '"></hex-color-picker></div>';
1228 $html .= '</div>';
1229 break;
1230
1231 case 'custom':
1232 if ( ! empty( $args['callback'] ) && is_callable( $args['callback'] ) )
1233 $html .= call_user_func( $args['callback'], $args );
1234 break;
1235
1236 case 'info':
1237 $html .= '<span' . ( ! empty( $args['subclass'] ) ? ' class="' . esc_attr( $args['subclass'] ) . '"' : '' ) . '>' . esc_html( $args['text'] ) . '</span>';
1238 break;
1239
1240 case 'class':
1241 case 'input':
1242 case 'text':
1243 default:
1244 $empty_disabled = empty( $args['disabled'] );
1245
1246 $html .= ( ! empty( $args['prepend'] ) ? wp_kses_post( $args['prepend'] ) : '' );
1247 $html .= '<input id="' . esc_attr( $args['html_id'] ) . '"' . ( ! empty( $args['subclass'] ) ? ' class="' . esc_attr( $args['subclass'] ) . '"' : '' ) . ' type="text" value="' . esc_attr( $args['value'] ) . '" name="' . esc_attr( $args['name'] ) . '" ' . disabled( $empty_disabled, false, false ) . '/>';
1248 $html .= ( ! empty( $args['append'] ) ? wp_kses_post( $args['append'] ) : '' );
1249
1250 if ( ! $empty_disabled )
1251 $html .= '<input' . ( $empty_disabled ? '' : ' class="hidden"' ) . ' type="text" value="' . esc_attr( $args['value'] ) . '" name="' . esc_attr( $args['name'] ) . '">';
1252 }
1253
1254 if ( ! empty( $args['after_field'] ) )
1255 $html .= $args['after_field'];
1256
1257 if ( ! empty( $args['description'] ) )
1258 $html .= '<p class="description">' . $args['description'] . '</p>';
1259
1260 $html .= '</div>';
1261
1262 if ( ! empty( $args['return'] ) )
1263 return $html;
1264 else
1265 echo $html;
1266 }
1267
1268 /**
1269 * Validate settings field.
1270 *
1271 * @param mixed $value Field value.
1272 * @param string $type Field type.
1273 * @param array $args Field arguments.
1274 * @return mixed Validated value.
1275 */
1276 public function validate_field( $value = null, $type = '', $args = [] ) {
1277 if ( is_null( $value ) )
1278 return null;
1279
1280 switch ( $type ) {
1281 case 'boolean':
1282 $value = ( $value === 'true' || $value === true );
1283 break;
1284
1285 case 'radio':
1286 $value = is_array( $value ) ? $args['default'] : sanitize_key( $value );
1287
1288 // disallow disabled radios
1289 if ( ! empty( $args['disabled'] ) && in_array( $value, $args['disabled'], true ) )
1290 $value = $args['default'];
1291 break;
1292
1293 case 'checkbox':
1294 if ( $value === 'empty' )
1295 $value = [];
1296 else {
1297 if ( is_array( $value ) && ! empty( $value ) ) {
1298 $value = array_map( 'sanitize_key', $value );
1299 $values = [];
1300
1301 foreach ( $value as $single_value ) {
1302 if ( array_key_exists( $single_value, $args['options'] ) )
1303 $values[] = $single_value;
1304 }
1305
1306 $value = $values;
1307 } else {
1308 $value = [];
1309 }
1310 }
1311 break;
1312
1313 case 'number':
1314 $value = (int) $value;
1315
1316 if ( isset( $args['min'] ) && $value < $args['min'] )
1317 $value = $args['min'];
1318
1319 if ( isset( $args['max'] ) && $value > $args['max'] )
1320 $value = $args['max'];
1321 break;
1322
1323 case 'range':
1324 $value = (int) $value;
1325
1326 if ( isset( $args['min'] ) && $value < $args['min'] )
1327 $value = $args['min'];
1328
1329 if ( isset( $args['max'] ) && $value > $args['max'] )
1330 $value = $args['max'];
1331 break;
1332
1333 case 'color':
1334 case 'color_picker':
1335 $value = sanitize_text_field( $value );
1336
1337 if ( ! preg_match( '/^#[a-f0-9]{3,6}$/i', $value ) )
1338 $value = $args['default'] ?? '#000000';
1339 break;
1340
1341 case 'info':
1342 $value = '';
1343 break;
1344
1345 case 'custom':
1346 // handled by custom validate callback
1347 break;
1348
1349 case 'class':
1350 $value = trim( $value );
1351
1352 if ( strpos( $value, ' ' ) !== false ) {
1353 $value = array_unique( array_filter( array_map( 'sanitize_html_class', explode( ' ', $value ) ) ) );
1354
1355 if ( ! empty( $value ) )
1356 $value = implode( ' ', $value );
1357 else
1358 $value = '';
1359 } else {
1360 $value = sanitize_html_class( $value, $args['default'] );
1361 }
1362 break;
1363
1364 case 'input':
1365 case 'text':
1366 case 'select':
1367 default:
1368 $value = is_array( $value ) ? array_map( 'sanitize_text_field', $value ) : sanitize_text_field( $value );
1369 break;
1370 }
1371
1372 return stripslashes_deep( $value );
1373 }
1374
1375 /**
1376 * Validate settings.
1377 *
1378 * @param array $input Input data.
1379 * @return array Validated data.
1380 */
1381 public function validate_settings( $input ) {
1382 // check capability
1383 if ( ! current_user_can( 'manage_options' ) )
1384 return $input;
1385
1386 // check option page
1387 if ( empty( $_POST['option_page'] ) )
1388 return $input;
1389
1390 // try to get setting name and ID
1391 foreach ( $this->settings as $id => $setting ) {
1392 // tabs?
1393 if ( is_array( $setting['option_name'] ) ) {
1394 foreach ( $setting['option_name'] as $tab => $option_name ) {
1395 // found valid setting?
1396 if ( $option_name === $_POST['option_page'] ) {
1397 $setting_id = $tab;
1398 $setting_name = $option_name;
1399 $setting_key = $id;
1400 break 2;
1401 }
1402 }
1403 } else {
1404 // found valid setting?
1405 if ( $setting['option_name'] === $_POST['option_page'] ) {
1406 $setting_key = $setting_id = $id;
1407 $setting_name = $setting['option_name'];
1408 break;
1409 }
1410 }
1411 }
1412
1413 // check setting id
1414 if ( empty( $setting_id ) )
1415 return $input;
1416
1417 // save settings
1418 if ( isset( $_POST['save_' . $setting_name] ) ) {
1419 $input = $this->validate_input_settings( $setting_id, $setting_key, $input );
1420
1421 add_settings_error( $setting_name, 'settings_saved', __( 'Settings saved.', 'responsive-lightbox' ), 'updated' );
1422 // reset settings
1423 } elseif ( isset( $_POST['reset_' . $setting_name] ) ) {
1424 // get default values
1425 $input = $this->object->defaults[$setting_id];
1426
1427 // check custom reset functions
1428 if ( ! empty( $this->settings[$setting_key]['fields'] ) ) {
1429 foreach ( $this->settings[$setting_key]['fields'] as $field_id => $field ) {
1430 // skip invalid tab field if any
1431 if ( ! empty( $field['tab'] ) && $field['tab'] !== $setting_id )
1432 continue;
1433
1434 // custom reset function?
1435 if ( ! empty( $field['reset'] ) ) {
1436 if ( $this->callback_function_exists( $field['reset'] ) ) {
1437 if ( $field['type'] === 'custom' )
1438 $input = call_user_func( $field['reset'], $input, $field );
1439 else
1440 $input[$field_id] = call_user_func( $field['reset'], $input[$field_id], $field );
1441 }
1442 }
1443 }
1444 }
1445
1446 add_settings_error( $setting_name, 'settings_restored', __( 'Settings restored to defaults.', 'responsive-lightbox' ), 'updated' );
1447 }
1448
1449 do_action( $this->prefix . '_configuration_updated', 'settings', $input );
1450
1451 return $input;
1452 }
1453
1454 /**
1455 * Validate input settings.
1456 *
1457 * @param string $setting_id Setting identifier.
1458 * @param string $setting_key Setting key.
1459 * @param array $input Input data.
1460 * @return array Validated data.
1461 */
1462 public function validate_input_settings( $setting_id, $setting_key, $input ) {
1463 $all_fields = [];
1464
1465 // collect fields from top-level 'fields' array
1466 if ( ! empty( $this->settings[$setting_key]['fields'] ) ) {
1467 foreach ( $this->settings[$setting_key]['fields'] as $field_key => $field ) {
1468 $all_fields[$field_key] = $field;
1469 }
1470 }
1471
1472 // collect fields from sections (PVC-style nested structure)
1473 if ( ! empty( $this->settings[$setting_key]['sections'] ) ) {
1474 foreach ( $this->settings[$setting_key]['sections'] as $section_id => $section ) {
1475 if ( ! empty( $section['fields'] ) ) {
1476 foreach ( $section['fields'] as $field_key => $field ) {
1477 // auto-assign section if not specified
1478 if ( empty( $field['section'] ) )
1479 $field['section'] = $section_id;
1480
1481 $all_fields[$field_key] = $field;
1482 }
1483 }
1484 }
1485 }
1486
1487 if ( ! empty( $all_fields ) ) {
1488 foreach ( $all_fields as $field_id => $field ) {
1489 // skip saving this field?
1490 if ( ! empty( $field['skip_saving'] ) )
1491 continue;
1492
1493 // skip invalid tab field if any
1494 if ( ! empty( $field['tab'] ) && $field['tab'] !== $setting_id )
1495 continue;
1496
1497 $field_type = ! empty( $field['type'] ) ? $field['type'] : 'text';
1498
1499 // custom validate function?
1500 if ( ! empty( $field['validate'] ) ) {
1501 if ( $this->callback_function_exists( $field['validate'] ) ) {
1502 if ( $field['type'] === 'custom' )
1503 $input = call_user_func( $field['validate'], $input, $field );
1504 else
1505 $input[$field_id] = isset( $input[$field_id] ) ? call_user_func( $field['validate'], $input[$field_id], $field ) : $this->object->defaults[$setting_id][$field_id];
1506 } else {
1507 $input[$field_id] = $this->object->defaults[$setting_id][$field_id] ?? ( $field['default'] ?? null );
1508 }
1509 } else {
1510 // field data?
1511 if ( isset( $input[$field_id] ) ) {
1512 // make sure default value is available
1513 if ( ! isset( $field['default'] ) )
1514 $field['default'] = $this->object->defaults[$setting_id][$field_id];
1515
1516 $input[$field_id] = $this->validate_field( $input[$field_id], $field['type'], $field );
1517 } else {
1518 if ( $field_type === 'boolean' ) {
1519 $input[$field_id] = false;
1520 } elseif ( $field_type === 'checkbox' ) {
1521 $input[$field_id] = [];
1522 } else {
1523 $input[$field_id] = $this->object->defaults[$setting_id][$field_id] ?? ( $field['default'] ?? null );
1524 }
1525 }
1526 }
1527
1528 // update input data
1529 $this->input_settings = $input;
1530
1531 // add this field as validated
1532 $this->validated_settings[] = $field_id;
1533 }
1534 }
1535
1536 return $input;
1537 }
1538
1539 /**
1540 * Check whether callback is a valid function.
1541 *
1542 * @param string|array $callback Callback to check.
1543 * @return bool Whether callback exists.
1544 */
1545 public function callback_function_exists( $callback ) {
1546 if ( is_array( $callback ) ) {
1547 list( $object, $function ) = $callback;
1548 $function_exists = method_exists( $object, $function );
1549 } elseif ( is_string( $callback ) ) {
1550 $function_exists = function_exists( $callback );
1551 } else {
1552 $function_exists = false;
1553 }
1554
1555 return $function_exists;
1556 }
1557
1558 /**
1559 * Get value based on minimum and maximum.
1560 *
1561 * @param array $data Data array.
1562 * @param string $setting_name Setting name.
1563 * @param int $default Default value.
1564 * @param int $min Minimum value.
1565 * @param int $max Maximum value.
1566 * @return int Validated integer value.
1567 */
1568 public function get_int_value( $data, $setting_name, $default, $min, $max ) {
1569 $value = array_key_exists( $setting_name, $data ) ? (int) $data[$setting_name] : $default;
1570
1571 if ( $value > $max || $value < $min )
1572 $value = $default;
1573
1574 return $value;
1575 }
1576
1577 /**
1578 * Add new capability to manage options.
1579 *
1580 * @return string Required capability.
1581 */
1582 public function manage_options_capability() {
1583 $rl = Responsive_Lightbox();
1584
1585 return $rl->options['capabilities']['active'] ? 'edit_lightbox_settings' : 'manage_options';
1586 }
1587 }
1588
1589