PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / trunk
Subscriptions for WooCommerce with Stripe Recurring Payments vtrunk
1.11.2 1.11.1 1.11.0 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.6 1.9.5 trunk 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 All 60 releases
subscription / includes / Admin / SettingsHelper.php

SettingsHelper.php in Subscriptions for WooCommerce with Stripe Recurring Payments trunk, at includes/Admin/SettingsHelper.php

731 lines 26.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Settings Helper File
4 *
5 * @package SpringDevs\Subscription\Admin
6 */
7
8 namespace SpringDevs\Subscription\Admin;
9
10 /**
11 * Settings Helper Class
12 *
13 * @package SpringDevs\Subscription\Admin
14 */
15 class SettingsHelper {
16 /**
17 * Singleton instance
18 *
19 * @var SettingsHelper|null
20 */
21 private static $instance = null;
22
23 /**
24 * Get singleton instance
25 *
26 * @return SettingsHelper
27 */
28 public static function get_instance() {
29 if ( null === self::$instance ) {
30 self::$instance = new self();
31 }
32 return self::$instance;
33 }
34
35 /**
36 * Initialize the class.
37 */
38 private function __construct() {
39 add_filter( 'process_subscrpt_settings_fields', [ $this, 'process_settings_fields' ], 100, 1 );
40 }
41
42 /**
43 * Process settings fields.
44 *
45 * @param array $fields Settings fields.
46 * @return array Processed settings fields.
47 */
48 public function process_settings_fields( $fields ) {
49 // Group settings fields.
50 $fields = $this->group_settings_fields( $fields );
51
52 // Sort fields by priority (groups & fields).
53 $fields = $this->sort_settings_fields( $fields );
54
55 return $fields;
56 }
57
58 /**
59 * Group settings fields.
60 *
61 * @param array $fields Settings fields.
62 * @return array Processed settings fields.
63 */
64 public function group_settings_fields( $fields ) {
65 $tmp_fields = [];
66 foreach ( $fields as $field ) {
67 $field_group = $field['group'] ?? 'main';
68
69 if ( $field['type'] === 'heading' ) {
70 $group_priority = $field['priority'] ?? 0;
71 $tmp_fields[ $field_group ]['priority'] = $group_priority;
72 $field['priority'] = -1;
73 }
74
75 $tmp_fields[ $field_group ]['fields'][] = $field;
76 }
77 return $tmp_fields;
78 }
79
80 /**
81 * Sort settings fields.
82 *
83 * @param array $fields Settings fields.
84 * @return array Processed settings fields.
85 */
86 public function sort_settings_fields( $fields ) {
87 // Sort groups by priority.
88 uasort(
89 $fields,
90 function ( $a, $b ) {
91 $priority_a = $a['priority'] ?? 0;
92 $priority_b = $b['priority'] ?? 0;
93 return $priority_a <=> $priority_b;
94 }
95 );
96
97 // Sort fields within each group by priority.
98 foreach ( $fields as $group_key => $group_data ) {
99 uasort(
100 $group_data['fields'],
101 function ( $a, $b ) {
102 $priority_a = $a['priority'] ?? 0;
103 $priority_b = $b['priority'] ?? 0;
104 return $priority_a <=> $priority_b;
105 }
106 );
107 $fields[ $group_key ]['fields'] = $group_data['fields'];
108 }
109 return $fields;
110 }
111
112 /**
113 * Render specified settings field.
114 *
115 * @param string $field Field type.
116 * @param array $args Field arguments.
117 * @param bool $should_print Whether to print the field or return as HTML string.
118 */
119 public static function render_settings_field( $field, $args, $should_print = true ) {
120 if ( empty( $field ) ) {
121 $field = 'input'; // Default field type.
122 subscrpt_write_debug_log( "[SettingsHelper] Field type not specified. Defaulting to 'input'." );
123 }
124
125 switch ( $field ) {
126 case 'heading':
127 return self::render_heading( $args, $should_print );
128 case 'switch':
129 case 'toggle':
130 return self::render_switch_field( $args, $should_print );
131 case 'select':
132 return self::render_select_field( $args, $should_print );
133 case 'multi_select':
134 return self::render_multiselect_field( $args, $should_print );
135 case 'join':
136 return self::render_joined_field( $args, $should_print );
137 case 'editlist':
138 return self::render_editlist_field( $args, $should_print );
139 case 'input':
140 default:
141 return self::render_input_field( $args, $should_print );
142 }
143 }
144
145 /**
146 * Pro badge markup, shown beside settings that require WPSubscription Pro.
147 *
148 * @return string Pre-escaped badge HTML.
149 */
150 public static function pro_badge_html() {
151 return '<span class="subscrpt-pro-badge" title="' . esc_attr__( 'WPSubscription Pro required', 'subscription' ) . '">' . esc_html__( 'Pro', 'subscription' ) . '</span>';
152 }
153
154
155 /**
156 * Text Element HTML.
157 *
158 * @param array $args Same as 'render_text_field'.
159 * @param bool $join_item Whether to return element for 'join' container or not.
160 */
161 public static function inp_element( $args = [], $join_item = false ) {
162 $id = $args['id'];
163 $value = $args['value'] ?? '';
164 $placeholder = $args['placeholder'] ?? '';
165 $type = $args['type'] ?? 'text';
166
167 $disabled_attr = isset( $args['disabled'] ) && $args['disabled'] ? 'disabled' : '';
168
169 $style_attr = '';
170 if ( isset( $args['style'] ) ) {
171 $style_attr = $args['style'];
172 }
173
174 $other_attrs_html = '';
175 foreach ( ( $args['attributes'] ?? [] ) as $attr_key => $attr_value ) {
176 $other_attrs_html .= sprintf( ' %s="%s" ', esc_attr( $attr_key ), esc_attr( $attr_value ) );
177 }
178
179 ob_start();
180 ?>
181 <input
182 id="<?php echo esc_attr( $id ); ?>"
183 name="<?php echo esc_attr( $id ); ?>"
184 class="wpsubs-input"
185 <?php
186 if ( $style_attr ) :
187 ?>
188 style="<?php echo esc_attr( $style_attr ); ?>"<?php endif; ?>
189 type="<?php echo esc_attr( $type ); ?>"
190 placeholder="<?php echo esc_attr( $placeholder ); ?>"
191 value="<?php echo esc_attr( $value ); ?>"
192 <?php echo esc_attr( $disabled_attr ); ?>
193 <?php echo wp_kses_post( $other_attrs_html ); ?>
194 />
195 <?php
196 return ob_get_clean();
197 }
198
199 /**
200 * Select Element HTML.
201 *
202 * @param array $args Same as 'render_select_field'.
203 * @param bool $join_item Whether to return element for 'join' container or not.
204 */
205 public static function select_element( $args = [], $join_item = false ) {
206 $id = $args['id'];
207
208 // Enhanced / multiselect → wpsubs-tag-select (pill input with filter).
209 if ( isset( $args['enhanced'] ) && $args['enhanced'] ) {
210 $multiple = isset( $args['attributes']['multiple'] ) && $args['attributes']['multiple'];
211 $adv_options = array();
212 foreach ( ( $args['options'] ?? [] ) as $opt_value => $opt_label ) {
213 $adv_options[] = array(
214 'value' => (string) $opt_value,
215 'label' => $opt_label,
216 );
217 }
218
219 ob_start();
220 wpsubs_render_tag_select(
221 array(
222 'name' => $id,
223 'value' => $args['selected'] ?? ( $multiple ? array() : '' ),
224 'options' => $adv_options,
225 'multiple' => $multiple,
226 )
227 );
228 return ob_get_clean();
229 }
230
231 // Regular select → wpsubs-adv-select (button-based custom dropdown).
232 $selected = (string) ( $args['selected'] ?? '' );
233 $adv_options = array();
234 foreach ( ( $args['options'] ?? [] ) as $opt_value => $opt_label ) {
235 $adv_options[] = array(
236 'value' => (string) $opt_value,
237 'label' => $opt_label,
238 'disabled' => isset( $args['disabled'] ) && ( is_array( $args['disabled'] )
239 ? in_array( $opt_value, $args['disabled'], true )
240 : $args['disabled'] === $opt_value ),
241 );
242 }
243
244 ob_start();
245 wpsubs_render_adv_select(
246 array(
247 'name' => $id,
248 'value' => $selected,
249 'options' => $adv_options,
250 'align' => 'left',
251 'class' => $args['class'] ?? '',
252 )
253 );
254 return ob_get_clean();
255 }
256
257 /**
258 * Render Field Heading.
259 *
260 * - Args:
261 * - title (string) - Field title.
262 * - description (string) - Field description (optional).
263 *
264 * @param array $args Field arguments.
265 * @param bool $should_print Whether to print the field or return as HTML string.
266 */
267 public static function render_heading( $args = [], $should_print = true ) {
268 $title = $args['title'] ?? '';
269 $description = $args['description'] ?? '';
270
271 ob_start();
272 ?>
273 <div class="wpsubs-settings-heading">
274 <h3 class="wpsubs-settings-heading__title"><?php echo esc_html( $title ); ?></h3>
275 <?php if ( ! empty( $description ) ) : ?>
276 <p class="wpsubs-settings-heading__desc"><?php echo wp_kses_post( $description ); ?></p>
277 <?php endif; ?>
278 </div>
279 <?php
280 $html_content = ob_get_clean();
281
282 // Output not escaped intentionally. Breaks the HTML structure when escaped.
283 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
284 return $should_print ? print( $html_content ) : $html_content;
285 }
286
287 /**
288 * Render Text field.
289 *
290 * - Args:
291 * - id (string) - Field ID.
292 * - title (string) - Field title.
293 * - description (string) - Field description (optional).
294 * - value (string) - Default value.
295 * - placeholder (string) - Default placeholder.
296 * - disabled (bool) - Disabled status.
297 * - type (string) - Input type [text, email, number, date, time, etc.].
298 *
299 * @param array $args Field arguments.
300 * @param bool $should_print Whether to print the field or return as HTML string.
301 */
302 public static function render_input_field( $args = [], $should_print = true ) {
303 $title = $args['title'] ?? '';
304 $description = $args['description'] ?? '';
305
306 // Return error if ID is not provided.
307 if ( empty( $args['id'] ?? '' ) ) {
308 $field_hint = empty( $title ) ? 'Error' : $title;
309 $no_id_msg = '<p><strong>' . $field_hint . ':</strong> ' . __( 'Field ID is required.', 'subscription' ) . '</p>';
310 return $should_print ? print wp_kses_post( $no_id_msg ) : $no_id_msg;
311 }
312
313 // Input HTML.
314 $text_el_html = self::inp_element( $args );
315
316 ob_start();
317 ?>
318 <div class="wpsubs-settings-field<?php echo ! empty( $args['pro_locked'] ) ? ' wpsubs-settings-field--locked' : ''; ?>">
319 <div class="wpsubs-settings-field__label">
320 <?php
321 if ( ! empty( $args['pro_locked'] ) ) {
322 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Pre-escaped badge markup.
323 echo self::pro_badge_html();
324 }
325 ?>
326 <?php echo esc_html( $title ); ?>
327 </div>
328 <div class="wpsubs-settings-field__control">
329 <?php
330 // Output intentionally not escaped as element is already escaped during generation & re-escaping breaks the HTML structure.
331 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
332 echo $text_el_html;
333 ?>
334 <?php if ( ! empty( $description ) ) : ?>
335 <p class="wpsubs-settings-field__hint"><?php echo wp_kses_post( $description ); ?></p>
336 <?php endif; ?>
337 </div>
338 </div>
339 <?php
340 $html_content = ob_get_clean();
341
342 // Output not escaped intentionally. Breaks the HTML structure when escaped.
343 // All form elements inside $html_content are pre-escaped during generation (esc_attr, esc_html).
344 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
345 return $should_print ? print( $html_content ) : $html_content;
346 }
347
348 /**
349 * Render Switch field.
350 *
351 * - Args:
352 * - id (string) - Field ID.
353 * - title (string) - Field title.
354 * - label (string) - Checkbox label.
355 * - description (string) - Field description (optional).
356 * - value (string) - Checked value.
357 * - checked (bool) - Checked status.
358 * - disabled (bool) - Disabled status.
359 *
360 * @param array $args Field arguments.
361 * @param bool $should_print Whether to print the field or return as HTML string.
362 */
363 public static function render_switch_field( $args = [], $should_print = true ) {
364 $id = $args['id'];
365 $title = $args['title'] ?? '';
366 $label = $args['label'] ?? '';
367 $description = $args['description'] ?? '';
368 $value = $args['value'] ?? '0';
369
370 // Return error if ID is not provided.
371 if ( empty( $args['id'] ?? '' ) ) {
372 $field_hint = empty( $title ) ? 'Error' : $title;
373 $no_id_msg = '<p><strong>' . $field_hint . ':</strong> ' . __( 'Field ID is required.', 'subscription' ) . '</p>';
374 return $should_print ? print wp_kses_post( $no_id_msg ) : $no_id_msg;
375 }
376
377 $description_html = '';
378 if ( ! empty( $description ) ) {
379 $description_html = sprintf(
380 '<p class="wpsubs-settings-field__hint">%s</p>',
381 wp_kses_post( $description )
382 );
383 }
384
385 $style_attr = '';
386 if ( isset( $args['style'] ) ) {
387 $style_attr .= ' ' . $args['style'];
388 }
389
390 $other_attrs_html = '';
391 foreach ( ( $args['attributes'] ?? [] ) as $attr_key => $attr_value ) {
392 $other_attrs_html .= sprintf( ' %s="%s" ', esc_attr( $attr_key ), esc_attr( $attr_value ) );
393 }
394
395 $checked_attr = isset( $args['checked'] ) && (bool) $args['checked'] ? 'checked' : '';
396 $disabled_attr = isset( $args['disabled'] ) && (bool) $args['disabled'] ? 'disabled' : '';
397
398 ob_start();
399 ?>
400 <div class="wpsubs-settings-field<?php echo ! empty( $args['pro_locked'] ) ? ' wpsubs-settings-field--locked' : ''; ?>">
401 <div class="wpsubs-settings-field__label">
402 <?php
403 if ( ! empty( $args['pro_locked'] ) ) {
404 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Pre-escaped badge markup.
405 echo self::pro_badge_html();
406 }
407 ?>
408 <?php echo esc_html( $title ); ?>
409 </div>
410 <div class="wpsubs-settings-field__control">
411 <label class="wpsubs-settings-toggle-label" for="<?php echo esc_attr( $id ); ?>">
412 <input
413 id="<?php echo esc_attr( $id ); ?>"
414 name="<?php echo esc_attr( $id ); ?>"
415 class="wpsubs-toggle"
416 type="checkbox"
417 value="<?php echo esc_attr( $value ); ?>"
418 <?php echo esc_attr( $checked_attr ); ?>
419 <?php echo esc_attr( $disabled_attr ); ?>
420 <?php
421 // Output intentionally not escaped as element is already escaped during generation & re-escaping breaks the HTML structure.
422 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
423 echo $other_attrs_html;
424 ?>
425 />
426 <span class="wpsubs-toggle-ui" aria-hidden="true"></span>
427 <?php if ( ! empty( $label ) ) : ?>
428 <span class="wpsubs-settings-toggle-label__text"><?php echo esc_html( $label ); ?></span>
429 <?php endif; ?>
430 </label>
431 <?php echo wp_kses_post( $description_html ); ?>
432 </div>
433 </div>
434 <?php
435 $html_content = ob_get_clean();
436
437 // Output not escaped intentionally. Breaks the HTML structure when escaped.
438 // All form elements inside $html_content are pre-escaped during generation (esc_attr, esc_html).
439 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
440 return $should_print ? print( $html_content ) : $html_content;
441 }
442
443 /**
444 * Render Select field.
445 *
446 * - Args:
447 * - id (string) - Field ID.
448 * - title (string) - Field title.
449 * - description (string) - Field description (optional).
450 * - options (array) - Field options [value => label].
451 * - selected (string) - Selected option value.
452 * - disabled (string|array) - Disabled option value(s).
453 *
454 * @param array $args Field arguments.
455 * @param bool $should_print Whether to print the field or return as HTML string.
456 */
457 public static function render_select_field( $args = [], $should_print = true ) {
458 $title = $args['title'] ?? '';
459 $description = $args['description'] ?? '';
460
461 // Return error if ID is not provided.
462 if ( empty( $args['id'] ?? '' ) ) {
463 $field_hint = empty( $title ) ? 'Error' : $title;
464 $no_id_msg = '<p><strong>' . $field_hint . ':</strong> ' . __( 'Field ID is required.', 'subscription' ) . '</p>';
465 return $should_print ? print wp_kses_post( $no_id_msg ) : $no_id_msg;
466 }
467
468 // Select HTML.
469 $select_el_html = self::select_element( $args );
470
471 ob_start();
472 ?>
473 <div class="wpsubs-settings-field<?php echo ! empty( $args['pro_locked'] ) ? ' wpsubs-settings-field--locked' : ''; ?>">
474 <div class="wpsubs-settings-field__label">
475 <?php
476 if ( ! empty( $args['pro_locked'] ) ) {
477 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Pre-escaped badge markup.
478 echo self::pro_badge_html();
479 }
480 ?>
481 <?php echo esc_html( $title ); ?>
482 </div>
483 <div class="wpsubs-settings-field__control">
484 <?php
485 // Output intentionally not escaped as element is already escaped during generation & re-escaping breaks the HTML structure.
486 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
487 echo $select_el_html;
488 ?>
489 <?php if ( ! empty( $description ) ) : ?>
490 <p class="wpsubs-settings-field__hint"><?php echo wp_kses_post( $description ); ?></p>
491 <?php endif; ?>
492 </div>
493 </div>
494 <?php
495 $html_content = ob_get_clean();
496
497 // Output not escaped intentionally. Breaks the HTML structure when escaped.
498 // All form elements inside $html_content are pre-escaped during generation (esc_attr, esc_html).
499 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
500 return $should_print ? print( $html_content ) : $html_content;
501 }
502
503 /**
504 * Render Multiselect field.
505 * Just a wrapper over 'render_select_field' with multiple attribute.
506 *
507 * @param array $args Field arguments.
508 * @param bool $should_print Whether to print the field or return as HTML string.
509 */
510 public static function render_multiselect_field( $args = [], $should_print = true ) {
511 $default_multiselect_args = [
512 'attributes' => [
513 'multiple' => 'multiple',
514 ],
515 'enhanced' => true,
516 ];
517
518 $args = wp_parse_args( $args, $default_multiselect_args );
519
520 return self::render_select_field( $args, $should_print );
521 }
522
523 /**
524 * Render Joined field with multiple elements.
525 *
526 * - Args:
527 * - title (string) - Field title.
528 * - description (string) - Field description (optional).
529 * - vertical (bool) - Whether to show items vertically or not.
530 * - elements ([...string]) - Array of HTML elements to join.
531 *
532 * @param array $args Field arguments.
533 * @param bool $should_print Whether to print the field or return as HTML string.
534 */
535 public static function render_joined_field( $args = [], $should_print = true ) {
536 $title = $args['title'] ?? '';
537 $description = $args['description'] ?? '';
538
539 $vertical_style = ( $args['vertical'] ?? false ) ? 'flex-direction:column;' : '';
540
541 ob_start();
542 ?>
543 <div class="wpsubs-settings-field<?php echo ! empty( $args['pro_locked'] ) ? ' wpsubs-settings-field--locked' : ''; ?>">
544 <div class="wpsubs-settings-field__label">
545 <?php
546 if ( ! empty( $args['pro_locked'] ) ) {
547 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Pre-escaped badge markup.
548 echo self::pro_badge_html();
549 }
550 ?>
551 <?php echo esc_html( $title ); ?>
552 </div>
553 <div class="wpsubs-settings-field__control">
554 <div class="wpsubs-input-group"
555 <?php
556 if ( $vertical_style ) :
557 ?>
558 style="<?php echo esc_attr( $vertical_style ); ?>"<?php endif; ?>>
559 <?php
560 foreach ( ( $args['elements'] ?? [] ) as $element_html ) {
561 // Output intentionally not escaped as element is already escaped during generation & re-escaping breaks the HTML structure.
562 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
563 echo $element_html;
564 }
565 ?>
566 </div>
567 <?php if ( ! empty( $description ) ) : ?>
568 <p class="wpsubs-settings-field__hint"><?php echo wp_kses_post( $description ); ?></p>
569 <?php endif; ?>
570 </div>
571 </div>
572 <?php
573 $html_content = ob_get_clean();
574
575 // Output not escaped intentionally. Breaks the HTML structure when escaped.
576 // All form elements inside $html_content are pre-escaped during generation (esc_attr, esc_html).
577 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
578 return $should_print ? print( $html_content ) : $html_content;
579 }
580
581 /**
582 * Render an editable, reorderable list field (the `wpsubs-editlist` component).
583 *
584 * Generic and reusable: a sortable list of text items with per-row remove/move
585 * controls and an inline input + add button. The ordered list is serialized as
586 * JSON (`[{ key, label }]`) into a hidden input so it submits with the form;
587 * behaviour is wired by `WPSubsEditList` (admin-components.js). All user-facing
588 * strings are overridable so the field carries no feature-specific text.
589 *
590 * When `modal` is true the list lives inside a `wpsubs-modal` (via the
591 * `WPSubsModal` component) and the settings row only shows a trigger button with
592 * a live item count.
593 *
594 * - Args:
595 * - id (string) - Field ID / option key.
596 * - title (string)
597 * - description (string)
598 * - value (array) - Ordered list of { key, label } entries.
599 * - add_placeholder (string) - Inline input placeholder.
600 * - add_label (string) - Add button accessible label.
601 * - empty_text (string) - Message shown when the list is empty.
602 * - modal (bool) - Present the list inside a modal (default false).
603 * - button_label (string) - Modal trigger button text (modal mode).
604 * - modal_title (string) - Modal header title (modal mode; defaults to title).
605 * - pro_locked (bool)
606 *
607 * @param array $args Field arguments.
608 * @param bool $should_print Whether to print the field or return as HTML string.
609 */
610 public static function render_editlist_field( $args = [], $should_print = true ) {
611 $id = $args['id'] ?? '';
612 $title = $args['title'] ?? '';
613 $description = $args['description'] ?? '';
614 $items = is_array( $args['value'] ?? null ) ? $args['value'] : [];
615 $locked = ! empty( $args['pro_locked'] );
616 $modal = ! empty( $args['modal'] );
617
618 $add_placeholder = $args['add_placeholder'] ?? __( 'Add an item…', 'subscription' );
619 $add_label = $args['add_label'] ?? __( 'Add item', 'subscription' );
620 $empty_text = $args['empty_text'] ?? __( 'No items yet. Add one below.', 'subscription' );
621 $button_label = $args['button_label'] ?? __( 'Manage list', 'subscription' );
622 $modal_title = $args['modal_title'] ?? ( '' !== $title ? $title : __( 'Edit list', 'subscription' ) );
623
624 if ( empty( $id ) ) {
625 $field_hint = empty( $title ) ? 'Error' : $title;
626 $no_id_msg = '<p><strong>' . $field_hint . ':</strong> ' . __( 'Field ID is required.', 'subscription' ) . '</p>';
627 return $should_print ? print wp_kses_post( $no_id_msg ) : $no_id_msg;
628 }
629
630 $body = self::editlist_body_html( $items, $add_placeholder, $add_label, $empty_text, $locked );
631
632 ob_start();
633 ?>
634 <div class="wpsubs-settings-field<?php echo $locked ? ' wpsubs-settings-field--locked' : ''; ?>">
635 <div class="wpsubs-settings-field__label">
636 <?php
637 if ( $locked ) {
638 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Pre-escaped badge markup.
639 echo self::pro_badge_html();
640 }
641 ?>
642 <?php echo esc_html( $title ); ?>
643 </div>
644 <div class="wpsubs-settings-field__control">
645 <div class="wpsubs-editlist<?php echo $locked ? ' wpsubs-editlist--locked' : ''; ?>">
646 <input type="hidden" id="<?php echo esc_attr( $id ); ?>" name="<?php echo esc_attr( $id ); ?>" value="<?php echo esc_attr( wp_json_encode( array_values( $items ) ) ); ?>" />
647 <?php if ( $modal ) : ?>
648 <button type="button" class="wpsubs-btn wpsubs-btn--outline wpsubs-editlist__trigger" data-wpsubs-modal-open="<?php echo esc_attr( $id . '_modal' ); ?>"<?php echo $locked ? ' disabled' : ''; ?>>
649 <?php echo esc_html( $button_label ); ?>
650 <span class="wpsubs-editlist__count"><?php echo esc_html( (string) count( $items ) ); ?></span>
651 </button>
652 <?php
653 wpsubs_render_modal(
654 [
655 'id' => $id . '_modal',
656 'title' => $modal_title,
657 'body' => $body,
658 'class' => 'wpsubs-modal--editlist',
659 'footer' => '<button type="button" class="wpsubs-btn wpsubs-btn--primary" data-wpsubs-modal-close>' . esc_html__( 'Done', 'subscription' ) . '</button>',
660 ]
661 );
662 ?>
663 <?php else : ?>
664 <?php
665 // Body markup is pre-escaped during generation.
666 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
667 echo $body;
668 ?>
669 <?php endif; ?>
670 </div>
671 <?php if ( ! empty( $description ) ) : ?>
672 <p class="wpsubs-settings-field__hint"><?php echo wp_kses_post( $description ); ?></p>
673 <?php endif; ?>
674 </div>
675 </div>
676 <?php
677 $html_content = ob_get_clean();
678
679 // Output not escaped intentionally. Breaks the HTML structure when escaped.
680 // All form elements inside $html_content are pre-escaped during generation (esc_attr, esc_html).
681 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
682 return $should_print ? print( $html_content ) : $html_content;
683 }
684
685 /**
686 * Build the inner markup of an edit list (items + empty message + add row).
687 *
688 * Shared by inline and modal presentations of {@see self::render_editlist_field}.
689 *
690 * @param array $items Ordered list of { key, label } entries.
691 * @param string $add_placeholder Inline input placeholder.
692 * @param string $add_label Add button accessible label.
693 * @param string $empty_text Message shown when the list is empty.
694 * @param bool $locked Whether the controls are disabled.
695 * @return string Pre-escaped HTML.
696 */
697 private static function editlist_body_html( array $items, $add_placeholder, $add_label, $empty_text, $locked ) {
698 ob_start();
699 ?>
700 <ul class="wpsubs-editlist__items">
701 <?php foreach ( $items as $item ) : ?>
702 <?php
703 $item_key = isset( $item['key'] ) ? (string) $item['key'] : '';
704 $item_label = isset( $item['label'] ) ? (string) $item['label'] : '';
705 if ( '' === $item_label ) {
706 continue;
707 }
708 ?>
709 <li class="wpsubs-editlist__item" data-key="<?php echo esc_attr( $item_key ); ?>">
710 <span class="wpsubs-editlist__handle" aria-hidden="true">&#8942;&#8942;</span>
711 <span class="wpsubs-editlist__label"><?php echo esc_html( $item_label ); ?></span>
712 <span class="wpsubs-editlist__actions">
713 <button type="button" class="wpsubs-editlist__btn" data-editlist-up aria-label="<?php esc_attr_e( 'Move up', 'subscription' ); ?>"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="18 15 12 9 6 15"/></svg></button>
714 <button type="button" class="wpsubs-editlist__btn" data-editlist-down aria-label="<?php esc_attr_e( 'Move down', 'subscription' ); ?>"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg></button>
715 <button type="button" class="wpsubs-editlist__btn wpsubs-editlist__btn--danger" data-editlist-remove aria-label="<?php esc_attr_e( 'Remove', 'subscription' ); ?>"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>
716 </span>
717 </li>
718 <?php endforeach; ?>
719 </ul>
720 <p class="wpsubs-editlist__empty"<?php echo empty( $items ) ? '' : ' hidden'; ?>><?php echo esc_html( $empty_text ); ?></p>
721 <div class="wpsubs-editlist__add">
722 <input type="text" class="wpsubs-input wpsubs-editlist__input" placeholder="<?php echo esc_attr( $add_placeholder ); ?>"<?php echo $locked ? ' disabled' : ''; ?> />
723 <button type="button" class="wpsubs-editlist__add-btn" data-editlist-add aria-label="<?php echo esc_attr( $add_label ); ?>"<?php echo $locked ? ' disabled' : ''; ?>>
724 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>
725 </button>
726 </div>
727 <?php
728 return ob_get_clean();
729 }
730 }
731