sanitize_text_field( wp_unslash( $_POST['set_name'] ?? '' ) ),
'kind' => sanitize_text_field( wp_unslash( $_POST['set_kind'] ?? 'choices' ) ),
'shape' => sanitize_text_field( wp_unslash( $_POST['set_shape'] ?? '' ) ),
// A tick, so an absent field means unticked. The sanitiser's "absent
// means yes" default is for sets saved BEFORE the switch existed, and
// the two must not be confused: a merchant who unticks it posts
// nothing, and that has to read as no.
'photo_swaps' => ! empty( $_POST['set_photo_swaps'] ),
'pin_chips' => ! empty( $_POST['set_pin_chips'] ),
'show_stock' => addon_boss_option_stock_enabled() ? ! empty( $_POST['set_show_stock'] ) : null,
'multi' => '1' === sanitize_text_field( wp_unslash( $_POST['set_multi'] ?? '' ) ),
'min' => absint( $_POST['set_min'] ?? 0 ),
'max' => absint( $_POST['set_max'] ?? 0 ),
'options' => $rows,
);
// Each registered kind reads its own panel's fields off the form, exactly
// as the real save does.
foreach ( addon_boss_option_set_kinds() as $bb_spec ) {
if ( is_callable( $bb_spec['collect'] ?? null ) ) {
$bb_raw = array_merge( $bb_raw, (array) call_user_func( $bb_spec['collect'] ) );
}
}
// Modules collect their own panel's fields off the same form, so the save
// and the preview read one assembly and can never drift apart.
return (array) apply_filters( 'addon_boss_option_set_from_form', $bb_raw );
}
// -----------------------------------------------------------------
// The panel, printed under the set editor form.
// -----------------------------------------------------------------
function addon_boss_set_preview_panel(): void {
?>
__( 'Sorry, you are not allowed to do that.', 'addon-boss' ) ), 403 );
}
$mode = sanitize_key( wp_unslash( $_POST['bb_epv_mode'] ?? 'draft' ) );
$sid = sanitize_text_field( wp_unslash( $_POST['set_id'] ?? '' ) );
if ( 'saved' === $mode ) {
if ( '' === $sid || ! addon_boss_option_set( $sid ) ) {
wp_send_json_error( array( 'message' => __( 'Save the option set once and its saved state previews here.', 'addon-boss' ) ) );
}
$render_id = $sid;
} else {
// The serialized form IS the save contract: the same assembly and
// sanitiser the real save runs, overlaid on the store for one read.
$draft = addon_boss_sanitize_option_set( addon_boss_option_set_from_form() );
$draft['id'] = '' !== $sid ? $sid : 'bb-preview-draft';
$render_id = $draft['id'];
$overlay = addon_boss_option_sets();
$overlay[ $render_id ] = $draft;
add_filter(
'pre_option_' . ADDON_BOSS_OPTION_SETS_STORE,
static function () use ( $overlay ) {
return $overlay;
}
);
}
// A throwaway in-memory product carries the set; nothing has an id and
// nothing is saved. Purchasability is forced for the render only, because
// an unsaved product can never pass the real check.
$bb_product = new WC_Product_Simple();
$bb_product->set_name( __( 'Your product', 'addon-boss' ) );
$bb_product->set_regular_price( '25' );
$bb_product->set_price( '25' );
$bb_product->update_meta_data(
ADDON_BOSS_PRODUCT_OPTIONS_META,
// The full attachment row shape the resolver expects: no per-product
// overrides, nothing hidden — the set previews exactly as stored.
array(
array(
'set' => $render_id,
'deltas' => array(),
'images' => array(),
'hide' => array(),
),
)
);
add_filter( 'woocommerce_is_purchasable', '__return_true' );
$prev_global = $GLOBALS['product'] ?? null;
$GLOBALS['product'] = $bb_product;
ob_start();
addon_boss_render_standalone_options();
$component = (string) ob_get_clean();
$GLOBALS['product'] = $prev_global;
remove_filter( 'woocommerce_is_purchasable', '__return_true' );
if ( '' === trim( $component ) ) {
wp_send_json_error( array( 'message' => __( 'Add an option to see the preview.', 'addon-boss' ) ) );
}
wp_send_json_success( array( 'html' => addon_boss_set_preview_document( $component ) ) );
}
/**
* A self-contained iframe document: the storefront stylesheet inlined (a
* linked sheet cannot be enqueued into srcdoc), live controls muted.
*/
function addon_boss_set_preview_document( string $component ): string {
// Every image loads NOW: lazy-loading inside a short iframe defers
// anything below its fold forever, which reads as broken photos.
$component = str_replace( ' loading="lazy"', ' loading="eager"', $component );
$css = (string) @file_get_contents( ADDON_BOSS_PATH . 'css/options.css' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- a missing stylesheet degrades to an unstyled preview, never an error page.
$doc = '';
$doc .= '';
$doc .= '';
$doc .= '';
$doc .= '';
$doc .= $component;
$doc .= '
' . esc_html__( 'This is a preview of how the options look. Prices and Add to cart run on the product pages this set is attached to.', 'addon-boss' ) . '
';
$doc .= '
';
return $doc;
}