maybe_save(); $id = isset($_GET['display']) ? absint(wp_unslash($_GET['display'])) : 0; // phpcs:ignore WordPress.Security.NonceVerification $this->display = $id ? SRPP_Display::get($id) : null; if (! $this->display) { $this->display = new SRPP_Display(); $this->display->title = __('New display', 'advanced-css3-related-posts-widget'); } $this->c = $this->display->config; echo '
'; SRPP_Admin::header( $this->display->id ? __('Edit display', 'advanced-css3-related-posts-widget') : __('Create a related-content display', 'advanced-css3-related-posts-widget'), __('Design → Content → Placement → Layout', 'advanced-css3-related-posts-widget'), sprintf( '%s', esc_url(add_query_arg('page', 'srpp-displays', admin_url('admin.php'))), esc_html__('Back to displays', 'advanced-css3-related-posts-widget') ), array('icon' => 'wand') ); echo '
'; wp_nonce_field('srpp_save_display', 'srpp_nonce'); printf('', absint($this->display->id)); echo '
'; echo '
'; $this->title_bar(); $this->step_design(); $this->step_content(); $this->step_placement(); $this->step_layout(); $this->step_advanced(); echo '
'; $this->preview_pane(); echo '
'; printf( '
%s
', esc_html__('Save display', 'advanced-css3-related-posts-widget'), esc_html__('Changes go live immediately on the front end.', 'advanced-css3-related-posts-widget') ); echo '
'; } /* --------------------------------------------------------------------- * Saving * ------------------------------------------------------------------ */ /** * Persist the form. */ protected function maybe_save() { if (! isset($_POST['srpp_nonce']) || ! SRPP_Admin::can()) { return; } if (! wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['srpp_nonce'])), 'srpp_save_display')) { return; } $raw = isset($_POST['srpp']) ? wp_unslash($_POST['srpp']) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput $raw = self::normalize_input((array) $raw); $id = SRPP_Display::save( isset($_POST['display_id']) ? absint(wp_unslash($_POST['display_id'])) : 0, array( 'title' => isset($_POST['srpp_title']) ? sanitize_text_field(wp_unslash($_POST['srpp_title'])) : __('Untitled display', 'advanced-css3-related-posts-widget'), 'status' => (isset($_POST['srpp_status']) && 'draft' === $_POST['srpp_status']) ? 'draft' : 'publish', 'config' => $raw, ) ); if (is_wp_error($id)) { return; } wp_safe_redirect( add_query_arg( array('page' => 'srpp-edit', 'display' => $id, 'srpp_msg' => 'saved'), admin_url('admin.php') ) ); exit; } /** * Turn the flat form shape into the config shape. * * Taxonomy rules arrive as taxonomy => [term ids]; the model stores a list of * { taxonomy, terms } rules. * * @param array $raw Raw POST data. * @return array */ public static function normalize_input($raw) { foreach (array('cond_include', 'cond_exclude', 'ex_terms') as $key) { if (empty($raw[$key]) || ! is_array($raw[$key])) { $raw[$key] = array(); continue; } $rules = array(); foreach ($raw[$key] as $taxonomy => $terms) { $terms = srpp_int_list($terms); if ($terms) { $rules[] = array('taxonomy' => $taxonomy, 'terms' => $terms); } } $raw[$key] = $rules; } return $raw; } /* --------------------------------------------------------------------- * Field helpers * ------------------------------------------------------------------ */ /** * Config value. * * @param string $key Key. * @return mixed */ protected function v($key) { return isset($this->c[$key]) ? $this->c[$key] : ''; } /** * Field name attribute. * * @param string $key Key. * @param string $sub Sub key. * @return string */ protected function n($key, $sub = '') { return 'srpp[' . $key . ']' . ('' !== $sub ? '[' . $sub . ']' : ''); } /** * Open a step panel. * * @param string $step Step number/label. * @param string $title Title. * @param string $intro Intro copy. * @param string $id DOM id. */ protected function open_step($step, $title, $intro = '', $id = '', $open = false) { printf( '
%s

%s

%s
', $id ? 'id="' . esc_attr($id) . '"' : '', $open ? 'open' : '', esc_html($step), esc_html($title), $intro ? '

' . esc_html($intro) . '

