'', 'description' => '', 'doc_url' => '', 'actions' => '', 'aside' => '', 'class' => '', ) ); $classes = 'wpsubs-page-header'; if ( '' !== $args['aside'] ) { $classes .= ' wpsubs-page-header--has-aside'; } if ( $args['class'] ) { $classes .= ' ' . $args['class']; } ?>

.

Page numbers in display order; `null` is an ellipsis. */ function wpsubs_pager_page_range( int $current, int $total ): array { $total = max( 1, $total ); $first = 1; $last = $total; $current = max( 1, min( $total, $current ) ); // Slide a 3-number window centred on the current page, excluding the pinned // first/last pages so they don't show twice. $near_start = max( 2, $current - 1 ); $near_end = min( $last - 1, $current + 1 ); $nearby = array(); for ( $i = $near_start; $i <= $near_end; $i++ ) { $nearby[] = $i; } // Dedupe while preserving order (first/last may already be in $nearby). $parts = array(); $seen = array(); $push = function ( int $n ) use ( &$parts, &$seen ) { if ( ! isset( $seen[ $n ] ) ) { $seen[ $n ] = true; $parts[] = $n; } }; $push( $first ); foreach ( $nearby as $n ) { $push( $n ); } if ( $last > $first ) { $push( $last ); } // Emit ellipsis for gaps > 1 between consecutive visible numbers. $range = array(); foreach ( $parts as $j => $p ) { if ( $j > 0 ) { $gap = $p - $parts[ $j - 1 ]; if ( 2 === $gap ) { $range[] = $parts[ $j - 1 ] + 1; // single hidden page — surface it } elseif ( $gap > 2 ) { $range[] = null; // ellipsis } } $range[] = $p; } return $range; } /** * Render a WPSubscription paginator footer (info text + prev / next / numbered * buttons + ellipsis). Single source of truth used by the subscriptions list * (server-side) and the subscription details cards (hydrated by JS). * * Pair with `WPSubsPager` in `assets/js/admin-components.js` for auto-init on * `.wpsubs-pager[data-wpsubs-pager]` elements. Pass `link_mode => 'cb'` from * the details page so buttons trigger the JS controller instead of navigating. * * Markup follows the BEM classes defined in `admin-components.css`: * .wpsubs-pagination * .wpsubs-pagination__info * .wpsubs-pagination__nav * .wpsubs-pagination__btn (mods: --active | --disabled | --ellipsis) * * @param array $args { * @type int $current Current page (1-indexed). Default 1. * @type int $total Total number of pages. Default 1. * @type bool $info Whether to render the "Showing X–Y of Z" info * text alongside the buttons. Default false (only * buttons are rendered). * @type int $per_page Items per page (used when $info is true). * @type int $item_count Total items (defaults to total * per_page). * @type string $base_url Base URL for server-side links (link_mode=url). * The component appends ?paged= and ?per_page= query args. * Ignored if `link_url_callback` is provided. * @type callable $link_url_callback Optional callable `function( int $page ): string` * that returns the URL for a given page. When supplied, * it replaces the default `add_query_arg( $base_url )` builder * and lets callers (e.g. Pro templates) build URLs with * admin_url() and arbitrary query args. The returned URL is * not re-escaped — callers must escape with esc_url() before * returning. * @type string $link_mode 'url' (default, server ) or 'cb' ('; } return '' . esc_html( $label ) . ''; }; $render_ellipsis = function (): string { return ''; }; $render_prev = function () use ( $has_prev, $current, $build_link, $args ): string { $page = $current - 1; $label = '‹'; $attrs = ' aria-label="' . esc_attr__( 'Previous page', 'subscription' ) . '"'; if ( $has_prev ) { if ( 'cb' === $args['link_mode'] ) { return ''; } return '' . $label . ''; } return ''; }; $render_next = function () use ( $has_next, $current, $build_link, $args ): string { $page = $current + 1; $label = '›'; $attrs = ' aria-label="' . esc_attr__( 'Next page', 'subscription' ) . '"'; if ( $has_next ) { if ( 'cb' === $args['link_mode'] ) { return ''; } return '' . $label . ''; } return ''; }; ?>
id="" role="navigation" aria-label="" data-wpsubs-pager data-current="" data-total="" data-per-page="" data-link-mode="cb" data-info-format="" >
Label pairs for options. * @type array|string $selected Optional. Selected value(s). Array, JSON, or CSV. * @type string $desc_tip Optional. Description tooltip text. * @type string $description Optional. Field description text. * @type string $wrapper_class Optional. Extra wrapper classes. * @type string $class Optional. Extra

SUBSCRPT_ASSETS . '/images/previews/subscrpt-health-preview.png', 'cta_title' => __( 'Upgrade to WPSubscription Pro', 'subscription' ), 'cta_description' => __( 'This page requires WPSubscription Pro. Unlock advanced features, priority support, and more with WPSubscription Pro.', 'subscription' ), 'cta_button_text' => __( '⚡ Upgrade to Pro', 'subscription' ), 'cta_button_url' => 'https://wpsubscription.co/?utm_source=plugin&utm_medium=admin&utm_campaign=upgrade_pro', ]; $args = wp_parse_args( $args, $defaults ); ob_start(); ?>
<?php esc_attr_e( 'page preview', 'subscription' ); ?>
. * A hidden carries the selected value for form submission. * JS (admin-components.js WPSubsAdvSelect) handles open/close and selection. * * @param array $args { * @type string $name Hidden input name attribute. Required. * @type string $placeholder Trigger label when nothing is selected. * @type string $value Initial hidden-input value (default: ''). * @type array $options Each item: { * string value Value submitted on selection. * string label Display text. * bool danger Red destructive style. * string confirm JS confirm() message before selecting. * bool divider Render a divider BEFORE this item. * bool disabled Non-selectable item. * } * @type string $align Menu alignment: 'left' (default) or 'right'. * @type string $id Optional id on the root element. * @type string $class Extra classes on the root element. * } */ function wpsubs_render_adv_select( array $args ): void { $args = wp_parse_args( $args, array( 'name' => '', 'placeholder' => __( 'Select', 'subscription' ), 'value' => '', 'options' => array(), 'align' => 'left', 'id' => '', 'class' => '', 'attrs' => array(), ) ); $root_classes = 'wpsubs-adv-select wpsubs-adv-select--' . ( 'right' === $args['align'] ? 'right' : 'left' ); if ( $args['class'] ) { $root_classes .= ' ' . $args['class']; } // Resolve trigger label: use matching option's label when a value is already set. $trigger_label = $args['placeholder']; $current_value = (string) $args['value']; if ( '' !== $current_value && '-1' !== $current_value ) { foreach ( $args['options'] as $opt ) { if ( (string) ( $opt['value'] ?? '' ) === $current_value ) { $trigger_label = $opt['label'] ?? $args['placeholder']; break; } } } $chevron_svg = ''; ?>
id="" data-placeholder="" data-default-value="" $attr_value ) : echo esc_attr( $attr_name ) . '="' . esc_attr( $attr_value ) . '" '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Both parts escaped. endforeach; ?> >
'', 'label' => '', 'danger' => false, 'confirm' => '', 'divider' => false, 'disabled' => false, ) ); if ( $option['divider'] ) : ?>
'10', 'options' => array( 10, 20, 50, 100 ), ), $args ); // Expand plain int page sizes into adv-select option arrays. $options = array(); foreach ( $args['options'] as $option ) { if ( is_array( $option ) ) { $options[] = $option; continue; } $options[] = array( 'value' => (string) $option, /* translators: %d: number of items shown per page. */ 'label' => sprintf( __( '%d / page', 'subscription' ), (int) $option ), ); } $args['options'] = $options; $args['value'] = (string) $args['value']; wpsubs_render_adv_select( $args ); } /** * Render a tag/pill select input with an inline filter and filterable dropdown. * Supports single and multiple selection. No external dependencies. * * JS: WPSubsTagSelect (admin-components.js) auto-inits elements. * Event fired on root: `wpsubs:select` — detail: { value, label, selected } * * @param array $args { * string $name Form field name (base name, without [] suffix). * string $placeholder Input placeholder shown when nothing is selected. * string|array $value Current value(s). Array for multiple, string for single. * array $options Options: array of { value, label, disabled? }. * bool $multiple Enable multi-select mode. * string $id Optional root element id. * string $class Extra CSS classes for the root element. * array $attrs Extra HTML attributes for the root element. * } */ function wpsubs_render_tag_select( array $args ): void { $args = wp_parse_args( $args, array( 'name' => '', 'placeholder' => __( 'Select...', 'subscription' ), 'value' => '', 'options' => array(), 'multiple' => false, 'id' => '', 'class' => '', 'attrs' => array(), ) ); $multiple = (bool) $args['multiple']; $current_value = $multiple ? (array) $args['value'] : (string) $args['value']; if ( $multiple ) { $selected_values = array_filter( array_map( 'strval', $current_value ), fn( $v ) => '' !== $v ); } else { $selected_values = ( '' !== $current_value ) ? array( $current_value ) : array(); } // Map selected values to their labels for pill rendering. $selected_labels = array(); foreach ( $args['options'] as $opt ) { $opt_val = (string) ( $opt['value'] ?? '' ); if ( in_array( $opt_val, $selected_values, true ) ) { $selected_labels[ $opt_val ] = $opt['label'] ?? $opt_val; } } $root_classes = 'wpsubs-tag-select'; if ( $multiple ) { $root_classes .= ' wpsubs-tag-select--multi'; } if ( $args['class'] ) { $root_classes .= ' ' . $args['class']; } $has_pills = ! empty( $selected_values ); $placeholder = $has_pills ? '' : esc_attr( $args['placeholder'] ); $chevron_svg = ''; ?>
id="" data-placeholder="" data-name="" data-multiple="1" $attr_value ) : echo esc_attr( $attr_name ) . '="' . esc_attr( $attr_value ) . '" '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Both parts escaped. endforeach; ?> >
$lbl ) : ?>
aria-multiselectable="true"> '', 'label' => '', 'disabled' => false, ) ); $opt_value = (string) $option['value']; $is_selected = in_array( $opt_value, $selected_values, true ); ?>
"`; the backdrop, header close, and * footer buttons close it; Escape closes it. The dialog is hidden until opened. * * @param array $args Modal arguments: `id` (required, matches the opener's target), * `title` (header title), `body` (pre-escaped body HTML), `footer` * (pre-escaped footer HTML, optional), `class` (extra root class, * optional). * @return void */ function wpsubs_render_modal( array $args ): void { $id = $args['id'] ?? ''; if ( empty( $id ) ) { return; } $title = $args['title'] ?? ''; $body = $args['body'] ?? ''; $footer = $args['footer'] ?? ''; $extra_class = $args['class'] ?? ''; ?> 'top' (default)|'bottom'|'left'|'right'; * 'align' => 'center' (default)|'start'|'end'; 'class' => extra trigger classes. * * @return string Escaped markup. */ function wpsubs_render_hint( string $text, array $args = array() ): string { $args = wp_parse_args( $args, array( 'placement' => 'top', 'align' => 'center', 'class' => '', ) ); $classes = 'wpsubs-tooltip wpsubs-tooltip--hint'; if ( in_array( $args['placement'], array( 'bottom', 'left', 'right' ), true ) ) { $classes .= ' wpsubs-tooltip--' . $args['placement']; } if ( in_array( $args['align'], array( 'start', 'end' ), true ) ) { $classes .= ' wpsubs-tooltip--' . $args['align']; } if ( '' !== $args['class'] ) { $classes .= ' ' . $args['class']; } // A (not a