get_settings(); if (empty($s['enabled'])) { return; } add_action('wp_loaded', array($this, 'remove_default_meta')); add_action('woocommerce_single_product_summary', array($this, 'output'), 40); add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts')); } public function get_settings($raw = false) { $stored = get_option(self::OPTION_KEY, array()); if (!is_array($stored)) { $stored = array(); } $legacy = empty($stored) ? get_option('om_ppe_product-meta', array()) : array(); if (!is_array($legacy)) { $legacy = array(); } $merged = array_merge(array( 'enabled' => false, 'show_sku' => true, 'show_barcode' => true, 'sku_label' => '', 'sku_label_plural' => '', 'barcode_label' => '', 'barcode_label_plural' => '', 'max_visible' => 4, 'more_text' => '', 'text_color' => '', 'label_color' => '', 'link_color' => '', 'title_bold' => 'no', 'font_size' => '', 'use_shortcode' => false, 'custom_class' => '', ), $legacy, $stored); return $raw ? $merged : self::fill_i18n_defaults($merged); } private static function i18n_labels() { return array( 'sku_label' => __( 'SKU', 'ai-eshop-optimizer' ), 'sku_label_plural' => __( 'SKUs', 'ai-eshop-optimizer' ), 'barcode_label' => __( 'Barcode', 'ai-eshop-optimizer' ), 'barcode_label_plural' => __( 'Barcodes', 'ai-eshop-optimizer' ), ); } private static function fill_i18n_defaults( array $s ) { if ( ! did_action( 'init' ) ) { return $s; } foreach ( self::i18n_labels() as $k => $v ) { if ( ! isset( $s[ $k ] ) || '' === trim( (string) $s[ $k ] ) ) { $s[ $k ] = $v; } } return $s; } public function ajax_save() { AIEO_DMM::verify_ajax_request(); update_option(self::OPTION_KEY, array( 'enabled' => !empty($_POST['enabled']), 'show_sku' => !empty($_POST['show_sku']), 'show_barcode' => !empty($_POST['show_barcode']), 'sku_label' => isset($_POST['sku_label']) ? sanitize_text_field(wp_unslash($_POST['sku_label'])) : '', 'sku_label_plural' => isset($_POST['sku_label_plural']) ? sanitize_text_field(wp_unslash($_POST['sku_label_plural'])) : '', 'barcode_label' => isset($_POST['barcode_label']) ? sanitize_text_field(wp_unslash($_POST['barcode_label'])) : '', 'barcode_label_plural' => isset($_POST['barcode_label_plural']) ? sanitize_text_field(wp_unslash($_POST['barcode_label_plural'])) : '', 'max_visible' => max(1, min(20, isset($_POST['max_visible']) ? (int) $_POST['max_visible'] : 4)), 'more_text' => isset($_POST['more_text']) ? sanitize_text_field(wp_unslash($_POST['more_text'])) : '', 'text_color' => isset($_POST['text_color']) ? (sanitize_hex_color(wp_unslash($_POST['text_color'])) ?: '') : '', 'label_color' => isset($_POST['label_color']) ? (sanitize_hex_color(wp_unslash($_POST['label_color'])) ?: '') : '', 'link_color' => isset($_POST['link_color']) ? (sanitize_hex_color(wp_unslash($_POST['link_color'])) ?: '') : '', 'title_bold' => !empty($_POST['title_bold']) ? 'yes' : 'no', 'font_size' => (isset($_POST['font_size']) && preg_match('/^[0-9]*\.?[0-9]+\s*(rem|em|px|%|vw|vh|pt)$/i', trim((string) wp_unslash($_POST['font_size'])))) ? trim((string) wp_unslash($_POST['font_size'])) : '', 'use_shortcode' => !empty($_POST['use_shortcode']), 'custom_class' => isset($_POST['custom_class']) ? sanitize_html_class(wp_unslash($_POST['custom_class'])) : '', ), false); wp_send_json_success(array('message' => __('Product Meta settings saved.', 'ai-eshop-optimizer'))); } public function remove_default_meta() { remove_action('woocommerce_single_product_summary', 'woocommerce_template_single_meta', 40); } public function enqueue_scripts() { if (!function_exists('is_product') || !is_product()) { return; } $js = '(function(){function tip(b){return document.getElementById(b.getAttribute("aria-controls")||"");}function set(b,open){var t=tip(b);if(!t)return;var tp=open&&t.querySelector("template");if(tp){t.appendChild(tp.content.cloneNode(true));tp.remove();}t.classList.toggle("visible",open);b.setAttribute("aria-expanded",open?"true":"false");}function closeAll(except){document.querySelectorAll(".aieo-product-meta__more[aria-expanded=\"true\"]").forEach(function(b){if(b!==except)set(b,false);});}function toggle(b){var open=b.getAttribute("aria-expanded")!=="true";closeAll(b);set(b,open);}document.addEventListener("click",function(e){var t=e.target.nodeType===1?e.target:e.target.parentElement;if(!t)return;var b=t.closest(".aieo-product-meta__more");if(b){e.preventDefault();toggle(b);return;}if(!t.closest(".aieo-product-meta__tooltip"))closeAll(null);});document.addEventListener("keydown",function(e){var t=e.target;var b=t&&t.closest?t.closest(".aieo-product-meta__more"):null;if(b&&(e.key==="Enter"||e.key===" ")){e.preventDefault();toggle(b);return;}if(e.key==="Escape"){var o=document.querySelector(".aieo-product-meta__more[aria-expanded=\"true\"]");if(o){set(o,false);o.focus();}}});})();'; wp_register_script('aieo-product-meta', false, array(), defined('AIEO_VERSION') ? AIEO_VERSION : '1.0', true); wp_enqueue_script('aieo-product-meta'); wp_add_inline_script('aieo-product-meta', $js); self::enqueue_table(); $picks = self::picks_css('.aieo-product-meta.aieo-product-meta--codes', $this->get_settings()); if ('' !== $picks) { wp_add_inline_style('aieo-product-meta', $picks); } } const SHIPPED_COLOURS = array( 'label_color' => array('--aieo-product-meta-label', '#333333'), 'text_color' => array('--aieo-product-meta-text', '#666666'), 'link_color' => array('--aieo-product-meta-link', '#0073aa'), ); const SHIPPED_FONT_SIZE = '0.92em'; public static function colour_vars(array $s) { $vars = array(); foreach (self::SHIPPED_COLOURS as $key => $def) { $c = sanitize_hex_color(trim((string) ($s[$key] ?? ''))); if (!$c) { continue; } $long = 4 === strlen($c) ? '#' . $c[1] . $c[1] . $c[2] . $c[2] . $c[3] . $c[3] : $c; if (0 !== strcasecmp($long, $def[1])) { $vars[$def[0]] = $c; } } return $vars; } public static function picks_css($selector, array $s) { $decl = ''; foreach (self::colour_vars($s) as $name => $c) { $decl .= $name . ':' . $c . ';'; } $size = trim((string) ($s['font_size'] ?? '')); if ('' !== $size && self::SHIPPED_FONT_SIZE !== $size && preg_match('/^[0-9]*\.?[0-9]+\s*(rem|em|px|%|vw|vh|pt)$/i', $size)) { $decl .= 'font-size:' . $size . ';'; } return '' === $decl ? '' : $selector . '{' . $decl . '}'; } public static function label_bold(array $s) { return 'yes' === ($s['title_bold'] ?? ''); } public static function table_css() { return '.aieo-product-meta{display:grid;grid-template-columns:var(--aieo-meta-label-w,8.5rem) minmax(0,1fr);gap:6px var(--wp--preset--spacing--4,1rem);margin:var(--wp--preset--spacing--4,1rem) 0 0;padding:0;font-size:var(--wp--preset--font-size--sm,0.875rem);}' . '.aieo-product-meta__row{display:grid;grid-column:1/-1;grid-template-columns:subgrid;margin:0;}' . '.aieo-product-meta__label{margin:0;font-weight:normal;color:var(--aieo-product-meta-label,var(--wp--preset--color--muted,color-mix(in srgb,currentColor 70%,transparent)));}' . '.aieo-product-meta--bold .aieo-product-meta__label{font-weight:600;}' . '.aieo-product-meta__codes{position:relative;min-width:0;margin:0;color:var(--aieo-product-meta-text,currentColor);font-variant-numeric:tabular-nums;overflow-wrap:anywhere;}' . '.aieo-product-meta__code{display:inline-block;max-width:100%;}' . '.aieo-product-meta a{color:var(--aieo-product-meta-link,currentColor);text-decoration:none;}' . '.aieo-product-meta a:is(:hover,:focus-visible){text-decoration:underline;}' . '.aieo-product-meta__more{display:inline;padding-block:2px;color:var(--aieo-product-meta-link,currentColor);cursor:pointer;text-decoration:underline dotted;text-underline-offset:2px;white-space:nowrap;user-select:none;}' . '.aieo-product-meta__more:is(:hover,[aria-expanded="true"]){text-decoration-style:solid;}' . '.aieo-product-meta__more:focus-visible{outline:0;border-radius:2px;box-shadow:var(--wp--custom--rooster--focus-ring,0 0 0 2px currentColor);}' . '.aieo-product-meta__tooltip{display:none;position:absolute;inset-inline:0;top:calc(100% + 6px);z-index:10;max-height:280px;overflow:auto;padding:var(--wp--preset--spacing--2,0.5rem) var(--wp--preset--spacing--3,0.75rem);background:var(--wp--preset--color--bg,Canvas);color:var(--wp--preset--color--fg,CanvasText);border:1px solid var(--wp--preset--color--line,color-mix(in srgb,currentColor 20%,transparent));border-radius:var(--wp--custom--rooster--shape--panel,0);box-shadow:var(--wp--custom--rooster--shadow--md,none);}' . '.aieo-product-meta__tooltip.visible{display:block;}' . '.aieo-product-meta__tooltip:focus-visible{outline:0;box-shadow:var(--wp--custom--rooster--focus-ring,0 0 0 2px currentColor);}' . self::joined_css(); } public static function joined_css() { if (!class_exists('AIEO_DMM_Product_Display')) { return ''; } $parts = array( 'product_meta' => '.wp-block-aieo-post-meta', 'product_categories' => '.aieo-dmm-bridge--product_categories', 'brand_display' => '.aieo-dmm-bridge--brand_display', ); $dom = array_flip(array_keys($parts)); $zones = (array) AIEO_DMM_Product_Display::get_cluster_zones(); $zone = static function ($id) use ($zones) { return (isset($zones[$id]) && 'under' === $zones[$id]) ? 'under' : 'info'; }; $pairs = array(); $run = array(); foreach (array_values((array) AIEO_DMM_Product_Display::get_layout_order()) as $id) { $last = end($run); if (isset($parts[$id]) && false !== $last && $zone($id) === $zone($last) && $dom[$id] > $dom[$last]) { foreach ($run as $before) { $pairs[] = $parts[$before] . '+' . $parts[$id] . '>.aieo-product-meta'; } $run[] = $id; continue; } $run = isset($parts[$id]) ? array($id) : array(); } return $pairs ? implode(',', $pairs) . '{margin-top:6px;}' : ''; } public static function enqueue_table() { if (!wp_style_is('aieo-product-meta', 'registered')) { wp_register_style('aieo-product-meta', false, array(), defined('AIEO_VERSION') ? AIEO_VERSION : '1.0'); } wp_enqueue_style('aieo-product-meta'); $css = self::table_css(); if (!in_array($css, (array) wp_styles()->get_data('aieo-product-meta', 'after'), true)) { wp_add_inline_style('aieo-product-meta', $css); } } public function is_active() { $s = $this->get_settings(); return !empty($s['enabled']); } public function output() { if (!$this->is_active()) { return; } $s = $this->get_settings(); if (!empty($s['use_shortcode'])) { return; } echo $this->get_html(); } public function shortcode_handler($atts = array()) { $s = $this->get_settings(); if (empty($s['enabled']) || empty($s['use_shortcode'])) { return ''; } return $this->get_html(); } public function render_for_block($product_id = 0) { if (empty($this->get_settings()['enabled'])) { return ''; } return $this->get_html($product_id > 0 && function_exists('wc_get_product') ? wc_get_product((int) $product_id) : null); } private function get_html($product = null) { if (!$product instanceof WC_Product) { require_once dirname(__DIR__) . '/blocks/class-aieo-block-item.php'; $product = AIEO_Block_Item::current_product(); } if (!$product) { return ''; } $s = $this->get_settings(); if (empty($s['show_sku']) && empty($s['show_barcode'])) { return ''; } $is_variable = $product->is_type('variable'); $codes = $this->get_codes($product, $is_variable); if (empty($codes['sku']) && empty($codes['barcode'])) { return ''; } $cls = 'aieo-product-meta aieo-product-meta--codes' . (self::label_bold($s) ? ' aieo-product-meta--bold' : '') . (!empty($s['custom_class']) ? ' ' . (string) $s['custom_class'] : ''); $sku_label = $is_variable ? (string) $s['sku_label_plural'] : (string) $s['sku_label']; $barcode_label = $is_variable ? (string) $s['barcode_label_plural'] : (string) $s['barcode_label']; $rows = ''; if (!empty($s['show_sku']) && !empty($codes['sku'])) { $rows .= self::row_html('aieo-product-meta__row--sku', $sku_label, $this->render_codes($codes['sku'], $is_variable, $codes['sku_total'])); } if (!empty($s['show_barcode']) && !empty($codes['barcode'])) { $rows .= self::row_html('aieo-product-meta__row--barcode', $barcode_label, $this->render_codes($codes['barcode'], $is_variable, $codes['barcode_total'])); } if ('' === $rows) { return ''; } return '
' . $rows . '
'; } public static function row_html($modifier, $label, $value, $fallback = '', $dd_class = '') { $dt = '' !== trim((string) $label) ? esc_html((string) $label) : ('' !== $fallback ? '' . esc_html($fallback) . '' : ''); return '
' . '
' . $dt . '
' . '
' . $value . '
' . '
'; } private function get_codes($product, $is_variable) { global $wpdb; $sku = trim((string) $product->get_sku()); $barcode = trim((string) get_post_meta($product->get_id(), '_global_unique_id', true)); if ($barcode === '') { $barcode = trim((string) get_post_meta($product->get_id(), '_barcode', true)); } if ($is_variable) { $tbl = $wpdb->prefix . 'aieo_core_product_vitals'; $exists = (string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tbl)) === $tbl; if ($exists) { $pid = (int) $product->get_id(); $row = $wpdb->get_row($wpdb->prepare( "SELECT sku_index, barcode_index FROM {$tbl} WHERE product_id = %d AND parent_id = %d LIMIT 1", $pid, $pid ), ARRAY_A); if (!$row) { $row = $wpdb->get_row($wpdb->prepare( "SELECT sku_index, barcode_index FROM {$tbl} WHERE product_id = %d LIMIT 1", $pid ), ARRAY_A); } $sku_csv = isset($row['sku_index']) ? (string) $row['sku_index'] : ''; $bar_csv = isset($row['barcode_index']) ? (string) $row['barcode_index'] : ''; return array( 'sku' => $this->csv_to_list($sku_csv), 'barcode' => $this->csv_to_list($bar_csv), 'sku_total' => null, 'barcode_total' => null, ); } } return array( 'sku' => $sku !== '' ? array($sku) : array(), 'barcode' => $barcode !== '' ? array($barcode) : array(), 'sku_total' => null, 'barcode_total' => null, ); } private function csv_to_list($csv) { if (!is_string($csv) || $csv === '') { return array(); } return array_values(array_filter(array_map('trim', explode(',', $csv)), function($v){ return $v !== ''; })); } private function render_codes($codes, $is_variable, $total = null) { $codes = array_values(array_filter(array_unique(array_map('trim', $codes)))); if (empty($codes)) { return ''; } $s = $this->get_settings(); $max = max(1, (int) $s['max_visible']); $units = static function (array $list) { $last = count($list) - 1; $out = array(); foreach (array_values($list) as $i => $code) { $out[] = '' . esc_html($code . ($i < $last ? ',' : '')) . ''; } return implode(' ', $out); }; if (!$is_variable) { return $units($codes); } $first = array_slice($codes, 0, $max); $rest = array_slice($codes, $max); $html = $units($first); $remaining = is_numeric($total) ? max(0, (int) $total - count($first)) : count($rest); if ($remaining > 0) { $more_tpl = trim((string) $s['more_text']); if ('' === $more_tpl) { $more_tpl = _n('and %d other', 'and %d others', $remaining, 'ai-eshop-optimizer'); } $label = sprintf($more_tpl, $remaining); if (!empty($rest)) { $list = wp_unique_id('aieo-pm-more-'); $html .= ' ' . '
'; } else { $html .= ' ' . esc_html($label); } } return $html; } public function render_subtab() { $s = $this->get_settings(true); $hint = self::i18n_labels(); $form_id = 'aieo-card-product-meta-overview-form'; list($aieo_card_open, $aieo_card_close) = AIEO_DMM_UI::card(array( 'anchor' => 'product-meta-overview', 'title' => __('Labels, overflow and colours', 'ai-eshop-optimizer'), 'description' => __('Show SKU + barcode below the price on single product pages. For variable products with the Rooster One/Pods vitals table, displays an aggregated index across all variations with an "and N more" overflow pill.', 'ai-eshop-optimizer'), 'footer' => class_exists('\Rooster\Admin\States') ? \Rooster\Admin\States::save_button($form_id) : '', )); echo $aieo_card_open; $id_of = static function ($name) { return 'aieo-pm-' . str_replace('_', '-', $name); }; $row = static function ($name, $label, $control, $help = '') use ($id_of) { $field = array('id' => $id_of($name), 'label' => $label, 'control' => $control); if ('' !== $help) { $field['help'] = $help; } return \Rooster\Admin\Ui::field_row($field); }; $switch = static function ($name, $text, $hidden_label = false) use ($s, $id_of) { $on = 'title_bold' === $name ? self::label_bold($s) : !empty($s[$name]); return \Rooster\Admin\Ui::toggle($name, $on, array('id' => $id_of($name), 'hidden' => false, 'label' => $text, 'label_hidden' => $hidden_label)); }; $colour = static function ($name, $fallback) use ($s, $id_of) { return \Rooster\Admin\Fields::color($name, (string) $s[$name], array('id' => $id_of($name), 'default' => $fallback)); }; $pair = static function ($key, $label) use ($s, $hint, $id_of) { $parts = array(); foreach (array($key => __('Singular', 'ai-eshop-optimizer'), $key . '_plural' => __('Plural', 'ai-eshop-optimizer')) as $name => $sub) { $parts[] = array( 'label' => $sub, 'id' => $id_of($name), 'control' => \Rooster\Admin\Ui::input($name, (string) $s[$name], array('id' => $id_of($name), 'medium' => true, 'placeholder' => isset($hint[$name]) ? $hint[$name] : '')), ); } return \Rooster\Admin\Ui::field_row(array('id' => $id_of($key) . '-pair', 'label' => $label, 'parts' => $parts)); }; $fields = $row('enabled', __('Enable', 'ai-eshop-optimizer'), $switch('enabled', __('On', 'ai-eshop-optimizer'), true)); $fields .= $row('show_sku', __('Show SKU', 'ai-eshop-optimizer'), $switch('show_sku', __('On', 'ai-eshop-optimizer'), true)); $fields .= $row('show_barcode', __('Show barcode', 'ai-eshop-optimizer'), $switch('show_barcode', __('On', 'ai-eshop-optimizer'), true)); $fields .= $pair('sku_label', __('SKU label (singular / plural)', 'ai-eshop-optimizer')); $fields .= $pair('barcode_label', __('Barcode label (singular / plural)', 'ai-eshop-optimizer')); $fields .= $row('max_visible', __('Max visible (variable products)', 'ai-eshop-optimizer'), \Rooster\Admin\Ui::input('max_visible', (string) (int) $s['max_visible'], array('id' => $id_of('max_visible'), 'type' => 'number', 'xs' => true, 'min' => 1, 'max' => 20))); $more_hint = sprintf(_n('and %d other', 'and %d others', 2, 'ai-eshop-optimizer'), 2); $more_help = __('Use %d for the count.', 'ai-eshop-optimizer'); $fields .= $row('more_text', __('Overflow text', 'ai-eshop-optimizer'), \Rooster\Admin\Ui::input('more_text', (string) $s['more_text'], array('id' => $id_of('more_text'), 'medium' => true, 'placeholder' => $more_hint)), $more_help); $fields .= $row('text_color', __('Text colour', 'ai-eshop-optimizer'), $colour('text_color', '#666666')); $fields .= $row('label_color', __('Label colour', 'ai-eshop-optimizer'), $colour('label_color', '#333333')); $fields .= $row('link_color', __('Link colour', 'ai-eshop-optimizer'), $colour('link_color', '#0073aa'), __('Colour of the “and N more” link. Leave blank to inherit.', 'ai-eshop-optimizer')); $advanced = ''; $advanced .= $row('title_bold', __('Bold the title', 'ai-eshop-optimizer'), $switch('title_bold', __('Render the label (e.g. “SKUs:”) in bold', 'ai-eshop-optimizer'))); $advanced .= $row('font_size', __('Font size', 'ai-eshop-optimizer'), \Rooster\Admin\Ui::input('font_size', (string) $s['font_size'], array('id' => $id_of('font_size'), 'short' => true, 'placeholder' => '0.92em')), __('CSS size, e.g. 0.92em or 13px. Match Categories / Brand for a consistent info column.', 'ai-eshop-optimizer')); $advanced .= $row('custom_class', __('Custom CSS class', 'ai-eshop-optimizer'), \Rooster\Admin\Ui::input('custom_class', (string) $s['custom_class'], array('id' => $id_of('custom_class'), 'medium' => true, 'code' => true))); $advanced .= $row('use_shortcode', __('Use shortcode', 'ai-eshop-optimizer'), $switch('use_shortcode', '[aieo_product_meta]')); $fold = \Rooster\Admin\Ui::fold(__('Advanced', 'ai-eshop-optimizer'), '
' . $advanced . '
'); echo AIEO_DMM_UI::card_form('aieo_dmm_product_meta_save', $this->get_id(), $form_id, '
' . $fields . '
' . $fold); echo $aieo_card_close; } }