| 1 |
<?php namespace TierPricingTable\Addons\LayoutConfigurator; |
| 2 |
|
| 3 |
use TierPricingTable\Addons\AbstractAddon; |
| 4 |
use TierPricingTable\Core\ServiceContainer; |
| 5 |
use TierPricingTable\Core\ServiceContainerTrait; |
| 6 |
use TierPricingTable\Settings\Sections\GeneralSection\GeneralSection; |
| 7 |
use TierPricingTable\Settings\Settings; |
| 8 |
use TierPricingTable\TierPricingTablePlugin; |
| 9 |
|
| 10 |
/** |
| 11 |
* Layout configurator: one settings field that controls every product-page layout option |
| 12 |
* (layout, design style, title, position, quantity format and unit, column headers, text |
| 13 |
* templates, tooltip icon, colours and the layout toggles) with a live preview inside a |
| 14 |
* product-page skeleton. The custom table columns add-on renders its editor inside it. Rendered by a React app (js-source, built |
| 15 |
* to build/ with wp-scripts). |
| 16 |
* |
| 17 |
* Every option keeps its own id and is submitted through hidden inputs inside the field, so |
| 18 |
* WooCommerce's settings save path is unchanged. The other fields of the settings page keep |
| 19 |
* reading the options through the hidden inputs (the app dispatches change events on them). |
| 20 |
*/ |
| 21 |
class LayoutConfigurator { |
| 22 |
|
| 23 |
use ServiceContainerTrait; |
| 24 |
|
| 25 |
const FIELD_TYPE = 'tpt_layout_configurator'; |
| 26 |
|
| 27 |
const SCRIPT_HANDLE = 'tiered-pricing/settings/layout-configurator'; |
| 28 |
|
| 29 |
/** |
| 30 |
* Options managed by the configurator: id (without prefix) => default. |
| 31 |
*/ |
| 32 |
const OPTIONS = array( |
| 33 |
'display' => 'yes', |
| 34 |
'display_type' => 'table', |
| 35 |
'pricing_table_style' => 'default', |
| 36 |
'pricing_blocks_style' => 'default', |
| 37 |
'pricing_options_style' => 'default', |
| 38 |
'pricing_dropdown_style' => 'default', |
| 39 |
'pricing_plain_text_style' => 'default', |
| 40 |
'table_title' => '', |
| 41 |
'position_hook' => 'woocommerce_before_add_to_cart_button', |
| 42 |
'quantity_type' => 'range', |
| 43 |
'tiers_order' => 'asc', |
| 44 |
'compact_layout' => 'no', |
| 45 |
'layout_spacing' => '', |
| 46 |
'cell_padding' => '', |
| 47 |
'discount_badge_color' => '', |
| 48 |
'table_active_border' => 'yes', |
| 49 |
'selected_quantity_color' => '#3858e9', |
| 50 |
'show_discount_column' => 'yes', |
| 51 |
'discount_format' => 'percentage', |
| 52 |
// options / dropdown layouts |
| 53 |
'options_option_text' => '', |
| 54 |
'options_show_default_option' => 'yes', |
| 55 |
'options_default_option_text' => '', |
| 56 |
'options_show_original_product_price' => 'yes', |
| 57 |
'options_show_total' => 'yes', |
| 58 |
// tooltip layout |
| 59 |
'tooltip_color' => '#3858e9', |
| 60 |
'tooltip_size' => '15', |
| 61 |
'tooltip_border' => 'yes', |
| 62 |
// table layouts: column headers (an empty header hides the column) |
| 63 |
'head_quantity_text' => '', |
| 64 |
'head_discount_text' => '', |
| 65 |
'head_price_text' => '', |
| 66 |
// plain text layout |
| 67 |
'plain_text_template' => '', |
| 68 |
'plain_text_show_first_tier' => 'yes', |
| 69 |
'plain_text_first_tier_template' => '', |
| 70 |
// layouts with selectable tiers |
| 71 |
'clickable_table_rows' => 'yes', |
| 72 |
// product page price line |
| 73 |
'product_page_price_format' => 'custom', |
| 74 |
'update_price_on_product_page' => 'yes', |
| 75 |
'show_tiered_price_as_discount' => 'yes', |
| 76 |
'show_total_price' => 'no', |
| 77 |
'show_total_price_non_tiered' => 'no', |
| 78 |
); |
| 79 |
|
| 80 |
/** |
| 81 |
* "You Save" badge options (the add-on's), managed here while that add-on is on. |
| 82 |
*/ |
| 83 |
const YOU_SAVE_OPTIONS = array( |
| 84 |
'you_save_enabled' => 'yes', |
| 85 |
'you_save_consider_sale_price' => 'yes', |
| 86 |
'you_save_non_tiered' => 'no', |
| 87 |
'you_save_template' => '', |
| 88 |
'you_save_text_color' => '#FF0000', |
| 89 |
); |
| 90 |
|
| 91 |
const YOU_SAVE_ADDON_SLUG = 'you-save'; |
| 92 |
|
| 93 |
/** |
| 94 |
* Pricing summary block options (the add-on's), managed here while that add-on is on. |
| 95 |
*/ |
| 96 |
const SUMMARY_OPTIONS = array( |
| 97 |
'display_summary' => 'yes', |
| 98 |
'display_summary_non_tiered' => 'no', |
| 99 |
'summary_title' => '', |
| 100 |
'summary_position_hook' => 'woocommerce_after_add_to_cart_button', |
| 101 |
'summary_type' => 'table', |
| 102 |
'summary_total_label' => '', |
| 103 |
'summary_each_label' => '', |
| 104 |
); |
| 105 |
|
| 106 |
const SUMMARY_ADDON_SLUG = 'pricing-summary'; |
| 107 |
|
| 108 |
/** |
| 109 |
* Shop & category price format options (the catalog prices add-on's), managed here while it is on. |
| 110 |
*/ |
| 111 |
const CATALOG_PRICE_OPTIONS = array( |
| 112 |
'tiered_price_at_catalog' => 'yes', |
| 113 |
'tiered_price_at_catalog_for_variable' => 'no', |
| 114 |
'tiered_price_at_catalog_type' => 'lowest', |
| 115 |
'tiered_price_at_catalog_custom_template' => '', |
| 116 |
'lowest_prefix' => '', |
| 117 |
); |
| 118 |
|
| 119 |
const CATALOG_PRICE_ADDON_SLUG = 'catalog-prices'; |
| 120 |
|
| 121 |
/** |
| 122 |
* Cart page price options (the cart options add-on's), managed here while it is on. |
| 123 |
*/ |
| 124 |
const CART_OPTIONS = array( |
| 125 |
'show_discount_in_cart' => 'yes', |
| 126 |
'show_subtotal_as_discount_in_cart' => 'yes', |
| 127 |
'consider_sale_price_as_discount_in_cart' => 'no', |
| 128 |
'cart_total_savings' => 'no', |
| 129 |
'cart_total_savings_label' => '', |
| 130 |
); |
| 131 |
|
| 132 |
const CART_ADDON_SLUG = 'cart-options'; |
| 133 |
|
| 134 |
/** |
| 135 |
* Cart upsell message options (the cart upsells add-on's), managed here while it is on. |
| 136 |
*/ |
| 137 |
const UPSELL_OPTIONS = array( |
| 138 |
'cart_upsell_enabled' => 'no', |
| 139 |
'cart_upsell_template' => '', |
| 140 |
'cart_upsell_color' => '#059669', |
| 141 |
'cart_upsell_link' => 'yes', |
| 142 |
'cart_upsell_progress' => 'no', |
| 143 |
); |
| 144 |
|
| 145 |
const UPSELL_ADDON_SLUG = 'cart-upsells'; |
| 146 |
|
| 147 |
/** |
| 148 |
* Options stored as HTML templates (sanitised with wp_kses_post, like the text-template field). |
| 149 |
*/ |
| 150 |
const HTML_OPTIONS = array( 'options_option_text', 'options_default_option_text', 'plain_text_template', 'plain_text_first_tier_template', 'you_save_template', 'tiered_price_at_catalog_custom_template', 'cart_upsell_template', 'badge_template' ); |
| 151 |
|
| 152 |
/** |
| 153 |
* Options that only work with premium code; shown locked otherwise. |
| 154 |
*/ |
| 155 |
const PREMIUM_OPTIONS = array( 'options_show_total', 'clickable_table_rows' ); |
| 156 |
|
| 157 |
/** |
| 158 |
* Options stored as singular/plural pairs (quantity unit labels), keyed by the layouts they apply to. |
| 159 |
*/ |
| 160 |
const UNIT_OPTIONS = array( |
| 161 |
'table_quantity_measurement' => array( 'table', 'tooltip', 'horizontal-table' ), |
| 162 |
'blocks_quantity_measurement' => array( 'blocks' ), |
| 163 |
); |
| 164 |
|
| 165 |
/** |
| 166 |
* Shop & category page options, managed by the configurator while the catalog add-on is on: |
| 167 |
* configurator key => option id (without the settings prefix). Keys shared with the product page |
| 168 |
* mean the same thing there; the last four are catalog-only. |
| 169 |
*/ |
| 170 |
const CATALOG_OPTIONS = array( |
| 171 |
'display' => 'shop_loop_display_enabled', |
| 172 |
'display_type' => 'shop_loop_display_layout', |
| 173 |
'pricing_table_style' => 'shop_loop_display_pricing_table_style', |
| 174 |
'pricing_blocks_style' => 'shop_loop_display_pricing_blocks_style', |
| 175 |
'pricing_options_style' => 'shop_loop_display_pricing_options_style', |
| 176 |
'pricing_dropdown_style' => 'shop_loop_display_pricing_dropdown_style', |
| 177 |
'pricing_plain_text_style' => 'shop_loop_display_pricing_plain_text_style', |
| 178 |
'table_title' => 'shop_loop_display_title', |
| 179 |
'position_hook' => 'shop_loop_display_position', |
| 180 |
'quantity_type' => 'shop_loop_display_quantity_type', |
| 181 |
'tiers_order' => 'shop_loop_display_tiers_order', |
| 182 |
'layout_spacing' => 'shop_loop_display_layout_spacing', |
| 183 |
'cell_padding' => 'shop_loop_display_cell_padding', |
| 184 |
'discount_badge_color' => 'shop_loop_display_discount_badge_color', |
| 185 |
'table_active_border' => 'shop_loop_display_table_active_border', |
| 186 |
'selected_quantity_color' => 'shop_loop_display_selected_quantity_color', |
| 187 |
'show_discount_column' => 'shop_loop_display_blocks_show_discount', |
| 188 |
'discount_format' => 'shop_loop_display_discount_format', |
| 189 |
'options_option_text' => 'shop_loop_display_options_option_text', |
| 190 |
'options_show_default_option' => 'shop_loop_display_options_show_default_option', |
| 191 |
'options_default_option_text' => 'shop_loop_display_options_default_option_text', |
| 192 |
'options_show_original_product_price' => 'shop_loop_display_options_show_original_product_price', |
| 193 |
'options_show_total' => 'shop_loop_display_options_show_total', |
| 194 |
'head_quantity_text' => 'shop_loop_display_quantity_column_title', |
| 195 |
'head_discount_text' => 'shop_loop_display_discount_column_title', |
| 196 |
'head_price_text' => 'shop_loop_display_price_column_title', |
| 197 |
'plain_text_template' => 'shop_loop_display_plain_text_template', |
| 198 |
'plain_text_show_first_tier' => 'shop_loop_display_plain_text_show_first_tier', |
| 199 |
'plain_text_first_tier_template' => 'shop_loop_display_plain_text_first_tier_template', |
| 200 |
'clickable_table_rows' => 'shop_loop_display_clickable_table_rows', |
| 201 |
// catalog-only |
| 202 |
'layout_settings' => 'shop_loop_display_layout_settings', |
| 203 |
'use_reduced_styles' => 'shop_loop_display_use_reduced_styles', |
| 204 |
'dynamic_price' => 'shop_loop_display_dynamic_price', |
| 205 |
'show_quantity_field' => 'shop_loop_display_show_quantity_field', |
| 206 |
'scope' => 'shop_loop_display_scope', |
| 207 |
'contexts' => 'shop_loop_display_contexts', |
| 208 |
'categories' => 'shop_loop_display_categories', |
| 209 |
'tiers_limit' => 'shop_loop_display_tiers_limit', |
| 210 |
'badge_enabled' => 'shop_loop_display_badge_enabled', |
| 211 |
'badge_template' => 'shop_loop_display_badge_template', |
| 212 |
'badge_position' => 'shop_loop_display_badge_position', |
| 213 |
'badge_color' => 'shop_loop_display_badge_color', |
| 214 |
); |
| 215 |
|
| 216 |
const CATALOG_UNIT_OPTIONS = array( |
| 217 |
'table_quantity_measurement' => 'shop_loop_display_table_quantity_measurement', |
| 218 |
'blocks_quantity_measurement' => 'shop_loop_display_blocks_quantity_measurement', |
| 219 |
); |
| 220 |
|
| 221 |
const CATALOG_PREMIUM_OPTIONS = array( 'options_show_total', 'clickable_table_rows', 'show_quantity_field' ); |
| 222 |
|
| 223 |
const CATALOG_ADDON_SLUG = 'shop-loop-display'; |
| 224 |
|
| 225 |
public function __construct() { |
| 226 |
add_action( 'woocommerce_admin_field_' . self::FIELD_TYPE, array( $this, 'render' ) ); |
| 227 |
|
| 228 |
$htmlOptions = self::HTML_OPTIONS; |
| 229 |
foreach ( self::HTML_OPTIONS as $key ) { |
| 230 |
if ( isset( self::CATALOG_OPTIONS[ $key ] ) ) { |
| 231 |
$htmlOptions[] = self::CATALOG_OPTIONS[ $key ]; |
| 232 |
} |
| 233 |
} |
| 234 |
foreach ( $htmlOptions as $id ) { |
| 235 |
add_filter( 'woocommerce_admin_settings_sanitize_option_' . self::getOptionId( $id ), function ( $value, $option, $rawValue ) { |
| 236 |
return wp_kses_post( is_string( $rawValue ) ? $rawValue : '' ); |
| 237 |
}, 10, 3 ); |
| 238 |
} |
| 239 |
|
| 240 |
// numbers: pixels within a range, or empty where empty means "auto" |
| 241 |
self::sanitizeNumber( 'tooltip_size', 8, 64, '15' ); |
| 242 |
self::sanitizeNumber( 'layout_spacing', 0, 80, '' ); |
| 243 |
self::sanitizeNumber( 'shop_loop_display_layout_spacing', 0, 80, '' ); |
| 244 |
self::sanitizeNumber( 'cell_padding', 0, 60, '' ); |
| 245 |
self::sanitizeNumber( 'shop_loop_display_cell_padding', 0, 60, '' ); |
| 246 |
self::sanitizeNumber( 'shop_loop_display_tiers_limit', 0, 20, '' ); |
| 247 |
|
| 248 |
// lists stored as comma-separated strings: known context keys, category ids |
| 249 |
add_filter( 'woocommerce_admin_settings_sanitize_option_' . self::getOptionId( 'shop_loop_display_contexts' ), function ( $value ) { |
| 250 |
$known = array_keys( \TierPricingTable\Addons\ProductCatalogLoop\Settings\ProductCatalogLoopSettingsSection::getContextOptions() ); |
| 251 |
|
| 252 |
return implode( ',', array_values( array_intersect( \TierPricingTable\Settings\CustomOptions\TPTCheckboxListOption::toArray( $value ), $known ) ) ); |
| 253 |
} ); |
| 254 |
add_filter( 'woocommerce_admin_settings_sanitize_option_' . self::getOptionId( 'shop_loop_display_categories' ), function ( $value ) { |
| 255 |
return implode( ',', array_filter( array_map( 'intval', \TierPricingTable\Settings\CustomOptions\TPTCheckboxListOption::toArray( $value ) ) ) ); |
| 256 |
} ); |
| 257 |
|
| 258 |
// colours: a hex colour, or the option's default |
| 259 |
foreach ( array( 'selected_quantity_color' => '#3858e9', 'tooltip_color' => '#3858e9', 'you_save_text_color' => '#FF0000', 'cart_upsell_color' => '#059669', 'shop_loop_display_selected_quantity_color' => '#3858e9' ) as $id => $default ) { |
| 260 |
add_filter( 'woocommerce_admin_settings_sanitize_option_' . self::getOptionId( $id ), function ( $value ) use ( $default ) { |
| 261 |
$color = is_string( $value ) ? sanitize_hex_color( trim( $value ) ) : null; |
| 262 |
|
| 263 |
return $color ? $color : $default; |
| 264 |
} ); |
| 265 |
} |
| 266 |
|
| 267 |
// optional colours: a hex colour, or empty (the design style's own colours) |
| 268 |
foreach ( array( 'discount_badge_color', 'shop_loop_display_discount_badge_color', 'shop_loop_display_badge_color' ) as $id ) { |
| 269 |
add_filter( 'woocommerce_admin_settings_sanitize_option_' . self::getOptionId( $id ), function ( $value ) { |
| 270 |
$color = is_string( $value ) ? sanitize_hex_color( trim( $value ) ) : null; |
| 271 |
|
| 272 |
return $color ? $color : ''; |
| 273 |
} ); |
| 274 |
} |
| 275 |
|
| 276 |
// choices: only a known value is saved; anything else keeps the stored value. The allowed values are |
| 277 |
// resolved when a save happens (they carry translated labels, which must not load this early). |
| 278 |
foreach ( self::CHOICE_OPTIONS as $id ) { |
| 279 |
add_filter( 'woocommerce_admin_settings_sanitize_option_' . self::getOptionId( $id ), function ( $value ) use ( $id ) { |
| 280 |
$allowed = self::getChoiceLists()[ $id ] ?? null; |
| 281 |
|
| 282 |
if ( null === $allowed || ( is_string( $value ) && in_array( $value, $allowed, true ) ) ) { |
| 283 |
return $value; |
| 284 |
} |
| 285 |
|
| 286 |
return ServiceContainer::getInstance()->getSettings()->get( $id, $allowed[0] ?? '' ); |
| 287 |
} ); |
| 288 |
} |
| 289 |
} |
| 290 |
|
| 291 |
/** |
| 292 |
* Single-choice options whose saved value is checked against getChoiceLists(). |
| 293 |
*/ |
| 294 |
const CHOICE_OPTIONS = array( |
| 295 |
'shop_loop_display_badge_position', |
| 296 |
'shop_loop_display_scope', |
| 297 |
'display_type', 'pricing_table_style', 'pricing_blocks_style', 'pricing_options_style', 'pricing_dropdown_style', 'pricing_plain_text_style', |
| 298 |
'quantity_type', 'tiers_order', 'discount_format', 'product_page_price_format', 'position_hook', 'summary_type', 'tiered_price_at_catalog_type', |
| 299 |
'shop_loop_display_layout', 'shop_loop_display_pricing_table_style', 'shop_loop_display_pricing_blocks_style', 'shop_loop_display_pricing_options_style', |
| 300 |
'shop_loop_display_pricing_dropdown_style', 'shop_loop_display_pricing_plain_text_style', 'shop_loop_display_quantity_type', 'shop_loop_display_tiers_order', |
| 301 |
'shop_loop_display_discount_format', 'shop_loop_display_layout_settings', 'shop_loop_display_position', |
| 302 |
); |
| 303 |
|
| 304 |
/** |
| 305 |
* Allowed values per single-choice option (product page and shop keys). |
| 306 |
*/ |
| 307 |
public static function getChoiceLists(): array { |
| 308 |
$layouts = array_keys( TierPricingTablePlugin::getAvailablePricingLayouts() ); |
| 309 |
$styles = GeneralSection::getStyleOptions(); |
| 310 |
$catalog = GeneralSection::getStyleOptions( true ); |
| 311 |
$lists = array( |
| 312 |
'display_type' => $layouts, |
| 313 |
'pricing_table_style' => array_keys( $styles['table'] ), |
| 314 |
'pricing_blocks_style' => array_keys( $styles['blocks'] ), |
| 315 |
'pricing_options_style' => array_keys( $styles['options'] ), |
| 316 |
'pricing_dropdown_style' => array_keys( $styles['dropdown'] ), |
| 317 |
'pricing_plain_text_style' => array_keys( $styles['plain-text'] ), |
| 318 |
'quantity_type' => array( 'range', 'static' ), |
| 319 |
'tiers_order' => array( 'asc', 'desc' ), |
| 320 |
'discount_format' => array( 'percentage', 'amount', 'both' ), |
| 321 |
'product_page_price_format' => array( 'custom', 'same_as_catalog' ), |
| 322 |
'shop_loop_display_badge_position' => array( 'top-left', 'top-right' ), |
| 323 |
'shop_loop_display_scope' => array( 'everywhere', 'selected' ), |
| 324 |
'position_hook' => array_merge( array_keys( self::getPositionOptions() ), array( GeneralSection::NONE_POSITION ) ), |
| 325 |
'shop_loop_display_layout' => array_values( array_diff( $layouts, array( 'tooltip' ) ) ), |
| 326 |
'shop_loop_display_pricing_table_style' => array_keys( $catalog['table'] ), |
| 327 |
'shop_loop_display_pricing_blocks_style' => array_keys( $catalog['blocks'] ), |
| 328 |
'shop_loop_display_pricing_options_style' => array_keys( $catalog['options'] ), |
| 329 |
'shop_loop_display_pricing_dropdown_style' => array_keys( $catalog['dropdown'] ), |
| 330 |
'shop_loop_display_pricing_plain_text_style' => array_keys( $catalog['plain-text'] ), |
| 331 |
'shop_loop_display_quantity_type' => array( 'range', 'static' ), |
| 332 |
'shop_loop_display_tiers_order' => array( 'asc', 'desc' ), |
| 333 |
'shop_loop_display_discount_format' => array( 'percentage', 'amount', 'both' ), |
| 334 |
'shop_loop_display_layout_settings' => array( 'default', 'custom' ), |
| 335 |
'shop_loop_display_position' => array_keys( self::getCatalogPositionOptions() ), |
| 336 |
); |
| 337 |
|
| 338 |
if ( self::isSummaryAvailable() ) { |
| 339 |
$lists['summary_type'] = array( 'detailed', 'compact', 'inline' ); |
| 340 |
} |
| 341 |
if ( self::isCatalogPriceAvailable() ) { |
| 342 |
$lists['tiered_price_at_catalog_type'] = array( 'lowest', 'range', 'custom' ); |
| 343 |
} |
| 344 |
|
| 345 |
return $lists; |
| 346 |
} |
| 347 |
|
| 348 |
/** |
| 349 |
* Save a numeric option as an integer within a range; an empty value stays empty when allowed. |
| 350 |
*/ |
| 351 |
protected static function sanitizeNumber( string $id, int $min, int $max, string $empty ) { |
| 352 |
add_filter( 'woocommerce_admin_settings_sanitize_option_' . self::getOptionId( $id ), function ( $value ) use ( $min, $max, $empty ) { |
| 353 |
if ( ! is_scalar( $value ) || '' === trim( (string) $value ) || ! is_numeric( $value ) ) { |
| 354 |
return $empty; |
| 355 |
} |
| 356 |
|
| 357 |
return (string) max( $min, min( $max, (int) $value ) ); |
| 358 |
} ); |
| 359 |
} |
| 360 |
|
| 361 |
/** |
| 362 |
* Option defaults, including the translated template defaults. |
| 363 |
*/ |
| 364 |
public static function getDefaults(): array { |
| 365 |
$defaults = self::OPTIONS; |
| 366 |
|
| 367 |
$defaults['options_option_text'] = __( '<strong>Buy {tp_quantity} pieces and save {tp_rounded_discount}%</strong>', 'tier-pricing-table' ); |
| 368 |
$defaults['options_default_option_text'] = __( '<strong>Buy {tp_quantity} pieces</strong>', 'tier-pricing-table' ); |
| 369 |
$defaults['head_quantity_text'] = __( 'Quantity', 'tier-pricing-table' ); |
| 370 |
$defaults['head_discount_text'] = __( 'Discount (%)', 'tier-pricing-table' ); |
| 371 |
$defaults['head_price_text'] = __( 'Price', 'tier-pricing-table' ); |
| 372 |
$defaults['plain_text_template'] = __( '<strong>Buy {tp_quantity} pieces for {tp_price} each and save {tp_rounded_discount}%</strong>', 'tier-pricing-table' ); |
| 373 |
$defaults['plain_text_first_tier_template'] = __( '<strong>Buy {tp_quantity} pieces for {tp_price} each</strong>', 'tier-pricing-table' ); |
| 374 |
// stores that used the old "tiered price at product page" switch keep the catalog format |
| 375 |
$defaults['product_page_price_format'] = 'yes' === ServiceContainer::getInstance()->getSettings()->get( 'tiered_price_at_product_page', 'no' ) ? 'same_as_catalog' : 'custom'; |
| 376 |
|
| 377 |
if ( self::isYouSaveAvailable() ) { |
| 378 |
$defaults = array_merge( $defaults, self::YOU_SAVE_OPTIONS ); |
| 379 |
$defaults['you_save_template'] = __( 'You save {tp_ys_total_price}', 'tier-pricing-table' ); |
| 380 |
} |
| 381 |
if ( self::isSummaryAvailable() ) { |
| 382 |
$defaults = array_merge( $defaults, self::SUMMARY_OPTIONS ); |
| 383 |
$defaults['summary_total_label'] = __( 'Total:', 'tier-pricing-table' ); |
| 384 |
$defaults['summary_each_label'] = __( 'Each: ', 'tier-pricing-table' ); |
| 385 |
} |
| 386 |
if ( self::isCatalogPriceAvailable() ) { |
| 387 |
$defaults = array_merge( $defaults, self::CATALOG_PRICE_OPTIONS ); |
| 388 |
$defaults['tiered_price_at_catalog_custom_template'] = __( 'From {tp_lowest_price} instead of {tp_original_price}', 'tier-pricing-table' ); |
| 389 |
$defaults['lowest_prefix'] = __( 'From', 'tier-pricing-table' ); |
| 390 |
} |
| 391 |
if ( self::isCartAvailable() ) { |
| 392 |
$defaults = array_merge( $defaults, self::CART_OPTIONS ); |
| 393 |
$defaults['cart_total_savings_label'] = __( 'Total savings', 'tier-pricing-table' ); |
| 394 |
} |
| 395 |
if ( self::isUpsellAvailable() ) { |
| 396 |
$defaults = array_merge( $defaults, self::UPSELL_OPTIONS ); |
| 397 |
$defaults['cart_upsell_template'] = __( 'Add <b>{tp_required_quantity}</b> more to get <b>{tp_next_price}</b> each', 'tier-pricing-table' ); |
| 398 |
} |
| 399 |
|
| 400 |
return $defaults; |
| 401 |
} |
| 402 |
|
| 403 |
public static function isCartAvailable(): bool { |
| 404 |
return AbstractAddon::isAddonEnabled( self::CART_ADDON_SLUG ); |
| 405 |
} |
| 406 |
|
| 407 |
public static function isUpsellAvailable(): bool { |
| 408 |
return AbstractAddon::isAddonEnabled( self::UPSELL_ADDON_SLUG ); |
| 409 |
} |
| 410 |
|
| 411 |
public static function isSummaryAvailable(): bool { |
| 412 |
return AbstractAddon::isAddonEnabled( self::SUMMARY_ADDON_SLUG ); |
| 413 |
} |
| 414 |
|
| 415 |
public static function isCatalogPriceAvailable(): bool { |
| 416 |
return AbstractAddon::isAddonEnabled( self::CATALOG_PRICE_ADDON_SLUG ); |
| 417 |
} |
| 418 |
|
| 419 |
/** |
| 420 |
* Whether the "You Save" add-on is on, i.e. the configurator also manages the badge options. |
| 421 |
*/ |
| 422 |
public static function isYouSaveAvailable(): bool { |
| 423 |
return AbstractAddon::isAddonEnabled( self::YOU_SAVE_ADDON_SLUG ); |
| 424 |
} |
| 425 |
|
| 426 |
/** |
| 427 |
* Sample amounts for the skeleton's price line and summary: the preview product at the first discounted tier. |
| 428 |
*/ |
| 429 |
public function getSamples(): array { |
| 430 |
$plain = function ( $html ) { |
| 431 |
return html_entity_decode( wp_strip_all_tags( (string) $html ), ENT_QUOTES, 'UTF-8' ); |
| 432 |
}; |
| 433 |
|
| 434 |
$price = LayoutPreview::PRICE; |
| 435 |
$tiers = LayoutPreview::SAMPLE_TIERS; |
| 436 |
$quantity = (int) array_key_first( $tiers ); |
| 437 |
$discount = (float) reset( $tiers ); |
| 438 |
$tierPrice = round( $price * ( 1 - $discount ), 2 ); |
| 439 |
$lowest = round( $price * ( 1 - max( $tiers ) ), 2 ); |
| 440 |
|
| 441 |
// the next tier after the sample quantity, for the cart upsell message |
| 442 |
$quantities = array_keys( $tiers ); |
| 443 |
$nextQuantity = (int) ( $quantities[1] ?? $quantities[0] ); |
| 444 |
$nextPrice = round( $price * ( 1 - (float) $tiers[ $nextQuantity ] ), 2 ); |
| 445 |
$percent = function ( float $from, float $to ) { |
| 446 |
$discount = $from > 0 ? ( $from - $to ) / $from * 100 : 0; |
| 447 |
|
| 448 |
return number_format( $discount, wc_get_price_decimals(), wc_get_price_decimal_separator(), wc_get_price_thousand_separator() ); |
| 449 |
}; |
| 450 |
|
| 451 |
$tierList = array( array( 'quantity' => 1, 'price' => (float) $price ) ); |
| 452 |
foreach ( $tiers as $tierQuantity => $tierDiscount ) { |
| 453 |
$tierList[] = array( 'quantity' => (int) $tierQuantity, 'price' => round( $price * ( 1 - (float) $tierDiscount ), 2 ) ); |
| 454 |
} |
| 455 |
|
| 456 |
return array( |
| 457 |
'regular' => (float) $price, |
| 458 |
'tiers' => $tierList, |
| 459 |
'upsell' => array( |
| 460 |
'requiredQuantity' => max( 0, $nextQuantity - $quantity ), |
| 461 |
'targetQuantity' => $nextQuantity, |
| 462 |
'progress' => $nextQuantity > 0 ? (int) round( $quantity / $nextQuantity * 100 ) : 0, |
| 463 |
'nextPrice' => $plain( wc_price( $nextPrice ) ), |
| 464 |
'nextDiscount' => $percent( $tierPrice, $nextPrice ), |
| 465 |
'actualDiscount' => $percent( $price, $tierPrice ), |
| 466 |
), |
| 467 |
'quantity' => $quantity, |
| 468 |
'price' => $plain( wc_price( $price ) ), |
| 469 |
'tierPrice' => $plain( wc_price( $tierPrice ) ), |
| 470 |
'lowest' => $plain( wc_price( $lowest ) ), |
| 471 |
'total' => $plain( wc_price( $price * $quantity ) ), |
| 472 |
'tierTotal' => $plain( wc_price( $tierPrice * $quantity ) ), |
| 473 |
'saving' => $plain( wc_price( $price - $tierPrice ) ), |
| 474 |
'savingTotal' => $plain( wc_price( ( $price - $tierPrice ) * $quantity ) ), |
| 475 |
'maxDiscount' => (int) round( max( $tiers ) * 100 ) . '%', |
| 476 |
'maxSaving' => $plain( wc_price( $price - $lowest ) ), |
| 477 |
'lowestQuantity' => (int) max( array_keys( $tiers ) ), |
| 478 |
'discount' => (int) round( $discount * 100 ), |
| 479 |
'productName' => __( 'Product name', 'tier-pricing-table' ), |
| 480 |
); |
| 481 |
} |
| 482 |
|
| 483 |
/** |
| 484 |
* Whether premium code is available (the woocommerce.com build has no Freemius and is always premium). |
| 485 |
*/ |
| 486 |
public static function isPremium(): bool { |
| 487 |
$premium = function_exists( 'tpt_fs' ) ? (bool) tpt_fs()->can_use_premium_code() : true; |
| 488 |
|
| 489 |
return (bool) apply_filters( 'tiered_pricing_table/settings/is_premium', $premium ); |
| 490 |
} |
| 491 |
|
| 492 |
public static function getOptionId( string $id ): string { |
| 493 |
return Settings::SETTINGS_PREFIX . $id; |
| 494 |
} |
| 495 |
|
| 496 |
/** |
| 497 |
* Settings entries for the options the configurator saves: rendered by nothing, saved by |
| 498 |
* WooCommerce from the hidden inputs. |
| 499 |
*/ |
| 500 |
public static function getSavedOnlyFields(): array { |
| 501 |
$fields = array(); |
| 502 |
foreach ( self::getDefaults() as $id => $default ) { |
| 503 |
if ( 'display_type' === $id ) { |
| 504 |
continue; // the configurator field itself carries this id |
| 505 |
} |
| 506 |
$fields[] = array( |
| 507 |
'id' => self::getOptionId( $id ), |
| 508 |
'type' => 'tpt_saved_only', |
| 509 |
'default' => $default, |
| 510 |
); |
| 511 |
} |
| 512 |
$others = self::getUnitDefaults(); |
| 513 |
if ( self::isCatalogAvailable() ) { |
| 514 |
$others = array_merge( $others, self::getCatalogDefaults(), self::getCatalogUnitDefaults() ); |
| 515 |
} |
| 516 |
foreach ( $others as $id => $default ) { |
| 517 |
$fields[] = array( |
| 518 |
'id' => self::getOptionId( $id ), |
| 519 |
'type' => 'tpt_saved_only', |
| 520 |
'default' => $default, |
| 521 |
); |
| 522 |
} |
| 523 |
|
| 524 |
return $fields; |
| 525 |
} |
| 526 |
|
| 527 |
/** |
| 528 |
* Default singular/plural unit labels per unit option. |
| 529 |
*/ |
| 530 |
public static function getUnitDefaults(): array { |
| 531 |
return array( |
| 532 |
'table_quantity_measurement' => array( 'singular' => '', 'plural' => '' ), |
| 533 |
'blocks_quantity_measurement' => array( |
| 534 |
'singular' => _n( 'piece', 'pieces', 1, 'tier-pricing-table' ), |
| 535 |
'plural' => _n( 'piece', 'pieces', 2, 'tier-pricing-table' ), |
| 536 |
), |
| 537 |
); |
| 538 |
} |
| 539 |
|
| 540 |
/** |
| 541 |
* Stored unit labels, normalised to singular/plural strings. |
| 542 |
*/ |
| 543 |
public static function getUnitValues(): array { |
| 544 |
$values = array(); |
| 545 |
foreach ( self::getUnitDefaults() as $id => $default ) { |
| 546 |
$stored = ServiceContainer::getInstance()->getSettings()->get( $id, $default ); |
| 547 |
$stored = is_array( $stored ) ? $stored : array(); |
| 548 |
$values[ $id ] = array( |
| 549 |
'singular' => isset( $stored['singular'] ) ? (string) $stored['singular'] : $default['singular'], |
| 550 |
'plural' => isset( $stored['plural'] ) ? (string) $stored['plural'] : $default['plural'], |
| 551 |
); |
| 552 |
} |
| 553 |
|
| 554 |
return $values; |
| 555 |
} |
| 556 |
|
| 557 |
/** |
| 558 |
* Whether the catalog add-on is on, i.e. the configurator also manages the shop & category options. |
| 559 |
*/ |
| 560 |
public static function isCatalogAvailable(): bool { |
| 561 |
return AbstractAddon::isAddonEnabled( self::CATALOG_ADDON_SLUG ); |
| 562 |
} |
| 563 |
|
| 564 |
/** |
| 565 |
* Catalog option defaults: option id => default. Shared options default like the product page ones; |
| 566 |
* the design styles follow the product page until a catalog style is saved. |
| 567 |
*/ |
| 568 |
public static function getCatalogDefaults(): array { |
| 569 |
$product = self::getDefaults(); |
| 570 |
$defaults = array(); |
| 571 |
foreach ( self::CATALOG_OPTIONS as $key => $id ) { |
| 572 |
$defaults[ $id ] = $product[ $key ] ?? ''; |
| 573 |
} |
| 574 |
$defaults['shop_loop_display_enabled'] = 'no'; |
| 575 |
$defaults['shop_loop_display_position'] = 'woocommerce_after_shop_loop_item__6'; |
| 576 |
$defaults['shop_loop_display_pricing_table_style'] = self::catalogStyle( 'table', GeneralSection::getPricingTableStyle() ); |
| 577 |
$defaults['shop_loop_display_pricing_blocks_style'] = self::catalogStyle( 'blocks', GeneralSection::getPricingBlocksStyle() ); |
| 578 |
$defaults['shop_loop_display_pricing_options_style'] = self::catalogStyle( 'options', GeneralSection::getPricingOptionsStyle() ); |
| 579 |
$defaults['shop_loop_display_pricing_dropdown_style'] = self::catalogStyle( 'dropdown', GeneralSection::getPricingDropdownStyle() ); |
| 580 |
$defaults['shop_loop_display_pricing_plain_text_style'] = self::catalogStyle( 'plain-text', GeneralSection::getPricingPlainTextStyle() ); |
| 581 |
$defaults['shop_loop_display_discount_column_title'] = __( 'Discount', 'tier-pricing-table' ); |
| 582 |
$defaults['shop_loop_display_layout_settings'] = 'default'; |
| 583 |
$defaults['shop_loop_display_use_reduced_styles'] = 'yes'; |
| 584 |
$defaults['shop_loop_display_dynamic_price'] = 'yes'; |
| 585 |
$defaults['shop_loop_display_show_quantity_field'] = 'no'; |
| 586 |
$defaults['shop_loop_display_scope'] = 'everywhere'; |
| 587 |
$defaults['shop_loop_display_contexts'] = ''; |
| 588 |
$defaults['shop_loop_display_categories'] = ''; |
| 589 |
$defaults['shop_loop_display_tiers_limit'] = ''; |
| 590 |
$defaults['shop_loop_display_badge_enabled'] = 'no'; |
| 591 |
$defaults['shop_loop_display_badge_template'] = \TierPricingTable\Addons\ProductCatalogLoop\Settings\ProductCatalogLoopSettingsSection::getDefaultBadgeTemplate(); |
| 592 |
$defaults['shop_loop_display_badge_position'] = 'top-left'; |
| 593 |
$defaults['shop_loop_display_badge_color'] = ''; |
| 594 |
|
| 595 |
return $defaults; |
| 596 |
} |
| 597 |
|
| 598 |
/** |
| 599 |
* A design style as shop and category pages render it: a withheld style falls back to the default. |
| 600 |
*/ |
| 601 |
public static function catalogStyle( string $layout, string $style ): string { |
| 602 |
$excluded = GeneralSection::getCatalogExcludedStyles(); |
| 603 |
|
| 604 |
return in_array( $style, (array) ( $excluded[ $layout ] ?? array() ), true ) ? 'default' : $style; |
| 605 |
} |
| 606 |
|
| 607 |
public static function getCatalogUnitDefaults(): array { |
| 608 |
$defaults = array(); |
| 609 |
foreach ( self::getUnitDefaults() as $key => $default ) { |
| 610 |
$defaults[ self::CATALOG_UNIT_OPTIONS[ $key ] ] = $default; |
| 611 |
} |
| 612 |
|
| 613 |
return $defaults; |
| 614 |
} |
| 615 |
|
| 616 |
/** |
| 617 |
* Stored catalog values (scalars as strings, unit labels normalised), keyed by option id. |
| 618 |
*/ |
| 619 |
public static function getCatalogValues(): array { |
| 620 |
$settings = ServiceContainer::getInstance()->getSettings(); |
| 621 |
$values = array(); |
| 622 |
// options where an empty string is a value of its own (an empty header hides that column) |
| 623 |
$keepEmpty = array( 'shop_loop_display_title', 'shop_loop_display_quantity_column_title', 'shop_loop_display_discount_column_title', 'shop_loop_display_price_column_title' ); |
| 624 |
foreach ( self::getCatalogDefaults() as $id => $default ) { |
| 625 |
$value = $settings->get( $id, $default ); |
| 626 |
$values[ $id ] = is_scalar( $value ) && ( '' !== $value || in_array( $id, $keepEmpty, true ) ) ? (string) $value : (string) $default; |
| 627 |
} |
| 628 |
foreach ( self::getCatalogUnitDefaults() as $id => $default ) { |
| 629 |
$stored = $settings->get( $id, $default ); |
| 630 |
$stored = is_array( $stored ) ? $stored : array(); |
| 631 |
$values[ $id ] = array( |
| 632 |
'singular' => isset( $stored['singular'] ) ? (string) $stored['singular'] : $default['singular'], |
| 633 |
'plural' => isset( $stored['plural'] ) ? (string) $stored['plural'] : $default['plural'], |
| 634 |
); |
| 635 |
} |
| 636 |
|
| 637 |
return $values; |
| 638 |
} |
| 639 |
|
| 640 |
public static function getCatalogPositionOptions(): array { |
| 641 |
return array( |
| 642 |
'woocommerce_after_shop_loop_item__6' => __( 'Above the add to cart button', 'tier-pricing-table' ), |
| 643 |
'woocommerce_after_shop_loop_item__15' => __( 'Below the add to cart button', 'tier-pricing-table' ), |
| 644 |
'woocommerce_shop_loop_item_title__5' => __( 'Above the product title', 'tier-pricing-table' ), |
| 645 |
'woocommerce_shop_loop_item_title__15' => __( 'Below the product title', 'tier-pricing-table' ), |
| 646 |
); |
| 647 |
} |
| 648 |
|
| 649 |
public static function getPositionOptions(): array { |
| 650 |
return array( |
| 651 |
'woocommerce_before_add_to_cart_button' => __( 'Above the add to cart button', 'tier-pricing-table' ), |
| 652 |
'woocommerce_after_add_to_cart_button' => __( 'Below the add to cart button', 'tier-pricing-table' ), |
| 653 |
'woocommerce_before_add_to_cart_form' => __( 'Above the add to cart form', 'tier-pricing-table' ), |
| 654 |
'woocommerce_after_add_to_cart_form' => __( 'Below the add to cart form', 'tier-pricing-table' ), |
| 655 |
'woocommerce_single_product_summary' => __( 'Above the product title', 'tier-pricing-table' ), |
| 656 |
'woocommerce_before_single_product_summary' => __( 'Before the product summary', 'tier-pricing-table' ), |
| 657 |
'woocommerce_after_single_product_summary' => __( 'After the product summary', 'tier-pricing-table' ), |
| 658 |
); |
| 659 |
} |
| 660 |
|
| 661 |
public static function getStyleOptions(): array { |
| 662 |
return GeneralSection::getStyleOptions(); |
| 663 |
} |
| 664 |
|
| 665 |
/** |
| 666 |
* Configuration handed to the app. |
| 667 |
*/ |
| 668 |
public function getConfig(): array { |
| 669 |
$values = array(); |
| 670 |
foreach ( self::getDefaults() as $id => $default ) { |
| 671 |
$value = $this->getContainer()->getSettings()->get( $id, $default ); |
| 672 |
$values[ $id ] = is_scalar( $value ) ? (string) $value : $default; |
| 673 |
} |
| 674 |
// classic "none" position: shown as automatic display switched off, position back to the default |
| 675 |
$values['display'] = GeneralSection::isAutomaticDisplayEnabled() ? 'yes' : 'no'; |
| 676 |
$values['position_hook'] = GeneralSection::getPositionHook(); |
| 677 |
$values = array_merge( $values, self::getUnitValues() ); |
| 678 |
if ( self::isCatalogAvailable() ) { |
| 679 |
$values = array_merge( $values, self::getCatalogValues() ); |
| 680 |
} |
| 681 |
|
| 682 |
/** @var LayoutPreview $preview */ |
| 683 |
$preview = $this->getContainer()->get( 'settings.layout_preview' ); |
| 684 |
$samples = $this->getSamples(); |
| 685 |
|
| 686 |
return array( |
| 687 |
'prefix' => Settings::SETTINGS_PREFIX, |
| 688 |
'title' => __( 'Pricing Display', 'tier-pricing-table' ), |
| 689 |
'values' => $values, |
| 690 |
'layouts' => TierPricingTablePlugin::getAvailablePricingLayouts(), |
| 691 |
'styles' => self::getStyleOptions(), |
| 692 |
'positions' => self::getPositionOptions(), |
| 693 |
'compactFor' => array( 'table', 'options', 'blocks', 'dropdown', 'horizontal-table' ), |
| 694 |
// styles whose gap between tiers the "Spacing" option controls (the others keep their own) |
| 695 |
'spacingFor' => array( |
| 696 |
'blocks' => array( 'default', 'style-1', 'style-2', 'style-3', 'style-4', 'style-5', 'style-7' ), |
| 697 |
'options' => array( 'default', 'style-3', 'style-4', 'style-5' ), |
| 698 |
'table' => array( 'style-2', 'style-3', 'style-4' ), |
| 699 |
), |
| 700 |
'spacingMax' => 40, |
| 701 |
// layouts whose cell padding the "Cell padding" option controls |
| 702 |
'cellPaddingFor' => array( 'table', 'horizontal-table' ), |
| 703 |
'cellPaddingMax' => 30, |
| 704 |
// design styles whose discount badge the "Discount badge colour" option recolours: the colour shown for "Auto" |
| 705 |
// ('active' is the active tier colour) and what the colour paints: a tint with coloured text, a solid badge |
| 706 |
// with white text, or the savings bar and its percentage |
| 707 |
'badgeColorStyles' => array( 'table' => array( |
| 708 |
'style-2' => array( 'default' => '#b91c1c', 'kind' => 'tint' ), |
| 709 |
'style-3' => array( 'default' => 'active', 'kind' => 'solid' ), |
| 710 |
'style-4' => array( 'default' => 'active', 'kind' => 'solid' ), |
| 711 |
'style-5' => array( 'default' => 'active', 'kind' => 'bar' ), |
| 712 |
'style-6' => array( 'default' => '#059669', 'kind' => 'tint' ), |
| 713 |
) ), |
| 714 |
// design styles that mark the active tier with a left border the "Active tier left border" toggle controls |
| 715 |
'activeBorderStyles' => array( 'table' => array( 'style-1', 'style-5', 'style-6' ) ), |
| 716 |
'discountFor' => array( 'blocks', 'options' ), |
| 717 |
// options styles that print a discount label (the toggle applies to those only) |
| 718 |
'discountStyles' => array( 'options' => array( 'style-3', 'style-4', 'style-5', 'style-6' ) ), |
| 719 |
// layouts (and, for options, the design styles) that print a discount per tier |
| 720 |
'discountFormatFor' => array( 'table', 'horizontal-table', 'tooltip', 'blocks', 'options', 'dropdown' ), |
| 721 |
'discountFormatStyles' => array( 'options' => array( 'style-3', 'style-4', 'style-5', 'style-6' ), 'dropdown' => array( 'style-1' ) ), |
| 722 |
'unitFor' => self::UNIT_OPTIONS, |
| 723 |
'tooltipFor' => array( 'tooltip' ), |
| 724 |
'columnsFor' => array( 'table', 'horizontal-table', 'tooltip' ), |
| 725 |
'clickableFor' => array( 'table', 'horizontal-table', 'blocks', 'options', 'tooltip', 'dropdown', 'plain-text' ), |
| 726 |
'plainTextFor' => array( 'plain-text' ), |
| 727 |
'customColumns' => has_action( 'tiered_pricing_table/settings/table_columns/end' ), |
| 728 |
'catalog' => self::isCatalogAvailable() ? array( |
| 729 |
'keys' => self::CATALOG_OPTIONS, |
| 730 |
'unitKeys' => self::CATALOG_UNIT_OPTIONS, |
| 731 |
'layouts' => array_diff_key( TierPricingTablePlugin::getAvailablePricingLayouts(), array( 'tooltip' => true ) ), |
| 732 |
'positions' => self::getCatalogPositionOptions(), |
| 733 |
'premiumOptions' => self::CATALOG_PREMIUM_OPTIONS, |
| 734 |
'excludedStyles' => GeneralSection::getCatalogExcludedStyles(), |
| 735 |
'contexts' => \TierPricingTable\Addons\ProductCatalogLoop\Settings\ProductCatalogLoopSettingsSection::getContextOptions(), |
| 736 |
'scopes' => \TierPricingTable\Addons\ProductCatalogLoop\Settings\ProductCatalogLoopSettingsSection::getScopeOptions(), |
| 737 |
// the chosen categories' names; other categories are searched through WooCommerce's AJAX category search |
| 738 |
'categoryNames' => (object) \TierPricingTable\Addons\ProductCatalogLoop\Settings\ProductCatalogLoopSettingsSection::getCategoryNames( \TierPricingTable\Settings\CustomOptions\TPTCheckboxListOption::toArray( ServiceContainer::getInstance()->getSettings()->get( 'shop_loop_display_categories', '' ) ) ), |
| 739 |
'categorySearch' => array( 'url' => admin_url( 'admin-ajax.php' ), 'action' => 'woocommerce_json_search_categories', 'nonce' => wp_create_nonce( 'search-categories' ) ), |
| 740 |
'badgePositions' => \TierPricingTable\Addons\ProductCatalogLoop\Settings\ProductCatalogLoopSettingsSection::getBadgePositionOptions(), |
| 741 |
) : null, |
| 742 |
'premium' => self::isPremium(), |
| 743 |
'premiumOptions' => self::PREMIUM_OPTIONS, |
| 744 |
'optionsLayout' => array( |
| 745 |
'templates' => array( 'options', 'dropdown' ), |
| 746 |
'original' => array( 'options', 'dropdown' ), |
| 747 |
'defaultOpt' => array( 'options' ), |
| 748 |
'total' => array( 'options' ), |
| 749 |
'totalStyles' => array( 'default', 'style-1', 'style-2', 'style-3', 'style-4', 'style-5', 'style-6' ), |
| 750 |
), |
| 751 |
'placeholders' => array( |
| 752 |
'options_option_text' => array( 'tp_quantity', 'tp_discount', 'tp_rounded_discount', 'tp_base_unit_name' ), |
| 753 |
'options_default_option_text' => array( 'tp_quantity', 'tp_base_unit_name' ), |
| 754 |
'plain_text_template' => array( 'tp_quantity', 'tp_discount', 'tp_price', 'tp_rounded_discount', 'tp_base_unit_name' ), |
| 755 |
'plain_text_first_tier_template' => array( 'tp_quantity', 'tp_price', 'tp_base_unit_name' ), |
| 756 |
'you_save_template' => array( 'tp_ys_price', 'tp_ys_total_price', 'tp_ys_percentage_discount' ), |
| 757 |
'tiered_price_at_catalog_custom_template' => array( 'tp_lowest_price', 'tp_prices_range', 'tp_original_price' ), |
| 758 |
'cart_upsell_template' => array( 'tp_required_quantity', 'tp_next_price', 'tp_next_discount', 'tp_actual_discount' ), |
| 759 |
'badge_template' => array( 'tp_max_discount', 'tp_lowest_price', 'tp_lowest_quantity', 'tp_max_saving' ), |
| 760 |
), |
| 761 |
'placeholder' => function_exists( 'wc_placeholder_img_src' ) ? wc_placeholder_img_src( 'woocommerce_single' ) : '', |
| 762 |
'youSave' => self::isYouSaveAvailable(), |
| 763 |
'samples' => $samples, |
| 764 |
'summary' => self::isSummaryAvailable() ? array( |
| 765 |
'positions' => self::getPositionOptions(), |
| 766 |
'types' => array( |
| 767 |
'detailed' => __( 'Detailed', 'tier-pricing-table' ), |
| 768 |
'table' => __( 'Compact', 'tier-pricing-table' ), |
| 769 |
'inline' => __( 'Inline labels', 'tier-pricing-table' ), |
| 770 |
), |
| 771 |
) : null, |
| 772 |
'cart' => self::isCartAvailable() || self::isUpsellAvailable() ? array( |
| 773 |
'prices' => self::isCartAvailable(), |
| 774 |
'upsells' => self::isUpsellAvailable(), |
| 775 |
) : null, |
| 776 |
'catalogPrice' => self::isCatalogPriceAvailable() ? array( |
| 777 |
'types' => array( |
| 778 |
'lowest' => array( |
| 779 |
'label' => __( 'Lowest price', 'tier-pricing-table' ), |
| 780 |
/* translators: %s: example price */ |
| 781 |
'example' => sprintf( __( 'e.g. From %s', 'tier-pricing-table' ), $samples['lowest'] ), |
| 782 |
), |
| 783 |
'range' => array( |
| 784 |
'label' => __( 'Price range', 'tier-pricing-table' ), |
| 785 |
/* translators: 1: lowest example price, 2: highest example price */ |
| 786 |
'example' => sprintf( __( 'e.g. %1$s - %2$s', 'tier-pricing-table' ), $samples['lowest'], $samples['price'] ), |
| 787 |
), |
| 788 |
'custom' => array( |
| 789 |
'label' => __( 'Custom template', 'tier-pricing-table' ), |
| 790 |
'example' => __( 'Your own text built from variables', 'tier-pricing-table' ), |
| 791 |
), |
| 792 |
), |
| 793 |
) : null, |
| 794 |
'priceFormats' => array( |
| 795 |
'same_as_catalog' => array( |
| 796 |
'label' => __( 'Match shop & categories', 'tier-pricing-table' ), |
| 797 |
'example' => __( 'Lowest price or a price range, as on the shop page', 'tier-pricing-table' ), |
| 798 |
), |
| 799 |
'custom' => array( |
| 800 |
'label' => __( 'Price for the selected quantity', 'tier-pricing-table' ), |
| 801 |
'example' => __( 'Follows the quantity the customer enters', 'tier-pricing-table' ), |
| 802 |
), |
| 803 |
), |
| 804 |
'quantityTypes' => array( |
| 805 |
'range' => array( 'label' => __( 'Range', 'tier-pricing-table' ), 'example' => __( '10 - 24 pieces', 'tier-pricing-table' ) ), |
| 806 |
'static' => array( 'label' => __( 'Static values', 'tier-pricing-table' ), 'example' => __( '10 pieces', 'tier-pricing-table' ) ), |
| 807 |
), |
| 808 |
'tierOrders' => array( |
| 809 |
'asc' => array( 'label' => __( 'Ascending', 'tier-pricing-table' ), 'example' => __( 'Smallest quantity first', 'tier-pricing-table' ) ), |
| 810 |
'desc' => array( 'label' => __( 'Descending', 'tier-pricing-table' ), 'example' => __( 'Biggest discount first', 'tier-pricing-table' ) ), |
| 811 |
), |
| 812 |
'discountFormats' => array( |
| 813 |
'percentage' => array( 'label' => __( 'Percentage', 'tier-pricing-table' ), 'example' => $samples['discount'] . '%' ), |
| 814 |
'amount' => array( 'label' => __( 'Amount saved', 'tier-pricing-table' ), 'example' => $samples['saving'] ), |
| 815 |
'both' => array( 'label' => __( 'Both', 'tier-pricing-table' ), 'example' => $samples['discount'] . '% (' . $samples['saving'] . ')' ), |
| 816 |
), |
| 817 |
'ajax' => array( |
| 818 |
'url' => admin_url( 'admin-ajax.php' ), |
| 819 |
'action' => LayoutPreview::AJAX_ACTION, |
| 820 |
'nonce' => wp_create_nonce( LayoutPreview::AJAX_ACTION ), |
| 821 |
), |
| 822 |
'stylesheets' => $preview->getStylesheetURLs(), |
| 823 |
'inlineStyles' => $preview->getInlineStyles(), |
| 824 |
// WooCommerce scopes its classic price colours to themes that are not block themes |
| 825 |
'blockTheme' => function_exists( 'wp_is_block_theme' ) && wp_is_block_theme(), |
| 826 |
'currency' => array( |
| 827 |
'price' => html_entity_decode( wp_strip_all_tags( wc_price( LayoutPreview::PRICE ) ), ENT_QUOTES, 'UTF-8' ), |
| 828 |
'symbol' => html_entity_decode( get_woocommerce_currency_symbol(), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8' ), |
| 829 |
'position' => get_option( 'woocommerce_currency_pos', 'left' ), |
| 830 |
'decimals' => wc_get_price_decimals(), |
| 831 |
'decimalSeparator' => wc_get_price_decimal_separator(), |
| 832 |
'thousandSeparator' => wc_get_price_thousand_separator(), |
| 833 |
), |
| 834 |
); |
| 835 |
} |
| 836 |
|
| 837 |
public function enqueueAssets() { |
| 838 |
$dir = plugin_dir_path( __FILE__ ) . 'build/'; |
| 839 |
$assetFile = file_exists( $dir . 'index.asset.php' ) ? include $dir . 'index.asset.php' : array( 'dependencies' => array(), 'version' => TierPricingTablePlugin::VERSION ); |
| 840 |
|
| 841 |
wp_enqueue_script( self::SCRIPT_HANDLE, plugins_url( 'build/index.js', __FILE__ ), $assetFile['dependencies'], $assetFile['version'], true ); |
| 842 |
wp_set_script_translations( self::SCRIPT_HANDLE, 'tier-pricing-table', $this->getContainer()->getFileManager()->getPluginDirectory() . 'languages' ); |
| 843 |
|
| 844 |
if ( file_exists( $dir . 'style-index.css' ) ) { |
| 845 |
wp_enqueue_style( self::SCRIPT_HANDLE, plugins_url( 'build/style-index.css', __FILE__ ), array( 'wp-components' ), $assetFile['version'] ); |
| 846 |
} else { |
| 847 |
wp_enqueue_style( 'wp-components' ); |
| 848 |
} |
| 849 |
|
| 850 |
wp_add_inline_script( self::SCRIPT_HANDLE, 'window.tptLayoutConfigurator = ' . wp_json_encode( $this->getConfig() ) . ';', 'before' ); |
| 851 |
} |
| 852 |
|
| 853 |
public function render( $value ) { |
| 854 |
$this->enqueueAssets(); |
| 855 |
|
| 856 |
$value = wp_parse_args( $value, array( |
| 857 |
'id' => self::getOptionId( 'display_type' ), |
| 858 |
'title' => '', |
| 859 |
'desc' => '', |
| 860 |
) ); |
| 861 |
|
| 862 |
$values = $this->getConfig()['values']; |
| 863 |
?> |
| 864 |
<?php |
| 865 |
// WooCommerce renders fields inside <table class="form-table">, whose fixed layout keeps a |
| 866 |
// spanning cell inside the label column. The configurator needs the full width, so it closes |
| 867 |
// the table, renders as a block and reopens the table for the fields that follow. |
| 868 |
?> |
| 869 |
</table> |
| 870 |
<div id="tpt-layout-configurator" class="tpt-layout-configurator" aria-label="<?php echo esc_attr( $value['title'] ); ?>"> |
| 871 |
<?php foreach ( $values as $id => $stored ) : ?> |
| 872 |
<?php if ( is_array( $stored ) ) : ?> |
| 873 |
<?php foreach ( array( 'singular', 'plural' ) as $form ) : ?> |
| 874 |
<input type="hidden" |
| 875 |
name="<?php echo esc_attr( self::getOptionId( $id ) . '[' . $form . ']' ); ?>" |
| 876 |
value="<?php echo esc_attr( $stored[ $form ] ); ?>" |
| 877 |
data-tpt-lc-option="<?php echo esc_attr( $id . '.' . $form ); ?>"> |
| 878 |
<?php endforeach; ?> |
| 879 |
<?php else : ?> |
| 880 |
<input type="hidden" |
| 881 |
name="<?php echo esc_attr( self::getOptionId( $id ) ); ?>" |
| 882 |
id="<?php echo esc_attr( self::getOptionId( $id ) ); ?>" |
| 883 |
value="<?php echo esc_attr( $stored ); ?>" |
| 884 |
data-tpt-lc-option="<?php echo esc_attr( $id ); ?>"> |
| 885 |
<?php endif; ?> |
| 886 |
<?php endforeach; ?> |
| 887 |
<div class="tpt-layout-configurator__app" data-tpt-lc-root> |
| 888 |
<p class="tpt-layout-configurator__loading"><?php esc_html_e( 'Loading the layout configurator…', 'tier-pricing-table' ); ?></p> |
| 889 |
</div> |
| 890 |
<?php if ( has_action( 'tiered_pricing_table/settings/table_columns/end' ) ) : ?> |
| 891 |
<?php // Custom table columns add-on. The app moves this block under the controls and shows it for table layouts. ?> |
| 892 |
<div class="tpt-layout-configurator__custom-columns" data-tpt-lc-custom-columns hidden> |
| 893 |
<?php do_action( 'tiered_pricing_table/settings/table_columns/after_fields' ); ?> |
| 894 |
<?php do_action( 'tiered_pricing_table/settings/table_columns/end' ); ?> |
| 895 |
</div> |
| 896 |
<?php endif; ?> |
| 897 |
<?php if ( ! empty( $value['desc'] ) ) : ?> |
| 898 |
<p class="description tpt-layout-configurator__desc"><?php echo esc_html( $value['desc'] ); ?></p> |
| 899 |
<?php endif; ?> |
| 900 |
</div> |
| 901 |
<table class="form-table"> |
| 902 |
<?php |
| 903 |
} |
| 904 |
} |
| 905 |
|