$attrs Extra attributes, name => value, escaped here: a set
* type's panel marker, an id. True prints the bare name
* (hidden); false or null leaves the attribute out.
*/
function addon_boss_editor_sec_open( string $key, string $title, string $sub = '', array $attrs = array() ): void {
echo ' $bb_value ) {
if ( null === $bb_value || false === $bb_value ) {
continue;
}
echo ' ' . esc_attr( sanitize_key( (string) $bb_name ) ) . ( true === $bb_value ? '' : '="' . esc_attr( (string) $bb_value ) . '"' );
}
echo '>';
echo '
' . esc_html( $title ) . '
';
if ( '' !== $sub ) {
echo '
' . esc_html( $sub ) . '
';
}
echo '
';
}
/** Close the section opened last. */
function addon_boss_editor_sec_close(): void {
echo '
';
}
/**
* A figure as plain text in the shop's own money format ("£24.00"), through
* the money seam but never converted: for the admin's own numbers (what a
* merchant typed, a template's price), which no display basis applies to.
* Converting one would apply the basis a second time.
*
* @param float $amount The figure.
* @param bool $sign Lead with + (a choice's charge).
*/
function addon_boss_editor_money_text( float $amount, bool $sign = false ): string {
return html_entity_decode(
addon_boss_money(
$amount,
null,
array(
'plain' => true,
'sign' => $sign,
'convert' => false,
)
),
ENT_QUOTES,
'UTF-8'
);
}
/**
* The shop's word for its sales tax, for the middle of a sentence. WooCommerce
* names it by the shop's country as a heading ("VAT", "Tax"); mid-sentence a
* plain "tax" reads right, while VAT, IVA or TVA keep their own capitals.
*/
function addon_boss_editor_tax_word(): string {
$tax = function_exists( 'WC' ) && WC()->countries ? (string) WC()->countries->tax_or_vat() : '';
if ( '' === $tax || __( 'Tax', 'woocommerce' ) === $tax ) { // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- comparing with WooCommerce's own word, never printed.
$tax = __( 'tax', 'addon-boss' );
}
return $tax;
}
/**
* The one line about tax, said where prices are typed, from the shop's own
* settings: whether the prices a merchant types include the tax. Nothing when
* the shop does not charge tax. The same on every set, because it is a fact
* about the shop, not about the set.
*/
function addon_boss_editor_vat_line(): string {
if ( ! function_exists( 'wc_tax_enabled' ) || ! wc_tax_enabled() ) {
return '';
}
$tax = addon_boss_editor_tax_word();
return wc_prices_include_tax()
/* translators: %s: the shop's word for sales tax, "VAT" or "tax" */
? sprintf( __( 'Prices you type include %s, as set in WooCommerce.', 'addon-boss' ), $tax )
/* translators: %s: the shop's word for sales tax, "VAT" or "tax" */
: sprintf( __( 'Prices you type exclude %s, as set in WooCommerce.', 'addon-boss' ), $tax );
}
/**
* The one line on the customer's page when the shop shows prices on a
* different basis than they are typed: "Your shop shows prices without VAT,
* so your £24.00 shows as £20.00." Worked from the product's own price, so
* the numbers are the shop's. Empty when the two bases agree, when the shop
* charges no tax, or when there is no price to show it with.
*
* @param WC_Product $product The product on the customer's page.
*/
function addon_boss_editor_vat_note( WC_Product $product ): string {
if ( ! function_exists( 'wc_tax_enabled' ) || ! wc_tax_enabled() || ! $product->is_taxable() ) {
return '';
}
$entered_inc = wc_prices_include_tax();
$shown_inc = 'incl' === get_option( 'woocommerce_tax_display_shop' );
if ( $entered_inc === $shown_inc ) {
return '';
}
$tax = addon_boss_editor_tax_word();
// The preview's stand-in product (no id) carries a price the editor
// worked out, not one the shop typed: "your £12.10 shows as £10.08"
// named a figure the owner never typed (walk, 30 Oct 2026). The rule
// alone, then.
if ( ! $product->get_id() ) {
return $shown_inc
/* translators: %s: the shop's word for sales tax, "VAT" or "tax" */
? sprintf( __( 'Your shop shows prices with %s added, so the prices here include it.', 'addon-boss' ), $tax )
/* translators: %s: the shop's word for sales tax, "VAT" or "tax" */
: sprintf( __( 'Your shop shows prices without %s, so the prices here leave it out.', 'addon-boss' ), $tax );
}
$typed = (float) $product->get_price( 'edit' );
if ( $typed <= 0 ) {
return '';
}
$shown = (float) wc_get_price_to_display( $product, array( 'price' => $typed ) );
if ( abs( $shown - $typed ) < 0.005 ) {
return '';
}
// The first figure is the one the merchant typed and the second is
// already WooCommerce's display of it: both print exactly as they are.
$from = addon_boss_editor_money_text( $typed );
$to = addon_boss_editor_money_text( $shown );
return $shown_inc
/* translators: 1: the shop's word for sales tax, 2: a price as typed, 3: the same price as shoppers see it */
? sprintf( __( 'Your shop shows prices with %1$s added, so your %2$s shows as %3$s.', 'addon-boss' ), $tax, $from, $to )
/* translators: 1: the shop's word for sales tax, 2: a price as typed, 3: the same price as shoppers see it */
: sprintf( __( 'Your shop shows prices without %1$s, so your %2$s shows as %3$s.', 'addon-boss' ), $tax, $from, $to );
}
/** The tax line, printed (or nothing). */
function addon_boss_editor_print_vat_line(): void {
$line = addon_boss_editor_vat_line();
if ( '' !== $line ) {
echo '
' . esc_html( $line ) . '
';
}
}
/**
* The Save bar: the form's one submit, kept in reach while scrolling, and a
* line that turns into "You have unsaved changes" once anything differs from
* what the page loaded with. A new set is unsaved from its first paint, so it
* says so from the start and keeps saying it.
*
* @param string $state 'saved' for a stored set, 'new' for an empty new one,
* 'template' for a new one a template filled.
*/
function addon_boss_editor_save_bar( string $state = 'saved' ): void {
$new = 'saved' !== $state;
if ( ! $new ) {
$clean = __( 'Saving updates every product that offers this set.', 'addon-boss' );
$dirty = __( 'You have unsaved changes.', 'addon-boss' );
} elseif ( 'template' === $state ) {
$clean = __( 'Started from a template. Nothing is saved until you press Save set.', 'addon-boss' );
$dirty = $clean;
} else {
$clean = __( 'Nothing is saved until you press Save set.', 'addon-boss' );
$dirty = $clean;
}
echo '
';
}
/**
* One switch with its name, and one line under it saying what it does. A
* switch whose meaning flips with it says what each side does: the line
* reads $hint while on and $hint_off while off.
*
* @param string $inputs The field markup: the checkbox, after any hidden
* field that posts the "off" value. Built by the
* caller from fixed strings and checked().
* @param string $title The switch's name.
* @param string $hint The line under it (while on, when $hint_off is given).
* @param string $hint_off The line while off; empty when one line serves both.
* @param bool $on Whether it is on now, for the first paint.
*/
function addon_boss_editor_toggle( string $inputs, string $title, string $hint, string $hint_off = '', bool $on = true ): void {
echo '
';
}
/**
* The line under One / Several: what a customer meets. A several-of set
* quotes the very sentence the product page prints over its choices.
*
* Its own bounds first, so "At least 0" reads as a set customers may leave
* empty, then what a product that requires it prints instead, where that
* differs. Required or optional is each product's to say, and the line read
* "Choose at least 1." under "At least 0" with no word of why (print shop
* tester, 18 Oct 2026).
*
* @param bool $multi Whether customers may pick several.
* @param int $min The fewest they must pick (0 or 1: no lower bound).
* @param int $max The most they may pick (0: no upper bound).
*/
function addon_boss_editor_pick_say( bool $multi, int $min, int $max ): string {
if ( ! $multi ) {
return __( 'Customers pick one. Tapping another replaces it.', 'addon-boss' );
}
$own = addon_boss_multi_hint( $min, $max );
$req = addon_boss_multi_hint( $min, $max, true );
/* translators: %s: the sentence customers read over the choices, e.g. Choose up to 3. */
$say = sprintf( __( 'Customers read it as “%s”', 'addon-boss' ), $own );
if ( $req !== $own ) {
/* translators: %s: the sentence customers read over the choices on a product that requires the set, e.g. Choose at least 1. */
$say .= ' ' . sprintf( __( 'Where a product requires it: “%s”', 'addon-boss' ), $req );
}
return $say;
}
/**
* The sentence a several-of set prints over its choices, stating its rule.
* One function for the product page and the editor, so the editor quotes
* exactly what customers read.
*
* A required set takes one at least, whatever its own lower bound says:
* "Choose as many as you like." stood over a set that Add to cart refuses
* empty (workwear tester, 18 Oct 2026).
*
* @param int $min The fewest a customer must pick (0 or 1: no lower bound).
* @param int $max The most a customer may pick (0: no upper bound).
* @param bool $required Whether the set must be answered.
*/
function addon_boss_multi_hint( int $min, int $max, bool $required = false ): string {
$min = $required ? max( 1, $min ) : max( 0, $min );
$max = max( 0, $max );
$floor = $min > 1 || $required;
if ( $floor && $max > 0 ) {
/* translators: 1: the smallest number of choices, 2: the largest number of choices */
return sprintf( __( 'Choose at least %1$s and up to %2$s.', 'addon-boss' ), number_format_i18n( $min ), number_format_i18n( $max ) );
}
if ( $floor ) {
/* translators: %s: the smallest number of choices */
return sprintf( __( 'Choose at least %s.', 'addon-boss' ), number_format_i18n( $min ) );
}
if ( $max > 0 ) {
/* translators: %s: the largest number of choices */
return sprintf( __( 'Choose up to %s.', 'addon-boss' ), number_format_i18n( $max ) );
}
return __( 'Choose as many as you like.', 'addon-boss' );
}
/**
* Set simple products' own prices to what their cheapest size costs, from a
* set editor's Shop page facts: shop and category pages show the product's
* price, and on a whole-price set (measured sizes, a price table) nothing else
* keeps it in step with the sizes.
*
* "Use £X" queues the price on the form and Save set applies it, with the
* set, like everything else on the page. It used to set the price the moment
* it was pressed, so a draft left unsaved left the shop showing a price for
* sizes it did not yet sell (fresh testers, 18 Oct 2026).
*
* @param array $queued Product id => price, as the form posted them.
* @return array The prices set, by product id.
*/
function addon_boss_editor_use_prices( array $queued ): array {
$done = array();
foreach ( $queued as $pid => $price ) {
$pid = absint( $pid );
$price = wc_format_decimal( sanitize_text_field( (string) $price ) );
$product = $pid ? wc_get_product( $pid ) : null;
// A product on sale shows its sale price, which a new regular price
// would not move: the shop page would still read the old figure.
if ( ! $product instanceof WC_Product || ! current_user_can( 'edit_post', $pid ) || ! addon_boss_editor_price_settable( $product ) || '' === $price || (float) $price < 0 ) {
continue;
}
$product->set_regular_price( $price );
$product->save();
$done[ $pid ] = (float) $product->get_regular_price( 'edit' );
}
return $done;
}
/**
* Can the set editors' "Use £X" set this product's price? Only a simple
* product that is not on sale: a variable product's price moves with its
* variations, and a sale price stays in front of any new regular price.
*
* @param WC_Product $product The product.
*/
function addon_boss_editor_price_settable( WC_Product $product ): bool {
return $product->is_type( 'simple' ) && ! $product->is_on_sale( 'edit' );
}
/**
* The products a set is on, as the set editors' Shop page facts need them:
* the ones the customer page offers, each with the one price its shop page
* shows (none for a product whose price moves with its variations) and
* whether "Use £X" can set that price from the editor.
*
* @param string $set_id The set ('' for one not saved yet).
* @return array
*/
function addon_boss_editor_set_products( string $set_id ): array {
$out = array();
if ( ! function_exists( 'addon_boss_set_preview_products' ) ) {
return $out;
}
foreach ( addon_boss_set_preview_products( $set_id ) as $pid => $title ) {
$product = wc_get_product( (int) $pid );
if ( ! $product instanceof WC_Product ) {
continue;
}
$simple = $product->is_type( 'simple' );
$price = $simple ? (string) $product->get_price( 'edit' ) : '';
// What one typed-basis unit of money reads as on the shop's pages
// (a million through the conversion, for the places), and the shop's
// price suffix where it is plain words: the editor's "Customers pay"
// says what the shop page says (walk, 30 Oct 2026).
$suffix = (string) get_option( 'woocommerce_price_display_suffix', '' );
$out[] = array(
'id' => (int) $pid,
'title' => (string) $title,
'price' => '' === $price ? null : (float) $price,
'settable' => addon_boss_editor_price_settable( $product ),
'tk' => (float) wc_get_price_to_display( $product, array( 'price' => 1000000.0 ) ) / 1000000,
'suffix' => false === strpos( $suffix, '{' ) ? trim( wp_strip_all_tags( $suffix ) ) : '',
);
}
return $out;
}
/**
* The frame's own stylesheet and script, on the option sets screen only.
*
* @param string $hook The admin page hook.
*/
function addon_boss_editor_frame_assets( string $hook ): void {
if ( '' === $hook || ! function_exists( 'addon_boss_option_sets_hook' ) || addon_boss_option_sets_hook() !== $hook || ! defined( 'ADDON_BOSS_URL' ) ) {
return;
}
$css = ADDON_BOSS_URL . 'css/editor.css';
$js = ADDON_BOSS_URL . 'js/editor.js';
wp_enqueue_style( 'addon-boss-editor', $css, array(), addon_boss_engine_asset_version( $css ) );
wp_enqueue_script( 'addon-boss-editor', $js, array(), addon_boss_engine_asset_version( $js ), true );
}
add_action( 'admin_enqueue_scripts', 'addon_boss_editor_frame_assets', 20 );