' : '' ); } /** * Close a step panel. */ protected function close_step() { echo '
'; } /** * A labelled row. * * @param string $label Label. * @param string $help Help text. * @param string $class Extra class. */ protected function open_row($label, $help = '', $class = '') { printf( '
%s
', esc_attr($class), esc_html($label), $help ? '' . esc_html($help) . '' : '' ); } /** * Close row. */ protected function close_row() { echo '
'; } /** * Checkbox pill group. * * @param string $key Config key. * @param array $choices value => label. */ protected function checkbox_group($key, $choices) { $selected = (array) $this->v($key); echo '
'; foreach ($choices as $value => $label) { printf( '', esc_attr($this->n($key, '') . '[]'), esc_attr($value), checked(in_array((string) $value, array_map('strval', $selected), true), true, false), esc_html($label) ); } echo '
'; } /** * Radio card group. * * @param string $key Config key. * @param array $choices value => [label, desc]. */ protected function radio_cards($key, $choices) { $current = (string) $this->v($key); echo '
'; foreach ($choices as $value => $data) { $label = is_array($data) ? $data[0] : $data; $desc = is_array($data) && isset($data[1]) ? $data[1] : ''; printf( '', $current === (string) $value ? 'is-selected' : '', esc_attr($this->n($key)), esc_attr($value), checked($current, (string) $value, false), esc_html($label), $desc ? '' . esc_html($desc) . '' : '' ); } echo '
'; } /** * Select field. * * @param string $key Config key. * @param array $choices value => label. * @param array $atts Extra attributes. */ protected function select($key, $choices, $atts = array()) { $current = (string) $this->v($key); $extra = ''; foreach ($atts as $attr => $value) { $extra .= sprintf(' %s="%s"', esc_attr($attr), esc_attr($value)); } printf(''; } /** * Number input. * * @param string $key Config key. * @param int $min Min. * @param int $max Max. * @param string $unit Suffix. */ protected function number($key, $min = 0, $max = 100, $unit = '') { printf( '%s', esc_attr($this->n($key)), esc_attr($this->v($key)), (int) $min, (int) $max, $unit ? '' . esc_html($unit) . '' : '' ); } /** * Text input. * * @param string $key Config key. * @param string $placeholder Placeholder. */ protected function text($key, $placeholder = '') { printf( '', esc_attr($this->n($key)), esc_attr($this->v($key)), esc_attr($placeholder) ); } /** * Toggle switch. * * @param string $key Config key. * @param string $label Label. */ protected function toggle($key, $label) { printf( '', esc_attr($this->n($key)), checked((int) $this->v($key), 1, false), esc_html($label) ); } /** * Post picker (search + chips). * * @param string $key Config key. * @param string $types Comma separated post types. */ protected function post_picker($key, $types = '') { $ids = srpp_int_list($this->v($key)); printf( '
', esc_attr($this->n($key) . '[]'), esc_attr($types) ); foreach ($ids as $post_id) { $title = get_the_title($post_id); printf( '%3$s', absint($post_id), esc_attr($this->n($key) . '[]'), esc_html($title ? $title : '#' . $post_id) ); } printf( '
', esc_attr__('Search posts, pages or products…', 'advanced-css3-related-posts-widget') ); } /** * Taxonomy/term rule picker. * * @param string $key Config key. * @param array $post_types Limit taxonomies to these post types. */ protected function term_rules($key, $post_types = array()) { $rules = (array) $this->v($key); $current = array(); foreach ($rules as $rule) { if (! empty($rule['taxonomy'])) { $current[$rule['taxonomy']] = array_map('intval', (array) $rule['terms']); } } $taxonomies = srpp_taxonomies($post_types); echo '
'; foreach ($taxonomies as $taxonomy => $label) { $terms = get_terms( array( 'taxonomy' => $taxonomy, 'hide_empty' => false, 'number' => 300, ) ); if (is_wp_error($terms) || empty($terms)) { continue; } printf( '
%s
'; } echo '
'; } /* --------------------------------------------------------------------- * Steps * ------------------------------------------------------------------ */ /** * Name + status bar. */ protected function title_bar() { ?>
open_step('2', __('What should be shown?', 'advanced-css3-related-posts-widget'), __('Pick the content to recommend and how relatedness is decided.', 'advanced-css3-related-posts-widget'), 'srpp-step-content'); $this->open_row(__('Content type', 'advanced-css3-related-posts-widget'), __('Every public post type on this site is available — including custom ones.', 'advanced-css3-related-posts-widget')); $this->checkbox_group('target_types', srpp_post_types()); $this->close_row(); $this->open_row(__('Selection mode', 'advanced-css3-related-posts-widget')); $this->radio_cards( 'selection_mode', array( 'auto' => array(__('Automatic', 'advanced-css3-related-posts-widget'), __('Let the relation engine choose.', 'advanced-css3-related-posts-widget')), 'manual' => array(__('Manual', 'advanced-css3-related-posts-widget'), __('Only the items you pick.', 'advanced-css3-related-posts-widget')), 'auto_manual' => array(__('Manual + automatic', 'advanced-css3-related-posts-widget'), __('Your picks first, the rest filled automatically.', 'advanced-css3-related-posts-widget')), ) ); $this->close_row(); $this->open_row(__('Manual picks', 'advanced-css3-related-posts-widget'), __('Shown first, in this order.', 'advanced-css3-related-posts-widget'), 'srpp-when-manual'); $this->post_picker('manual_ids'); $this->close_row(); $this->open_row(__('Related by', 'advanced-css3-related-posts-widget'), __('Shared terms in these taxonomies make two items related.', 'advanced-css3-related-posts-widget'), 'srpp-when-auto'); $this->checkbox_group('relate_by', srpp_taxonomies()); $this->close_row(); $this->open_row(__('Relation engine', 'advanced-css3-related-posts-widget'), '', 'srpp-when-auto'); $this->radio_cards( 'engine', array( 'basic' => array(__('Basic', 'advanced-css3-related-posts-widget'), __('Any shared term counts the same.', 'advanced-css3-related-posts-widget')), 'smart' => array(__('Smart (weighted)', 'advanced-css3-related-posts-widget'), __('Score candidates, then rank by score.', 'advanced-css3-related-posts-widget')), ) ); $this->close_row(); $this->open_row(__('Weights', 'advanced-css3-related-posts-widget'), __('Two posts sharing a category and three tags outrank one sharing only a category.', 'advanced-css3-related-posts-widget'), 'srpp-when-smart'); echo '
'; $labels = array( 'primary_term' => __('Primary category', 'advanced-css3-related-posts-widget'), 'category' => __('Same category', 'advanced-css3-related-posts-widget'), 'post_tag' => __('Each matching tag', 'advanced-css3-related-posts-widget'), 'taxonomy' => __('Custom taxonomy', 'advanced-css3-related-posts-widget'), 'attribute' => __('Product attribute', 'advanced-css3-related-posts-widget'), 'author' => __('Same author', 'advanced-css3-related-posts-widget'), 'recency' => __('Recently published', 'advanced-css3-related-posts-widget'), ); $weights = wp_parse_args((array) $this->v('weights'), srpp_default_weights()); foreach ($labels as $key => $label) { printf( '', esc_html($label), esc_attr($this->n('weights', $key)), (int) $weights[$key] ); } echo '
'; printf( '

