'#0878a8', 'button_bg_hover' => '#06678f', 'button_text' => '#ffffff', 'button_text_hover' => '#ffffff', 'button_border' => '#0878a8', 'button_border_hover' => '#06678f', 'button_border_width' => 1, 'button_radius' => 4, 'button_font_size' => 15, 'button_font_weight' => 700, 'button_padding_y' => 11, 'button_padding_x' => 18, 'cart_bubble_bg' => '#f0b429', 'cart_bubble_text' => '#17324d', 'accent_color' => '#0878a8', 'panel_bg' => '#ffffff', 'sideways_left_bg' => '#eef5f8', 'font_color' => '#17324d', 'muted_color' => '#5d7185', 'border_color' => '#dbe6ed', 'highlight_color' => '#ffe08a', 'font_family' => 'inherit', 'font_size' => 14, 'parent_font_size' => 15, 'category_font_weight' => 600, 'panel_radius' => 4, 'control_radius' => 4, 'image_size' => 20, 'image_radius' => 0, ); } public static function get_settings( $post_id ) { $defaults = array_merge( self::style_defaults(), array( 'source' => 'taxonomy', // taxonomy | menu 'taxonomy' => 'product_cat', 'menu_id' => 0, 'layout' => 'columns', // columns | sideways 'desktop_display' => 'adaptive', // adaptive | modal 'columns' => 4, 'depth' => 3, 'trigger' => __( 'All categories', 'amber-mega-menu' ), 'icon_only_breakpoint' => 0, 'show_cart' => 0, 'cart_position' => 'after', 'cart_icon' => '', 'show_counts' => 0, 'show_images' => 0, 'hide_empty' => 0, 'width' => 1200, 'all_link' => '', 'all_label' => __( 'All categories', 'amber-mega-menu' ), 'tab_label' => '', 'tabs' => array(), 'frontend_text' => array(), ) ); $saved = get_post_meta( $post_id, '_smm_settings', true ); if ( ! is_array( $saved ) ) { $saved = array(); } return array_merge( $defaults, $saved ); } /** * Manual ordering for a pane. Pane 0 is the main menu, 1..n are the * additional tabs (stored together in one meta array keyed by tab index). */ public static function get_order( $post_id, $pane = 0 ) { $pane = (int) $pane; if ( $pane > 0 ) { $all = get_post_meta( $post_id, '_smm_order_tabs', true ); if ( is_string( $all ) && '' !== $all ) { $all = json_decode( $all, true ); } $key = $pane - 1; return ( is_array( $all ) && isset( $all[ $key ] ) && is_array( $all[ $key ] ) ) ? $all[ $key ] : array(); } $order = get_post_meta( $post_id, '_smm_order', true ); if ( is_string( $order ) && '' !== $order ) { $order = json_decode( $order, true ); } return is_array( $order ) ? $order : array(); } /** Settings for an additional tab: base settings with the tab's own source. */ public static function tab_settings( $settings, $tab ) { $settings['source'] = $tab['source']; $settings['taxonomy'] = $tab['taxonomy']; $settings['menu_id'] = $tab['menu_id']; return $settings; } /** Flat map of available source items: id => array(id, parent, label, url, count, image) */ public static function fetch_items( $settings ) { $items = array(); if ( 'menu' === $settings['source'] ) { $menu_id = (int) $settings['menu_id']; if ( ! $menu_id ) { return $items; } $objects = wp_get_nav_menu_items( $menu_id ); if ( ! $objects ) { return $items; } foreach ( $objects as $obj ) { $id = 'item:' . $obj->ID; $items[ $id ] = array( 'id' => $id, 'parent' => $obj->menu_item_parent ? 'item:' . (int) $obj->menu_item_parent : '', 'label' => $obj->title, 'url' => $obj->url, 'count' => 0, 'image' => '', ); } return $items; } $taxonomy = $settings['taxonomy'] ? $settings['taxonomy'] : 'product_cat'; if ( ! taxonomy_exists( $taxonomy ) ) { return $items; } $terms = get_terms( array( 'taxonomy' => $taxonomy, 'hide_empty' => false, ) ); if ( is_wp_error( $terms ) ) { return $items; } foreach ( $terms as $term ) { $id = 'term:' . $term->term_id; $image = ''; if ( ! empty( $settings['show_images'] ) ) { $thumb_id = get_term_meta( $term->term_id, 'thumbnail_id', true ); if ( $thumb_id ) { $image = wp_get_attachment_image_url( $thumb_id, 'thumbnail' ); } } $items[ $id ] = array( 'id' => $id, 'parent' => $term->parent ? 'term:' . (int) $term->parent : '', 'label' => $term->name, 'url' => get_term_link( $term ), 'count' => (int) $term->count, 'image' => $image ? $image : '', ); } return $items; } /** Natural tree straight from the source. */ public static function natural_tree( $items ) { $by_parent = array(); foreach ( $items as $item ) { $by_parent[ $item['parent'] ][] = $item['id']; } $build = function ( $parent ) use ( &$build, $by_parent ) { $out = array(); if ( empty( $by_parent[ $parent ] ) ) { return $out; } foreach ( $by_parent[ $parent ] as $id ) { $out[] = array( 'id' => $id, 'label' => '', 'hidden' => 0, 'children' => $build( $id ), ); } return $out; }; return $build( '' ); } /** * Merge saved manual order with the live source: * - keeps saved order / nesting / custom labels / hidden flags * - drops nodes that no longer exist * - appends brand new source items at the end of their natural parent */ public static function merged_tree( $post_id, $settings = null, $pane = 0 ) { $settings = $settings ? $settings : self::get_settings( $post_id ); $items = self::fetch_items( $settings ); if ( empty( $items ) ) { return array(); } $saved = self::get_order( $post_id, $pane ); if ( empty( $saved ) ) { return self::decorate( self::natural_tree( $items ), $items ); } $seen = array(); $relocate = array(); $clean = self::filter_saved( $saved, $items, $seen, $relocate ); // Append new items that were not in the saved tree. $natural = self::natural_tree( $items ); self::append_new( $natural, $clean, $seen ); // Move saved items whose real parent changed (or that an older version // wrongly stored at the top level) back under their real parent. foreach ( $relocate as $id ) { self::relocate( $clean, $id, $items[ $id ]['parent'] ); } return self::decorate( $clean, $items ); } /** * Copy the saved tree, keeping only live items. * * Each saved node may carry 'np' — the source parent it had when the order * was saved. A node is queued for relocation when: * - 'np' is stored and differs from the current source parent (the item was * re-parented in WordPress/WooCommerce after the order was saved), or * - 'np' is missing (saved by an older version), the node sits at the top * level and the source says it is a child — older versions appended new * child categories at the top level and then persisted that on save. * When 'np' matches, the saved position is a deliberate manual move and wins. */ protected static function filter_saved( $nodes, $items, &$seen, &$relocate = array(), $saved_parent = '' ) { $out = array(); foreach ( (array) $nodes as $node ) { if ( empty( $node['id'] ) || ! isset( $items[ $node['id'] ] ) || isset( $seen[ $node['id'] ] ) ) { continue; } $id = $node['id']; $seen[ $id ] = true; $source_parent = (string) $items[ $id ]['parent']; $has_np = isset( $node['np'] ) && is_string( $node['np'] ); if ( $source_parent !== $saved_parent ) { if ( $has_np ? ( $node['np'] !== $source_parent ) : ( '' === $saved_parent ) ) { $relocate[] = $id; } } $out[] = array( 'id' => $id, 'label' => isset( $node['label'] ) ? $node['label'] : '', 'hidden' => ! empty( $node['hidden'] ) ? 1 : 0, 'hide_mode' => ( isset( $node['hide_mode'] ) && 'self' === $node['hide_mode'] ) ? 'self' : 'branch', 'children' => isset( $node['children'] ) ? self::filter_saved( $node['children'], $items, $seen, $relocate, $id ) : array(), ); } return $out; } /** Detach a node (with its subtree) and append it under $parent_id (or root). */ protected static function relocate( &$clean, $id, $parent_id ) { $node = self::detach( $clean, $id ); if ( null === $node ) { return; } if ( '' !== $parent_id && ! self::subtree_has( $node, $parent_id ) ) { $branch = &self::find_node( $clean, $parent_id ); if ( $branch ) { $branch['children'][] = $node; return; } unset( $branch ); } $clean[] = $node; } protected static function detach( &$nodes, $id ) { foreach ( $nodes as $i => &$node ) { if ( $node['id'] === $id ) { $found = $node; unset( $node ); array_splice( $nodes, $i, 1 ); return $found; } if ( ! empty( $node['children'] ) ) { $found = self::detach( $node['children'], $id ); if ( null !== $found ) { return $found; } } } unset( $node ); return null; } protected static function subtree_has( $node, $id ) { foreach ( $node['children'] as $child ) { if ( $child['id'] === $id || self::subtree_has( $child, $id ) ) { return true; } } return false; } /** * Walk the natural tree; push any unseen node onto its parent branch in * $clean (or root). The natural parent id is carried through the recursion * — looking it up from the local subtree would lose the ancestor context * and dump new children at the top level. */ protected static function append_new( $natural, &$clean, &$seen, $parent_id = '' ) { foreach ( $natural as $node ) { if ( ! isset( $seen[ $node['id'] ] ) ) { $seen[ $node['id'] ] = true; $branch = null; // try to place under the same parent if that parent exists in $clean if ( $parent_id ) { $branch = &self::find_node( $clean, $parent_id ); } $new = array( 'id' => $node['id'], 'label' => '', 'hidden' => 0, 'children' => array(), ); if ( $branch ) { $branch['children'][] = $new; } else { $clean[] = $new; } unset( $branch ); } if ( ! empty( $node['children'] ) ) { self::append_new( $node['children'], $clean, $seen, $node['id'] ); } } } protected static function &find_node( &$nodes, $id ) { $null = null; foreach ( $nodes as &$node ) { if ( $node['id'] === $id ) { return $node; } if ( ! empty( $node['children'] ) ) { $found = &self::find_node( $node['children'], $id ); if ( $found ) { return $found; } unset( $found ); } } return $null; } /** * Tree straight from a source, without the manual ordering stored on the post. * Used by additional menu tabs, which follow their own source order. */ public static function source_tree( $settings ) { $items = self::fetch_items( $settings ); if ( empty( $items ) ) { return array(); } return self::decorate( self::natural_tree( $items ), $items ); } /** Normalised list of additional tab definitions. */ public static function get_tabs( $settings ) { $out = array(); if ( empty( $settings['tabs'] ) || ! is_array( $settings['tabs'] ) ) { return $out; } foreach ( $settings['tabs'] as $tab ) { if ( ! is_array( $tab ) ) { continue; } $source = ( isset( $tab['source'] ) && 'menu' === $tab['source'] ) ? 'menu' : 'taxonomy'; $label = isset( $tab['label'] ) ? trim( (string) $tab['label'] ) : ''; $tax = isset( $tab['taxonomy'] ) ? $tab['taxonomy'] : 'product_cat'; $menu = isset( $tab['menu_id'] ) ? (int) $tab['menu_id'] : 0; if ( 'menu' === $source && ! $menu ) { continue; } if ( 'taxonomy' === $source && ! $tax ) { continue; } if ( '' === $label ) { $label = ( 'menu' === $source ) ? wp_get_nav_menu_name( $menu ) : $tax; } $out[] = array( 'label' => $label, 'source' => $source, 'taxonomy' => $tax, 'menu_id' => $menu, ); } return $out; } /** Attach live label/url/count to each node. */ protected static function decorate( $nodes, $items ) { $out = array(); foreach ( $nodes as $node ) { if ( ! isset( $items[ $node['id'] ] ) ) { continue; } $src = $items[ $node['id'] ]; $label = ! empty( $node['label'] ) ? $node['label'] : $src['label']; $out[] = array( 'id' => $node['id'], 'label' => $label, 'custom' => ! empty( $node['label'] ) ? $node['label'] : '', 'url' => $src['url'], 'count' => $src['count'], 'image' => $src['image'], 'hidden' => ! empty( $node['hidden'] ) ? 1 : 0, 'hide_mode' => ( isset( $node['hide_mode'] ) && 'self' === $node['hide_mode'] ) ? 'self' : 'branch', 'np' => (string) $src['parent'], 'children' => ! empty( $node['children'] ) ? self::decorate( $node['children'], $items ) : array(), ); } return $out; } }