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
← All changes | includes/admin/settings/class-settings-form.php +457 -149 4.3.1 → 4.5.2 View file →
@@ -1,11 +1,11 @@
1 1 <?php
2 2 /**
3 3 * Generates the settings form.
4 4 *
5 - * @link https://webberzone.com
5 + * @link https://webberzone.com
6 6 *
7 - * @package WebberZone\Top_Ten
7 + * @package WebberZone\Top_Ten
8 8 */
9 9
10 10 namespace WebberZone\Top_Ten\Admin\Settings;
11 11
@@ -15,11 +15,14 @@
15 15 }
16 16
17 17 /**
18 18 * Generates the settings form.
19 + *
20 + * @since 4.0.0
19 21 */
20 22 class Settings_Form {
21 23
24 +
22 25 /**
23 26 * Settings Key.
24 27 *
25 28 * @var string Settings Key.
@@ -43,12 +46,12 @@
43 46 /**
44 47 * Main constructor class.
45 48 *
46 49 * @param mixed $args {
47 - * Array or string of arguments. Default is blank array.
48 - * @type string $settings_key Settings key.
49 - * @type string $prefix Prefix.
50 - * @type array $translation_strings Translation strings.
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.
51 54 * }
52 55 */
53 56 public function __construct( $args ) {
54 57 $defaults = array(
@@ -70,9 +73,9 @@
70 73 *
71 74 * @return string Description of the field.
72 75 */
73 76 public function get_field_description( $args ) {
74 - $desc = ! empty( $args['desc'] ) ? '<p class="description">' . wp_kses_post( $args['desc'] ) . '</p>' : '';
77 + $desc = ! empty( $args['desc'] ) ? '<p class="description">' . wp_kses( $args['desc'], $this->get_allowed_html() ) . '</p>' : '';
75 78
76 79 /**
77 80 * After Settings Output filter
78 81 *
@@ -80,16 +83,170 @@
80 83 * @param array $args Arguments array.
81 84 */
82 85 $desc = apply_filters( $this->prefix . '_setting_field_description', $desc, $args ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
83 86
84 - return $desc;
87 + return $this->get_modified_indicator( $args ) . $desc . $this->get_default_description( $args );
85 88 }
86 89
87 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 + /**
88 245 * Get the value of a settings field.
89 246 *
90 - * @param string $option Settings field name.
91 - * @param mixed $default_value Default value if option is not found.
247 + * @param string $option Settings field name.
248 + * @param mixed $default_value Default value if option is not found.
92 249 * @return mixed
93 250 */
94 251 public function get_option( $option, $default_value = '' ) {
95 252
@@ -104,9 +261,9 @@
104 261
105 262 /**
106 263 * Get field value from args or options.
107 264 *
108 - * @param array $args Field arguments.
265 + * @param array $args Field arguments.
109 266 * @return mixed Field value.
110 267 */
111 268 protected function get_field_value( $args ) {
112 269 return $args['value'] ?? $this->get_option( $args['id'], $args['default'] ?? '' );
@@ -114,10 +271,10 @@
114 271
115 272 /**
116 273 * Get sanitized field class string.
117 274 *
118 - * @param array $args Field arguments.
119 - * @param string $default_class Default class to prepend.
275 + * @param array $args Field arguments.
276 + * @param string $default_class Default class to prepend.
120 277 * @return string Sanitized class string.
121 278 */
122 279 protected function get_field_class( $args, $default_class = '' ) {
123 280 $class = implode( ' ', array_map( 'sanitize_html_class', explode( ' ', $args['field_class'] ?? '' ) ) );
@@ -123,15 +280,15 @@
123 280 $class = implode( ' ', array_map( 'sanitize_html_class', explode( ' ', $args['field_class'] ?? '' ) ) );
124 281 if ( $default_class ) {
125 282 $class = $default_class . ' ' . $class;
126 283 }
127 - return trim( $class );
284 + return trim( $class, " \t\n\r\0\x0B" );
128 285 }
129 286
130 287 /**
131 288 * Get placeholder attribute string.
132 289 *
133 - * @param array $args Field arguments.
290 + * @param array $args Field arguments.
134 291 * @return string Placeholder attribute or empty string.
135 292 */
136 293 protected function get_placeholder_attribute( $args ) {
137 294 return empty( $args['placeholder'] ) ? '' : ' placeholder="' . esc_attr( $args['placeholder'] ) . '"';
@@ -139,13 +296,13 @@
139 296
140 297 /**
141 298 * Get boolean state attributes (disabled, readonly, required).
142 299 *
143 - * @param array $args Field arguments.
300 + * @param array $args Field arguments.
144 301 * @return string Concatenated boolean attributes.
145 302 */
146 303 protected function get_boolean_attributes( $args ) {
147 - $disabled = ( ! empty( $args['disabled'] ) || ! empty( $args['pro'] ) ) ? ' disabled="disabled"' : '';
304 + $disabled = $this->is_field_disabled( $args ) ? ' disabled="disabled"' : '';
148 305 $readonly = ( isset( $args['readonly'] ) && true === $args['readonly'] ) ? ' readonly="readonly"' : '';
149 306 $required = ( isset( $args['required'] ) && true === $args['required'] ) ? ' required' : '';
150 307 return $disabled . $readonly . $required;
151 308 }
@@ -150,11 +307,21 @@
150 307 return $disabled . $readonly . $required;
151 308 }
152 309
153 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 + /**
154 321 * Get field ID and name attributes.
155 322 *
156 - * @param array $args Field arguments.
323 + * @param array $args Field arguments.
157 324 * @return array Array containing field_id and field_name.
158 325 */
159 326 protected function get_field_attributes( $args ) {
160 327 $id = sanitize_key( $args['id'] );
@@ -184,8 +351,20 @@
184 351 );
185 352 }
186 353
187 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 + /**
188 367 * Returns the allowed HTML tags and attributes for settings form output.
189 368 *
190 369 * Use the `{prefix}_settings_form_allowed_html` filter to add extra tags or
191 370 * attributes needed by custom field types or `field_attributes` entries.
@@ -266,14 +445,33 @@
266 445 'class' => true,
267 446 'style' => true,
268 447 ),
269 448 'button' => array(
270 - 'type' => true,
271 - 'id' => true,
272 - 'class' => true,
273 - 'style' => true,
274 - 'disabled' => true,
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,
275 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 + ),
276 474 'template' => array(
277 475 'class' => true,
278 476 'data-id' => true,
279 477 ),
@@ -294,9 +492,9 @@
294 492
295 493 /**
296 494 * Miscellaneous callback funcion
297 495 *
298 - * @param array $args Arguments array.
496 + * @param array $args Arguments array.
299 497 * @return void
300 498 */
301 499 public function callback_missing( $args ) {
302 500 /* translators: 1: Code. */
@@ -307,9 +505,9 @@
307 505 * Header Callback
308 506 *
309 507 * Renders the header.
310 508 *
311 - * @param array $args Arguments passed by the setting.
509 + * @param array $args Arguments passed by the setting.
312 510 * @return void
313 511 */
314 512 public function callback_header( $args ) {
315 513 $html = $this->get_field_description( $args );
@@ -327,9 +525,9 @@
327 525 * Descriptive text callback.
328 526 *
329 527 * Renders descriptive text onto the settings field.
330 528 *
331 - * @param array $args Array of arguments.
529 + * @param array $args Array of arguments.
332 530 * @return void
333 531 */
334 532 public function callback_descriptive_text( $args ) {
335 533 $this->callback_header( $args );
@@ -337,9 +535,9 @@
337 535
338 536 /**
339 537 * Build additional attributes string from field_attributes.
340 538 *
341 - * @param array $args Field arguments.
539 + * @param array $args Field arguments.
342 540 * @return string Additional attributes string.
343 541 */
344 542 protected function build_field_attributes( $args ) {
345 543 $attributes = '';
@@ -381,13 +579,41 @@
381 579 $placeholder
382 580 );
383 581 $html .= $this->get_field_description( $args );
384 582
385 - /** This filter has been defined in class-settings-api.php */
583 + /**
584 + * This filter has been defined in class-settings-api.php
585 +*/
386 586 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
387 587 }
388 588
389 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 + /**
390 616 * Display url fields.
391 617 *
392 618 * @param array $args Array of arguments.
393 619 */
@@ -435,9 +661,9 @@
435 661
436 662 /**
437 663 * Display textarea.
438 664 *
439 - * @param array $args Array of arguments.
665 + * @param array $args Array of arguments.
440 666 * @return void
441 667 */
442 668 public function callback_textarea( $args ) {
443 669 $value = $this->get_field_value( $args );
@@ -457,9 +683,11 @@
457 683 $placeholder
458 684 );
459 685 $html .= $this->get_field_description( $args );
460 686
461 - /** This filter has been defined in class-settings-api.php */
687 + /**
688 + * This filter has been defined in class-settings-api.php
689 +*/
462 690 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
463 691 }
464 692
465 693 /**
@@ -464,9 +692,9 @@
464 692
465 693 /**
466 694 * Display CSS fields.
467 695 *
468 - * @param array $args Array of arguments.
696 + * @param array $args Array of arguments.
469 697 * @return void
470 698 */
471 699 public function callback_css( $args ) {
472 700 $this->callback_textarea( $args );
@@ -474,9 +702,9 @@
474 702
475 703 /**
476 704 * Display HTML fields.
477 705 *
478 - * @param array $args Array of arguments.
706 + * @param array $args Array of arguments.
479 707 * @return void
480 708 */
481 709 public function callback_html( $args ) {
482 710 $this->callback_textarea( $args );
@@ -484,54 +712,89 @@
484 712
485 713 /**
486 714 * Get disabled attribute for pro/premium fields.
487 715 *
488 - * @param array $args Field arguments.
716 + * @param array $args Field arguments.
489 717 * @return string Disabled attribute or empty string.
490 718 */
491 719 protected function get_disabled_attribute( $args ) {
492 - return ( ! empty( $args['disabled'] ) || ! empty( $args['pro'] ) ) ? ' disabled="disabled"' : '';
720 + return $this->is_field_disabled( $args ) ? ' disabled="disabled"' : '';
493 721 }
494 722
495 723 /**
496 724 * Display checkboxes.
497 725 *
498 - * @param array $args Array of arguments.
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.
499 732 * @return void
500 733 */
501 734 public function callback_checkbox( $args ) {
502 - $value = $this->get_field_value( $args );
503 - $checked = ! empty( $value ) ? checked( 1, $value, false ) : '';
504 - $default = isset( $args['default'] ) ? (int) $args['default'] : '';
505 - $disabled = $this->get_disabled_attribute( $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 );
506 740
507 741 $field_attributes = $this->get_field_attributes( $args );
508 742
509 - $html = sprintf(
510 - '<input type="hidden" name="%1$s" value="-1" />',
511 - $field_attributes['field_name']
512 - );
513 - $html .= sprintf(
514 - '<input type="checkbox" id="%1$s" name="%2$s" value="1" %3$s %4$s />',
515 - $field_attributes['field_id'],
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 />',
516 747 $field_attributes['field_name'],
517 - $checked,
518 748 $disabled
519 749 );
520 - $checkbox_modified = $this->translation_strings['checkbox_modified'] ?? 'Modified from default setting';
521 - $html .= ( (bool) $value !== (bool) $default ) ? '<em style="color:#9B0800">' . $checkbox_modified . '</em>' : '';
522 - $html .= $this->get_field_description( $args );
523 750
524 - /** This filter has been defined in class-settings-api.php */
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 +*/
525 776 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
526 777 }
527 778
528 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 + /**
529 792 * Multicheck Callback
530 793 *
531 794 * Renders multiple checkboxes.
532 795 *
533 - * @param array $args Array of arguments.
796 + * @param array $args Array of arguments.
534 797 * @return void
535 798 */
536 799 public function callback_multicheck( $args ) {
537 800 $html = '';
@@ -554,20 +817,20 @@
554 817 } else {
555 818 $enabled = null;
556 819 }
557 820
558 - $option_id = $field_attributes['field_id'] . '-' . sanitize_key( $key );
559 - $option_name = $field_attributes['field_name'] . '[' . sanitize_key( $key ) . ']';
821 + $option_id = $field_attributes['field_id'] . '-' . sanitize_key( $key );
822 + $option_name = $field_attributes['field_name'] . '[' . sanitize_key( $key ) . ']';
560 823
561 - $html .= sprintf(
562 - '<input name="%1$s" id="%2$s" type="checkbox" value="%3$s" %4$s %5$s /> ',
563 - $option_name,
564 - $option_id,
565 - esc_attr( $key ),
566 - checked( $key, $enabled, false ),
567 - $disabled
568 - );
569 - $html .= sprintf(
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(
570 833 '<label for="%1$s">%2$s</label> <br />',
571 834 $option_id,
572 835 wp_kses_post( $option )
573 836 );
@@ -574,9 +837,11 @@
574 837 }
575 838 }
576 839 $html .= $this->get_field_description( $args );
577 840
578 - /** This filter has been defined in class-settings-api.php */
841 + /**
842 + * This filter has been defined in class-settings-api.php
843 +*/
579 844 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
580 845 }
581 846
582 847 /**
@@ -583,9 +848,9 @@
583 848 * Radio Callback
584 849 *
585 850 * Renders radio boxes.
586 851 *
587 - * @param array $args Array of arguments.
852 + * @param array $args Array of arguments.
588 853 * @return void
589 854 */
590 855 public function callback_radio( $args ) {
591 856 $html = '';
@@ -614,9 +879,11 @@
614 879 }
615 880
616 881 $html .= $this->get_field_description( $args );
617 882
618 - /** This filter has been defined in class-settings-api.php */
883 + /**
884 + * This filter has been defined in class-settings-api.php
885 +*/
619 886 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
620 887 }
621 888
622 889 /**
@@ -623,9 +890,9 @@
623 890 * Radio callback with description.
624 891 *
625 892 * Renders radio boxes with each item having it separate description.
626 893 *
627 - * @param array $args Array of arguments.
894 + * @param array $args Array of arguments.
628 895 * @return void
629 896 */
630 897 public function callback_radiodesc( $args ) {
631 898 $html = '';
@@ -657,9 +924,11 @@
657 924 }
658 925
659 926 $html .= $this->get_field_description( $args );
660 927
661 - /** This filter has been defined in class-settings-api.php */
928 + /**
929 + * This filter has been defined in class-settings-api.php
930 +*/
662 931 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
663 932 }
664 933
665 934 /**
@@ -666,9 +935,9 @@
666 935 * Radio callback with description.
667 936 *
668 937 * Renders radio boxes with each item having it separate description.
669 938 *
670 - * @param array $args Array of arguments.
939 + * @param array $args Array of arguments.
671 940 * @return void
672 941 */
673 942 public function callback_thumbsizes( $args ) {
674 943 $html = '';
@@ -709,9 +978,11 @@
709 978 }
710 979
711 980 $html .= $this->get_field_description( $args );
712 981
713 - /** This filter has been defined in class-settings-api.php */
982 + /**
983 + * This filter has been defined in class-settings-api.php
984 +*/
714 985 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
715 986 }
716 987
717 988 /**
@@ -718,9 +989,9 @@
718 989 * Number Callback
719 990 *
720 991 * Renders number fields.
721 992 *
722 - * @param array $args Array of arguments.
993 + * @param array $args Array of arguments.
723 994 * @return void
724 995 */
725 996 public function callback_number( $args ) {
726 997 $value = $this->get_field_value( $args );
@@ -725,9 +996,9 @@
725 996 public function callback_number( $args ) {
726 997 $value = $this->get_field_value( $args );
727 998 $max = isset( $args['max'] ) ? intval( $args['max'] ) : 999999;
728 999 $min = isset( $args['min'] ) ? intval( $args['min'] ) : 0;
729 - $step = isset( $args['step'] ) ? intval( $args['step'] ) : 1;
1000 + $step = isset( $args['step'] ) && ( is_numeric( $args['step'] ) || 'any' === $args['step'] ) ? $args['step'] : 1;
730 1001 $size = $args['size'] ?? 'regular';
731 1002 $placeholder = $this->get_placeholder_attribute( $args );
732 1003 $attributes = $this->get_boolean_attributes( $args );
733 1004
@@ -746,9 +1017,11 @@
746 1017 $attributes
747 1018 );
748 1019 $html .= $this->get_field_description( $args );
749 1020
750 - /** This filter has been defined in class-settings-api.php */
1021 + /**
1022 + * This filter has been defined in class-settings-api.php
1023 +*/
751 1024 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
752 1025 }
753 1026
754 1027 /**
@@ -755,9 +1028,9 @@
755 1028 * Select Callback
756 1029 *
757 1030 * Renders select fields.
758 1031 *
759 - * @param array $args Array of arguments.
1032 + * @param array $args Array of arguments.
760 1033 * @return void
761 1034 */
762 1035 public function callback_select( $args ) {
763 1036 $value = $this->get_field_value( $args );
@@ -785,9 +1058,11 @@
785 1058
786 1059 $html .= '</select>';
787 1060 $html .= $this->get_field_description( $args );
788 1061
789 - /** This filter has been defined in class-settings-api.php */
1062 + /**
1063 + * This filter has been defined in class-settings-api.php
1064 +*/
790 1065 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
791 1066 }
792 1067
793 1068 /**
@@ -792,9 +1067,9 @@
792 1067
793 1068 /**
794 1069 * Display posttypes fields.
795 1070 *
796 - * @param array $args Array of arguments.
1071 + * @param array $args Array of arguments.
797 1072 * @return void
798 1073 */
799 1074 public function callback_posttypes( $args ) {
800 1075 $html = '';
@@ -842,9 +1117,11 @@
842 1117 }
843 1118
844 1119 $html .= $this->get_field_description( $args );
845 1120
846 - /** This filter has been defined in class-settings-api.php */
1121 + /**
1122 + * This filter has been defined in class-settings-api.php
1123 +*/
847 1124 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
848 1125 }
849 1126
850 1127
@@ -850,9 +1127,9 @@
850 1127
851 1128 /**
852 1129 * Display taxonomies fields.
853 1130 *
854 - * @param array $args Array of arguments.
1131 + * @param array $args Array of arguments.
855 1132 * @return void
856 1133 */
857 1134 public function callback_taxonomies( $args ) {
858 1135 $html = '';
@@ -896,9 +1173,11 @@
896 1173 }
897 1174
898 1175 $html .= $this->get_field_description( $args );
899 1176
900 - /** This filter has been defined in class-settings-api.php */
1177 + /**
1178 + * This filter has been defined in class-settings-api.php
1179 +*/
901 1180 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
902 1181 }
903 1182
904 1183
@@ -957,9 +1236,11 @@
957 1236 );
958 1237 $html .= sprintf( '<input type="button" class="button button-secondary file-browser" value="%s" />', esc_attr( $label ) );
959 1238 $html .= $this->get_field_description( $args );
960 1239
961 - /** This filter has been defined in class-settings-api.php */
1240 + /**
1241 + * This filter has been defined in class-settings-api.php
1242 +*/
962 1243 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
963 1244 }
964 1245
965 1246 /**
@@ -983,9 +1264,11 @@
983 1264 ! empty( $value ) ? 'placeholder="' . esc_attr( $this->translation_strings['previous_saved'] ) . '"' : ''
984 1265 );
985 1266 $html .= $this->get_field_description( $args );
986 1267
987 - /** This filter has been defined in class-settings-api.php */
1268 + /**
1269 + * This filter has been defined in class-settings-api.php
1270 +*/
988 1271 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
989 1272 }
990 1273
991 1274 /**
@@ -990,9 +1273,9 @@
990 1273
991 1274 /**
992 1275 * Callback for repeater field.
993 1276 *
994 - * @param array $args Array of arguments.
1277 + * @param array $args Array of arguments.
995 1278 * @return void
996 1279 */
997 1280 public function callback_repeater( $args ) {
998 1281 $raw_value = $this->get_field_value( $args );
@@ -1000,8 +1283,13 @@
1000 1283
1001 1284 $class = $this->get_field_class( $args );
1002 1285 $attributes = $this->get_boolean_attributes( $args ) . $this->build_field_attributes( $args );
1003 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 +
1004 1292 $data_index = (string) count( $value );
1005 1293 $live_update_field = ! empty( $args['live_update_field'] ) ? $args['live_update_field'] : 'name';
1006 1294 $fallback_title = ! empty( $args['new_item_text'] ) ? $args['new_item_text'] : $this->translation_strings['repeater_new_item'];
1007 1295 $live_update_options = ! empty( $args['live_update_field_options'] ) && is_array( $args['live_update_field_options'] ) ? $args['live_update_field_options'] : array();
@@ -1012,28 +1300,31 @@
1012 1300 id="<?php echo esc_attr( $args['id'] ); ?>-wrapper"
1013 1301 data-index="<?php echo esc_attr( $data_index ); ?>"
1014 1302 data-live-update-field="<?php echo esc_attr( $live_update_field ); ?>"
1015 1303 data-fallback-title="<?php echo esc_attr( $fallback_title ); ?>"
1016 - <?php if ( ! empty( $live_update_options ) ) : ?>
1304 + <?php if ( ! empty( $live_update_options ) ) : ?>
1017 1305 data-live-update-field-options="<?php echo esc_attr( wp_json_encode( $live_update_options ) ); ?>"
1018 - <?php endif; ?>
1019 - <?php echo $attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
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 ?>>
1020 1311
1021 1312 <div class="<?php echo esc_attr( $args['id'] ); ?>-items wz-repeater-items">
1022 - <?php
1023 - if ( ! empty( $value ) ) {
1024 - foreach ( array_values( $value ) as $index => $item ) {
1025 - $this->render_repeater_item( $args, $index, $item );
1026 - }
1027 - }
1028 - ?>
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 + ?>
1029 1320 </div>
1030 - <button type="button" class="button add-item" data-target="<?php echo esc_attr( $args['id'] ); ?>">
1031 - <?php echo esc_html( ! empty( $args['add_button_text'] ) ? $args['add_button_text'] : 'Add Item' ); ?>
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' ); ?>
1032 1323 </button>
1033 1324
1034 1325 <template class="repeater-template" data-id="<?php echo esc_attr( $args['id'] ); ?>">
1035 - <?php $this->render_repeater_item( $args, '{{INDEX}}' ); ?>
1326 + <?php $this->render_repeater_item( $args, '{{INDEX}}' ); ?>
1036 1327 </template>
1037 1328 </div>
1038 1329 <?php
1039 1330 $html = ob_get_clean();
@@ -1038,9 +1329,11 @@
1038 1329 <?php
1039 1330 $html = ob_get_clean();
1040 1331 $html .= $this->get_field_description( $args );
1041 1332
1042 - /** This filter has been defined in class-settings-api.php */
1333 + /**
1334 + * This filter has been defined in class-settings-api.php
1335 +*/
1043 1336 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1044 1337 }
1045 1338
1046 1339 /**
@@ -1045,11 +1338,11 @@
1045 1338
1046 1339 /**
1047 1340 * Render a single repeater item.
1048 1341 *
1049 - * @param array $args Repeater field arguments.
1050 - * @param string|int $index Current item index.
1051 - * @param array|null $item Item data if exists.
1342 + * @param array $args Repeater field arguments.
1343 + * @param string|int $index Current item index.
1344 + * @param array|null $item Item data if exists.
1052 1345 * @return void
1053 1346 */
1054 1347 public function render_repeater_item( $args, $index, $item = null ) {
1055 1348 if ( empty( $args['fields'] ) || ! is_array( $args['fields'] ) ) {
@@ -1069,84 +1362,101 @@
1069 1362 // For new items, use a placeholder that will be replaced.
1070 1363 $item_id = '{{ROW_ID}}';
1071 1364 }
1072 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 + );
1073 1373 ?>
1074 1374 <div class="wz-repeater-item" data-row-id="<?php echo esc_attr( $item_id ); ?>">
1075 - <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 ); ?>" />
1076 - <div class="repeater-item-header">
1077 - <?php
1078 - $display_field = ! empty( $args['live_update_field'] ) ? $args['live_update_field'] : 'name';
1079 - $live_options = ! empty( $args['live_update_field_options'] ) && is_array( $args['live_update_field_options'] ) ? $args['live_update_field_options'] : array();
1080 - $raw_live_value = ! empty( $item['fields'][ $display_field ] ) ? (string) $item['fields'][ $display_field ] : '';
1081 - $display_value = '' !== $raw_live_value
1082 - ? ( isset( $live_options[ $raw_live_value ] ) ? $live_options[ $raw_live_value ] : $raw_live_value )
1083 - : $fallback_title;
1084 - ?>
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 + ?>
1085 1385 <span class="repeater-title"><?php echo esc_html( $display_value ); ?></span>
1086 - <span class="toggle-icon">▼</span>
1087 - </div>
1088 - <div class="repeater-item-content" style="display: none;">
1089 - <?php
1090 - foreach ( $args['fields'] as $field ) {
1091 - $field_id = sanitize_key( $field['id'] );
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'] );
1092 1392
1093 - $field_args = array_merge(
1094 - (array) $field,
1095 - array(
1096 - 'value' => isset( $item['fields'][ $field_id ] ) ? $item['fields'][ $field_id ] : ( isset( $field['default'] ) ? $field['default'] : '' ),
1097 - '_repeater_id' => $args['id'],
1098 - '_index' => $index,
1099 - )
1100 - );
1101 - $field_args = Settings_API::parse_field_args( $field_args, $args['section'] );
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 + );
1102 1401
1103 - if ( ! isset( $field['type'] ) || ! is_string( $field['type'] ) ) {
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'] ) ) {
1104 1414 continue;
1105 - }
1106 - ?>
1107 - <?php $repeater_field_attributes = $this->get_field_attributes( $field_args ); ?>
1415 + }
1416 + ?>
1417 + <?php $repeater_field_attributes = $this->get_field_attributes( $field_args ); ?>
1108 1418 <div class="wz-repeater-field">
1109 1419 <div class="wz-repeater-field-header">
1110 1420 <label class="wz-repeater-field-label" for="<?php echo esc_attr( $repeater_field_attributes['field_id'] ); ?>">
1111 - <?php echo esc_html( $field['name'] ); ?>
1112 - <?php if ( ! empty( $field['required'] ) ) : ?>
1421 + <?php echo esc_html( $field['name'] ); ?>
1422 + <?php if ( ! empty( $field['required'] ) ) : ?>
1113 1423 <span class="required" title="<?php echo esc_attr( $this->translation_strings['required_label'] ); ?>">*</span>
1114 - <?php endif; ?>
1424 + <?php endif; ?>
1115 1425 </label>
1116 1426 </div>
1117 1427
1118 1428 <div class="wz-repeater-field-input">
1119 - <?php
1120 - $callback = 'callback_' . $field['type'];
1429 + <?php
1430 + $callback = 'callback_' . $field['type'];
1121 1431
1122 - if ( method_exists( $this, $callback ) ) {
1123 - $this->$callback( $field_args );
1124 - } else {
1125 - do_action( "{$this->prefix}_repeater_field_{$field['type']}", $field_args, $index ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1126 - }
1127 - ?>
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 + ?>
1128 1438 </div>
1129 1439 </div>
1130 - <?php } ?>
1440 + <?php } ?>
1131 1441 </div>
1132 1442
1133 1443 <div class="repeater-item-footer">
1134 1444 <div class="repeater-item-actions">
1135 - <button type="button" class="button button-secondary move-up">
1136 - <span class="dashicons dashicons-arrow-up-alt2"></span>
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>
1137 1447 </button>
1138 - <button type="button" class="button button-secondary move-down">
1139 - <span class="dashicons dashicons-arrow-down-alt2"></span>
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>
1140 1450 </button>
1141 - <button type="button" class="button button-secondary remove-item">
1142 - <span class="dashicons dashicons-trash"></span>
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>
1143 1453 </button>
1144 1454 </div>
1145 1455 </div>
1146 1456 </div>
1147 1457
1148 - <?php
1458 + <?php
1149 1459 }
1150 1460
1151 1461
1152 1462
@@ -1151,10 +1461,8 @@
1151 1461
1152 1462
1153 1463 /**
1154 1464 * Display sensitive fields.
1155 - *
1156 - * @since 1.0.0
1157 1465 *
1158 1466 * @param array $args Array of arguments.
1159 1467 */
1160 1468 public function callback_sensitive( $args ) {