%s

', esc_html__('related_score = category + tags + taxonomies + attributes + author + recency', 'advanced-css3-related-posts-widget') ); $this->close_row(); $this->open_row(__('Recency window', 'advanced-css3-related-posts-widget'), __('Items published inside this window get the recency bonus.', 'advanced-css3-related-posts-widget'), 'srpp-when-smart'); $this->number('recency_days', 0, 3650, __('days', 'advanced-css3-related-posts-widget')); $this->close_row(); $this->open_row(__('Cross-content matching', 'advanced-css3-related-posts-widget'), __('Lets a blog post recommend products (and the reverse) by matching term slugs across taxonomies.', 'advanced-css3-related-posts-widget'), 'srpp-when-auto'); $this->toggle('cross_match', __('Match terms across content types', 'advanced-css3-related-posts-widget')); $this->close_row(); $this->open_row(__('If there are not enough matches', 'advanced-css3-related-posts-widget'), '', 'srpp-when-auto'); $this->select( 'fallback', array( 'recent' => __('Fill with the most recent items', 'advanced-css3-related-posts-widget'), 'random' => __('Fill with random items', 'advanced-css3-related-posts-widget'), 'popular' => __('Fill with most-commented items', 'advanced-css3-related-posts-widget'), 'none' => __('Show fewer items', 'advanced-css3-related-posts-widget'), ) ); $this->close_row(); $this->open_row(__('Order', 'advanced-css3-related-posts-widget')); $this->select( 'order', array( 'score' => __('Relevance score', 'advanced-css3-related-posts-widget'), 'date' => __('Newest first', 'advanced-css3-related-posts-widget'), 'modified' => __('Recently updated', 'advanced-css3-related-posts-widget'), 'title' => __('Title (A→Z)', 'advanced-css3-related-posts-widget'), 'rand' => __('Random', 'advanced-css3-related-posts-widget'), ) ); $this->close_row(); $this->close_step(); } /** * Step 2 — where it appears. */ protected function step_placement() { $this->open_step('3', __('Where should it appear?', 'advanced-css3-related-posts-widget'), __('Insertion is HTML-aware: a block is only ever placed between top-level elements, never inside a list, table or shortcode.', 'advanced-css3-related-posts-widget'), 'srpp-step-placement'); $this->open_row(__('Placement', 'advanced-css3-related-posts-widget')); $this->select('placement', SRPP_Placement::grouped_placements(), array('id' => 'srpp-placement')); $this->close_row(); $this->open_row(__('Position value', 'advanced-css3-related-posts-widget'), __('Paragraph number, or percentage of the article.', 'advanced-css3-related-posts-widget'), 'srpp-when-position'); $this->number('placement_value', 0, 100); $this->close_row(); $this->open_row(__('Repeat interval', 'advanced-css3-related-posts-widget'), __('Insert another block every X paragraphs or words.', 'advanced-css3-related-posts-widget'), 'srpp-when-repeat'); $this->number('repeat_every', 0, 50); $this->close_row(); $this->open_row(__('Maximum blocks', 'advanced-css3-related-posts-widget'), '', 'srpp-when-repeat'); $this->number('repeat_max', 1, 10); $this->close_row(); $this->open_row(__('Run on these post types', 'advanced-css3-related-posts-widget'), __('Where the display is generated from.', 'advanced-css3-related-posts-widget')); $this->checkbox_group('cond_types', srpp_post_types()); $this->close_row(); $this->open_row(__('Only when the post has', 'advanced-css3-related-posts-widget'), __('Leave empty to run everywhere.', 'advanced-css3-related-posts-widget')); $this->term_rules('cond_include'); $this->close_row(); $this->open_row(__('Rule logic', 'advanced-css3-related-posts-widget')); $this->radio_cards( 'cond_logic', array( 'and' => array(__('AND', 'advanced-css3-related-posts-widget'), __('Every rule must match.', 'advanced-css3-related-posts-widget')), 'or' => array(__('OR', 'advanced-css3-related-posts-widget'), __('Any rule may match.', 'advanced-css3-related-posts-widget')), ) ); $this->close_row(); $this->open_row(__('Never when the post has', 'advanced-css3-related-posts-widget')); $this->term_rules('cond_exclude'); $this->close_row(); $this->open_row(__('Only on these items', 'advanced-css3-related-posts-widget')); $this->post_picker('cond_ids_include'); $this->close_row(); $this->open_row(__('Never on these items', 'advanced-css3-related-posts-widget')); $this->post_picker('cond_ids_exclude'); $this->close_row(); if (srpp_is_woo_active()) { $this->open_row(__('Product price range', 'advanced-css3-related-posts-widget'), __('Only run on products inside this range.', 'advanced-css3-related-posts-widget')); printf( ' ', esc_attr($this->n('cond_price_min')), esc_attr($this->v('cond_price_min')), esc_attr__('Min', 'advanced-css3-related-posts-widget'), esc_attr($this->n('cond_price_max')), esc_attr($this->v('cond_price_max')), esc_attr__('Max', 'advanced-css3-related-posts-widget') ); $this->close_row(); } $this->open_row(__('Visitors', 'advanced-css3-related-posts-widget')); $this->select( 'cond_users', array( 'all' => __('Everyone', 'advanced-css3-related-posts-widget'), 'logged_in' => __('Logged-in visitors only', 'advanced-css3-related-posts-widget'), 'logged_out' => __('Logged-out visitors only', 'advanced-css3-related-posts-widget'), ) ); $this->close_row(); $this->open_row(__('Priority', 'advanced-css3-related-posts-widget'), __('Lower numbers are placed first when several displays run on one article.', 'advanced-css3-related-posts-widget')); $this->number('priority', 1, 999); $this->close_row(); $this->close_step(); } /** * Step 3 — layout. */ protected function step_layout() { $this->open_step('4', __('How many, and how do they flow?', 'advanced-css3-related-posts-widget'), __('The number of items is deliberately separate from the number of columns.', 'advanced-css3-related-posts-widget'), 'srpp-step-layout'); $this->open_row(__('Layout', 'advanced-css3-related-posts-widget')); $choices = array(); foreach (srpp_layouts() as $key => $label) { $choices[$key] = $label; } $this->radio_cards('layout', $choices); $this->close_row(); $this->open_row(__('Total items', 'advanced-css3-related-posts-widget')); $this->number('items', 1, 60); $this->close_row(); $this->open_row(__('Columns', 'advanced-css3-related-posts-widget'), __('Responsive control is a core setting, not an advanced one.', 'advanced-css3-related-posts-widget')); echo '
'; foreach ( array( 'cols_d' => __('Desktop', 'advanced-css3-related-posts-widget'), 'cols_t' => __('Tablet', 'advanced-css3-related-posts-widget'), 'cols_m' => __('Mobile', 'advanced-css3-related-posts-widget'), ) as $key => $label ) { printf( '', esc_html($label), esc_attr($this->n($key)), (int) $this->v($key) ); } echo '
'; $this->close_row(); $this->open_row(__('Gap', 'advanced-css3-related-posts-widget')); $this->number('gap', 0, 80, 'px'); $this->close_row(); $this->close_step(); } /** * Step 4 — design. */ protected function step_design() { $this->open_step('1', __('How should it look?', 'advanced-css3-related-posts-widget'), __('Choose a template, then fine-tune only if you want to.', 'advanced-css3-related-posts-widget'), 'srpp-step-design', true); $this->open_row(__('Template', 'advanced-css3-related-posts-widget')); echo '
'; $current = (string) $this->v('template'); foreach (SRPP_Templates::all() as $id => $template) { printf( '', $current === $id ? 'is-selected' : '', esc_attr($template['layout']), esc_attr($this->n('template')), esc_attr($id), checked($current, $id, false), esc_attr($id), esc_html($template['name']), esc_html($template['desc']) ); } echo '
'; $this->close_row(); $this->open_row(__('Heading', 'advanced-css3-related-posts-widget'), __('Leave empty for no heading.', 'advanced-css3-related-posts-widget')); $this->text('heading', __('You may also like', 'advanced-css3-related-posts-widget')); $this->close_row(); $this->open_row(__('Heading tag & alignment', 'advanced-css3-related-posts-widget')); $this->select('heading_tag', array('h2' => 'H2', 'h3' => 'H3', 'h4' => 'H4', 'h5' => 'H5', 'div' => 'div', 'span' => 'span')); $this->select('heading_align', array('left' => __('Left', 'advanced-css3-related-posts-widget'), 'center' => __('Center', 'advanced-css3-related-posts-widget'), 'right' => __('Right', 'advanced-css3-related-posts-widget'))); $this->close_row(); $this->open_row(__('Show', 'advanced-css3-related-posts-widget'), __('Fields a template can render. Unsupported fields are ignored.', 'advanced-css3-related-posts-widget')); $this->checkbox_group('fields', srpp_fields()); $this->close_row(); $this->open_row(__('Excerpt length', 'advanced-css3-related-posts-widget'), __('0 hides the excerpt entirely.', 'advanced-css3-related-posts-widget')); $this->number('excerpt_length', 0, 80, __('words', 'advanced-css3-related-posts-widget')); $this->close_row(); $this->open_row(__('Image ratio', 'advanced-css3-related-posts-widget')); $this->select('image_ratio', srpp_ratios()); $this->close_row(); $this->open_row(__('Links', 'advanced-css3-related-posts-widget')); $this->select('link_target', array('_self' => __('Open in same tab', 'advanced-css3-related-posts-widget'), '_blank' => __('Open in new tab', 'advanced-css3-related-posts-widget'))); $this->close_row(); $this->open_row(__('Button text', 'advanced-css3-related-posts-widget')); $this->text('button_text', __('Read more', 'advanced-css3-related-posts-widget')); $this->close_row(); echo '
' . esc_html__('Customize template', 'advanced-css3-related-posts-widget') . '
'; $style = wp_parse_args((array) $this->v('style'), srpp_default_style()); $colors = array( 'accent' => __('Accent', 'advanced-css3-related-posts-widget'), 'card_bg' => __('Card background', 'advanced-css3-related-posts-widget'), 'title_color' => __('Title', 'advanced-css3-related-posts-widget'), 'text_color' => __('Text', 'advanced-css3-related-posts-widget'), 'meta_color' => __('Meta', 'advanced-css3-related-posts-widget'), 'border' => __('Border', 'advanced-css3-related-posts-widget'), ); echo '
'; foreach ($colors as $key => $label) { printf( '', esc_html($label), esc_attr($this->n('style', $key)), esc_attr($style[$key]), esc_attr($style[$key]) ); } echo '
'; $sliders = array( 'radius' => array(__('Corner radius', 'advanced-css3-related-posts-widget'), 0, 40), 'img_radius' => array(__('Image radius', 'advanced-css3-related-posts-widget'), 0, 40), 'padding' => array(__('Card padding', 'advanced-css3-related-posts-widget'), 0, 48), 'title_size' => array(__('Title size', 'advanced-css3-related-posts-widget'), 10, 40), 'text_size' => array(__('Text size', 'advanced-css3-related-posts-widget'), 10, 24), ); foreach ($sliders as $key => $data) { printf( '', esc_html($data[0]), (int) $data[1], (int) $data[2], esc_attr($this->n('style', $key)), (int) $style[$key], (int) $style[$key] ); } echo '
'; $this->open_row(__('Shadow', 'advanced-css3-related-posts-widget')); printf(''; $this->close_row(); echo '
'; $this->close_step(); } /** * Advanced panel — exclusions and performance. */ protected function step_advanced() { printf( '
5

