| @@ -1,7 +1,85 @@ | ||
| 1 | 1 | <?php |
| 2 | +/** | |
| 3 | + * Shared admin UI component renderers. | |
| 4 | + * | |
| 5 | + * @package SpringDevs\Subscription\Admin | |
| 6 | + */ | |
| 2 | 7 | |
| 8 | +// Exit if accessed directly. | |
| 9 | +if ( ! defined( 'ABSPATH' ) ) { | |
| 10 | + exit; | |
| 11 | +} | |
| 12 | + | |
| 3 | 13 | /** |
| 14 | + * Render the standard admin page header: title, optional description, optional | |
| 15 | + * right-aligned actions, an optional right-hand aside (e.g. stat cards), and a | |
| 16 | + * dashed rule beneath. The one header for every admin screen in both plugins — | |
| 17 | + * use this instead of hand-writing the markup on a new page. | |
| 18 | + * | |
| 19 | + * @param array $args { | |
| 20 | + * Header args. | |
| 21 | + * | |
| 22 | + * @type string $title Page title. Required. | |
| 23 | + * @type string $description One-line description under the title. | |
| 24 | + * @type string $doc_url Docs URL; appends a "view documentation" link to | |
| 25 | + * the description. | |
| 26 | + * @type string $actions Pre-escaped HTML on the right of the title row. | |
| 27 | + * @type string $aside Pre-escaped HTML for a right-hand column beside | |
| 28 | + * the header (e.g. stat cards). | |
| 29 | + * @type string $class Extra classes on the wrapper. | |
| 30 | + * } | |
| 31 | + * @return void | |
| 32 | + */ | |
| 33 | +function wpsubs_render_page_header( array $args ): void { | |
| 34 | + $args = wp_parse_args( | |
| 35 | + $args, | |
| 36 | + array( | |
| 37 | + 'title' => '', | |
| 38 | + 'description' => '', | |
| 39 | + 'doc_url' => '', | |
| 40 | + 'actions' => '', | |
| 41 | + 'aside' => '', | |
| 42 | + 'class' => '', | |
| 43 | + ) | |
| 44 | + ); | |
| 45 | + | |
| 46 | + $classes = 'wpsubs-page-header'; | |
| 47 | + if ( '' !== $args['aside'] ) { | |
| 48 | + $classes .= ' wpsubs-page-header--has-aside'; | |
| 49 | + } | |
| 50 | + if ( $args['class'] ) { | |
| 51 | + $classes .= ' ' . $args['class']; | |
| 52 | + } | |
| 53 | + ?> | |
| 54 | + <div class="<?php echo esc_attr( $classes ); ?>"> | |
| 55 | + <div class="wpsubs-page-header__main"> | |
| 56 | + <div class="wpsubs-page-header__row"> | |
| 57 | + <h1 class="wpsubs-page-header__title"><?php echo esc_html( $args['title'] ); ?></h1> | |
| 58 | + <?php if ( '' !== $args['actions'] ) : ?> | |
| 59 | + <span class="wpsubs-toolbar__spacer"></span> | |
| 60 | + <?php echo $args['actions']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- caller passes pre-escaped markup. ?> | |
| 61 | + <?php endif; ?> | |
| 62 | + </div> | |
| 63 | + <?php if ( '' !== $args['description'] ) : ?> | |
| 64 | + <p class="wpsubs-page-header__desc"> | |
| 65 | + <?php echo esc_html( $args['description'] ); ?> | |
| 66 | + <?php if ( '' !== $args['doc_url'] ) : ?> | |
| 67 | + <?php esc_html_e( 'For more information,', 'subscription' ); ?> | |
| 68 | + <a href="<?php echo esc_url( $args['doc_url'] ); ?>" target="_blank" rel="noopener"><?php esc_html_e( 'view documentation', 'subscription' ); ?></a>. | |
| 69 | + <?php endif; ?> | |
| 70 | + </p> | |
| 71 | + <?php endif; ?> | |
| 72 | + <div class="wpsubs-page-header__rule"></div> | |
| 73 | + </div> | |
| 74 | + <?php if ( '' !== $args['aside'] ) : ?> | |
| 75 | + <div class="wpsubs-page-header__aside"><?php echo $args['aside']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- caller passes pre-escaped markup. ?></div> | |
| 76 | + <?php endif; ?> | |
| 77 | + </div> | |
| 78 | + <?php | |
| 79 | +} | |
| 80 | + | |
| 81 | +/** | |
| 4 | 82 | * Compute the visible page list for a paginator (current ± 1 window with |
| 5 | 83 | * ellipsis for wider gaps, page 1 and the last page always pinned). |
| 6 | 84 | * |
| 7 | 85 | * Mirrors the JS algorithm used by WPSubsPager so server- and client-side |
| @@ -610,8 +688,56 @@ | ||
| 610 | 688 | <?php |
| 611 | 689 | } |
| 612 | 690 | |
| 613 | 691 | /** |
| 692 | + * Render a "per page" selector as an Advanced Select. | |
| 693 | + * | |
| 694 | + * Wraps wpsubs_render_adv_select() with the project-standard page-size options | |
| 695 | + * (10 / 20 / 50 / 100) so every list uses the same choices. Everything is | |
| 696 | + * overridable: pass `options` (an array of ints, or full adv-select option | |
| 697 | + * arrays) to change the choices, `value` for the initial selection, and any | |
| 698 | + * other wpsubs_render_adv_select() arg (name, align, id, class, attrs) — they | |
| 699 | + * pass straight through. | |
| 700 | + * | |
| 701 | + * @param array $args { | |
| 702 | + * @type string $name Hidden input name. | |
| 703 | + * @type string $value Initial value. Default '10'. | |
| 704 | + * @type array $options Page sizes (ints) or option arrays. Default 10/20/50/100. | |
| 705 | + * @type mixed ... Any other wpsubs_render_adv_select() arg. | |
| 706 | + * } | |
| 707 | + * | |
| 708 | + * @return void | |
| 709 | + */ | |
| 710 | +function wpsubs_render_per_page_select( array $args = array() ): void { | |
| 711 | + $args = array_merge( | |
| 712 | + array( | |
| 713 | + 'value' => '10', | |
| 714 | + 'options' => array( 10, 20, 50, 100 ), | |
| 715 | + ), | |
| 716 | + $args | |
| 717 | + ); | |
| 718 | + | |
| 719 | + // Expand plain int page sizes into adv-select option arrays. | |
| 720 | + $options = array(); | |
| 721 | + foreach ( $args['options'] as $option ) { | |
| 722 | + if ( is_array( $option ) ) { | |
| 723 | + $options[] = $option; | |
| 724 | + continue; | |
| 725 | + } | |
| 726 | + $options[] = array( | |
| 727 | + 'value' => (string) $option, | |
| 728 | + /* translators: %d: number of items shown per page. */ | |
| 729 | + 'label' => sprintf( __( '%d / page', 'subscription' ), (int) $option ), | |
| 730 | + ); | |
| 731 | + } | |
| 732 | + | |
| 733 | + $args['options'] = $options; | |
| 734 | + $args['value'] = (string) $args['value']; | |
| 735 | + | |
| 736 | + wpsubs_render_adv_select( $args ); | |
| 737 | +} | |
| 738 | + | |
| 739 | +/** | |
| 614 | 740 | * Render a tag/pill select input with an inline filter and filterable dropdown. |
| 615 | 741 | * Supports single and multiple selection. No external dependencies. |
| 616 | 742 | * |
| 617 | 743 | * JS: WPSubsTagSelect (admin-components.js) auto-inits elements. |
| @@ -817,5 +943,53 @@ | ||
| 817 | 943 | <?php endif; ?> |
| 818 | 944 | </div> |
| 819 | 945 | </div> |
| 820 | 946 | <?php |
| 947 | +} | |
| 948 | + | |
| 949 | +/** | |
| 950 | + * Render an inline help hint: a help icon that reveals text on hover. | |
| 951 | + * | |
| 952 | + * Thin wrapper over the canonical `wpsubs-tooltip` component | |
| 953 | + * (admin-components/tooltip.css): a help dashicon wrapped in a data-tip span. | |
| 954 | + * Returns the markup so it can be concatenated into a label; place it inside an | |
| 955 | + * overflow:visible container so the bubble is not clipped. | |
| 956 | + * | |
| 957 | + * @param string $text Hint text. | |
| 958 | + * @param array $args Optional. 'placement' => 'top' (default)|'bottom'|'left'|'right'; | |
| 959 | + * 'align' => 'center' (default)|'start'|'end'; 'class' => extra trigger classes. | |
| 960 | + * | |
| 961 | + * @return string Escaped markup. | |
| 962 | + */ | |
| 963 | +function wpsubs_render_hint( string $text, array $args = array() ): string { | |
| 964 | + $args = wp_parse_args( | |
| 965 | + $args, | |
| 966 | + array( | |
| 967 | + 'placement' => 'top', | |
| 968 | + 'align' => 'center', | |
| 969 | + 'class' => '', | |
| 970 | + ) | |
| 971 | + ); | |
| 972 | + | |
| 973 | + $classes = 'wpsubs-tooltip wpsubs-tooltip--hint'; | |
| 974 | + if ( in_array( $args['placement'], array( 'bottom', 'left', 'right' ), true ) ) { | |
| 975 | + $classes .= ' wpsubs-tooltip--' . $args['placement']; | |
| 976 | + } | |
| 977 | + if ( in_array( $args['align'], array( 'start', 'end' ), true ) ) { | |
| 978 | + $classes .= ' wpsubs-tooltip--' . $args['align']; | |
| 979 | + } | |
| 980 | + if ( '' !== $args['class'] ) { | |
| 981 | + $classes .= ' ' . $args['class']; | |
| 982 | + } | |
| 983 | + | |
| 984 | + // A <span> (not a <button>/<input>) is NOT a labelable element, so nesting | |
| 985 | + // it inside a <label> does not associate the label with it — hovering the | |
| 986 | + // label text therefore never reveals the tooltip, only hovering the icon | |
| 987 | + // does. The text is exposed to assistive tech via role="img" + aria-label. | |
| 988 | + return sprintf( | |
| 989 | + '<span class="%1$s" data-tip="%2$s" role="img" aria-label="%2$s">' | |
| 990 | + . '<span class="dashicons dashicons-editor-help wpsubs-tooltip__icon" aria-hidden="true"></span>' | |
| 991 | + . '</span>', | |
| 992 | + esc_attr( $classes ), | |
| 993 | + esc_attr( $text ) | |
| 994 | + ); | |
| 821 | 995 | } |