null,
'table_style' => 'table',
'blocks_style' => 'blocks',
'options_style' => 'options',
);
/**
* Form fields (option ids without prefix) the preview reacts to, mapped to renderer settings.
* "bool" fields arrive as yes/no.
*/
const FIELDS = array(
'display_type' => array( 'display_type', 'text' ),
'pricing_table_style' => array( 'table_style', 'text' ),
'pricing_blocks_style' => array( 'blocks_style', 'text' ),
'pricing_options_style' => array( 'options_style', 'text' ),
'pricing_dropdown_style' => array( 'dropdown_style', 'text' ),
'pricing_plain_text_style' => array( 'plain_text_style', 'text' ),
'quantity_type' => array( 'quantity_type', 'text' ),
'tiers_order' => array( 'tiers_order', 'text' ),
'discount_format' => array( 'discount_format', 'text' ),
'table_title' => array( 'title', 'text' ),
'selected_quantity_color' => array( 'active_tier_color', 'color' ),
'compact_layout' => array( 'compact_layout', 'yesno' ),
'layout_spacing' => array( 'layout_spacing', 'text' ),
'cell_padding' => array( 'cell_padding', 'text' ),
'discount_badge_color' => array( 'discount_badge_color', 'optional-color' ),
'table_active_border' => array( 'active_tier_border', 'yesno' ),
'tiers_limit' => array( 'tiers_limit', 'text' ),
'head_quantity_text' => array( 'quantity_column_title', 'text' ),
'head_price_text' => array( 'price_column_title', 'text' ),
'head_discount_text' => array( 'discount_column_title', 'text' ),
'show_discount_column' => array( 'show_discount_column', 'bool' ),
'clickable_table_rows' => array( 'clickable_rows', 'bool' ),
'options_show_total' => array( 'options_show_total', 'bool' ),
'options_show_original_product_price' => array( 'options_show_original_product_price', 'bool' ),
'options_show_default_option' => array( 'options_show_default_option', 'bool' ),
'options_default_option_text' => array( 'options_default_option_text', 'html' ),
'options_option_text' => array( 'options_option_text', 'html' ),
'tooltip_border' => array( 'tooltip_border', 'bool' ),
'plain_text_template' => array( 'plain_text_option_text', 'html' ),
'plain_text_show_first_tier' => array( 'plain_text_show_default_option', 'bool' ),
'plain_text_first_tier_template' => array( 'plain_text_default_option_text', 'html' ),
);
const CONTEXTS = array( 'product-page', 'shop-loop' );
/**
* Regular price of the preview product (store currency). The skeleton shows the same amount.
*/
const PRICE = 45;
protected ?WC_Product $product = null;
protected bool $productResolved = false;
public function __construct() {
add_action( 'wp_ajax_' . self::AJAX_ACTION, array( $this, 'ajax' ) );
}
/**
* Field names the JS collects from the form.
*/
public static function getFieldNames(): array {
return array_map( function ( $id ) {
return Settings::SETTINGS_PREFIX . $id;
}, array_keys( self::FIELDS ) );
}
public function getFrontendStylesheetURL(): string {
return $this->getContainer()->getFileManager()->locateCSSAsset( 'frontend/main.css' );
}
/**
* Stylesheets for the preview document: the active theme's style.css (parent first when a child
* theme is active), then the plugin's frontend stylesheet.
*/
/**
* The theme's global styles (theme.json: colours, fonts, element styles) and its font faces, as WordPress
* prints them inline on the storefront. Empty for a theme without them.
*/
public function getInlineStyles(): string {
$css = '';
if ( function_exists( 'wp_print_font_faces' ) ) {
ob_start();
wp_print_font_faces();
$css .= preg_replace( '#?style[^>]*>#i', '', (string) ob_get_clean() );
}
if ( function_exists( 'wp_get_global_stylesheet' ) ) {
$css .= "\n" . wp_get_global_stylesheet();
}
// the CSS is inlined into the preview document
return str_replace( 'exists() ) {
if ( $theme->parent() ) {
$urls[] = add_query_arg( 'ver', $theme->parent()->get( 'Version' ), get_template_directory_uri() . '/style.css' );
}
$urls[] = add_query_arg( 'ver', $theme->get( 'Version' ), get_stylesheet_uri() );
}
// WooCommerce's storefront stylesheets, in the storefront's order: the block theme sheet comes first,
// the plugin's sheet next and WooCommerce's general sheets last (a theme that ships its own WooCommerce
// styles drops them through WooCommerce's filter)
$woocommerce = function_exists( 'WC' ) && class_exists( 'WC_Frontend_Scripts' );
$version = defined( 'WC_VERSION' ) ? WC_VERSION : '';
if ( $woocommerce && function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) {
$urls[] = add_query_arg( 'ver', $version, WC()->plugin_url() . '/assets/css/woocommerce-blocktheme.css' );
}
// the plugin's sheet is versioned with its modification time as well: the plugin version alone lets a
// browser keep a cached copy while the templates move on (development, git deployments)
$stylesheet = $this->getFrontendStylesheetURL();
$file = dirname( __DIR__, 3 ) . '/assets/frontend/' . basename( (string) wp_parse_url( $stylesheet, PHP_URL_PATH ) );
$urls[] = add_query_arg( 'ver', TierPricingTablePlugin::VERSION . ( file_exists( $file ) ? '.' . filemtime( $file ) : '' ), $stylesheet );
if ( $woocommerce ) {
foreach ( \WC_Frontend_Scripts::get_styles() as $handle => $style ) {
// the small screen sheet is bound to a media query the preview frame does not honour
if ( 'woocommerce-smallscreen' === $handle || empty( $style['src'] ) ) {
continue;
}
$urls[] = add_query_arg( 'ver', $style['version'] ?? $version, $style['src'] );
}
}
return array_values( array_unique( $urls ) );
}
/**
* Full document for the preview iframe. The wrapper mirrors a product page (body classes, div.product
* and .summary) so theme rules written for WooCommerce product pages apply.
*/
public function getDocument( string $fragment ): string {
$links = '';
foreach ( $this->getStylesheetURLs() as $url ) {
$links .= '';
}
return '
' . $links
. ''
. '
'
. $fragment . '
';
}
/**
* Values currently stored, in the form the AJAX request uses (id without prefix => value).
*/
public function getStoredValues(): array {
$values = array();
foreach ( self::FIELDS as $id => $map ) {
$values[ $id ] = $this->getContainer()->getSettings()->get( $id );
}
return array_merge( $values, LayoutConfigurator::getUnitValues() );
}
/**
* Render the preview fragment for a panel.
*
* @param string $kind One of self::KINDS.
* @param array $values Form values (id without prefix => value).
*/
public function render( string $kind, array $values, string $context = 'product-page' ): string {
if ( ! array_key_exists( $kind, self::KINDS ) ) {
return '';
}
$settings = 'shop-loop' === $context ? $this->mapCatalogSettings( $values ) : $this->mapSettings( $values );
if ( self::KINDS[ $kind ] ) {
$settings['display_type'] = self::KINDS[ $kind ];
}
if ( 'tooltip' === $settings['display_type'] ) {
$note = 'shop-loop' === $context
? esc_html__( 'The tooltip layout is not available on shop and category pages. Choose "Custom" and pick another layout.', 'tier-pricing-table' )
: esc_html__( 'The tooltip layout shows the pricing table next to the product price when the customer hovers over an icon. Open a product page to see it in action.', 'tier-pricing-table' );
return '