%s

', esc_html__('Exclusions, duplicates & performance', 'advanced-css3-related-posts-widget') ); $this->open_row(__('Never recommend', 'advanced-css3-related-posts-widget')); echo '
'; $this->toggle('ex_current', __('The current post or product', 'advanced-css3-related-posts-widget')); $this->toggle('ex_password', __('Password protected content', 'advanced-css3-related-posts-widget')); $this->toggle('ex_private', __('Private content', 'advanced-css3-related-posts-widget')); $this->toggle('ex_noindex', __('Content marked noindex (Yoast / Rank Math)', 'advanced-css3-related-posts-widget')); if (srpp_is_woo_active()) { $this->toggle('ex_outofstock', __('Out-of-stock products', 'advanced-css3-related-posts-widget')); } $this->toggle('no_duplicates', __('The same item twice on one page', 'advanced-css3-related-posts-widget')); echo '
'; $this->close_row(); $this->open_row(__('Excluded terms', 'advanced-css3-related-posts-widget')); $this->term_rules('ex_terms'); $this->close_row(); $this->open_row(__('Excluded items', 'advanced-css3-related-posts-widget')); $this->post_picker('ex_ids'); $this->close_row(); $this->open_row(__('Excluded authors', 'advanced-css3-related-posts-widget')); $users = get_users(array('fields' => array('ID', 'display_name'), 'number' => 200)); $chosen = srpp_int_list($this->v('ex_authors')); printf(''; $this->close_row(); $this->open_row(__('Cache lifetime', 'advanced-css3-related-posts-widget'), __('Resolved recommendation IDs are stored per post and regenerated when the post, its terms or this display change.', 'advanced-css3-related-posts-widget')); $this->select( 'cache_ttl', array( 0 => __('Disabled (query on every request)', 'advanced-css3-related-posts-widget'), HOUR_IN_SECONDS => __('1 hour', 'advanced-css3-related-posts-widget'), DAY_IN_SECONDS => __('1 day', 'advanced-css3-related-posts-widget'), WEEK_IN_SECONDS => __('1 week', 'advanced-css3-related-posts-widget'), MONTH_IN_SECONDS => __('1 month', 'advanced-css3-related-posts-widget'), ) ); $this->close_row(); echo '
'; // srpp-step-advanced } /** * Live preview column. */ protected function preview_pane() { ?>