get_settings(); if ((string) (isset($settings['enabled']) ? $settings['enabled'] : 'no') !== 'yes') { return; } if (is_admin()) { add_action('admin_notices', array($this, 'render_cache_notice')); } add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_assets'), 20); add_action('save_post_product', array($this, 'invalidate_for_product'), 20, 1); add_action('woocommerce_update_product', array($this, 'invalidate_for_product'), 20, 1); add_action('woocommerce_new_product_variation', array($this, 'invalidate_for_variation'), 20, 1); add_action('woocommerce_update_product_variation', array($this, 'invalidate_for_variation'), 20, 1); add_action('woocommerce_delete_product_variation', array($this, 'invalidate_for_variation'), 20, 1); add_action('woocommerce_trash_product_variation', array($this, 'invalidate_for_variation'), 20, 1); add_action('edited_term', array($this, 'invalidate_on_term_edit'), 10, 3); add_action('delete_term', array($this, 'invalidate_on_term_edit'), 10, 4); add_action('update_option_' . self::SETTINGS_OPTION, array($this, 'on_settings_update'), 10, 2); add_action('update_option_' . self::LEGACY_SETTINGS_OPTION, array($this, 'on_legacy_settings_update'), 10, 2); add_action('add_option_' . self::LEGACY_SETTINGS_OPTION, array($this, 'on_legacy_settings_add'), 10, 2); if (is_admin() && !class_exists('OM_Product_Swatches_Product_Settings')) { add_filter('woocommerce_product_data_tabs', array($this, 'add_product_data_tab')); add_action('woocommerce_product_data_panels', array($this, 'render_product_data_panel')); add_action('woocommerce_process_product_meta', array($this, 'save_product_meta')); add_action('admin_enqueue_scripts', array($this, 'enqueue_product_scripts')); } } public function default_settings() { return array( 'enabled' => 'yes', 'tooltip' => true, 'mouseover' => false, 'on_shop_catalog' => true, 'display_variation_name_on_product_title' => true, 'color_width' => 24, 'color_height' => 24, 'color_gap' => 8, 'color_border_radius' => 50, 'color_border_size' => 1, 'color_border_color' => '#dedede', 'color_border_size_hover' => 2, 'color_border_color_hover' => '#212121', 'color_border_size_selected' => 2, 'color_border_color_selected' => '#212121', 'button_text_color' => '#212121', 'button_bg_color' => '#ffffff', 'button_border_color' => '#212121', 'button_border_radius' => 5, 'button_border_size' => 1, 'button_padding_top' => 8, 'button_padding_right' => 12, 'button_padding_bottom' => 8, 'button_padding_left' => 12, 'button_gap' => 8, 'image_width' => 72, 'image_height' => 72, 'image_gap' => 12, 'image_border_radius' => 0, 'image_border_size' => 1, 'image_border_color' => '#dedede', 'image_loading_strategy' => 'lazy', 'image_width_shop_archive' => 26, 'image_height_shop_archive' => 26, 'image_gap_shop_archive' => 6, 'image_width_mobile' => 56, 'image_height_mobile' => 56, 'image_width_shop_archive_mobile' => 22, 'image_height_shop_archive_mobile' => 22, 'button_padding_top_mobile' => 4, 'button_padding_right_mobile' => 8, 'button_padding_bottom_mobile' => 4, 'button_padding_left_mobile' => 8, 'button_font_size_mobile' => 11, 'button_stack_vertical_archive' => false, 'button_circle_archive' => false, 'button_circle_diameter_archive' => 36, 'button_font_size_shop_archive' => 11, 'image_swatch_limit_desktop' => 3, 'image_swatch_limit_mobile' => 3, 'color_swatch_limit_desktop' => 5, 'color_swatch_limit_mobile' => 3, 'button_swatch_limit_desktop' => 5, 'button_swatch_limit_mobile' => 3, 'multi_select_enabled' => false, 'multi_select_display_style' => 'grid', 'multi_select_button_text' => '', 'multi_select_button_position' => 'above', 'multi_select_show_quantity' => false, 'multi_select_row_order' => array('check', 'image', 'name', 'price', 'qty'), 'multi_select_row_image' => true, 'multi_select_row_price' => true, 'multi_select_row_qty' => true, 'multi_select_categories' => array(), 'cart_sync_enabled' => true, 'cart_summary_visible' => true, 'cart_variation_image' => 'swatch', 'track_per_variation_atc' => false, 'see_all_display_mode' => 'swatches_only', 'see_all_text' => '', 'see_all_modal_title' => '', 'see_all_view_product_text' => '', ); } public function get_settings() { $stored = get_option(self::SETTINGS_OPTION, null); if (!is_array($stored)) { $stored = get_option(self::LEGACY_SETTINGS_OPTION, array()); } return wp_parse_args(is_array($stored) ? $stored : array(), $this->default_settings()); } public function get($key, $default = null) { $s = $this->get_settings(); return array_key_exists($key, $s) ? $s[$key] : $default; } public function save_settings(array $patch) { $current = $this->get_settings(); $merged = array_replace($current, $patch); update_option(self::SETTINGS_OPTION, $merged, false); if (class_exists('OM_Product_Swatches')) { update_option(self::LEGACY_SETTINGS_OPTION, $merged, false); } update_option(self::SETTINGS_HASH_OPTION, md5((string) wp_json_encode($merged)), false); return $merged; } public function on_settings_update($old_value, $new_value) { if ($new_value === $old_value) { return; } update_option(self::SETTINGS_HASH_OPTION, md5((string) wp_json_encode($new_value)), false); } public function on_legacy_settings_update($old_value, $new_value) { if (!is_array($new_value)) { return; } $aieo_current = get_option(self::SETTINGS_OPTION, array()); if (is_array($aieo_current) && $aieo_current === $new_value) { return; } update_option(self::SETTINGS_OPTION, $new_value, false); update_option(self::SETTINGS_HASH_OPTION, md5((string) wp_json_encode($new_value)), false); } public function on_legacy_settings_add($option_name, $value) { if (!is_array($value)) { return; } $aieo_current = get_option(self::SETTINGS_OPTION, null); if (is_array($aieo_current) && $aieo_current === $value) { return; } update_option(self::SETTINGS_OPTION, $value, false); update_option(self::SETTINGS_HASH_OPTION, md5((string) wp_json_encode($value)), false); } public function get_settings_hash() { $h = get_option(self::SETTINGS_HASH_OPTION, ''); if ($h === '') { $h = md5((string) wp_json_encode($this->get_settings())); update_option(self::SETTINGS_HASH_OPTION, $h, false); } return $h . self::SWATCH_PAYLOAD_SUFFIX; } public static function compose_swatch_label($desc, array $attributes) { $desc = trim((string) $desc); $labels = array(); foreach ($attributes as $akey => $aval) { $aval = (string) $aval; if ($aval === '' || strpos((string) $akey, 'attribute_') !== 0) { continue; } $tax = substr((string) $akey, 10); if ($tax !== '' && taxonomy_exists($tax)) { $term = get_term_by('slug', $aval, $tax); $labels[] = ($term && !is_wp_error($term)) ? $term->name : $aval; } else { $labels[] = $aval; } } $attr_label = implode(' / ', $labels); if ($desc !== '' && $attr_label !== '') { return $desc . ' - ' . $attr_label; } return $desc !== '' ? $desc : $attr_label; } private static function variation_description($variation_id) { $variation_id = (int) $variation_id; if ($variation_id <= 0) { return ''; } $v = wc_get_product($variation_id); if (!$v || !is_a($v, 'WC_Product')) { return ''; } return trim(wp_strip_all_tags((string) $v->get_description())); } private static function cart_desc_label() { return (string) apply_filters('aieo_dmm_swatch_cart_desc_label', __('Variant', 'ai-eshop-optimizer')); } public static function cart_item_variation_description($item_data, $cart_item) { $vid = isset($cart_item['variation_id']) ? (int) $cart_item['variation_id'] : 0; if ($vid <= 0) { return $item_data; } $product = (isset($cart_item['data']) && is_a($cart_item['data'], 'WC_Product')) ? $cart_item['data'] : null; $desc = $product ? trim(wp_strip_all_tags((string) $product->get_description())) : self::variation_description($vid); if ($desc !== '') { $item_data[] = array( 'key' => self::cart_desc_label(), 'value' => $desc, 'display' => esc_html($desc), ); } return $item_data; } public static function order_item_variation_description($item, $cart_item_key, $values, $order) { $vid = isset($values['variation_id']) ? (int) $values['variation_id'] : 0; if ($vid <= 0) { return; } $product = (isset($values['data']) && is_a($values['data'], 'WC_Product')) ? $values['data'] : null; $desc = $product ? trim(wp_strip_all_tags((string) $product->get_description())) : self::variation_description($vid); if ($desc !== '') { $item->add_meta_data(self::cart_desc_label(), $desc, true); } } public function build_cache_for_product($product_id) { $product_id = (int) $product_id; if ($product_id <= 0) { return false; } $product = wc_get_product($product_id); if (!$product || !$product->is_type('variable')) { $this->delete_cache_rows($product_id); return false; } $archive = $this->build_variations_payload($product, 'archive'); $single = $this->build_variations_payload($product, 'single'); $this->write_cache_row($product_id, 'archive', $archive); $this->write_cache_row($product_id, 'single', $single); $this->write_variation_map_rows($product_id, $single['variation_data']); if (class_exists('OM_Product_Swatches_Cache')) { $this->shadow_write_legacy($product_id, 'archive', $archive); $this->shadow_write_legacy($product_id, 'single', $single); } return true; } private function build_variations_payload($product, $context) { $product_id = (int) $product->get_id(); $settings = $this->get_settings(); $hide_oos = get_option('woocommerce_hide_out_of_stock_items', 'no') === 'yes'; if ($context === 'archive') { $img_w = (int) ($settings['image_width_shop_archive'] ?? 26); $img_h = (int) ($settings['image_height_shop_archive'] ?? 26); $dlim = (int) ($settings['image_swatch_limit_desktop'] ?? 5); $mlim = (int) ($settings['image_swatch_limit_mobile'] ?? 3); $max_limit = max($dlim, $mlim); if (class_exists('AIEO_Card', false) && AIEO_Card::enabled()) { $max_limit = max($max_limit, AIEO_Card::CAP); } } else { $img_w = (int) ($settings['image_width'] ?? 72); $img_h = (int) ($settings['image_height'] ?? 72); $max_limit = 0; } $all_ids = $product->get_children(); if (!is_array($all_ids)) { $all_ids = array(); } $to_process = array(); $total_available = 0; if ($context === 'archive' && $hide_oos) { foreach ($all_ids as $vid) { $stock = get_post_meta((int) $vid, '_stock_status', true); if ($stock === 'instock' || $stock === 'onbackorder') { $to_process[] = (int) $vid; } } $total_available = count($to_process); } else { $to_process = array_map('intval', $all_ids); $total_available = count($to_process); } $variation_data = array(); $count = 0; $parent_image_id = (int) $product->get_image_id(); foreach ($to_process as $vid) { if ($context === 'archive' && $max_limit > 0 && $count >= $max_limit) { break; } $meta = get_post_meta((int) $vid); if (!is_array($meta)) { continue; } $attributes = array(); foreach ($meta as $mkey => $mval) { if (strpos($mkey, 'attribute_') === 0) { $attributes[$mkey] = isset($mval[0]) ? wc_clean($mval[0]) : ''; } } $stock_status = isset($meta['_stock_status'][0]) ? (string) $meta['_stock_status'][0] : ''; $available = ($stock_status === 'instock' || $stock_status === 'onbackorder'); $image_id = isset($meta['_thumbnail_id'][0]) ? (int) $meta['_thumbnail_id'][0] : 0; if ($image_id <= 0) { $image_id = $parent_image_id; } $img_url = ''; if ($image_id > 0) { $intermediate = image_get_intermediate_size($image_id, array($img_w, $img_h)); if (is_array($intermediate) && !empty($intermediate['url'])) { $img_url = (string) $intermediate['url']; } else { $full = wp_get_attachment_image_src($image_id, 'full'); $img_url = is_array($full) ? (string) $full[0] : ''; } } $price_html = ''; $variation_obj = wc_get_product((int) $vid); if ($variation_obj) { $price_html = (string) $variation_obj->get_price_html(); } $sku = isset($meta['_sku'][0]) ? (string) $meta['_sku'][0] : ''; $vdesc = isset($meta['_variation_description'][0]) ? trim(wp_strip_all_tags((string) $meta['_variation_description'][0])) : ''; $swatch_label = self::compose_swatch_label($vdesc, $attributes); $variation_data[] = array( 'variation_id' => (int) $vid, 'available' => $available, 'display' => '', 'base' => '', 'img_url' => $img_url, 'img_w' => $img_w, 'img_h' => $img_h, 'attributes' => $attributes, 'price_html' => $price_html, 'sku' => $sku, 'stock_status' => $stock_status, 'variation_description' => $vdesc, 'swatch_label' => $swatch_label, ); $count++; } return array( 'variation_data' => $variation_data, 'total_available' => $total_available, 'stored_count' => count($variation_data), ); } private function write_cache_row($product_id, $context, array $payload) { global $wpdb; $table = $wpdb->prefix . 'aieo_dmm_swatches_cache'; if (!aieo_dmm_table_exists($table)) { return; } $variation_data = $payload['variation_data']; $variation_ids = array_map('intval', array_column($variation_data, 'variation_id')); $map = array(); foreach ($variation_data as $v) { $hash = sha1((string) wp_json_encode($v['attributes'])); $map[$hash] = (int) $v['variation_id']; } $row = array( 'product_id' => (int) $product_id, 'context' => (string) $context, 'total_available' => (int) $payload['total_available'], 'stored_count' => (int) $payload['stored_count'], 'variations_json' => (string) wp_json_encode($variation_data), 'variation_ids_json' => (string) wp_json_encode($variation_ids), 'variations_map_json' => (string) wp_json_encode($map), 'grid_html' => null, 'cache_hash' => $this->get_settings_hash(), 'built_at' => current_time('mysql', true), ); $wpdb->query($wpdb->prepare( "INSERT INTO {$table} (product_id, context, total_available, stored_count, variations_json, variation_ids_json, variations_map_json, grid_html, cache_hash, built_at) VALUES (%d, %s, %d, %d, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE total_available = VALUES(total_available), stored_count = VALUES(stored_count), variations_json = VALUES(variations_json), variation_ids_json = VALUES(variation_ids_json), variations_map_json = VALUES(variations_map_json), grid_html = VALUES(grid_html), cache_hash = VALUES(cache_hash), built_at = VALUES(built_at)", $row['product_id'], $row['context'], $row['total_available'], $row['stored_count'], $row['variations_json'], $row['variation_ids_json'], $row['variations_map_json'], $row['grid_html'], $row['cache_hash'], $row['built_at'] )); } private function write_variation_map_rows($product_id, array $variation_data) { global $wpdb; $table = $wpdb->prefix . 'aieo_dmm_swatches_variation_map'; if (!aieo_dmm_table_exists($table)) { return; } $term_swatch_url_cache = array(); $now = current_time('mysql', true); foreach ($variation_data as $v) { $hash = sha1((string) wp_json_encode($v['attributes'] ?? array())); $swatch_url = $this->resolve_variation_swatch_image_url( $v['attributes'] ?? array(), $term_swatch_url_cache ); $wpdb->query($wpdb->prepare( "INSERT INTO {$table} (variation_id, product_id, attribute_combo_hash, image_url, swatch_image_url, sku, stock_status, updated_at) VALUES (%d, %d, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE product_id = VALUES(product_id), attribute_combo_hash = VALUES(attribute_combo_hash), image_url = VALUES(image_url), swatch_image_url = VALUES(swatch_image_url), sku = VALUES(sku), stock_status = VALUES(stock_status), updated_at = VALUES(updated_at)", (int) $v['variation_id'], (int) $product_id, $hash, (string) ($v['img_url'] ?? ''), (string) $swatch_url, (string) ($v['sku'] ?? ''), (string) ($v['stock_status'] ?? ''), $now )); } } private function resolve_variation_swatch_image_url(array $attributes, array &$cache) { foreach ($attributes as $attr_key => $attr_value) { if ($attr_value === '') { continue; } $taxonomy = strpos((string) $attr_key, 'attribute_') === 0 ? substr((string) $attr_key, 10) : (string) $attr_key; if ($taxonomy === '' || ! taxonomy_exists($taxonomy)) { continue; } $term = get_term_by('slug', (string) $attr_value, $taxonomy); if (! $term || is_wp_error($term)) { continue; } $term_id = (int) $term->term_id; if (array_key_exists($term_id, $cache)) { if ($cache[$term_id] !== '') { return $cache[$term_id]; } continue; } $image_id = (int) get_term_meta($term_id, 'product_attribute_image', true); if ($image_id <= 0) { $cache[$term_id] = ''; continue; } $src = wp_get_attachment_image_src($image_id, 'woocommerce_thumbnail'); $url = is_array($src) && ! empty($src[0]) ? (string) $src[0] : ''; $cache[$term_id] = $url; if ($url !== '') { return $url; } } return ''; } public function delete_cache_rows($product_id) { global $wpdb; $cache = $wpdb->prefix . 'aieo_dmm_swatches_cache'; $map = $wpdb->prefix . 'aieo_dmm_swatches_variation_map'; if (aieo_dmm_table_exists($cache)) { $wpdb->delete($cache, array('product_id' => (int) $product_id), array('%d')); } if (aieo_dmm_table_exists($map)) { $wpdb->delete($map, array('product_id' => (int) $product_id), array('%d')); } if (class_exists('OM_Product_Swatches_Cache')) { OM_Product_Swatches_Cache::delete_by_product_id((int) $product_id); } } private function shadow_write_legacy($product_id, $context, array $payload) { if (!class_exists('OM_Product_Swatches_Cache')) { return; } $variation_data = $payload['variation_data']; $total = count($variation_data); if ($total <= 10) { $key = "om_swatch_{$context}_{$product_id}"; OM_Product_Swatches_Cache::set($key, $variation_data, 0, $product_id, "swatch_{$context}"); if ($context === 'archive' && (int) $payload['total_available'] > $total) { $mkey = "om_swatch_{$context}_{$product_id}_meta"; OM_Product_Swatches_Cache::set($mkey, array( 'total_available' => (int) $payload['total_available'], 'stored_count' => $total, ), 0, $product_id, "swatch_{$context}_meta"); } } else { foreach ($variation_data as $v) { $key = "om_swatch_{$context}_{$product_id}_var_{$v['variation_id']}"; OM_Product_Swatches_Cache::set($key, $v, 0, $product_id, "swatch_{$context}_var"); } $index_key = "om_swatch_{$context}_{$product_id}_index"; OM_Product_Swatches_Cache::set($index_key, array( 'total' => $total, 'ids' => array_column($variation_data, 'variation_id'), 'total_available' => (int) $payload['total_available'], ), 0, $product_id, "swatch_{$context}_index"); } } public function invalidate_for_product($product_id) { $product_id = (int) $product_id; if ($product_id <= 0) { return; } $this->schedule_single_rebuild($product_id); } public function invalidate_for_variation($variation_id) { $variation_id = (int) $variation_id; if ($variation_id <= 0) { return; } $parent = wp_get_post_parent_id($variation_id); if ($parent) { $this->schedule_single_rebuild((int) $parent); } } public function invalidate_on_term_edit($term_id, $tt_id, $taxonomy, $deleted_term = null) { if (!is_string($taxonomy) || strpos($taxonomy, 'pa_') !== 0) { return; } $term = ($deleted_term instanceof WP_Term) ? $deleted_term : get_term((int) $term_id); if (!($term instanceof WP_Term) || (string) $term->slug === '') { return; } $this->pending_term_lookups['attribute_' . $taxonomy][(string) $term->slug] = true; if (!$this->term_flush_hooked) { $this->term_flush_hooked = true; add_action('shutdown', array($this, 'flush_term_rebuild_queue')); } } public function flush_term_rebuild_queue() { if (empty($this->pending_term_lookups)) { return; } $lookups = $this->pending_term_lookups; $this->pending_term_lookups = array(); global $wpdb; $parents = array(); foreach ($lookups as $meta_key => $slugs) { foreach (array_keys($slugs) as $slug) { $last = 0; do { $rows = $wpdb->get_col($wpdb->prepare( "SELECT DISTINCT p.post_parent FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE pm.meta_key = %s AND pm.meta_value = %s AND p.post_type = 'product_variation' AND p.post_parent > %d ORDER BY p.post_parent ASC LIMIT %d", $meta_key, (string) $slug, $last, self::TERM_PARENT_PAGE_SIZE )); $got = count($rows); if ($got > 0) { foreach ($rows as $pid) { $parents[(int) $pid] = true; } $last = (int) end($rows); } } while ($got === self::TERM_PARENT_PAGE_SIZE); } } foreach (array_keys($parents) as $pid) { if (function_exists('as_enqueue_async_action')) { as_enqueue_async_action(self::REBUILD_JOB_ACTION, array(array($pid)), 'aieo-dmm-swatches', true); } else { wp_schedule_single_event(time() + 1, self::REBUILD_JOB_ACTION, array(array($pid))); } } } private $request_builds = array(); private function builds_in_request() { return is_admin() && !wp_doing_cron() && !(defined('WP_CLI') && WP_CLI) && $this->cache_operational() && $this->cache_tables_exist(); } public function flush_request_builds() { $ids = array_keys($this->request_builds); $this->request_builds = array(); foreach ($ids as $i => $pid) { if ($i < self::REQUEST_BUILD_LIMIT) { $this->build_cache_for_product($pid); } else { $this->enqueue_rebuild($pid); } } } private function schedule_single_rebuild($product_id) { $product_id = (int) $product_id; if ($this->builds_in_request()) { if (empty($this->request_builds)) { add_action('shutdown', array($this, 'flush_request_builds'), 5); } $this->request_builds[$product_id] = true; return; } $this->enqueue_rebuild($product_id); } private function enqueue_rebuild($product_id) { if (function_exists('as_enqueue_async_action')) { as_enqueue_async_action(self::REBUILD_JOB_ACTION, array(array((int) $product_id)), 'aieo-dmm-swatches', true); return; } wp_schedule_single_event(time() + 1, self::REBUILD_JOB_ACTION, array(array((int) $product_id))); } public function rebuild_batch($product_ids) { if (!is_array($product_ids)) { return; } foreach ($product_ids as $pid) { $this->build_cache_for_product((int) $pid); } } public function get_stats() { global $wpdb; $cache_table = $wpdb->prefix . 'aieo_dmm_swatches_cache'; $map_table = $wpdb->prefix . 'aieo_dmm_swatches_variation_map'; $stats = array( 'cache_rows' => 0, 'map_rows' => 0, 'variable_products' => 0, 'coverage_pct' => 0, 'stale_rows' => 0, 'last_built_at' => null, 'object_cache' => wp_using_ext_object_cache(), 'runtime_enabled' => (string) get_option(self::RUNTIME_FLAG_OPTION, 'no') === 'yes', 'settings_hash' => $this->get_settings_hash(), 'legacy_plugin_active' => class_exists('OM_Product_Swatches'), ); if (aieo_dmm_table_exists($cache_table)) { $stats['cache_rows'] = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$cache_table}"); $stats['last_built_at'] = $wpdb->get_var("SELECT MAX(built_at) FROM {$cache_table}"); $hash = $this->get_settings_hash(); $stats['stale_rows'] = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$cache_table} WHERE cache_hash <> %s", $hash )); } if (aieo_dmm_table_exists($map_table)) { $stats['map_rows'] = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$map_table}"); } $stats['variable_products'] = (int) $wpdb->get_var( "SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id INNER JOIN {$wpdb->terms} t ON t.term_id = tt.term_id WHERE p.post_type = 'product' AND p.post_status = 'publish' AND tt.taxonomy = 'product_type' AND t.slug = 'variable'" ); $total_context_rows_expected = $stats['variable_products'] * 2; if ($total_context_rows_expected > 0) { $stats['coverage_pct'] = round(100 * $stats['cache_rows'] / $total_context_rows_expected, 1); } return $stats; } public function ajax_stats() { AIEO_DMM::verify_ajax_request(); wp_send_json_success($this->get_stats()); } public function ajax_save_settings() { AIEO_DMM::verify_ajax_request(); $raw = isset($_POST['settings']) && is_array($_POST['settings']) ? map_deep(wp_unslash($_POST['settings']), 'sanitize_text_field') : array(); $patch = $this->sanitize_settings($raw); $merged = $this->save_settings($patch); wp_send_json_success(array( 'settings' => $merged, 'message' => __('Settings saved.', 'ai-eshop-optimizer'), )); } public function ajax_clear() { AIEO_DMM::verify_ajax_request(); global $wpdb; $cache = $wpdb->prefix . 'aieo_dmm_swatches_cache'; $map = $wpdb->prefix . 'aieo_dmm_swatches_variation_map'; if (aieo_dmm_table_exists($cache)) { $wpdb->query("TRUNCATE TABLE {$cache}"); } if (aieo_dmm_table_exists($map)) { $wpdb->query("TRUNCATE TABLE {$map}"); } if (class_exists('OM_Product_Swatches_Cache')) { OM_Product_Swatches_Cache::delete_all(); } $this->set_cache_ready(false); wp_send_json_success(array( 'message' => __('Swatch cache cleared. Swatches are off, and the shop shows WooCommerce’s own variation pickers, until the cache is built again.', 'ai-eshop-optimizer'), 'stats' => $this->get_stats(), )); } public function ajax_rebuild_all() { AIEO_DMM::verify_ajax_request(); global $wpdb; $ids = $wpdb->get_col( "SELECT DISTINCT p.ID FROM {$wpdb->posts} p INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id INNER JOIN {$wpdb->terms} t ON t.term_id = tt.term_id WHERE p.post_type = 'product' AND p.post_status = 'publish' AND tt.taxonomy = 'product_type' AND t.slug = 'variable'" ); $ids = array_map('intval', (array) $ids); $chunks = array_chunk($ids, 100); $enqueued = 0; foreach ($chunks as $chunk) { if (function_exists('as_enqueue_async_action')) { as_enqueue_async_action(self::REBUILD_JOB_ACTION, array($chunk), 'aieo-dmm-swatches'); } else { wp_schedule_single_event(time() + ($enqueued * 5), self::REBUILD_JOB_ACTION, array($chunk)); } $enqueued++; } wp_send_json_success(array( 'total_products' => count($ids), 'chunks_enqueued' => $enqueued, 'message' => sprintf( __('Queued %1$d products for rebuild in %2$d batches of 100.', 'ai-eshop-optimizer'), count($ids), $enqueued ), )); } private function cache_operational() { if (class_exists('AIEO_DMM_Swatches_Runtime') && method_exists('AIEO_DMM_Swatches_Runtime', 'cache_operational')) { return AIEO_DMM_Swatches_Runtime::cache_operational(); } return 'yes' === get_option(self::CACHE_READY_OPTION, ''); } private function set_cache_ready($ready) { update_option(self::CACHE_READY_OPTION, $ready ? 'yes' : 'no', true); delete_transient(self::READINESS_TRANSIENT); } private function cache_tables_exist() { global $wpdb; return aieo_dmm_table_exists($wpdb->prefix . 'aieo_dmm_swatches_cache') && aieo_dmm_table_exists($wpdb->prefix . 'aieo_dmm_swatches_variation_map'); } private function products_without_cache($after = 0, $saved_before = '', $limit = 0) { global $wpdb; $cache = $wpdb->prefix . 'aieo_dmm_swatches_cache'; $where = $wpdb->prepare('p.ID > %d', (int) $after); if ('' !== $saved_before) { $where .= $wpdb->prepare(' AND p.post_modified_gmt < %s', $saved_before); } $from = "FROM {$wpdb->posts} p INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = 'product_type' INNER JOIN {$wpdb->terms} t ON t.term_id = tt.term_id AND t.slug = 'variable' WHERE p.post_type = 'product' AND p.post_status = 'publish' AND {$where} AND (NOT EXISTS (SELECT 1 FROM {$cache} c WHERE c.product_id = p.ID AND c.context = 'single') OR NOT EXISTS (SELECT 1 FROM {$cache} c WHERE c.product_id = p.ID AND c.context = 'archive'))"; if ($limit > 0) { return array_map('intval', (array) $wpdb->get_col("SELECT p.ID {$from} ORDER BY p.ID ASC LIMIT " . (int) $limit)); } return (int) $wpdb->get_var("SELECT COUNT(*) {$from}"); } public function cache_readiness() { $state = get_transient(self::READINESS_TRANSIENT); if (is_array($state) && isset($state['tables'], $state['operational'], $state['missing'])) { return $state; } $tables = $this->cache_tables_exist(); $operational = $tables && $this->cache_operational(); if ($tables && !$operational && 0 === $this->products_without_cache()) { $this->set_cache_ready(true); $operational = true; } $state = array( 'tables' => $tables, 'operational' => $operational, 'missing' => $tables ? $this->products_without_cache(0, gmdate('Y-m-d H:i:s', time() - self::BUILD_GRACE_MINUTES * MINUTE_IN_SECONDS)) : 0, ); set_transient(self::READINESS_TRANSIENT, $state, 5 * MINUTE_IN_SECONDS); return $state; } public function render_cache_notice() { if (!current_user_can('manage_woocommerce')) { return; } if ((string) get_option(self::RUNTIME_FLAG_OPTION, 'no') !== 'yes') { return; } $state = $this->cache_readiness(); if ($state['tables'] && $state['operational'] && $state['missing'] <= 0) { return; } if (!$state['tables']) { $why = __('The swatch cache table does not exist, so product pages and cards show WooCommerce’s own variation picker instead of swatches.', 'ai-eshop-optimizer'); } elseif (!$state['operational']) { $why = __('The swatch cache has not been built, so swatches are off and the shop shows WooCommerce’s own variation picker.', 'ai-eshop-optimizer'); } else { $why = sprintf( _n( '%s variable product has no swatch data yet, so it shows WooCommerce’s own variation picker instead of swatches.', '%s variable products have no swatch data yet, so they show WooCommerce’s own variation picker instead of swatches.', $state['missing'], 'ai-eshop-optimizer' ), number_format_i18n($state['missing']) ); } $i18n = array( 'starting' => __('Building…', 'ai-eshop-optimizer'), 'failed' => __('Not built:', 'ai-eshop-optimizer'), 'network' => __('Network error.', 'ai-eshop-optimizer'), ); ?>

