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) . '' . $label_tag . '>';
echo '<' . $field_tag . '>';
if ($type === 'color') {
$this->render_term_swatch_color_fields($term);
} else {
$this->render_term_swatch_image_field($term);
}
echo '' . $field_tag . '>';
echo '' . $wrap_tag . '>';
}
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);
}