PluginProbe
WebberZone Top 10 — Popular Posts / 4.5.2
WebberZone Top 10 — Popular Posts v4.5.2
4.5.2 4.5.1 4.5.0 4.4.3 4.4.2 4.4.1 4.4.0 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 trunk 1.0 1.0.1 1.1 1.2 1.3 1.4 1.4.1 1.5 1.5.1 1.5.2 1.5.3 1.6 All 118 releases
top-10 / includes / admin / settings / class-settings-form.php

class-settings-form.php in WebberZone Top 10 — Popular Posts 4.5.2, at includes/admin/settings/class-settings-form.php

1,477 lines 46.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Generates the settings form.
4 *
5 * @link https://webberzone.com
6 *
7 * @package WebberZone\Top_Ten
8 */
9
10 namespace WebberZone\Top_Ten\Admin\Settings;
11
12 // If this file is called directly, abort.
13 if ( ! defined( 'WPINC' ) ) {
14 die;
15 }
16
17 /**
18 * Generates the settings form.
19 *
20 * @since 4.0.0
21 */
22 class Settings_Form {
23
24
25 /**
26 * Settings Key.
27 *
28 * @var string Settings Key.
29 */
30 public $settings_key;
31
32 /**
33 * Prefix which is used for creating the unique filters and actions.
34 *
35 * @var string Prefix.
36 */
37 public $prefix;
38
39 /**
40 * Translation strings.
41 *
42 * @var array Translation strings.
43 */
44 public $translation_strings;
45
46 /**
47 * Main constructor class.
48 *
49 * @param mixed $args {
50 * Array or string of arguments. Default is blank array.
51 * @type string $settings_key Settings key.
52 * @type string $prefix Prefix.
53 * @type array $translation_strings Translation strings.
54 * }
55 */
56 public function __construct( $args ) {
57 $defaults = array(
58 'settings_key' => '',
59 'prefix' => '',
60 'translation_strings' => array(),
61 );
62 $args = wp_parse_args( $args, $defaults );
63
64 foreach ( $args as $name => $value ) {
65 $this->$name = $value;
66 }
67 }
68
69 /**
70 * Get field description for display.
71 *
72 * @param array $args settings Arguments array.
73 *
74 * @return string Description of the field.
75 */
76 public function get_field_description( $args ) {
77 $desc = ! empty( $args['desc'] ) ? '<p class="description">' . wp_kses( $args['desc'], $this->get_allowed_html() ) . '</p>' : '';
78
79 /**
80 * After Settings Output filter
81 *
82 * @param string $desc Description of the field.
83 * @param array $args Arguments array.
84 */
85 $desc = apply_filters( $this->prefix . '_setting_field_description', $desc, $args ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
86
87 return $this->get_modified_indicator( $args ) . $desc . $this->get_default_description( $args );
88 }
89
90 /**
91 * Get the "Default: value" explanation shown below a field's description.
92 *
93 * Checkbox/toggle fields are excluded - their default state is visible
94 * from the toggle itself and the modified indicator.
95 *
96 * @param array $args Field arguments.
97 * @return string Default description HTML or empty string.
98 */
99 protected function get_default_description( $args ) {
100 $type = $args['type'] ?? 'text';
101
102 if ( in_array( $type, array( 'checkbox', 'toggle', 'header', 'descriptive_text', 'repeater' ), true ) ) {
103 return '';
104 }
105
106 $default = $this->get_field_default( $args );
107 if ( is_array( $default ) ) {
108 $default = implode( ',', array_map( 'strval', $default ) );
109 }
110 $default = trim( (string) $default, " \t\n\r\0\x0B" );
111
112 // Map option keys to their labels where the field has a choices list.
113 if ( ! empty( $args['options'] ) && is_array( $args['options'] ) ) {
114 if ( 'radiodesc' === $type && '' !== $default ) {
115 foreach ( $args['options'] as $option ) {
116 if ( isset( $option['id'], $option['name'] ) && (string) $option['id'] === $default ) {
117 $default = (string) $option['name'];
118 break;
119 }
120 }
121 } elseif ( in_array( $type, array( 'select', 'radio' ), true ) ) {
122 if ( isset( $args['options'][ $default ] ) && is_string( $args['options'][ $default ] ) ) {
123 $default = $args['options'][ $default ];
124 }
125 } elseif ( '' !== $default && 'multicheck' === $type ) {
126 $labels = array();
127 foreach ( explode( ',', $default ) as $key ) {
128 $key = trim( $key, " \t\n\r\0\x0B" );
129 $labels[] = isset( $args['options'][ $key ] ) && is_string( $args['options'][ $key ] ) ? $args['options'][ $key ] : $key;
130 }
131 $default = implode( ', ', $labels );
132 }
133 }
134
135 $label = $this->translation_strings['default_label'] ?? 'Default';
136 $none = $this->translation_strings['default_none'] ?? 'None';
137
138 $value_html = '' === $default ? esc_html( $none ) : '<code>' . esc_html( $default ) . '</code>';
139
140 return '<p class="description wz-default-value">' . esc_html( $label ) . ': ' . $value_html . '</p>';
141 }
142
143 /**
144 * Get the default value of a field, mirroring the back-compat logic in
145 * Settings_API::settings_defaults().
146 *
147 * @param array $args Field arguments.
148 * @return mixed Default value.
149 */
150 protected function get_field_default( $args ) {
151 if ( isset( $args['default'] ) ) {
152 return $args['default'];
153 }
154
155 $type = $args['type'] ?? 'text';
156
157 // Back-compat: checkbox used a truthy 'options' to indicate checked by default.
158 if ( 'checkbox' === $type || 'toggle' === $type ) {
159 return empty( $args['options'] ) ? 0 : 1;
160 }
161
162 // Back-compat: legacy configs used 'options' to store default values for text-like fields.
163 if ( in_array( $type, array( 'textarea', 'css', 'html', 'text', 'url', 'csv', 'color', 'numbercsv', 'postids', 'posttypes', 'number', 'wysiwyg', 'file', 'password' ), true ) && isset( $args['options'] ) && is_scalar( $args['options'] ) ) {
164 return $args['options'];
165 }
166
167 return '';
168 }
169
170 /**
171 * Whether the current value of a field differs from its default.
172 *
173 * @param array $args Field arguments.
174 * @return bool True when the saved value does not match the default.
175 */
176 protected function is_field_modified( $args ) {
177 $type = $args['type'] ?? 'text';
178
179 if ( in_array( $type, array( 'header', 'descriptive_text', 'repeater' ), true ) ) {
180 return false;
181 }
182
183 // Fields can opt out, e.g. when the default is environment-derived
184 // (URLs based on the plugin location) and literal comparison is noise.
185 if ( isset( $args['modified_indicator'] ) && false === $args['modified_indicator'] ) {
186 return false;
187 }
188
189 $default = $this->get_field_default( $args );
190 $value = $this->get_field_value( $args );
191
192 if ( 'checkbox' === $type || 'toggle' === $type ) {
193 return (bool) $value !== (bool) $default;
194 }
195
196 if ( is_array( $value ) || is_array( $default ) || in_array( $type, array( 'multicheck', 'posttypes', 'taxonomies' ), true ) ) {
197 return $this->normalize_list_value( $value ) !== $this->normalize_list_value( $default );
198 }
199
200 return trim( (string) $value, " \t\n\r\0\x0B" ) !== trim( (string) $default, " \t\n\r\0\x0B" );
201 }
202
203 /**
204 * Normalize a list-style value (array or comma-separated string) into a
205 * sorted comma-separated string for comparison.
206 *
207 * @param mixed $value Value to normalize.
208 * @return string Normalized value.
209 */
210 protected function normalize_list_value( $value ) {
211 if ( is_array( $value ) ) {
212 $value = implode( ',', array_map( 'strval', $value ) );
213 }
214 $list = array_filter(
215 array_map( 'trim', explode( ',', (string) $value ) ),
216 static function ( $item ) {
217 return '' !== $item;
218 }
219 );
220 sort( $list );
221
222 return implode( ',', $list );
223 }
224
225 /**
226 * Get the modified-from-default indicator for a field.
227 *
228 * Returns a small dot with a tooltip when the saved value differs from
229 * the default; the legend below the form buttons explains it.
230 *
231 * @param array $args Field arguments.
232 * @return string Indicator HTML or empty string.
233 */
234 public function get_modified_indicator( $args ) {
235 if ( ! $this->is_field_modified( $args ) ) {
236 return '';
237 }
238
239 $title = $this->translation_strings['modified_field'] ?? 'Modified from default setting';
240
241 return sprintf( '<span class="wz-modified-dot" title="%s"></span>', esc_attr( $title ) );
242 }
243
244 /**
245 * Get the value of a settings field.
246 *
247 * @param string $option Settings field name.
248 * @param mixed $default_value Default value if option is not found.
249 * @return mixed
250 */
251 public function get_option( $option, $default_value = '' ) {
252
253 $options = \get_option( $this->settings_key );
254
255 if ( isset( $options[ $option ] ) ) {
256 return $options[ $option ];
257 }
258
259 return $default_value;
260 }
261
262 /**
263 * Get field value from args or options.
264 *
265 * @param array $args Field arguments.
266 * @return mixed Field value.
267 */
268 protected function get_field_value( $args ) {
269 return $args['value'] ?? $this->get_option( $args['id'], $args['default'] ?? '' );
270 }
271
272 /**
273 * Get sanitized field class string.
274 *
275 * @param array $args Field arguments.
276 * @param string $default_class Default class to prepend.
277 * @return string Sanitized class string.
278 */
279 protected function get_field_class( $args, $default_class = '' ) {
280 $class = implode( ' ', array_map( 'sanitize_html_class', explode( ' ', $args['field_class'] ?? '' ) ) );
281 if ( $default_class ) {
282 $class = $default_class . ' ' . $class;
283 }
284 return trim( $class, " \t\n\r\0\x0B" );
285 }
286
287 /**
288 * Get placeholder attribute string.
289 *
290 * @param array $args Field arguments.
291 * @return string Placeholder attribute or empty string.
292 */
293 protected function get_placeholder_attribute( $args ) {
294 return empty( $args['placeholder'] ) ? '' : ' placeholder="' . esc_attr( $args['placeholder'] ) . '"';
295 }
296
297 /**
298 * Get boolean state attributes (disabled, readonly, required).
299 *
300 * @param array $args Field arguments.
301 * @return string Concatenated boolean attributes.
302 */
303 protected function get_boolean_attributes( $args ) {
304 $disabled = $this->is_field_disabled( $args ) ? ' disabled="disabled"' : '';
305 $readonly = ( isset( $args['readonly'] ) && true === $args['readonly'] ) ? ' readonly="readonly"' : '';
306 $required = ( isset( $args['required'] ) && true === $args['required'] ) ? ' required' : '';
307 return $disabled . $readonly . $required;
308 }
309
310 /**
311 * Whether a field is disabled, either explicitly or because it is a pro-only field.
312 *
313 * @param array $args Field arguments.
314 * @return bool Whether the field is disabled.
315 */
316 protected function is_field_disabled( $args ) {
317 return ! empty( $args['disabled'] ) || ! empty( $args['pro'] );
318 }
319
320 /**
321 * Get field ID and name attributes.
322 *
323 * @param array $args Field arguments.
324 * @return array Array containing field_id and field_name.
325 */
326 protected function get_field_attributes( $args ) {
327 $id = sanitize_key( $args['id'] );
328 if ( isset( $args['_repeater_id'] ) && isset( $args['_index'] ) ) {
329 $field_id = sprintf(
330 '%s-%s-%s-fields-%s',
331 $this->settings_key,
332 $args['_repeater_id'],
333 $args['_index'],
334 $id
335 );
336 $field_name = sprintf(
337 '%s[%s][%s][fields][%s]',
338 $this->settings_key,
339 $args['_repeater_id'],
340 $args['_index'],
341 $id
342 );
343 } else {
344 $field_id = $this->settings_key . '-' . $id;
345 $field_name = $this->settings_key . '[' . $id . ']';
346 }
347
348 return array(
349 'field_id' => $field_id,
350 'field_name' => $field_name,
351 );
352 }
353
354 /**
355 * Get the rendered field ID for a field definition.
356 *
357 * @param array $args Field arguments.
358 * @return string Field ID.
359 */
360 public function get_field_id( $args ) {
361 $field_attributes = $this->get_field_attributes( $args );
362
363 return $field_attributes['field_id'];
364 }
365
366 /**
367 * Returns the allowed HTML tags and attributes for settings form output.
368 *
369 * Use the `{prefix}_settings_form_allowed_html` filter to add extra tags or
370 * attributes needed by custom field types or `field_attributes` entries.
371 *
372 * @return array<string, array<string, bool>>
373 */
374 protected function get_allowed_html(): array {
375 $allowed = wp_kses_allowed_html( 'post' );
376
377 $form_tags = array(
378 'input' => array(
379 'type' => true,
380 'name' => true,
381 'id' => true,
382 'value' => true,
383 'class' => true,
384 'style' => true,
385 'checked' => true,
386 'disabled' => true,
387 'readonly' => true,
388 'required' => true,
389 'placeholder' => true,
390 'size' => true,
391 'min' => true,
392 'max' => true,
393 'step' => true,
394 'multiple' => true,
395 'autocomplete' => true,
396 // Tom Select data attributes (built-in Settings API feature).
397 'data-wp-prefix' => true,
398 'data-wp-action' => true,
399 'data-wp-nonce' => true,
400 'data-wp-endpoint' => true,
401 'data-ts-config' => true,
402 'data-wp-extra-fields' => true,
403 ),
404 'select' => array(
405 'id' => true,
406 'name' => true,
407 'class' => true,
408 'style' => true,
409 'disabled' => true,
410 'multiple' => true,
411 'size' => true,
412 'autocomplete' => true,
413 // Tom Select data attributes (built-in Settings API feature).
414 'data-wp-prefix' => true,
415 'data-wp-action' => true,
416 'data-wp-nonce' => true,
417 'data-wp-endpoint' => true,
418 'data-ts-config' => true,
419 'data-wp-extra-fields' => true,
420 ),
421 'option' => array(
422 'value' => true,
423 'selected' => true,
424 'disabled' => true,
425 ),
426 'optgroup' => array(
427 'label' => true,
428 'disabled' => true,
429 ),
430 'textarea' => array(
431 'id' => true,
432 'name' => true,
433 'class' => true,
434 'style' => true,
435 'rows' => true,
436 'cols' => true,
437 'disabled' => true,
438 'readonly' => true,
439 'required' => true,
440 'placeholder' => true,
441 'autocomplete' => true,
442 ),
443 'label' => array(
444 'for' => true,
445 'class' => true,
446 'style' => true,
447 ),
448 'button' => array(
449 'type' => true,
450 'id' => true,
451 'class' => true,
452 'style' => true,
453 'disabled' => true,
454 'aria-controls' => true,
455 'aria-expanded' => true,
456 'aria-label' => true,
457 ),
458 'div' => array(
459 'id' => true,
460 'class' => true,
461 'style' => true,
462 'aria-hidden' => true,
463 'aria-busy' => true,
464 'role' => true,
465 ),
466 'span' => array(
467 'class' => true,
468 'style' => true,
469 'aria-hidden' => true,
470 'aria-live' => true,
471 'aria-atomic' => true,
472 'role' => true,
473 ),
474 'template' => array(
475 'class' => true,
476 'data-id' => true,
477 ),
478 );
479
480 $allowed = array_replace_recursive( $allowed, $form_tags );
481
482 /**
483 * Filters the allowed HTML tags and attributes for settings form output.
484 *
485 * Use this filter to add data attributes or custom elements required by
486 * field_attributes entries in your registered settings.
487 *
488 * @param array<string, array<string, bool>> $allowed Allowed HTML tags/attributes.
489 */
490 return apply_filters( $this->prefix . '_settings_form_allowed_html', $allowed ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
491 }
492
493 /**
494 * Miscellaneous callback funcion
495 *
496 * @param array $args Arguments array.
497 * @return void
498 */
499 public function callback_missing( $args ) {
500 /* translators: 1: Code. */
501 printf( 'The callback function used for the %1$s setting is missing.', '<strong>' . esc_attr( $args['id'] ) . '</strong>' );
502 }
503
504 /**
505 * Header Callback
506 *
507 * Renders the header.
508 *
509 * @param array $args Arguments passed by the setting.
510 * @return void
511 */
512 public function callback_header( $args ) {
513 $html = $this->get_field_description( $args );
514
515 /**
516 * After Settings Output filter
517 *
518 * @param string $html HTML string.
519 * @param array $args Arguments array.
520 */
521 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
522 }
523
524 /**
525 * Descriptive text callback.
526 *
527 * Renders descriptive text onto the settings field.
528 *
529 * @param array $args Array of arguments.
530 * @return void
531 */
532 public function callback_descriptive_text( $args ) {
533 $this->callback_header( $args );
534 }
535
536 /**
537 * Build additional attributes string from field_attributes.
538 *
539 * @param array $args Field arguments.
540 * @return string Additional attributes string.
541 */
542 protected function build_field_attributes( $args ) {
543 $attributes = '';
544 foreach ( (array) ( $args['field_attributes'] ?? array() ) as $attribute => $val ) {
545 $attr_name = sanitize_key( $attribute );
546 if ( empty( $attr_name ) ) {
547 continue;
548 }
549 if ( true === $val ) {
550 $attributes .= ' ' . $attr_name;
551 } elseif ( false !== $val ) {
552 $attributes .= sprintf( ' %1$s="%2$s"', $attr_name, esc_attr( $val ) );
553 }
554 }
555 return $attributes;
556 }
557
558 /**
559 * Display text fields.
560 *
561 * @param array $args Array of arguments.
562 */
563 public function callback_text( $args ) {
564 $value = $this->get_field_value( $args );
565 $size = sanitize_html_class( $args['size'] ?? 'regular' );
566 $class = $this->get_field_class( $args );
567 $placeholder = $this->get_placeholder_attribute( $args );
568 $attributes = $this->get_boolean_attributes( $args ) . $this->build_field_attributes( $args );
569
570 $field_attributes = $this->get_field_attributes( $args );
571
572 $html = sprintf(
573 '<input type="text" id="%1$s" name="%2$s" class="%3$s" value="%4$s" %5$s %6$s />',
574 $field_attributes['field_id'],
575 $field_attributes['field_name'],
576 $class . ' ' . $size . '-text',
577 esc_attr( stripslashes( $value ) ),
578 $attributes,
579 $placeholder
580 );
581 $html .= $this->get_field_description( $args );
582
583 /**
584 * This filter has been defined in class-settings-api.php
585 */
586 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
587 }
588
589 /**
590 * Display local date/time fields.
591 *
592 * @param array $args Field arguments.
593 */
594 public function callback_datetime( $args ) {
595 $value = $this->get_field_value( $args );
596 $class = $this->get_field_class( $args );
597 $attributes = $this->get_boolean_attributes( $args ) . $this->build_field_attributes( $args );
598 $placeholder = $this->get_placeholder_attribute( $args );
599 $field_attributes = $this->get_field_attributes( $args );
600
601 $html = sprintf(
602 '<input type="datetime-local" id="%1$s" name="%2$s" class="%3$s" value="%4$s" step="60" %5$s %6$s />',
603 $field_attributes['field_id'],
604 $field_attributes['field_name'],
605 $class . ' regular-text',
606 esc_attr( stripslashes( (string) $value ) ),
607 $attributes,
608 $placeholder
609 );
610 $html .= $this->get_field_description( $args );
611
612 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
613 }
614
615 /**
616 * Display url fields.
617 *
618 * @param array $args Array of arguments.
619 */
620 public function callback_url( $args ) {
621 $this->callback_text( $args );
622 }
623
624 /**
625 * Display csv fields.
626 *
627 * @param array $args Array of arguments.
628 */
629 public function callback_csv( $args ) {
630 $this->callback_text( $args );
631 }
632
633 /**
634 * Display color fields.
635 *
636 * @param array $args Array of arguments.
637 */
638 public function callback_color( $args ) {
639 // Add color-field class for wpColorPicker initialization.
640 $args['field_class'] = ! empty( $args['field_class'] ) ? $args['field_class'] . ' color-field' : 'color-field';
641 $this->callback_text( $args );
642 }
643
644 /**
645 * Display numbercsv fields.
646 *
647 * @param array $args Array of arguments.
648 */
649 public function callback_numbercsv( $args ) {
650 $this->callback_text( $args );
651 }
652
653 /**
654 * Display postids fields.
655 *
656 * @param array $args Array of arguments.
657 */
658 public function callback_postids( $args ) {
659 $this->callback_text( $args );
660 }
661
662 /**
663 * Display textarea.
664 *
665 * @param array $args Array of arguments.
666 * @return void
667 */
668 public function callback_textarea( $args ) {
669 $value = $this->get_field_value( $args );
670 $class = $this->get_field_class( $args, 'large-text' );
671 $placeholder = $this->get_placeholder_attribute( $args );
672 $attributes = $this->get_boolean_attributes( $args );
673
674 $field_attributes = $this->get_field_attributes( $args );
675
676 $html = sprintf(
677 '<textarea class="%4$s" cols="50" rows="5" id="%1$s" name="%2$s" %5$s %6$s>%3$s</textarea>',
678 $field_attributes['field_id'],
679 $field_attributes['field_name'],
680 esc_textarea( stripslashes( $value ) ),
681 $class,
682 $attributes,
683 $placeholder
684 );
685 $html .= $this->get_field_description( $args );
686
687 /**
688 * This filter has been defined in class-settings-api.php
689 */
690 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
691 }
692
693 /**
694 * Display CSS fields.
695 *
696 * @param array $args Array of arguments.
697 * @return void
698 */
699 public function callback_css( $args ) {
700 $this->callback_textarea( $args );
701 }
702
703 /**
704 * Display HTML fields.
705 *
706 * @param array $args Array of arguments.
707 * @return void
708 */
709 public function callback_html( $args ) {
710 $this->callback_textarea( $args );
711 }
712
713 /**
714 * Get disabled attribute for pro/premium fields.
715 *
716 * @param array $args Field arguments.
717 * @return string Disabled attribute or empty string.
718 */
719 protected function get_disabled_attribute( $args ) {
720 return $this->is_field_disabled( $args ) ? ' disabled="disabled"' : '';
721 }
722
723 /**
724 * Display checkboxes.
725 *
726 * Renders as a toggle switch by default: a native checkbox wrapped in a
727 * styled label. Pass `no-toggle` in `field_class` to render a plain
728 * checkbox instead. Posting semantics are identical either way - a hidden
729 * field posts -1 when unchecked and the checkbox posts 1 when checked.
730 *
731 * @param array $args Array of arguments.
732 * @return void
733 */
734 public function callback_checkbox( $args ) {
735 $value = $this->get_field_value( $args );
736 $checked = ! empty( $value ) ? checked( 1, $value, false ) : '';
737 $disabled = $this->get_disabled_attribute( $args );
738 $class = $this->get_field_class( $args );
739 $no_toggle = in_array( 'no-toggle', explode( ' ', $class ), true );
740
741 $field_attributes = $this->get_field_attributes( $args );
742
743 // The hidden input is also disabled when the field is disabled so that
744 // the setting key is not saved at all and retains its default value.
745 $html = sprintf(
746 '<input type="hidden" name="%1$s" value="-1" %2$s />',
747 $field_attributes['field_name'],
748 $disabled
749 );
750
751 if ( $no_toggle ) {
752 $html .= sprintf(
753 '<input type="checkbox" id="%1$s" name="%2$s" value="1" class="%3$s" %4$s %5$s />',
754 $field_attributes['field_id'],
755 $field_attributes['field_name'],
756 $class,
757 $checked,
758 $disabled
759 );
760 } else {
761 $html .= sprintf(
762 '<label class="wz-toggle"><input type="checkbox" id="%1$s" name="%2$s" value="1" class="%3$s" %4$s %5$s /><span class="wz-toggle-slider"></span></label>',
763 $field_attributes['field_id'],
764 $field_attributes['field_name'],
765 $class,
766 $checked,
767 $disabled
768 );
769 }
770
771 $html .= $this->get_field_description( $args );
772
773 /**
774 * This filter has been defined in class-settings-api.php
775 */
776 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
777 }
778
779 /**
780 * Toggle Callback
781 *
782 * Alias of the checkbox type, which renders as a toggle switch.
783 *
784 * @param array $args Array of arguments.
785 * @return void
786 */
787 public function callback_toggle( $args ) {
788 $this->callback_checkbox( $args );
789 }
790
791 /**
792 * Multicheck Callback
793 *
794 * Renders multiple checkboxes.
795 *
796 * @param array $args Array of arguments.
797 * @return void
798 */
799 public function callback_multicheck( $args ) {
800 $html = '';
801
802 $value = $this->get_field_value( $args );
803 $value_array = wp_parse_list( $value );
804 $disabled = $this->get_disabled_attribute( $args );
805
806 $field_attributes = $this->get_field_attributes( $args );
807
808 if ( ! empty( $args['options'] ) ) {
809 $html .= sprintf(
810 '<input type="hidden" name="%1$s" value="-1" />',
811 $field_attributes['field_name']
812 );
813
814 foreach ( $args['options'] as $key => $option ) {
815 if ( in_array( $key, $value_array, true ) ) {
816 $enabled = $key;
817 } else {
818 $enabled = null;
819 }
820
821 $option_id = $field_attributes['field_id'] . '-' . sanitize_key( $key );
822 $option_name = $field_attributes['field_name'] . '[' . sanitize_key( $key ) . ']';
823
824 $html .= sprintf(
825 '<input name="%1$s" id="%2$s" type="checkbox" value="%3$s" %4$s %5$s /> ',
826 $option_name,
827 $option_id,
828 esc_attr( $key ),
829 checked( $key, $enabled, false ),
830 $disabled
831 );
832 $html .= sprintf(
833 '<label for="%1$s">%2$s</label> <br />',
834 $option_id,
835 wp_kses_post( $option )
836 );
837 }
838 }
839 $html .= $this->get_field_description( $args );
840
841 /**
842 * This filter has been defined in class-settings-api.php
843 */
844 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
845 }
846
847 /**
848 * Radio Callback
849 *
850 * Renders radio boxes.
851 *
852 * @param array $args Array of arguments.
853 * @return void
854 */
855 public function callback_radio( $args ) {
856 $html = '';
857
858 $value = $this->get_field_value( $args );
859 $disabled = $this->get_disabled_attribute( $args );
860
861 $field_attributes = $this->get_field_attributes( $args );
862
863 foreach ( $args['options'] as $key => $option ) {
864 $option_id = $field_attributes['field_id'] . '-' . $key;
865
866 $html .= sprintf(
867 '<input name="%1$s" id="%2$s" type="radio" value="%3$s" %4$s %5$s /> ',
868 $field_attributes['field_name'],
869 $option_id,
870 $key,
871 checked( $value, $key, false ),
872 $disabled
873 );
874 $html .= sprintf(
875 '<label for="%1$s">%2$s</label> <br />',
876 $option_id,
877 wp_kses_post( $option )
878 );
879 }
880
881 $html .= $this->get_field_description( $args );
882
883 /**
884 * This filter has been defined in class-settings-api.php
885 */
886 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
887 }
888
889 /**
890 * Radio callback with description.
891 *
892 * Renders radio boxes with each item having it separate description.
893 *
894 * @param array $args Array of arguments.
895 * @return void
896 */
897 public function callback_radiodesc( $args ) {
898 $html = '';
899
900 $value = $this->get_field_value( $args );
901 $disabled = $this->get_disabled_attribute( $args );
902
903 $field_attributes = $this->get_field_attributes( $args );
904
905 foreach ( $args['options'] as $option ) {
906 $option_id = $field_attributes['field_id'] . '-' . $option['id'];
907
908 $html .= sprintf(
909 '<input name="%1$s" id="%2$s" type="radio" value="%3$s" %4$s %5$s /> ',
910 $field_attributes['field_name'],
911 $option_id,
912 $option['id'],
913 checked( $value, $option['id'], false ),
914 $disabled
915 );
916 $html .= sprintf(
917 '<label for="%1$s">%2$s: <em>%3$s</em></label>',
918 $option_id,
919 wp_kses_post( $option['name'] ),
920 wp_kses_post( $option['description'] )
921 );
922
923 $html .= '<br />';
924 }
925
926 $html .= $this->get_field_description( $args );
927
928 /**
929 * This filter has been defined in class-settings-api.php
930 */
931 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
932 }
933
934 /**
935 * Radio callback with description.
936 *
937 * Renders radio boxes with each item having it separate description.
938 *
939 * @param array $args Array of arguments.
940 * @return void
941 */
942 public function callback_thumbsizes( $args ) {
943 $html = '';
944
945 $thumb_size = $this->prefix . '_thumbnail';
946
947 if ( ! isset( $args['options'][ $thumb_size ] ) ) {
948 $args['options'][ $thumb_size ] = array(
949 'name' => $thumb_size,
950 'width' => call_user_func_array( $this->prefix . '_get_option', array( 'thumb_width', 150 ) ),
951 'height' => call_user_func_array( $this->prefix . '_get_option', array( 'thumb_height', 150 ) ),
952 'crop' => call_user_func_array( $this->prefix . '_get_option', array( 'thumb_crop', true ) ),
953 );
954 }
955
956 $value = $this->get_field_value( $args );
957
958 $field_attributes = $this->get_field_attributes( $args );
959
960 foreach ( $args['options'] as $name => $option ) {
961 $option_id = $field_attributes['field_id'] . '-' . $name;
962
963 $html .= sprintf(
964 '<input name="%1$s" id="%2$s" type="radio" value="%3$s" %4$s /> ',
965 $field_attributes['field_name'],
966 $option_id,
967 $name,
968 checked( $value, $name, false )
969 );
970 $html .= sprintf(
971 '<label for="%1$s">%2$s (%3$sx%4$s%5$s)</label> <br />',
972 $option_id,
973 wp_kses_post( $name ),
974 (int) $option['width'],
975 (int) $option['height'],
976 (bool) $option['crop'] ? ' cropped' : ''
977 );
978 }
979
980 $html .= $this->get_field_description( $args );
981
982 /**
983 * This filter has been defined in class-settings-api.php
984 */
985 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
986 }
987
988 /**
989 * Number Callback
990 *
991 * Renders number fields.
992 *
993 * @param array $args Array of arguments.
994 * @return void
995 */
996 public function callback_number( $args ) {
997 $value = $this->get_field_value( $args );
998 $max = isset( $args['max'] ) ? intval( $args['max'] ) : 999999;
999 $min = isset( $args['min'] ) ? intval( $args['min'] ) : 0;
1000 $step = isset( $args['step'] ) && ( is_numeric( $args['step'] ) || 'any' === $args['step'] ) ? $args['step'] : 1;
1001 $size = $args['size'] ?? 'regular';
1002 $placeholder = $this->get_placeholder_attribute( $args );
1003 $attributes = $this->get_boolean_attributes( $args );
1004
1005 $field_attributes = $this->get_field_attributes( $args );
1006
1007 $html = sprintf(
1008 '<input type="number" step="%1$s" max="%2$s" min="%3$s" class="%4$s" id="%5$s" name="%6$s" value="%7$s" %8$s %9$s />',
1009 esc_attr( (string) $step ),
1010 esc_attr( (string) $max ),
1011 esc_attr( (string) $min ),
1012 sanitize_html_class( $size ) . '-text',
1013 $field_attributes['field_id'],
1014 $field_attributes['field_name'],
1015 esc_attr( stripslashes( $value ) ),
1016 $placeholder,
1017 $attributes
1018 );
1019 $html .= $this->get_field_description( $args );
1020
1021 /**
1022 * This filter has been defined in class-settings-api.php
1023 */
1024 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1025 }
1026
1027 /**
1028 * Select Callback
1029 *
1030 * Renders select fields.
1031 *
1032 * @param array $args Array of arguments.
1033 * @return void
1034 */
1035 public function callback_select( $args ) {
1036 $value = $this->get_field_value( $args );
1037 $class = $this->get_field_class( $args );
1038 $required = ( isset( $args['required'] ) && true === $args['required'] ) ? ' required' : '';
1039 $attributes = $this->get_disabled_attribute( $args ) . $required . $this->build_field_attributes( $args );
1040
1041 if ( isset( $args['chosen'] ) ) {
1042 $class .= ' chosen';
1043 }
1044
1045 $field_attributes = $this->get_field_attributes( $args );
1046
1047 $html = sprintf(
1048 '<select id="%1$s" name="%2$s" class="%3$s" %4$s />',
1049 $field_attributes['field_id'],
1050 $field_attributes['field_name'],
1051 $class,
1052 $attributes
1053 );
1054
1055 foreach ( (array) $args['options'] as $option => $name ) {
1056 $html .= sprintf( '<option value="%1$s" %2$s>%3$s</option>', sanitize_key( $option ), selected( $option, $value, false ), esc_html( $name ) );
1057 }
1058
1059 $html .= '</select>';
1060 $html .= $this->get_field_description( $args );
1061
1062 /**
1063 * This filter has been defined in class-settings-api.php
1064 */
1065 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1066 }
1067
1068 /**
1069 * Display posttypes fields.
1070 *
1071 * @param array $args Array of arguments.
1072 * @return void
1073 */
1074 public function callback_posttypes( $args ) {
1075 $html = '';
1076
1077 $options = $this->get_field_value( $args );
1078 $disabled = $this->get_disabled_attribute( $args );
1079
1080 // If post_types contains a query string then parse it with wp_parse_args.
1081 if ( is_string( $options ) && strpos( $options, '=' ) ) {
1082 $post_types = wp_parse_args( $options );
1083 } else {
1084 $post_types = wp_parse_list( $options );
1085 }
1086
1087 $wp_post_types = get_post_types(
1088 array(
1089 'public' => true,
1090 ),
1091 'objects'
1092 );
1093
1094 $posts_types_inc = array_intersect( wp_list_pluck( $wp_post_types, 'name' ), $post_types );
1095
1096 $field_attributes = $this->get_field_attributes( $args );
1097
1098 $html .= sprintf(
1099 '<input type="hidden" name="%1$s" value="-1" />',
1100 $field_attributes['field_name']
1101 );
1102
1103 foreach ( $wp_post_types as $wp_post_type ) {
1104 $option_id = $field_attributes['field_id'] . '-' . esc_attr( $wp_post_type->name );
1105 $option_name = $field_attributes['field_name'] . '[' . esc_attr( $wp_post_type->name ) . ']';
1106
1107 $html .= sprintf(
1108 '<label for="%1$s"><input name="%2$s" id="%1$s" type="checkbox" value="%3$s" %4$s %5$s /> %6$s</label><br />',
1109 $option_id,
1110 $option_name,
1111 esc_attr( $wp_post_type->name ),
1112 checked( true, in_array( $wp_post_type->name, $posts_types_inc, true ), false ),
1113 $disabled,
1114 esc_html( $wp_post_type->label )
1115 );
1116
1117 }
1118
1119 $html .= $this->get_field_description( $args );
1120
1121 /**
1122 * This filter has been defined in class-settings-api.php
1123 */
1124 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1125 }
1126
1127
1128 /**
1129 * Display taxonomies fields.
1130 *
1131 * @param array $args Array of arguments.
1132 * @return void
1133 */
1134 public function callback_taxonomies( $args ) {
1135 $html = '';
1136
1137 $options = $this->get_field_value( $args );
1138
1139 // If taxonomies contains a query string then parse it with wp_parse_args.
1140 if ( is_string( $options ) && strpos( $options, '=' ) ) {
1141 $taxonomies = wp_parse_args( $options );
1142 } else {
1143 $taxonomies = wp_parse_list( $options );
1144 }
1145
1146 /* Fetch taxonomies */
1147 $argsc = array(
1148 'public' => true,
1149 );
1150 $output = 'objects';
1151 $operator = 'and';
1152 $wp_taxonomies = get_taxonomies( $argsc, $output, $operator );
1153
1154 $taxonomies_inc = array_intersect( wp_list_pluck( (array) $wp_taxonomies, 'name' ), $taxonomies );
1155
1156 $field_attributes = $this->get_field_attributes( $args );
1157
1158 $html .= sprintf( '<input type="hidden" name="%1$s" value="-1" />', $field_attributes['field_name'] );
1159
1160 foreach ( $wp_taxonomies as $wp_taxonomy ) {
1161 $option_id = $field_attributes['field_id'] . '-' . esc_attr( $wp_taxonomy->name );
1162 $option_name = $field_attributes['field_name'] . '[' . esc_attr( $wp_taxonomy->name ) . ']';
1163
1164 $html .= sprintf(
1165 '<label for="%1$s"><input name="%2$s" id="%1$s" type="checkbox" value="%3$s" %4$s /> %5$s (%3$s)</label><br />',
1166 $option_id,
1167 $option_name,
1168 esc_attr( $wp_taxonomy->name ),
1169 checked( true, in_array( $wp_taxonomy->name, $taxonomies_inc, true ), false ),
1170 esc_html( $wp_taxonomy->labels->name )
1171 );
1172
1173 }
1174
1175 $html .= $this->get_field_description( $args );
1176
1177 /**
1178 * This filter has been defined in class-settings-api.php
1179 */
1180 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1181 }
1182
1183
1184 /**
1185 * Displays a rich text textarea for a settings field.
1186 *
1187 * @param array $args Array of arguments.
1188 */
1189 public function callback_wysiwyg( $args ) {
1190 $value = $this->get_field_value( $args );
1191 $size = $args['size'] ?? '500px';
1192
1193 $field_attributes = $this->get_field_attributes( $args );
1194
1195 // wp_editor requires a unique ID without brackets.
1196 $editor_id = sanitize_key( str_replace( array( '[', ']' ), array( '-', '' ), $field_attributes['field_id'] ) );
1197
1198 printf( '<div style="max-width: %1$s;">', esc_attr( $size ) );
1199
1200 $editor_settings = array(
1201 'teeny' => true,
1202 'textarea_name' => $field_attributes['field_name'],
1203 'textarea_rows' => 10,
1204 );
1205
1206 if ( isset( $args['options'] ) && is_array( $args['options'] ) ) {
1207 $editor_settings = array_merge( $editor_settings, $args['options'] );
1208 }
1209
1210 wp_editor( $value, $editor_id, $editor_settings );
1211
1212 printf( '</div>' );
1213
1214 echo wp_kses_post( $this->get_field_description( $args ) );
1215 }
1216
1217 /**
1218 * Displays a file upload field for a settings field.
1219 *
1220 * @param array $args Array of arguments.
1221 */
1222 public function callback_file( $args ) {
1223 $value = $this->get_field_value( $args );
1224 $size = sanitize_html_class( $args['size'] ?? 'regular' );
1225 $class = $this->get_field_class( $args );
1226 $label = $args['options']['button_label'] ?? $this->translation_strings['button_label'];
1227
1228 $field_attributes = $this->get_field_attributes( $args );
1229
1230 $html = sprintf(
1231 '<input type="text" class="%1$s" id="%2$s" name="%3$s" value="%4$s"/>',
1232 $class . ' ' . $size . '-text file-url',
1233 $field_attributes['field_id'],
1234 $field_attributes['field_name'],
1235 esc_attr( $value )
1236 );
1237 $html .= sprintf( '<input type="button" class="button button-secondary file-browser" value="%s" />', esc_attr( $label ) );
1238 $html .= $this->get_field_description( $args );
1239
1240 /**
1241 * This filter has been defined in class-settings-api.php
1242 */
1243 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1244 }
1245
1246 /**
1247 * Displays a password field for a settings field.
1248 *
1249 * @param array $args Array of arguments.
1250 */
1251 public function callback_password( $args ) {
1252 $value = $this->get_field_value( $args );
1253 $size = sanitize_html_class( $args['size'] ?? 'regular' );
1254 $class = $this->get_field_class( $args );
1255
1256 $field_attributes = $this->get_field_attributes( $args );
1257
1258 $html = sprintf(
1259 '<input type="password" class="%1$s" id="%2$s" name="%3$s" value="%4$s" %5$s />',
1260 "$class $size-text",
1261 $field_attributes['field_id'],
1262 $field_attributes['field_name'],
1263 esc_attr( $value ),
1264 ! empty( $value ) ? 'placeholder="' . esc_attr( $this->translation_strings['previous_saved'] ) . '"' : ''
1265 );
1266 $html .= $this->get_field_description( $args );
1267
1268 /**
1269 * This filter has been defined in class-settings-api.php
1270 */
1271 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1272 }
1273
1274 /**
1275 * Callback for repeater field.
1276 *
1277 * @param array $args Array of arguments.
1278 * @return void
1279 */
1280 public function callback_repeater( $args ) {
1281 $raw_value = $this->get_field_value( $args );
1282 $value = ! empty( $raw_value ) && is_array( $raw_value ) ? $raw_value : array();
1283
1284 $class = $this->get_field_class( $args );
1285 $attributes = $this->get_boolean_attributes( $args ) . $this->build_field_attributes( $args );
1286
1287 // `disabled` does nothing on the wrapper; the class neutralises the buttons.
1288 if ( $this->is_field_disabled( $args ) ) {
1289 $class .= ' wz-repeater-disabled';
1290 }
1291
1292 $data_index = (string) count( $value );
1293 $live_update_field = ! empty( $args['live_update_field'] ) ? $args['live_update_field'] : 'name';
1294 $fallback_title = ! empty( $args['new_item_text'] ) ? $args['new_item_text'] : $this->translation_strings['repeater_new_item'];
1295 $live_update_options = ! empty( $args['live_update_field_options'] ) && is_array( $args['live_update_field_options'] ) ? $args['live_update_field_options'] : array();
1296
1297 ob_start();
1298 ?>
1299 <div class="<?php echo esc_attr( $class ); ?> wz-repeater-wrapper"
1300 id="<?php echo esc_attr( $args['id'] ); ?>-wrapper"
1301 data-index="<?php echo esc_attr( $data_index ); ?>"
1302 data-live-update-field="<?php echo esc_attr( $live_update_field ); ?>"
1303 data-fallback-title="<?php echo esc_attr( $fallback_title ); ?>"
1304 <?php if ( ! empty( $live_update_options ) ) : ?>
1305 data-live-update-field-options="<?php echo esc_attr( wp_json_encode( $live_update_options ) ); ?>"
1306 <?php endif; ?>
1307 <?php if ( ! empty( $args['unique_field'] ) ) : ?>
1308 data-unique-field="<?php echo esc_attr( $args['unique_field'] ); ?>"
1309 <?php endif; ?>
1310 <?php echo $attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
1311
1312 <div class="<?php echo esc_attr( $args['id'] ); ?>-items wz-repeater-items">
1313 <?php
1314 if ( ! empty( $value ) ) {
1315 foreach ( array_values( $value ) as $index => $item ) {
1316 $this->render_repeater_item( $args, $index, $item );
1317 }
1318 }
1319 ?>
1320 </div>
1321 <button type="button" class="button add-item" data-target="<?php echo esc_attr( $args['id'] ); ?>" <?php disabled( $this->is_field_disabled( $args ) ); ?>>
1322 <?php echo esc_html( ! empty( $args['add_button_text'] ) ? $args['add_button_text'] : 'Add Item' ); ?>
1323 </button>
1324
1325 <template class="repeater-template" data-id="<?php echo esc_attr( $args['id'] ); ?>">
1326 <?php $this->render_repeater_item( $args, '{{INDEX}}' ); ?>
1327 </template>
1328 </div>
1329 <?php
1330 $html = ob_get_clean();
1331 $html .= $this->get_field_description( $args );
1332
1333 /**
1334 * This filter has been defined in class-settings-api.php
1335 */
1336 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1337 }
1338
1339 /**
1340 * Render a single repeater item.
1341 *
1342 * @param array $args Repeater field arguments.
1343 * @param string|int $index Current item index.
1344 * @param array|null $item Item data if exists.
1345 * @return void
1346 */
1347 public function render_repeater_item( $args, $index, $item = null ) {
1348 if ( empty( $args['fields'] ) || ! is_array( $args['fields'] ) ) {
1349 return;
1350 }
1351
1352 $fallback_title = ! empty( $args['new_item_text'] ) ? $args['new_item_text'] : $this->translation_strings['repeater_new_item'];
1353
1354 // Generate or retrieve unique row ID.
1355 $item_id = '';
1356 if ( is_array( $item ) && isset( $item['row_id'] ) ) {
1357 $item_id = $item['row_id'];
1358 } elseif ( '{{INDEX}}' !== $index ) {
1359 // For existing items without row_id, generate a persistent one.
1360 $item_id = 'row_' . md5( $args['id'] . '_' . $index );
1361 } else {
1362 // For new items, use a placeholder that will be replaced.
1363 $item_id = '{{ROW_ID}}';
1364 }
1365
1366 $parent_disabled = $this->is_field_disabled( $args );
1367 $content_id = sprintf(
1368 '%s-%s-%s-content',
1369 sanitize_key( $this->settings_key ),
1370 sanitize_key( $args['id'] ),
1371 '{{ROW_ID}}' === $item_id ? '{{ROW_ID}}' : sanitize_key( $item_id )
1372 );
1373 ?>
1374 <div class="wz-repeater-item" data-row-id="<?php echo esc_attr( $item_id ); ?>">
1375 <input type="hidden" name="<?php echo esc_attr( $this->settings_key ); ?>[<?php echo esc_attr( $args['id'] ); ?>][<?php echo esc_attr( $index ); ?>][row_id]" value="<?php echo esc_attr( $item_id ); ?>" <?php disabled( $parent_disabled ); ?> />
1376 <button type="button" class="repeater-item-header" aria-expanded="false" aria-controls="<?php echo esc_attr( $content_id ); ?>">
1377 <?php
1378 $display_field = ! empty( $args['live_update_field'] ) ? $args['live_update_field'] : 'name';
1379 $live_options = ! empty( $args['live_update_field_options'] ) && is_array( $args['live_update_field_options'] ) ? $args['live_update_field_options'] : array();
1380 $raw_live_value = ! empty( $item['fields'][ $display_field ] ) ? (string) $item['fields'][ $display_field ] : '';
1381 $display_value = '' !== $raw_live_value
1382 ? ( isset( $live_options[ $raw_live_value ] ) ? $live_options[ $raw_live_value ] : $raw_live_value )
1383 : $fallback_title;
1384 ?>
1385 <span class="repeater-title"><?php echo esc_html( $display_value ); ?></span>
1386 <span class="toggle-icon" aria-hidden="true">▼</span>
1387 </button>
1388 <div id="<?php echo esc_attr( $content_id ); ?>" class="repeater-item-content" aria-hidden="true" style="display: none;">
1389 <?php
1390 foreach ( $args['fields'] as $field ) {
1391 $field_id = sanitize_key( $field['id'] );
1392
1393 $field_args = array_merge(
1394 (array) $field,
1395 array(
1396 'value' => isset( $item['fields'][ $field_id ] ) ? $item['fields'][ $field_id ] : ( isset( $field['default'] ) ? $field['default'] : '' ),
1397 '_repeater_id' => $args['id'],
1398 '_index' => $index,
1399 )
1400 );
1401
1402 // Subfields do not inherit the repeater's own arguments.
1403 if ( $parent_disabled ) {
1404 $field_args['disabled'] = true;
1405 }
1406
1407 // Rows collapse, and a browser blocks submit on a hidden required
1408 // control without reporting why. The label keeps its asterisk.
1409 $field_args['required'] = false;
1410
1411 $field_args = Settings_API::parse_field_args( $field_args, $args['section'] );
1412
1413 if ( ! isset( $field['type'] ) || ! is_string( $field['type'] ) ) {
1414 continue;
1415 }
1416 ?>
1417 <?php $repeater_field_attributes = $this->get_field_attributes( $field_args ); ?>
1418 <div class="wz-repeater-field">
1419 <div class="wz-repeater-field-header">
1420 <label class="wz-repeater-field-label" for="<?php echo esc_attr( $repeater_field_attributes['field_id'] ); ?>">
1421 <?php echo esc_html( $field['name'] ); ?>
1422 <?php if ( ! empty( $field['required'] ) ) : ?>
1423 <span class="required" title="<?php echo esc_attr( $this->translation_strings['required_label'] ); ?>">*</span>
1424 <?php endif; ?>
1425 </label>
1426 </div>
1427
1428 <div class="wz-repeater-field-input">
1429 <?php
1430 $callback = 'callback_' . $field['type'];
1431
1432 if ( method_exists( $this, $callback ) ) {
1433 $this->$callback( $field_args );
1434 } else {
1435 do_action( "{$this->prefix}_repeater_field_{$field['type']}", $field_args, $index ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1436 }
1437 ?>
1438 </div>
1439 </div>
1440 <?php } ?>
1441 </div>
1442
1443 <div class="repeater-item-footer">
1444 <div class="repeater-item-actions">
1445 <button type="button" class="button button-secondary move-up" aria-label="<?php echo esc_attr( $this->translation_strings['repeater_move_up'] ?? 'Move item up' ); ?>" <?php disabled( $parent_disabled ); ?>>
1446 <span class="dashicons dashicons-arrow-up-alt2" aria-hidden="true"></span>
1447 </button>
1448 <button type="button" class="button button-secondary move-down" aria-label="<?php echo esc_attr( $this->translation_strings['repeater_move_down'] ?? 'Move item down' ); ?>" <?php disabled( $parent_disabled ); ?>>
1449 <span class="dashicons dashicons-arrow-down-alt2" aria-hidden="true"></span>
1450 </button>
1451 <button type="button" class="button button-secondary remove-item" aria-label="<?php echo esc_attr( $this->translation_strings['repeater_remove_item'] ?? 'Remove item' ); ?>" <?php disabled( $parent_disabled ); ?>>
1452 <span class="dashicons dashicons-trash" aria-hidden="true"></span>
1453 </button>
1454 </div>
1455 </div>
1456 </div>
1457
1458 <?php
1459 }
1460
1461
1462
1463 /**
1464 * Display sensitive fields.
1465 *
1466 * @param array $args Array of arguments.
1467 */
1468 public function callback_sensitive( $args ) {
1469 $encrypted_key = $this->get_field_value( $args );
1470 $decrypted_key = Settings_API::decrypt_api_key( $encrypted_key );
1471
1472 $args['value'] = $decrypted_key ? str_repeat( '*', strlen( $decrypted_key ) - 4 ) . substr( $decrypted_key, -4 ) : '';
1473
1474 $this->callback_text( $args );
1475 }
1476 }
1477