__('You are not allowed to build the swatch data.', 'ai-eshop-optimizer')), 403); } global $wpdb; if (!$this->cache_tables_exist()) { if (!function_exists('aieo_dmm_create_swatches_cache_table')) { require_once AIEO_DIR . 'includes/dmm/sql/dmm-schema.php'; } aieo_dmm_create_swatches_cache_table(); aieo_dmm_create_swatches_variation_map_table(); if (!$this->cache_tables_exist()) { wp_send_json_error(array('message' => sprintf( __('The swatch tables could not be created: %s', 'ai-eshop-optimizer'), '' !== (string) $wpdb->last_error ? (string) $wpdb->last_error : __('the database gave no reason.', 'ai-eshop-optimizer') ))); } } $after = isset($_POST['after']) ? absint(wp_unslash($_POST['after'])) : 0; $done = isset($_POST['built']) ? absint(wp_unslash($_POST['built'])) : 0; $started = microtime(true); $built = 0; foreach ($this->products_without_cache($after, '', 25) as $pid) { $this->build_cache_for_product($pid); $built++; $after = $pid; if (microtime(true) - $started > 8) { break; } } $done += $built; delete_transient(self::READINESS_TRANSIENT); $remaining = $built > 0 ? $this->products_without_cache($after) : 0; if ($remaining > 0) { wp_send_json_success(array( 'done' => false, 'after' => $after, 'built' => $done, 'message' => sprintf( __('Built %1$s of %2$s products…', 'ai-eshop-optimizer'), number_format_i18n($done), number_format_i18n($done + $remaining) ), )); } $this->set_cache_ready(true); $left = $this->products_without_cache(); $message = sprintf( _n('Swatch data built for %s product. Its pages show the swatches now.', 'Swatch data built for %s products. Their pages show the swatches now.', $done, 'ai-eshop-optimizer'), number_format_i18n($done) ); if ($left > 0) { $message .= ' ' . sprintf( _n('%s product could not be built.', '%s products could not be built.', $left, 'ai-eshop-optimizer'), number_format_i18n($left) ); } wp_send_json_success(array('done' => true, 'built' => $done, 'left' => $left, 'message' => $message)); } public function ajax_migrate_legacy() { AIEO_DMM::verify_ajax_request(); $result = $this->migrate_legacy(); wp_send_json_success(array_merge( $result, array('message' => sprintf( __('Migrated %d products from the legacy swatches cache.', 'ai-eshop-optimizer'), (int) $result['migrated'] )) )); } public function ajax_get_categories() { AIEO_DMM::verify_ajax_request(); $terms = get_terms(array( 'taxonomy' => 'product_cat', 'hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC', )); $out = array(); if (!is_wp_error($terms)) { foreach ($terms as $t) { $out[] = array( 'id' => (int) $t->term_id, 'name' => (string) $t->name, 'count' => (int) $t->count, ); } } wp_send_json_success($out); } public function ajax_toggle_runtime() { AIEO_DMM::verify_ajax_request(); $enable = !empty($_POST['enabled']); update_option(self::RUNTIME_FLAG_OPTION, $enable ? 'yes' : 'no', false); wp_send_json_success(array( 'runtime_enabled' => $enable, 'message' => $enable ? __('AIEO runtime renderer enabled. The legacy om-product-swatches frontend will step aside.', 'ai-eshop-optimizer') : __('AIEO runtime disabled. The legacy om-product-swatches frontend is authoritative.', 'ai-eshop-optimizer'), )); } public function migrate_legacy() { $result = array('migrated' => 0, 'skipped' => 0, 'errors' => 0, 'options_migrated' => false); if (get_option(self::SETTINGS_OPTION, null) === null) { $legacy_settings = get_option(self::LEGACY_SETTINGS_OPTION, null); if (is_array($legacy_settings)) { update_option(self::SETTINGS_OPTION, $legacy_settings, false); update_option(self::SETTINGS_HASH_OPTION, md5((string) wp_json_encode($legacy_settings)), false); $result['options_migrated'] = true; } } global $wpdb; $ids = $wpdb->get_col( "SELECT DISTINCT p.ID FROM {$wpdb->posts} p INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id INNER JOIN {$wpdb->terms} t ON t.term_id = tt.term_id WHERE p.post_type = 'product' AND p.post_status = 'publish' AND tt.taxonomy = 'product_type' AND t.slug = 'variable'" ); $ids = array_map('intval', (array) $ids); $chunks = array_chunk($ids, 100); foreach ($chunks as $chunk) { if (function_exists('as_enqueue_async_action')) { as_enqueue_async_action(self::REBUILD_JOB_ACTION, array($chunk), 'aieo-dmm-swatches'); } else { wp_schedule_single_event(time() + 1, self::REBUILD_JOB_ACTION, array($chunk)); } $result['migrated'] += count($chunk); } return $result; } private function sanitize_settings(array $input) { $bool_keys = array( 'enabled', 'tooltip', 'mouseover', 'on_shop_catalog', 'display_variation_name_on_product_title', 'multi_select_enabled', 'multi_select_show_quantity', 'multi_select_row_image', 'multi_select_row_price', 'multi_select_row_qty', 'cart_sync_enabled', 'cart_summary_visible', 'track_per_variation_atc', 'button_stack_vertical_archive', 'button_circle_archive', ); $int_keys = array( 'color_width', 'color_height', 'color_gap', 'color_border_radius', 'color_border_size', 'color_border_size_hover', 'color_border_size_selected', 'color_width_shop_archive', 'color_height_shop_archive', 'color_gap_shop_archive', 'color_border_radius_shop_archive', 'color_border_size_shop_archive', 'color_border_size_hover_shop_archive', 'color_border_size_selected_shop_archive', 'button_border_radius', 'button_border_size', 'button_padding_top', 'button_padding_right', 'button_padding_bottom', 'button_padding_left', 'button_gap', 'button_border_radius_shop_archive', 'button_border_size_shop_archive', 'button_padding_top_shop_archive', 'button_padding_right_shop_archive', 'button_padding_bottom_shop_archive', 'button_padding_left_shop_archive', 'button_gap_shop_archive', 'image_width', 'image_height', 'image_gap', 'image_border_radius', 'image_border_size', 'image_border_size_hover', 'image_border_size_selected', 'image_width_shop_archive', 'image_height_shop_archive', 'image_gap_shop_archive', 'image_border_radius_shop_archive', 'image_border_size_shop_archive', 'image_border_size_hover_shop_archive', 'image_border_size_selected_shop_archive', 'image_width_mobile', 'image_height_mobile', 'image_width_shop_archive_mobile', 'image_height_shop_archive_mobile', 'button_padding_top_mobile', 'button_padding_right_mobile', 'button_padding_bottom_mobile', 'button_padding_left_mobile', 'button_font_size_mobile', 'button_circle_diameter_archive', 'button_font_size_shop_archive', 'image_swatch_limit_desktop', 'image_swatch_limit_mobile', 'color_swatch_limit_desktop', 'color_swatch_limit_mobile', 'button_swatch_limit_desktop', 'button_swatch_limit_mobile', ); $color_keys = array( 'color_border_color', 'color_border_color_hover', 'color_border_color_selected', 'color_border_color_shop_archive', 'color_border_color_hover_shop_archive', 'color_border_color_selected_shop_archive', 'button_text_color', 'button_text_color_hover', 'button_text_color_selected', 'button_bg_color', 'button_bg_color_hover', 'button_bg_color_selected', 'button_border_color','button_border_color_hover','button_border_color_selected', 'button_text_color_shop_archive', 'button_text_color_hover_shop_archive', 'button_text_color_selected_shop_archive', 'button_bg_color_shop_archive', 'button_bg_color_hover_shop_archive', 'button_bg_color_selected_shop_archive', 'button_border_color_shop_archive','button_border_color_hover_shop_archive','button_border_color_selected_shop_archive', 'image_border_color', 'image_border_color_hover', 'image_border_color_selected', 'image_border_color_shop_archive', 'image_border_color_hover_shop_archive', 'image_border_color_selected_shop_archive', ); $enum_keys = array( 'image_loading_strategy' => array('lazy', 'eager', 'auto', 'swap'), 'multi_select_display_style' => array('grid', 'dropdown'), 'multi_select_button_position' => array('above', 'below'), 'cart_variation_image' => array('product', 'variation', 'gallery', 'swatch'), 'see_all_display_mode' => array('swatches_only', 'text_only', 'both'), ); $text_keys = array( 'multi_select_button_text', 'see_all_text', 'see_all_modal_title', 'see_all_view_product_text', ); $out = array(); foreach ($bool_keys as $k) { if (!array_key_exists($k, $input)) { continue; } if ($k === 'enabled') { $out[$k] = (isset($input[$k]) && ($input[$k] === 'yes' || !empty($input[$k]))) ? 'yes' : 'no'; } else { $out[$k] = !empty($input[$k]); } } foreach ($int_keys as $k) { if (isset($input[$k])) { $out[$k] = absint($input[$k]); } } foreach ($color_keys as $k) { if (!isset($input[$k])) { continue; } $raw = (string) $input[$k]; $hex = function_exists('sanitize_hex_color') ? sanitize_hex_color($raw) : null; if ($hex !== null && $hex !== '') { $out[$k] = $hex; } elseif (preg_match('/^[a-zA-Z_][a-zA-Z0-9_\-\(\), \.%]*$/', $raw)) { $out[$k] = sanitize_text_field($raw); } } foreach ($enum_keys as $k => $allowed) { if (isset($input[$k]) && in_array($input[$k], $allowed, true)) { $out[$k] = $input[$k]; } } foreach ($text_keys as $k) { if (isset($input[$k])) { $out[$k] = sanitize_text_field($input[$k]); } } foreach (array('multi_select_categories', 'dropdown_style_categories', 'grid_style_categories', 'quantity_selector_categories') as $cat_key) { if (isset($input[$cat_key]) && is_array($input[$cat_key])) { $out[$cat_key] = array_values(array_unique(array_map('absint', array_filter($input[$cat_key])))); } else { $out[$cat_key] = array(); } } if (isset($input['multi_select_row_order']) && is_array($input['multi_select_row_order'])) { $parts = array('check', 'image', 'name', 'price', 'qty'); $order = array_values(array_intersect(array_map('strval', $input['multi_select_row_order']), $parts)); $out['multi_select_row_order'] = array_values(array_unique(array_merge($order, $parts))); } return $out; } public function render_subtab() { $this->render_template('swatches.php', array( 'module' => $this, 'settings' => $this->get_settings(), 'stats' => $this->get_stats(), )); } public function enqueue_frontend_assets() { if (is_admin()) { return; } if ((string) get_option(self::RUNTIME_FLAG_OPTION, 'no') !== 'yes') { return; } if (!$this->cache_operational()) { return; } if (defined('AIEO_RUNTIME_OWNS_DISPLAY')) { return; } if (method_exists('AIEO_DMM', 'is_iapi_enabled') && AIEO_DMM::is_iapi_enabled('swatches')) { return; } if (!function_exists('is_woocommerce')) { return; } $aieo_swatches_ctx = is_woocommerce() || is_cart() || (is_search() && ('product' === get_query_var('post_type') || is_post_type_archive('product'))); if (!apply_filters('aieo_swatches_assets_in_context', $aieo_swatches_ctx)) { return; } wp_dequeue_style('om-product-swatches'); wp_dequeue_script('om-product-swatches'); wp_deregister_style('om-product-swatches'); wp_deregister_script('om-product-swatches'); $css_path = AIEO_DIR . 'assets/css/aieo-swatches.css'; $js_path = AIEO_DIR . 'assets/js/aieo-swatches.js'; $css_ver = file_exists($css_path) ? (string) filemtime($css_path) : AIEO_VERSION; $js_ver = file_exists($js_path) ? (string) filemtime($js_path) : AIEO_VERSION; wp_enqueue_style( 'aieo-swatches', AIEO_URI . 'assets/css/aieo-swatches.css', array(), $css_ver ); wp_enqueue_script( 'aieo-swatches', AIEO_URI . 'assets/js/aieo-swatches.js', array('jquery'), $js_ver, array('in_footer' => true, 'strategy' => 'defer') ); $card_js_path = AIEO_DIR . 'assets/js/aieo-card-swatches.js'; if (file_exists($card_js_path)) { wp_enqueue_script( 'aieo-card-swatches', AIEO_URI . 'assets/js/aieo-card-swatches.js', array(), (string) filemtime($card_js_path), array('in_footer' => true, 'strategy' => 'async') ); } wp_add_inline_script( 'aieo-swatches', 'window.botigaInCartFlag = window.botigaInCartFlag || { variations_in_cart: [] };', 'before' ); $settings = get_option(self::SETTINGS_OPTION, array()); if (!is_array($settings)) { $settings = array(); } $loc = array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('aieo_swatches_frontend'), 'legacyNonce' => wp_create_nonce('om_product_swatches_frontend'), 'settings' => $settings, 'actionPrefix' => 'aieo_swatch_', 'currency' => array( 'symbol' => html_entity_decode(get_woocommerce_currency_symbol(), ENT_QUOTES), 'position' => get_option('woocommerce_currency_pos', 'left'), 'decimalSep' => wc_get_price_decimal_separator(), 'thousandSep' => wc_get_price_thousand_separator(), 'decimals' => wc_get_price_decimals(), ), ); wp_localize_script('aieo-swatches', 'omProductSwatches', $loc); wp_localize_script('aieo-swatches', 'aieoSwatches', $loc); $is_mobile = AIEO_Device::is_mobile(); $image_limit = $is_mobile ? (int) ($settings['image_swatch_limit_mobile'] ?? 3) : (int) ($settings['image_swatch_limit_desktop'] ?? 5); $color_limit = $is_mobile ? (int) ($settings['color_swatch_limit_mobile'] ?? 3) : (int) ($settings['color_swatch_limit_desktop'] ?? 5); $button_limit = $is_mobile ? (int) ($settings['button_swatch_limit_mobile'] ?? 3) : (int) ($settings['button_swatch_limit_desktop'] ?? 5); $multi_select_categories = isset($settings['multi_select_categories']) ? array_map('absint', (array) $settings['multi_select_categories']) : array(); $cart_sync_enabled = !empty($settings['cart_sync_enabled']); $script_data = array( 'customizerOptions' => array( 'tooltip' => !empty($settings['tooltip']), 'displayVariationNameOnProductTitle' => !empty($settings['display_variation_name_on_product_title']), 'imageSwatchLimitShopArchive' => $image_limit, 'colorSwatchLimitShopArchive' => $color_limit, 'buttonSwatchLimitShopArchive' => $button_limit, 'imageLoadingStrategy' => (string) ($settings['image_loading_strategy'] ?? 'lazy'), 'moreVariationsLabel' => esc_html__('View %d more variations', 'ai-eshop-optimizer'), 'multiSelectEnabled' => !empty($settings['multi_select_enabled']), 'multiSelectDisplayStyle' => (string) ($settings['multi_select_display_style'] ?? 'grid'), 'multiSelectButtonText' => self::copy_or($settings['multi_select_button_text'] ?? '', __('Add Selected to Cart', 'ai-eshop-optimizer')), 'multiSelectButtonPosition' => (string) ($settings['multi_select_button_position'] ?? 'above'), 'multiSelectShowQuantity' => !empty($settings['multi_select_show_quantity']), 'multiSelectCategories' => $multi_select_categories, 'cartSyncEnabled' => $cart_sync_enabled, 'cartSummaryVisible' => !empty($settings['cart_summary_visible']), 'seeAllDisplayMode' => (string) ($settings['see_all_display_mode'] ?? 'swatches_only'), 'seeAllText' => self::copy_or($settings['see_all_text'] ?? '', __('See all {count} variations', 'ai-eshop-optimizer')), 'seeAllModalTitle' => self::copy_or($settings['see_all_modal_title'] ?? '', __('All Variations', 'ai-eshop-optimizer')), 'seeAllViewProductText' => self::copy_or($settings['see_all_view_product_text'] ?? '', __('View product page', 'ai-eshop-optimizer')), ), 'ajax_url' => admin_url('admin-ajax.php'), 'security' => wp_create_nonce('aieo_swatches_frontend'), 'i18n' => array( 'selected' => __('selected', 'ai-eshop-optimizer'), 'variation' => __('variation', 'ai-eshop-optimizer'), 'variations' => __('variations', 'ai-eshop-optimizer'), 'item' => __('item', 'ai-eshop-optimizer'), 'items' => __('items', 'ai-eshop-optimizer'), 'subtotal' => __('Subtotal', 'ai-eshop-optimizer'), 'quantityOf' => __('Quantity of %s', 'ai-eshop-optimizer'), 'itemAdded' => __('1 item added to the cart', 'ai-eshop-optimizer'), 'cartUpdated' => __('Cart updated', 'ai-eshop-optimizer'), 'itemsAdded' => __('%d items added to the cart', 'ai-eshop-optimizer'), 'selectVariations' => __('Select variations', 'ai-eshop-optimizer'), 'loadingVariations' => __('Loading variations...', 'ai-eshop-optimizer'), 'noVariations' => __('No variations available', 'ai-eshop-optimizer'), 'failedToLoad' => __('Failed to load variations', 'ai-eshop-optimizer'), 'couldNotAdd' => __('Could not add items to cart.', 'ai-eshop-optimizer'), 'networkError' => __('Network error. Please try again.', 'ai-eshop-optimizer'), 'promptMessage' => apply_filters('aieo_swatches_prompt_message', (string) (!empty($settings['multi_select_prompt_message']) ? $settings['multi_select_prompt_message'] : __('Select the variations you want to add to your cart', 'ai-eshop-optimizer'))), 'successMessage' => apply_filters('aieo_swatches_success_message', (string) (!empty($settings['multi_select_success_message']) ? $settings['multi_select_success_message'] : __('Great! {count} items added to your cart', 'ai-eshop-optimizer'))), 'clearSelections' => __('Clear selections', 'ai-eshop-optimizer'), 'addingToCart' => __('Adding...', 'ai-eshop-optimizer'), 'addedToCart' => __('Added!', 'ai-eshop-optimizer'), 'addToCart' => __('Add to cart', 'ai-eshop-optimizer'), 'updateCart' => __('Update cart', 'ai-eshop-optimizer'), 'outOfStock' => __('Out of stock', 'ai-eshop-optimizer'), 'close' => __('Close', 'ai-eshop-optimizer'), 'inCart' => __('in cart', 'ai-eshop-optimizer'), 'in' => __('in', 'ai-eshop-optimizer'), 'viewDetails' => __('View details', 'ai-eshop-optimizer'), 'hideDetails' => __('Hide details', 'ai-eshop-optimizer'), 'variationOf' => __('variation of this product', 'ai-eshop-optimizer'), 'variationsOf' => __('variations of this product', 'ai-eshop-optimizer'), 'viewProductPage' => __('View product page', 'ai-eshop-optimizer'), ), 'cartVariations' => array(), ); if ($cart_sync_enabled && function_exists('WC') && WC()->cart) { $cart_variations = array(); foreach (WC()->cart->get_cart() as $cart_item_key => $cart_item) { $pid = isset($cart_item['product_id']) ? (int) $cart_item['product_id'] : 0; $vid = isset($cart_item['variation_id']) ? (int) $cart_item['variation_id'] : 0; $qty = isset($cart_item['quantity']) ? (int) $cart_item['quantity'] : 1; if ($pid <= 0 || $vid <= 0) { continue; } if (!isset($cart_variations[$pid])) { $cart_variations[$pid] = array(); } if (isset($cart_variations[$pid][$vid])) { $cart_variations[$pid][$vid]['qty'] += $qty; } else { $cart_variations[$pid][$vid] = array( 'qty' => $qty, 'name' => '', 'cart_item_key' => (string) $cart_item_key, ); } } $script_data['cartVariations'] = $cart_variations; } wp_localize_script('aieo-swatches', 'omSwatchSettings', $script_data); } public function enqueue_admin_assets() { } public function add_product_data_tab($tabs) { $tabs['aieo_swatches'] = array( 'label' => __('Swatches', 'ai-eshop-optimizer'), 'target' => 'aieo_swatches_product_data', 'class' => array('show_if_variable'), 'priority' => 65, ); return $tabs; } public function render_product_data_panel() { global $post; if (!$post || get_post_type($post) !== 'product') { return; } $product_id = (int) $post->ID; $display_style = (string) get_post_meta($product_id, '_om_swatch_display_style', true); $multi_select = (string) get_post_meta($product_id, '_om_swatch_multi_select', true); $show_quantity = (string) get_post_meta($product_id, '_om_swatch_show_quantity', true); ?> array('', 'grid', 'dropdown'), '_om_swatch_multi_select' => array('', 'yes', 'no'), '_om_swatch_show_quantity' => array('', 'yes', 'no'), ); foreach ($fields as $meta_key => $allowed) { $raw = isset($_POST[$meta_key]) ? sanitize_text_field(wp_unslash($_POST[$meta_key])) : ''; if (!in_array($raw, $allowed, true)) { continue; } if ($raw === '') { delete_post_meta($product_id, $meta_key); } else { update_post_meta($product_id, $meta_key, $raw); } } } public function enqueue_product_scripts($hook) { if (!in_array($hook, array('post.php', 'post-new.php'), true)) { return; } global $post; if (!$post || $post->post_type !== 'product') { return; } wp_add_inline_style('woocommerce_admin_styles', '#woocommerce-product-data ul.wc-tabs li.aieo_swatches_options a::before{content:"\f111";font-family:dashicons;}' . '#woocommerce-product-data .aieo-sw-hierarchy{padding:0 12px 12px;}' ); } public function render_custom_attribute_terms_select($attribute_taxonomy, $index, $attribute) { if (!$attribute_taxonomy || !is_object($attribute_taxonomy)) { return; } $type = isset($attribute_taxonomy->attribute_type) ? (string) $attribute_taxonomy->attribute_type : ''; if (!in_array($type, array('color', 'image', 'button'), true)) { return; } if (!is_object($attribute) || !method_exists($attribute, 'is_taxonomy') || !$attribute->is_taxonomy()) { return; } $term_limit = absint(apply_filters('woocommerce_admin_terms_metabox_datalimit', 50)); $orderby = !empty($attribute_taxonomy->attribute_orderby) ? $attribute_taxonomy->attribute_orderby : 'name'; $taxonomy = $attribute->get_taxonomy(); $selected = $attribute->get_terms(); ?> prefix . 'aieo_dmm_color_attributes'; } public function attribute_is_colour($taxonomy) { global $wpdb; $taxonomy = (string) $taxonomy; if ($taxonomy === '') { return false; } $table = $this->colour_attributes_table(); return (bool) $wpdb->get_var($wpdb->prepare("SELECT 1 FROM {$table} WHERE taxonomy = %s", $taxonomy)); } public function render_attribute_colour_flag_add() { ?>

attribute_is_colour($taxonomy); ?> />

store_attribute_colour_flag('pa_' . $name, (int) $id, $label); } public function save_attribute_colour_flag_updated($id, $data, $old_taxonomy) { $name = isset($data['attribute_name']) ? (string) $data['attribute_name'] : ''; $new_taxonomy = $name !== '' ? 'pa_' . $name : ''; if ($old_taxonomy && $old_taxonomy !== $new_taxonomy) { $this->delete_attribute_colour_flag((string) $old_taxonomy); } if ($new_taxonomy === '') { return; } $label = isset($data['attribute_label']) && $data['attribute_label'] !== '' ? (string) $data['attribute_label'] : $name; $this->store_attribute_colour_flag($new_taxonomy, (int) $id, $label); } private function store_attribute_colour_flag($taxonomy, $attribute_id, $label) { global $wpdb; $is_colour = isset($_POST['aieo_attribute_is_colour']) && (string) wp_unslash($_POST['aieo_attribute_is_colour']) === '1'; if (!$is_colour) { $this->delete_attribute_colour_flag($taxonomy); return; } $table = $this->colour_attributes_table(); $wpdb->query($wpdb->prepare( "INSERT INTO {$table} (taxonomy, attribute_id, attribute_label) VALUES (%s, %d, %s) ON DUPLICATE KEY UPDATE attribute_id = VALUES(attribute_id), attribute_label = VALUES(attribute_label)", (string) $taxonomy, (int) $attribute_id, (string) $label )); } private function delete_attribute_colour_flag($taxonomy) { global $wpdb; $wpdb->delete($this->colour_attributes_table(), array('taxonomy' => (string) $taxonomy), array('%s')); } public function get_colour_flag_count() { global $wpdb; $table = $this->colour_attributes_table(); return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}"); } public function ajax_bulk_colour_flags() { AIEO_DMM::verify_ajax_request(); global $wpdb; $op = isset($_POST['op']) ? sanitize_key(wp_unslash($_POST['op'])) : ''; $table = $this->colour_attributes_table(); $wc = $wpdb->prefix . 'woocommerce_attribute_taxonomies'; if ($op === 'flag_image' || $op === 'flag_image_button') { $types = ($op === 'flag_image_button') ? "'image','button'" : "'image'"; $wpdb->query( "INSERT IGNORE INTO {$table} (taxonomy, attribute_id, attribute_label) SELECT CONCAT('pa_', attribute_name), attribute_id, attribute_label FROM {$wc} WHERE attribute_type IN ({$types})" ); $msg = __('Swatch attributes flagged as colour.', 'ai-eshop-optimizer'); } elseif ($op === 'clear') { $wpdb->query("DELETE FROM {$table}"); $msg = __('All colour flags cleared.', 'ai-eshop-optimizer'); } else { wp_send_json_error(array('message' => __('Unknown operation.', 'ai-eshop-optimizer'))); } wp_send_json_success(array( 'message' => $msg, 'flagged_count' => $this->get_colour_flag_count(), )); } public function available_attribute_types($types) { if (!is_array($types)) { $types = array(); } if (!isset($types['select'])) { $types['select'] = __('Select', 'ai-eshop-optimizer'); } $types['image'] = __('Image', 'ai-eshop-optimizer'); $types['button'] = __('Button', 'ai-eshop-optimizer'); if (!isset($types['wc-visual'])) { $types['color'] = __('Color', 'ai-eshop-optimizer'); } return $types; } public function register_term_admin_hooks() { if (!function_exists('wc_get_attribute_taxonomies')) { return; } $taxes = wc_get_attribute_taxonomies(); if (empty($taxes)) { return; } foreach ($taxes as $att_tax) { $tax_slug = 'pa_' . $att_tax->attribute_name; add_action($tax_slug . '_add_form_fields', array($this, 'render_term_swatch_fields_add')); add_action($tax_slug . '_edit_form_fields', array($this, 'render_term_swatch_fields_edit'), 10, 2); } } public function save_term_swatch_fields($term_id, $tt_id, $taxonomy) { if (strpos((string) $taxonomy, 'pa_') !== 0) { return; } $att = $this->get_attribute_taxonomy_by_slug($taxonomy); if (!$att) { return; } $type = (string) $att->attribute_type; $term_id = (int) $term_id; if ($type === 'wc-visual' && !isset($_POST['tag-color']) && !isset($_POST['tag-color-type'])) { return; } if ($type === 'color' || $type === 'wc-visual') { $color_type = isset($_POST['tag-color-type']) ? sanitize_text_field(wp_unslash($_POST['tag-color-type'])) : 'background'; $bg_color = isset($_POST['tag-color']) ? sanitize_text_field(wp_unslash($_POST['tag-color'])) : ''; $grad_c1 = isset($_POST['tag-color-gradient-color-1']) ? sanitize_text_field(wp_unslash($_POST['tag-color-gradient-color-1'])) : ''; $grad_c2 = isset($_POST['tag-color-gradient-color-2']) ? sanitize_text_field(wp_unslash($_POST['tag-color-gradient-color-2'])) : ''; $grad_dir = isset($_POST['tag-color-gradient-direction']) ? sanitize_text_field(wp_unslash($_POST['tag-color-gradient-direction'])) : ''; $multi_dir = isset($_POST['tag-color-multiple-direction']) ? sanitize_text_field(wp_unslash($_POST['tag-color-multiple-direction'])) : ''; $multi_colors = array_filter(array( isset($_POST['tag-color-multiple-color-1']) ? sanitize_text_field(wp_unslash($_POST['tag-color-multiple-color-1'])) : '', isset($_POST['tag-color-multiple-color-2']) ? sanitize_text_field(wp_unslash($_POST['tag-color-multiple-color-2'])) : '', isset($_POST['tag-color-multiple-color-3']) ? sanitize_text_field(wp_unslash($_POST['tag-color-multiple-color-3'])) : '', isset($_POST['tag-color-multiple-color-4']) ? sanitize_text_field(wp_unslash($_POST['tag-color-multiple-color-4'])) : '', )); $canonical_hex = ''; if ($color_type === 'gradient' && $grad_c1 !== '') { $canonical_hex = $grad_c1; } elseif ($color_type === 'multiple' && !empty($multi_colors)) { $canonical_hex = reset($multi_colors); } elseif ($bg_color !== '') { $canonical_hex = $bg_color; } $sanitised = $canonical_hex !== '' ? sanitize_hex_color($canonical_hex) : ''; if ($sanitised) { update_term_meta($term_id, 'color', $sanitised); } else { delete_term_meta($term_id, 'color'); } if ($color_type !== '' && $color_type !== 'background') { update_term_meta($term_id, '_aieo_swatch_color_type', $color_type); } else { delete_term_meta($term_id, '_aieo_swatch_color_type'); } if ($color_type === 'gradient' && ($grad_c1 !== '' || $grad_c2 !== '')) { update_term_meta($term_id, '_aieo_swatch_gradient', array( 'color_1' => $grad_c1, 'color_2' => $grad_c2, 'direction' => $grad_dir, )); } else { delete_term_meta($term_id, '_aieo_swatch_gradient'); } if ($color_type === 'multiple' && !empty($multi_colors)) { update_term_meta($term_id, '_aieo_swatch_multiple', array( 'colors' => array_values($multi_colors), 'direction' => $multi_dir, )); } else { delete_term_meta($term_id, '_aieo_swatch_multiple'); } } elseif ($type === 'image') { $image_id = isset($_POST['tag-image']) ? absint($_POST['tag-image']) : 0; if ($image_id > 0) { update_term_meta($term_id, '_aieo_swatch_image_id', $image_id); } else { delete_term_meta($term_id, '_aieo_swatch_image_id'); } } } public function render_term_swatch_fields_add($taxonomy) { $att = $this->get_attribute_taxonomy_by_slug($taxonomy); if ($att) { $this->render_term_swatch_fields($att->attribute_type, $att->attribute_label, null, false); } } public function render_term_swatch_fields_edit($term, $taxonomy) { $att = $this->get_attribute_taxonomy_by_slug($taxonomy); if ($att) { $this->render_term_swatch_fields($att->attribute_type, $att->attribute_label, $term, true); } } private function render_term_swatch_fields($type, $label, $term, $is_edit) { if ($type !== 'color' && $type !== 'image') { return; } $wrap_tag = $is_edit ? 'tr' : 'div'; $label_tag = $is_edit ? 'th' : 'label'; $field_tag = $is_edit ? 'td' : 'div'; echo '<' . $wrap_tag . ' class="form-field aieo-swatch-form-field aieo-swatch-form-field--' . esc_attr($type) . '">'; echo '<' . $label_tag . '>' . esc_html($label) . ''; echo '<' . $field_tag . '>'; if ($type === 'color') { $this->render_term_swatch_color_fields($term); } else { $this->render_term_swatch_image_field($term); } echo ''; echo ''; } private function render_term_swatch_color_fields($term) { $color_types = array( 'background' => __('Background', 'ai-eshop-optimizer'), 'gradient' => __('Gradient', 'ai-eshop-optimizer'), 'multiple' => __('Multiple Colours', 'ai-eshop-optimizer'), ); $directions = array( 'to bottom' => '↓', 'to right' => '→', '135deg' => '↘', '-135deg' => '↙', ); $all_meta = $term ? get_term_meta($term->term_id) : array(); $g = static function ($k) use ($all_meta) { return isset($all_meta[$k][0]) ? (string) $all_meta[$k][0] : ''; }; $color_type = $g('product_attribute_color_type'); $color = $g('product_attribute_color'); $g_c1 = $g('product_attribute_color_gradient_color_1'); $g_c2 = $g('product_attribute_color_gradient_color_2'); $g_dir = $g('product_attribute_color_gradient_direction'); $m_c1 = $g('product_attribute_color_multiple_color_1'); $m_c2 = $g('product_attribute_color_multiple_color_2'); $m_c3 = $g('product_attribute_color_multiple_color_3'); $m_c4 = $g('product_attribute_color_multiple_color_4'); $m_dir = $g('product_attribute_color_multiple_direction'); $shown_type = $color_type !== '' ? $color_type : 'background'; $hidden = static function ($want) use ($shown_type) { return $shown_type === $want ? '' : ' hidden'; }; ?>








