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