PluginProbe
WebberZone Top 10 — Popular Posts / trunk
WebberZone Top 10 — Popular Posts vtrunk
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 1.6.1 1.6.2 All 116 releases
top-10 / includes / admin / settings / class-settings-form.php

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

1,414 lines 43.6 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_post( $args['desc'] ) . '</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 * Returns the allowed HTML tags and attributes for settings form output.
356 *
357 * Use the `{prefix}_settings_form_allowed_html` filter to add extra tags or
358 * attributes needed by custom field types or `field_attributes` entries.
359 *
360 * @return array<string, array<string, bool>>
361 */
362 protected function get_allowed_html(): array {
363 $allowed = wp_kses_allowed_html( 'post' );
364
365 $form_tags = array(
366 'input' => array(
367 'type' => true,
368 'name' => true,
369 'id' => true,
370 'value' => true,
371 'class' => true,
372 'style' => true,
373 'checked' => true,
374 'disabled' => true,
375 'readonly' => true,
376 'required' => true,
377 'placeholder' => true,
378 'size' => true,
379 'min' => true,
380 'max' => true,
381 'step' => true,
382 'multiple' => true,
383 'autocomplete' => true,
384 // Tom Select data attributes (built-in Settings API feature).
385 'data-wp-prefix' => true,
386 'data-wp-action' => true,
387 'data-wp-nonce' => true,
388 'data-wp-endpoint' => true,
389 'data-ts-config' => true,
390 'data-wp-extra-fields' => true,
391 ),
392 'select' => array(
393 'id' => true,
394 'name' => true,
395 'class' => true,
396 'style' => true,
397 'disabled' => true,
398 'multiple' => true,
399 'size' => true,
400 'autocomplete' => true,
401 // Tom Select data attributes (built-in Settings API feature).
402 'data-wp-prefix' => true,
403 'data-wp-action' => true,
404 'data-wp-nonce' => true,
405 'data-wp-endpoint' => true,
406 'data-ts-config' => true,
407 'data-wp-extra-fields' => true,
408 ),
409 'option' => array(
410 'value' => true,
411 'selected' => true,
412 'disabled' => true,
413 ),
414 'optgroup' => array(
415 'label' => true,
416 'disabled' => true,
417 ),
418 'textarea' => array(
419 'id' => true,
420 'name' => true,
421 'class' => true,
422 'style' => true,
423 'rows' => true,
424 'cols' => true,
425 'disabled' => true,
426 'readonly' => true,
427 'required' => true,
428 'placeholder' => true,
429 'autocomplete' => true,
430 ),
431 'label' => array(
432 'for' => true,
433 'class' => true,
434 'style' => true,
435 ),
436 'button' => array(
437 'type' => true,
438 'id' => true,
439 'class' => true,
440 'style' => true,
441 'disabled' => true,
442 ),
443 'template' => array(
444 'class' => true,
445 'data-id' => true,
446 ),
447 );
448
449 $allowed = array_replace_recursive( $allowed, $form_tags );
450
451 /**
452 * Filters the allowed HTML tags and attributes for settings form output.
453 *
454 * Use this filter to add data attributes or custom elements required by
455 * field_attributes entries in your registered settings.
456 *
457 * @param array<string, array<string, bool>> $allowed Allowed HTML tags/attributes.
458 */
459 return apply_filters( $this->prefix . '_settings_form_allowed_html', $allowed ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
460 }
461
462 /**
463 * Miscellaneous callback funcion
464 *
465 * @param array $args Arguments array.
466 * @return void
467 */
468 public function callback_missing( $args ) {
469 /* translators: 1: Code. */
470 printf( 'The callback function used for the %1$s setting is missing.', '<strong>' . esc_attr( $args['id'] ) . '</strong>' );
471 }
472
473 /**
474 * Header Callback
475 *
476 * Renders the header.
477 *
478 * @param array $args Arguments passed by the setting.
479 * @return void
480 */
481 public function callback_header( $args ) {
482 $html = $this->get_field_description( $args );
483
484 /**
485 * After Settings Output filter
486 *
487 * @param string $html HTML string.
488 * @param array $args Arguments array.
489 */
490 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
491 }
492
493 /**
494 * Descriptive text callback.
495 *
496 * Renders descriptive text onto the settings field.
497 *
498 * @param array $args Array of arguments.
499 * @return void
500 */
501 public function callback_descriptive_text( $args ) {
502 $this->callback_header( $args );
503 }
504
505 /**
506 * Build additional attributes string from field_attributes.
507 *
508 * @param array $args Field arguments.
509 * @return string Additional attributes string.
510 */
511 protected function build_field_attributes( $args ) {
512 $attributes = '';
513 foreach ( (array) ( $args['field_attributes'] ?? array() ) as $attribute => $val ) {
514 $attr_name = sanitize_key( $attribute );
515 if ( empty( $attr_name ) ) {
516 continue;
517 }
518 if ( true === $val ) {
519 $attributes .= ' ' . $attr_name;
520 } elseif ( false !== $val ) {
521 $attributes .= sprintf( ' %1$s="%2$s"', $attr_name, esc_attr( $val ) );
522 }
523 }
524 return $attributes;
525 }
526
527 /**
528 * Display text fields.
529 *
530 * @param array $args Array of arguments.
531 */
532 public function callback_text( $args ) {
533 $value = $this->get_field_value( $args );
534 $size = sanitize_html_class( $args['size'] ?? 'regular' );
535 $class = $this->get_field_class( $args );
536 $placeholder = $this->get_placeholder_attribute( $args );
537 $attributes = $this->get_boolean_attributes( $args ) . $this->build_field_attributes( $args );
538
539 $field_attributes = $this->get_field_attributes( $args );
540
541 $html = sprintf(
542 '<input type="text" id="%1$s" name="%2$s" class="%3$s" value="%4$s" %5$s %6$s />',
543 $field_attributes['field_id'],
544 $field_attributes['field_name'],
545 $class . ' ' . $size . '-text',
546 esc_attr( stripslashes( $value ) ),
547 $attributes,
548 $placeholder
549 );
550 $html .= $this->get_field_description( $args );
551
552 /**
553 * This filter has been defined in class-settings-api.php
554 */
555 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
556 }
557
558 /**
559 * Display url fields.
560 *
561 * @param array $args Array of arguments.
562 */
563 public function callback_url( $args ) {
564 $this->callback_text( $args );
565 }
566
567 /**
568 * Display csv fields.
569 *
570 * @param array $args Array of arguments.
571 */
572 public function callback_csv( $args ) {
573 $this->callback_text( $args );
574 }
575
576 /**
577 * Display color fields.
578 *
579 * @param array $args Array of arguments.
580 */
581 public function callback_color( $args ) {
582 // Add color-field class for wpColorPicker initialization.
583 $args['field_class'] = ! empty( $args['field_class'] ) ? $args['field_class'] . ' color-field' : 'color-field';
584 $this->callback_text( $args );
585 }
586
587 /**
588 * Display numbercsv fields.
589 *
590 * @param array $args Array of arguments.
591 */
592 public function callback_numbercsv( $args ) {
593 $this->callback_text( $args );
594 }
595
596 /**
597 * Display postids fields.
598 *
599 * @param array $args Array of arguments.
600 */
601 public function callback_postids( $args ) {
602 $this->callback_text( $args );
603 }
604
605 /**
606 * Display textarea.
607 *
608 * @param array $args Array of arguments.
609 * @return void
610 */
611 public function callback_textarea( $args ) {
612 $value = $this->get_field_value( $args );
613 $class = $this->get_field_class( $args, 'large-text' );
614 $placeholder = $this->get_placeholder_attribute( $args );
615 $attributes = $this->get_boolean_attributes( $args );
616
617 $field_attributes = $this->get_field_attributes( $args );
618
619 $html = sprintf(
620 '<textarea class="%4$s" cols="50" rows="5" id="%1$s" name="%2$s" %5$s %6$s>%3$s</textarea>',
621 $field_attributes['field_id'],
622 $field_attributes['field_name'],
623 esc_textarea( stripslashes( $value ) ),
624 $class,
625 $attributes,
626 $placeholder
627 );
628 $html .= $this->get_field_description( $args );
629
630 /**
631 * This filter has been defined in class-settings-api.php
632 */
633 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
634 }
635
636 /**
637 * Display CSS fields.
638 *
639 * @param array $args Array of arguments.
640 * @return void
641 */
642 public function callback_css( $args ) {
643 $this->callback_textarea( $args );
644 }
645
646 /**
647 * Display HTML fields.
648 *
649 * @param array $args Array of arguments.
650 * @return void
651 */
652 public function callback_html( $args ) {
653 $this->callback_textarea( $args );
654 }
655
656 /**
657 * Get disabled attribute for pro/premium fields.
658 *
659 * @param array $args Field arguments.
660 * @return string Disabled attribute or empty string.
661 */
662 protected function get_disabled_attribute( $args ) {
663 return $this->is_field_disabled( $args ) ? ' disabled="disabled"' : '';
664 }
665
666 /**
667 * Display checkboxes.
668 *
669 * Renders as a toggle switch by default: a native checkbox wrapped in a
670 * styled label. Pass `no-toggle` in `field_class` to render a plain
671 * checkbox instead. Posting semantics are identical either way - a hidden
672 * field posts -1 when unchecked and the checkbox posts 1 when checked.
673 *
674 * @param array $args Array of arguments.
675 * @return void
676 */
677 public function callback_checkbox( $args ) {
678 $value = $this->get_field_value( $args );
679 $checked = ! empty( $value ) ? checked( 1, $value, false ) : '';
680 $disabled = $this->get_disabled_attribute( $args );
681 $class = $this->get_field_class( $args );
682 $no_toggle = in_array( 'no-toggle', explode( ' ', $class ), true );
683
684 $field_attributes = $this->get_field_attributes( $args );
685
686 // The hidden input is also disabled when the field is disabled so that
687 // the setting key is not saved at all and retains its default value.
688 $html = sprintf(
689 '<input type="hidden" name="%1$s" value="-1" %2$s />',
690 $field_attributes['field_name'],
691 $disabled
692 );
693
694 if ( $no_toggle ) {
695 $html .= sprintf(
696 '<input type="checkbox" id="%1$s" name="%2$s" value="1" class="%3$s" %4$s %5$s />',
697 $field_attributes['field_id'],
698 $field_attributes['field_name'],
699 $class,
700 $checked,
701 $disabled
702 );
703 } else {
704 $html .= sprintf(
705 '<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>',
706 $field_attributes['field_id'],
707 $field_attributes['field_name'],
708 $class,
709 $checked,
710 $disabled
711 );
712 }
713
714 $html .= $this->get_field_description( $args );
715
716 /**
717 * This filter has been defined in class-settings-api.php
718 */
719 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
720 }
721
722 /**
723 * Toggle Callback
724 *
725 * Alias of the checkbox type, which renders as a toggle switch.
726 *
727 * @param array $args Array of arguments.
728 * @return void
729 */
730 public function callback_toggle( $args ) {
731 $this->callback_checkbox( $args );
732 }
733
734 /**
735 * Multicheck Callback
736 *
737 * Renders multiple checkboxes.
738 *
739 * @param array $args Array of arguments.
740 * @return void
741 */
742 public function callback_multicheck( $args ) {
743 $html = '';
744
745 $value = $this->get_field_value( $args );
746 $value_array = wp_parse_list( $value );
747 $disabled = $this->get_disabled_attribute( $args );
748
749 $field_attributes = $this->get_field_attributes( $args );
750
751 if ( ! empty( $args['options'] ) ) {
752 $html .= sprintf(
753 '<input type="hidden" name="%1$s" value="-1" />',
754 $field_attributes['field_name']
755 );
756
757 foreach ( $args['options'] as $key => $option ) {
758 if ( in_array( $key, $value_array, true ) ) {
759 $enabled = $key;
760 } else {
761 $enabled = null;
762 }
763
764 $option_id = $field_attributes['field_id'] . '-' . sanitize_key( $key );
765 $option_name = $field_attributes['field_name'] . '[' . sanitize_key( $key ) . ']';
766
767 $html .= sprintf(
768 '<input name="%1$s" id="%2$s" type="checkbox" value="%3$s" %4$s %5$s /> ',
769 $option_name,
770 $option_id,
771 esc_attr( $key ),
772 checked( $key, $enabled, false ),
773 $disabled
774 );
775 $html .= sprintf(
776 '<label for="%1$s">%2$s</label> <br />',
777 $option_id,
778 wp_kses_post( $option )
779 );
780 }
781 }
782 $html .= $this->get_field_description( $args );
783
784 /**
785 * This filter has been defined in class-settings-api.php
786 */
787 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
788 }
789
790 /**
791 * Radio Callback
792 *
793 * Renders radio boxes.
794 *
795 * @param array $args Array of arguments.
796 * @return void
797 */
798 public function callback_radio( $args ) {
799 $html = '';
800
801 $value = $this->get_field_value( $args );
802 $disabled = $this->get_disabled_attribute( $args );
803
804 $field_attributes = $this->get_field_attributes( $args );
805
806 foreach ( $args['options'] as $key => $option ) {
807 $option_id = $field_attributes['field_id'] . '-' . $key;
808
809 $html .= sprintf(
810 '<input name="%1$s" id="%2$s" type="radio" value="%3$s" %4$s %5$s /> ',
811 $field_attributes['field_name'],
812 $option_id,
813 $key,
814 checked( $value, $key, false ),
815 $disabled
816 );
817 $html .= sprintf(
818 '<label for="%1$s">%2$s</label> <br />',
819 $option_id,
820 wp_kses_post( $option )
821 );
822 }
823
824 $html .= $this->get_field_description( $args );
825
826 /**
827 * This filter has been defined in class-settings-api.php
828 */
829 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
830 }
831
832 /**
833 * Radio callback with description.
834 *
835 * Renders radio boxes with each item having it separate description.
836 *
837 * @param array $args Array of arguments.
838 * @return void
839 */
840 public function callback_radiodesc( $args ) {
841 $html = '';
842
843 $value = $this->get_field_value( $args );
844 $disabled = $this->get_disabled_attribute( $args );
845
846 $field_attributes = $this->get_field_attributes( $args );
847
848 foreach ( $args['options'] as $option ) {
849 $option_id = $field_attributes['field_id'] . '-' . $option['id'];
850
851 $html .= sprintf(
852 '<input name="%1$s" id="%2$s" type="radio" value="%3$s" %4$s %5$s /> ',
853 $field_attributes['field_name'],
854 $option_id,
855 $option['id'],
856 checked( $value, $option['id'], false ),
857 $disabled
858 );
859 $html .= sprintf(
860 '<label for="%1$s">%2$s: <em>%3$s</em></label>',
861 $option_id,
862 wp_kses_post( $option['name'] ),
863 wp_kses_post( $option['description'] )
864 );
865
866 $html .= '<br />';
867 }
868
869 $html .= $this->get_field_description( $args );
870
871 /**
872 * This filter has been defined in class-settings-api.php
873 */
874 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
875 }
876
877 /**
878 * Radio callback with description.
879 *
880 * Renders radio boxes with each item having it separate description.
881 *
882 * @param array $args Array of arguments.
883 * @return void
884 */
885 public function callback_thumbsizes( $args ) {
886 $html = '';
887
888 $thumb_size = $this->prefix . '_thumbnail';
889
890 if ( ! isset( $args['options'][ $thumb_size ] ) ) {
891 $args['options'][ $thumb_size ] = array(
892 'name' => $thumb_size,
893 'width' => call_user_func_array( $this->prefix . '_get_option', array( 'thumb_width', 150 ) ),
894 'height' => call_user_func_array( $this->prefix . '_get_option', array( 'thumb_height', 150 ) ),
895 'crop' => call_user_func_array( $this->prefix . '_get_option', array( 'thumb_crop', true ) ),
896 );
897 }
898
899 $value = $this->get_field_value( $args );
900
901 $field_attributes = $this->get_field_attributes( $args );
902
903 foreach ( $args['options'] as $name => $option ) {
904 $option_id = $field_attributes['field_id'] . '-' . $name;
905
906 $html .= sprintf(
907 '<input name="%1$s" id="%2$s" type="radio" value="%3$s" %4$s /> ',
908 $field_attributes['field_name'],
909 $option_id,
910 $name,
911 checked( $value, $name, false )
912 );
913 $html .= sprintf(
914 '<label for="%1$s">%2$s (%3$sx%4$s%5$s)</label> <br />',
915 $option_id,
916 wp_kses_post( $name ),
917 (int) $option['width'],
918 (int) $option['height'],
919 (bool) $option['crop'] ? ' cropped' : ''
920 );
921 }
922
923 $html .= $this->get_field_description( $args );
924
925 /**
926 * This filter has been defined in class-settings-api.php
927 */
928 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
929 }
930
931 /**
932 * Number Callback
933 *
934 * Renders number fields.
935 *
936 * @param array $args Array of arguments.
937 * @return void
938 */
939 public function callback_number( $args ) {
940 $value = $this->get_field_value( $args );
941 $max = isset( $args['max'] ) ? intval( $args['max'] ) : 999999;
942 $min = isset( $args['min'] ) ? intval( $args['min'] ) : 0;
943 $step = isset( $args['step'] ) ? intval( $args['step'] ) : 1;
944 $size = $args['size'] ?? 'regular';
945 $placeholder = $this->get_placeholder_attribute( $args );
946 $attributes = $this->get_boolean_attributes( $args );
947
948 $field_attributes = $this->get_field_attributes( $args );
949
950 $html = sprintf(
951 '<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 />',
952 esc_attr( (string) $step ),
953 esc_attr( (string) $max ),
954 esc_attr( (string) $min ),
955 sanitize_html_class( $size ) . '-text',
956 $field_attributes['field_id'],
957 $field_attributes['field_name'],
958 esc_attr( stripslashes( $value ) ),
959 $placeholder,
960 $attributes
961 );
962 $html .= $this->get_field_description( $args );
963
964 /**
965 * This filter has been defined in class-settings-api.php
966 */
967 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
968 }
969
970 /**
971 * Select Callback
972 *
973 * Renders select fields.
974 *
975 * @param array $args Array of arguments.
976 * @return void
977 */
978 public function callback_select( $args ) {
979 $value = $this->get_field_value( $args );
980 $class = $this->get_field_class( $args );
981 $required = ( isset( $args['required'] ) && true === $args['required'] ) ? ' required' : '';
982 $attributes = $this->get_disabled_attribute( $args ) . $required . $this->build_field_attributes( $args );
983
984 if ( isset( $args['chosen'] ) ) {
985 $class .= ' chosen';
986 }
987
988 $field_attributes = $this->get_field_attributes( $args );
989
990 $html = sprintf(
991 '<select id="%1$s" name="%2$s" class="%3$s" %4$s />',
992 $field_attributes['field_id'],
993 $field_attributes['field_name'],
994 $class,
995 $attributes
996 );
997
998 foreach ( (array) $args['options'] as $option => $name ) {
999 $html .= sprintf( '<option value="%1$s" %2$s>%3$s</option>', sanitize_key( $option ), selected( $option, $value, false ), esc_html( $name ) );
1000 }
1001
1002 $html .= '</select>';
1003 $html .= $this->get_field_description( $args );
1004
1005 /**
1006 * This filter has been defined in class-settings-api.php
1007 */
1008 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1009 }
1010
1011 /**
1012 * Display posttypes fields.
1013 *
1014 * @param array $args Array of arguments.
1015 * @return void
1016 */
1017 public function callback_posttypes( $args ) {
1018 $html = '';
1019
1020 $options = $this->get_field_value( $args );
1021 $disabled = $this->get_disabled_attribute( $args );
1022
1023 // If post_types contains a query string then parse it with wp_parse_args.
1024 if ( is_string( $options ) && strpos( $options, '=' ) ) {
1025 $post_types = wp_parse_args( $options );
1026 } else {
1027 $post_types = wp_parse_list( $options );
1028 }
1029
1030 $wp_post_types = get_post_types(
1031 array(
1032 'public' => true,
1033 ),
1034 'objects'
1035 );
1036
1037 $posts_types_inc = array_intersect( wp_list_pluck( $wp_post_types, 'name' ), $post_types );
1038
1039 $field_attributes = $this->get_field_attributes( $args );
1040
1041 $html .= sprintf(
1042 '<input type="hidden" name="%1$s" value="-1" />',
1043 $field_attributes['field_name']
1044 );
1045
1046 foreach ( $wp_post_types as $wp_post_type ) {
1047 $option_id = $field_attributes['field_id'] . '-' . esc_attr( $wp_post_type->name );
1048 $option_name = $field_attributes['field_name'] . '[' . esc_attr( $wp_post_type->name ) . ']';
1049
1050 $html .= sprintf(
1051 '<label for="%1$s"><input name="%2$s" id="%1$s" type="checkbox" value="%3$s" %4$s %5$s /> %6$s</label><br />',
1052 $option_id,
1053 $option_name,
1054 esc_attr( $wp_post_type->name ),
1055 checked( true, in_array( $wp_post_type->name, $posts_types_inc, true ), false ),
1056 $disabled,
1057 esc_html( $wp_post_type->label )
1058 );
1059
1060 }
1061
1062 $html .= $this->get_field_description( $args );
1063
1064 /**
1065 * This filter has been defined in class-settings-api.php
1066 */
1067 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1068 }
1069
1070
1071 /**
1072 * Display taxonomies fields.
1073 *
1074 * @param array $args Array of arguments.
1075 * @return void
1076 */
1077 public function callback_taxonomies( $args ) {
1078 $html = '';
1079
1080 $options = $this->get_field_value( $args );
1081
1082 // If taxonomies contains a query string then parse it with wp_parse_args.
1083 if ( is_string( $options ) && strpos( $options, '=' ) ) {
1084 $taxonomies = wp_parse_args( $options );
1085 } else {
1086 $taxonomies = wp_parse_list( $options );
1087 }
1088
1089 /* Fetch taxonomies */
1090 $argsc = array(
1091 'public' => true,
1092 );
1093 $output = 'objects';
1094 $operator = 'and';
1095 $wp_taxonomies = get_taxonomies( $argsc, $output, $operator );
1096
1097 $taxonomies_inc = array_intersect( wp_list_pluck( (array) $wp_taxonomies, 'name' ), $taxonomies );
1098
1099 $field_attributes = $this->get_field_attributes( $args );
1100
1101 $html .= sprintf( '<input type="hidden" name="%1$s" value="-1" />', $field_attributes['field_name'] );
1102
1103 foreach ( $wp_taxonomies as $wp_taxonomy ) {
1104 $option_id = $field_attributes['field_id'] . '-' . esc_attr( $wp_taxonomy->name );
1105 $option_name = $field_attributes['field_name'] . '[' . esc_attr( $wp_taxonomy->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 (%3$s)</label><br />',
1109 $option_id,
1110 $option_name,
1111 esc_attr( $wp_taxonomy->name ),
1112 checked( true, in_array( $wp_taxonomy->name, $taxonomies_inc, true ), false ),
1113 esc_html( $wp_taxonomy->labels->name )
1114 );
1115
1116 }
1117
1118 $html .= $this->get_field_description( $args );
1119
1120 /**
1121 * This filter has been defined in class-settings-api.php
1122 */
1123 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1124 }
1125
1126
1127 /**
1128 * Displays a rich text textarea for a settings field.
1129 *
1130 * @param array $args Array of arguments.
1131 */
1132 public function callback_wysiwyg( $args ) {
1133 $value = $this->get_field_value( $args );
1134 $size = $args['size'] ?? '500px';
1135
1136 $field_attributes = $this->get_field_attributes( $args );
1137
1138 // wp_editor requires a unique ID without brackets.
1139 $editor_id = sanitize_key( str_replace( array( '[', ']' ), array( '-', '' ), $field_attributes['field_id'] ) );
1140
1141 printf( '<div style="max-width: %1$s;">', esc_attr( $size ) );
1142
1143 $editor_settings = array(
1144 'teeny' => true,
1145 'textarea_name' => $field_attributes['field_name'],
1146 'textarea_rows' => 10,
1147 );
1148
1149 if ( isset( $args['options'] ) && is_array( $args['options'] ) ) {
1150 $editor_settings = array_merge( $editor_settings, $args['options'] );
1151 }
1152
1153 wp_editor( $value, $editor_id, $editor_settings );
1154
1155 printf( '</div>' );
1156
1157 echo wp_kses_post( $this->get_field_description( $args ) );
1158 }
1159
1160 /**
1161 * Displays a file upload field for a settings field.
1162 *
1163 * @param array $args Array of arguments.
1164 */
1165 public function callback_file( $args ) {
1166 $value = $this->get_field_value( $args );
1167 $size = sanitize_html_class( $args['size'] ?? 'regular' );
1168 $class = $this->get_field_class( $args );
1169 $label = $args['options']['button_label'] ?? $this->translation_strings['button_label'];
1170
1171 $field_attributes = $this->get_field_attributes( $args );
1172
1173 $html = sprintf(
1174 '<input type="text" class="%1$s" id="%2$s" name="%3$s" value="%4$s"/>',
1175 $class . ' ' . $size . '-text file-url',
1176 $field_attributes['field_id'],
1177 $field_attributes['field_name'],
1178 esc_attr( $value )
1179 );
1180 $html .= sprintf( '<input type="button" class="button button-secondary file-browser" value="%s" />', esc_attr( $label ) );
1181 $html .= $this->get_field_description( $args );
1182
1183 /**
1184 * This filter has been defined in class-settings-api.php
1185 */
1186 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1187 }
1188
1189 /**
1190 * Displays a password field for a settings field.
1191 *
1192 * @param array $args Array of arguments.
1193 */
1194 public function callback_password( $args ) {
1195 $value = $this->get_field_value( $args );
1196 $size = sanitize_html_class( $args['size'] ?? 'regular' );
1197 $class = $this->get_field_class( $args );
1198
1199 $field_attributes = $this->get_field_attributes( $args );
1200
1201 $html = sprintf(
1202 '<input type="password" class="%1$s" id="%2$s" name="%3$s" value="%4$s" %5$s />',
1203 "$class $size-text",
1204 $field_attributes['field_id'],
1205 $field_attributes['field_name'],
1206 esc_attr( $value ),
1207 ! empty( $value ) ? 'placeholder="' . esc_attr( $this->translation_strings['previous_saved'] ) . '"' : ''
1208 );
1209 $html .= $this->get_field_description( $args );
1210
1211 /**
1212 * This filter has been defined in class-settings-api.php
1213 */
1214 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1215 }
1216
1217 /**
1218 * Callback for repeater field.
1219 *
1220 * @param array $args Array of arguments.
1221 * @return void
1222 */
1223 public function callback_repeater( $args ) {
1224 $raw_value = $this->get_field_value( $args );
1225 $value = ! empty( $raw_value ) && is_array( $raw_value ) ? $raw_value : array();
1226
1227 $class = $this->get_field_class( $args );
1228 $attributes = $this->get_boolean_attributes( $args ) . $this->build_field_attributes( $args );
1229
1230 // `disabled` does nothing on the wrapper; the class neutralises the buttons.
1231 if ( $this->is_field_disabled( $args ) ) {
1232 $class .= ' wz-repeater-disabled';
1233 }
1234
1235 $data_index = (string) count( $value );
1236 $live_update_field = ! empty( $args['live_update_field'] ) ? $args['live_update_field'] : 'name';
1237 $fallback_title = ! empty( $args['new_item_text'] ) ? $args['new_item_text'] : $this->translation_strings['repeater_new_item'];
1238 $live_update_options = ! empty( $args['live_update_field_options'] ) && is_array( $args['live_update_field_options'] ) ? $args['live_update_field_options'] : array();
1239
1240 ob_start();
1241 ?>
1242 <div class="<?php echo esc_attr( $class ); ?> wz-repeater-wrapper"
1243 id="<?php echo esc_attr( $args['id'] ); ?>-wrapper"
1244 data-index="<?php echo esc_attr( $data_index ); ?>"
1245 data-live-update-field="<?php echo esc_attr( $live_update_field ); ?>"
1246 data-fallback-title="<?php echo esc_attr( $fallback_title ); ?>"
1247 <?php if ( ! empty( $live_update_options ) ) : ?>
1248 data-live-update-field-options="<?php echo esc_attr( wp_json_encode( $live_update_options ) ); ?>"
1249 <?php endif; ?>
1250 <?php if ( ! empty( $args['unique_field'] ) ) : ?>
1251 data-unique-field="<?php echo esc_attr( $args['unique_field'] ); ?>"
1252 <?php endif; ?>
1253 <?php echo $attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
1254
1255 <div class="<?php echo esc_attr( $args['id'] ); ?>-items wz-repeater-items">
1256 <?php
1257 if ( ! empty( $value ) ) {
1258 foreach ( array_values( $value ) as $index => $item ) {
1259 $this->render_repeater_item( $args, $index, $item );
1260 }
1261 }
1262 ?>
1263 </div>
1264 <button type="button" class="button add-item" data-target="<?php echo esc_attr( $args['id'] ); ?>" <?php disabled( $this->is_field_disabled( $args ) ); ?>>
1265 <?php echo esc_html( ! empty( $args['add_button_text'] ) ? $args['add_button_text'] : 'Add Item' ); ?>
1266 </button>
1267
1268 <template class="repeater-template" data-id="<?php echo esc_attr( $args['id'] ); ?>">
1269 <?php $this->render_repeater_item( $args, '{{INDEX}}' ); ?>
1270 </template>
1271 </div>
1272 <?php
1273 $html = ob_get_clean();
1274 $html .= $this->get_field_description( $args );
1275
1276 /**
1277 * This filter has been defined in class-settings-api.php
1278 */
1279 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1280 }
1281
1282 /**
1283 * Render a single repeater item.
1284 *
1285 * @param array $args Repeater field arguments.
1286 * @param string|int $index Current item index.
1287 * @param array|null $item Item data if exists.
1288 * @return void
1289 */
1290 public function render_repeater_item( $args, $index, $item = null ) {
1291 if ( empty( $args['fields'] ) || ! is_array( $args['fields'] ) ) {
1292 return;
1293 }
1294
1295 $fallback_title = ! empty( $args['new_item_text'] ) ? $args['new_item_text'] : $this->translation_strings['repeater_new_item'];
1296
1297 // Generate or retrieve unique row ID.
1298 $item_id = '';
1299 if ( is_array( $item ) && isset( $item['row_id'] ) ) {
1300 $item_id = $item['row_id'];
1301 } elseif ( '{{INDEX}}' !== $index ) {
1302 // For existing items without row_id, generate a persistent one.
1303 $item_id = 'row_' . md5( $args['id'] . '_' . $index );
1304 } else {
1305 // For new items, use a placeholder that will be replaced.
1306 $item_id = '{{ROW_ID}}';
1307 }
1308
1309 $parent_disabled = $this->is_field_disabled( $args );
1310 ?>
1311 <div class="wz-repeater-item" data-row-id="<?php echo esc_attr( $item_id ); ?>">
1312 <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 ); ?> />
1313 <div class="repeater-item-header">
1314 <?php
1315 $display_field = ! empty( $args['live_update_field'] ) ? $args['live_update_field'] : 'name';
1316 $live_options = ! empty( $args['live_update_field_options'] ) && is_array( $args['live_update_field_options'] ) ? $args['live_update_field_options'] : array();
1317 $raw_live_value = ! empty( $item['fields'][ $display_field ] ) ? (string) $item['fields'][ $display_field ] : '';
1318 $display_value = '' !== $raw_live_value
1319 ? ( isset( $live_options[ $raw_live_value ] ) ? $live_options[ $raw_live_value ] : $raw_live_value )
1320 : $fallback_title;
1321 ?>
1322 <span class="repeater-title"><?php echo esc_html( $display_value ); ?></span>
1323 <span class="toggle-icon"></span>
1324 </div>
1325 <div class="repeater-item-content" style="display: none;">
1326 <?php
1327 foreach ( $args['fields'] as $field ) {
1328 $field_id = sanitize_key( $field['id'] );
1329
1330 $field_args = array_merge(
1331 (array) $field,
1332 array(
1333 'value' => isset( $item['fields'][ $field_id ] ) ? $item['fields'][ $field_id ] : ( isset( $field['default'] ) ? $field['default'] : '' ),
1334 '_repeater_id' => $args['id'],
1335 '_index' => $index,
1336 )
1337 );
1338
1339 // Subfields do not inherit the repeater's own arguments.
1340 if ( $parent_disabled ) {
1341 $field_args['disabled'] = true;
1342 }
1343
1344 // Rows collapse, and a browser blocks submit on a hidden required
1345 // control without reporting why. The label keeps its asterisk.
1346 $field_args['required'] = false;
1347
1348 $field_args = Settings_API::parse_field_args( $field_args, $args['section'] );
1349
1350 if ( ! isset( $field['type'] ) || ! is_string( $field['type'] ) ) {
1351 continue;
1352 }
1353 ?>
1354 <?php $repeater_field_attributes = $this->get_field_attributes( $field_args ); ?>
1355 <div class="wz-repeater-field">
1356 <div class="wz-repeater-field-header">
1357 <label class="wz-repeater-field-label" for="<?php echo esc_attr( $repeater_field_attributes['field_id'] ); ?>">
1358 <?php echo esc_html( $field['name'] ); ?>
1359 <?php if ( ! empty( $field['required'] ) ) : ?>
1360 <span class="required" title="<?php echo esc_attr( $this->translation_strings['required_label'] ); ?>">*</span>
1361 <?php endif; ?>
1362 </label>
1363 </div>
1364
1365 <div class="wz-repeater-field-input">
1366 <?php
1367 $callback = 'callback_' . $field['type'];
1368
1369 if ( method_exists( $this, $callback ) ) {
1370 $this->$callback( $field_args );
1371 } else {
1372 do_action( "{$this->prefix}_repeater_field_{$field['type']}", $field_args, $index ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1373 }
1374 ?>
1375 </div>
1376 </div>
1377 <?php } ?>
1378 </div>
1379
1380 <div class="repeater-item-footer">
1381 <div class="repeater-item-actions">
1382 <button type="button" class="button button-secondary move-up" <?php disabled( $parent_disabled ); ?>>
1383 <span class="dashicons dashicons-arrow-up-alt2"></span>
1384 </button>
1385 <button type="button" class="button button-secondary move-down" <?php disabled( $parent_disabled ); ?>>
1386 <span class="dashicons dashicons-arrow-down-alt2"></span>
1387 </button>
1388 <button type="button" class="button button-secondary remove-item" <?php disabled( $parent_disabled ); ?>>
1389 <span class="dashicons dashicons-trash"></span>
1390 </button>
1391 </div>
1392 </div>
1393 </div>
1394
1395 <?php
1396 }
1397
1398
1399
1400 /**
1401 * Display sensitive fields.
1402 *
1403 * @param array $args Array of arguments.
1404 */
1405 public function callback_sensitive( $args ) {
1406 $encrypted_key = $this->get_field_value( $args );
1407 $decrypted_key = Settings_API::decrypt_api_key( $encrypted_key );
1408
1409 $args['value'] = $decrypted_key ? str_repeat( '*', strlen( $decrypted_key ) - 4 ) . substr( $decrypted_key, -4 ) : '';
1410
1411 $this->callback_text( $args );
1412 }
1413 }
1414