term_id, 'product_attribute_image', true) : 0; $placeholder = function_exists('wc_placeholder_img_src') ? wc_placeholder_img_src('thumbnail') : ''; $thumb = $placeholder; if ($image_id > 0) { $img = wp_get_attachment_image_src($image_id, 'thumbnail'); if (is_array($img) && !empty($img[0])) { $thumb = $img[0]; } } $remove_hidden = $image_id > 0 ? '' : ' hidden'; ?>

attribute_name === $taxonomy) { return $t; } } return null; } public function enqueue_term_admin_assets($hook) { if ($hook !== 'edit-tags.php' && $hook !== 'term.php') { return; } $tax = isset($_GET['taxonomy']) ? sanitize_key((string) $_GET['taxonomy']) : ''; if (strpos($tax, 'pa_') !== 0) { return; } wp_enqueue_media(); wp_enqueue_style('wp-color-picker'); wp_enqueue_script('wp-color-picker'); $css = ' .aieo-swatch-color-wrapper{max-width:480px} .aieo-swatch-color-wrapper>p{margin:0 0 10px} .aieo-swatch-color-set{padding:10px 12px;background:#f8fafc;border:1px solid #e5e7eb;border-radius:4px;margin:0 0 10px} .aieo-swatch-color-set.hidden,.aieo-swatch-image-field .button.hidden{display:none} .aieo-swatch-image-field{display:flex;align-items:center;gap:14px;flex-wrap:wrap} .aieo-swatch-image-preview{margin:0;border:1px solid #d1d5db;border-radius:4px;width:80px;height:80px;overflow:hidden;background:#fff;display:flex;align-items:center;justify-content:center} .aieo-swatch-image-preview img{max-width:100%;max-height:100%;display:block} '; wp_register_style('aieo-swatch-term-admin', false); wp_enqueue_style('aieo-swatch-term-admin'); wp_add_inline_style('aieo-swatch-term-admin', $css); $js = " jQuery(function($){ // 1. WP color-picker on every hex input. $('.aieo-swatch-color-picker').each(function(){ if (!$(this).hasClass('wp-color-picker-init')) { $(this).addClass('wp-color-picker-init').wpColorPicker(); } }); // 2. Toggle visible color-set on type change. function syncColorSets(){ var t = $('.aieo-swatch-color-type').val() || 'background'; $('.aieo-swatch-color-set').addClass('hidden'); $('.aieo-swatch-color-set--' + t).removeClass('hidden'); } $(document).on('change', '.aieo-swatch-color-type', syncColorSets); syncColorSets(); // 3. Image picker — open WP media frame, set hidden id + thumb. var frame; $(document).on('click', '.aieo-swatch-image-upload', function(e){ e.preventDefault(); var \$wrap = $(this).closest('.aieo-swatch-image-field'); if (frame) { frame.off('select'); } frame = wp.media({title:'" . esc_js(__('Choose image', 'ai-eshop-optimizer')) . "',multiple:false,library:{type:'image'}}); frame.on('select', function(){ var att = frame.state().get('selection').first().toJSON(); \$wrap.find('.aieo-swatch-image-input').val(att.id); var url = (att.sizes && att.sizes.thumbnail) ? att.sizes.thumbnail.url : att.url; \$wrap.find('.aieo-swatch-image-preview img').attr('src', url); \$wrap.find('.aieo-swatch-image-remove').removeClass('hidden'); }); frame.open(); }); $(document).on('click', '.aieo-swatch-image-remove', function(e){ e.preventDefault(); var \$wrap = $(this).closest('.aieo-swatch-image-field'); \$wrap.find('.aieo-swatch-image-input').val(''); var \$img = \$wrap.find('.aieo-swatch-image-preview img'); \$img.attr('src', \$img.data('placeholder') || ''); $(this).addClass('hidden'); }); }); "; wp_register_script('aieo-swatch-term-admin', '', array('jquery', 'wp-color-picker'), AIEO_VERSION , true); wp_enqueue_script('aieo-swatch-term-admin'); wp_add_inline_script('aieo-swatch-term-admin', $js); } private static function copy_or($value, $fallback) { $value = trim((string) $value); return '' !== $value ? $value : (string) $fallback; } } if (!has_filter('woocommerce_get_item_data', array('AIEO_DMM_Swatches', 'cart_item_variation_description'))) { add_filter('woocommerce_get_item_data', array('AIEO_DMM_Swatches', 'cart_item_variation_description'), 10, 2); add_action('woocommerce_checkout_create_order_line_item', array('AIEO_DMM_Swatches', 'order_item_variation_description'), 10, 4); }