*/ public function get_keywords(): array { return ['shop', 'product', 'filter', 'filters', 'ajax', 'woocommerce', 'category', 'attribute', 'faceted', 'smart filters']; } /** * Widget icon. * * @return string */ public function get_icon(): string { return 'king-addons-icon king-addons-facet-taxonomy'; } /** * Categories. * * @return array */ public function get_categories(): array { return ['king-addons']; } /** * Style dependencies. * * @return array */ public function get_style_depends(): array { return [ KING_ADDONS_ASSETS_UNIQUE_KEY . '-facet-taxonomy-style', ]; } /** * Script dependencies. * * @return array */ public function get_script_depends(): array { return [ KING_ADDONS_ASSETS_UNIQUE_KEY . '-facet-taxonomy-script', ]; } public function get_custom_help_url() { return 'mailto:bug@kingaddons.com?subject=Bug Report - King Addons&body=Please describe the issue'; } /** * Register controls. * * @return void */ public function register_controls(): void { $this->start_controls_section( 'kng_facet_section', [ 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Taxonomy Filter', 'king-addons'), 'tab' => Controls_Manager::TAB_CONTENT, ] ); $this->add_control( 'kng_filters_query_id', [ 'label' => esc_html__('Shop Filters ID', 'king-addons'), 'type' => Controls_Manager::TEXT, 'placeholder' => esc_html__('shop_grid_1', 'king-addons'), 'description' => esc_html__('Must match the Shop Filters ID on the product grid.', 'king-addons'), ] ); $this->add_control( 'kng_taxonomy', [ 'label' => esc_html__('Taxonomy', 'king-addons'), 'type' => Controls_Manager::SELECT, 'options' => $this->get_taxonomy_options(), 'default' => 'product_cat', ] ); $this->add_control( 'kng_terms_mode', [ 'label' => esc_html__('Terms Source', 'king-addons'), 'type' => Controls_Manager::SELECT, 'options' => [ 'auto' => esc_html__('Auto (all terms)', 'king-addons'), 'manual' => esc_html__('Manual list', 'king-addons'), ], 'default' => 'auto', ] ); $this->add_control( 'kng_terms_select', [ 'label' => esc_html__('Select Terms', 'king-addons'), 'type' => Controls_Manager::SELECT2, 'multiple' => true, 'options' => $this->get_editor_term_options(), 'condition' => [ 'kng_terms_mode' => 'auto', ], ] ); $this->add_control( 'kng_terms', [ 'label' => esc_html__('Terms (slugs, one per line)', 'king-addons'), 'type' => Controls_Manager::TEXTAREA, 'placeholder' => "shoes\nboots\nsneakers", 'condition' => [ 'kng_terms_mode' => 'manual', ], ] ); $this->add_control( 'kng_show_counts', [ 'label' => esc_html__('Show Counts', 'king-addons'), 'type' => Controls_Manager::SWITCHER, 'return_value' => 'yes', 'default' => '', ] ); $this->add_control( 'kng_hide_zero', [ 'label' => esc_html__('Hide Zero Count', 'king-addons'), 'type' => Controls_Manager::SWITCHER, 'return_value' => 'yes', 'default' => '', 'condition' => [ 'kng_show_counts' => 'yes', ], ] ); $this->add_control( 'kng_hierarchy', [ 'label' => sprintf(__('Show child terms %s', 'king-addons'), ''), 'type' => Controls_Manager::SWITCHER, 'return_value' => 'yes', 'default' => '', 'description' => esc_html__('Indent child categories under their parent. Applies to hierarchical taxonomies such as product categories.', 'king-addons'), ] ); Core::renderUpgradeProNotice($this, Controls_Manager::RAW_HTML, 'facet-taxonomy', 'kng_hierarchy', 'yes'); $this->add_control( 'kng_display_mode', [ 'label' => esc_html__('Display Mode', 'king-addons'), 'type' => Controls_Manager::SELECT, 'options' => [ 'text' => esc_html__('Text', 'king-addons'), 'swatch' => sprintf(__('Swatch %s', 'king-addons'), ''), ], 'default' => 'text', ] ); Core::renderUpgradeProNotice($this, Controls_Manager::RAW_HTML, 'facet-taxonomy', 'kng_display_mode', 'swatch'); $this->add_control( 'kng_swatch_type', [ 'label' => sprintf(__('Swatch Type %s', 'king-addons'), ''), 'type' => Controls_Manager::SELECT, 'options' => [ 'color' => esc_html__('Color', 'king-addons'), 'image' => esc_html__('Image URL', 'king-addons'), 'text' => esc_html__('Text (fallback)', 'king-addons'), ], 'default' => 'color', 'condition' => [ 'kng_display_mode' => 'swatch', ], ] ); $this->add_control( 'kng_swatch_map', [ 'label' => sprintf(__('Swatch Map %s', 'king-addons'), ''), 'type' => Controls_Manager::TEXTAREA, 'placeholder' => "red:#ff0000\nblue:#0000ff\npattern:https://example.com/pattern.jpg", 'description' => esc_html__('Optional. A line here overrides the color or image saved on the WooCommerce attribute.', 'king-addons'), 'condition' => [ 'kng_display_mode' => 'swatch', ], ] ); $this->end_controls_section(); require_once KING_ADDONS_PATH . 'includes/helpers/Faceted/Style_Controls.php'; Facet_Style_Controls::list($this); } /** * Render widget output. * * @return void */ public function render(): void { $settings = $this->get_settings_for_display(); $query_id = sanitize_title($settings['kng_filters_query_id'] ?? ''); $taxonomy = sanitize_key($settings['kng_taxonomy'] ?? ''); if ('' === $query_id || '' === $taxonomy) { return; } $terms = $this->resolve_terms($taxonomy, $settings); if (empty($terms)) { return; } $can_pro = function_exists('king_addons_can_use_pro') && king_addons_can_use_pro(); $hierarchical = $can_pro && ($settings['kng_hierarchy'] ?? '') === 'yes' && is_taxonomy_hierarchical($taxonomy); if ($hierarchical) { $terms = $this->with_ancestors($terms, $taxonomy); } $can_swatch = $can_pro; $swatch_map = $can_swatch ? $this->parse_swatch_map($settings['kng_swatch_map'] ?? '') : []; $is_swatch = $can_swatch && ($settings['kng_display_mode'] ?? 'text') === 'swatch'; $swatch_type = $settings['kng_swatch_type'] ?? 'color'; $taxonomy_obj = get_taxonomy($taxonomy); $taxonomy_label = ($taxonomy_obj && !empty($taxonomy_obj->labels->singular_name)) ? (string) $taxonomy_obj->labels->singular_name : $taxonomy; echo '
'; $this->render_term_branch( $terms, $hierarchical ? 0 : -1, $hierarchical, $query_id, $taxonomy, $taxonomy_label, $settings, $is_swatch, $swatch_type, $swatch_map ); echo '
'; } /** * Terms chosen for this filter. * * @param string $taxonomy Taxonomy name. * @param array $settings Widget settings. * @return array */ private function resolve_terms(string $taxonomy, array $settings): array { if (($settings['kng_terms_mode'] ?? 'auto') === 'manual') { $terms_raw = $settings['kng_terms'] ?? ''; $slugs = array_filter(array_map('sanitize_title', preg_split("/\r\n|\n|\r/", (string) $terms_raw))); $terms = []; foreach ($slugs as $slug) { $term = get_term_by('slug', $slug, $taxonomy); if ($term instanceof \WP_Term) { $terms[] = $term; } } return $terms; } $selected = $settings['kng_terms_select'] ?? []; $args = [ 'taxonomy' => $taxonomy, 'hide_empty' => false, 'number' => 50, 'orderby' => 'name', 'order' => 'ASC', ]; if (!empty($selected) && is_array($selected)) { $args['slug'] = array_map('sanitize_title', $selected); unset($args['number']); } $terms = get_terms($args); return is_array($terms) ? $terms : []; } /** * Include missing parents so a child is not rendered as a root term. * * @param array $terms Terms already selected. * @param string $taxonomy Taxonomy name. * @return array */ private function with_ancestors(array $terms, string $taxonomy): array { $by_id = []; foreach ($terms as $term) { $by_id[$term->term_id] = $term; $parent_id = (int) $term->parent; while ($parent_id > 0 && !isset($by_id[$parent_id])) { $parent = get_term($parent_id, $taxonomy); if (!$parent instanceof \WP_Term) { break; } $by_id[$parent->term_id] = $parent; $parent_id = (int) $parent->parent; } } return array_values($by_id); } /** * Render one level of the term list. Parent -1 means a flat list. * * @param array $terms All terms. * @param int $parent Parent term id, or -1 for a flat list. * @param bool $hierarchical Whether to nest children. * @param string $query_id Shop Filters ID. * @param string $taxonomy Taxonomy name. * @param string $taxonomy_label Singular taxonomy label. * @param array $settings Widget settings. * @param bool $is_swatch Swatch display. * @param string $swatch_type color|image|text. * @param array $swatch_map Optional overrides. * @return void */ private function render_term_branch( array $terms, int $parent, bool $hierarchical, string $query_id, string $taxonomy, string $taxonomy_label, array $settings, bool $is_swatch, string $swatch_type, array $swatch_map ): void { $level = []; foreach ($terms as $term) { if ($parent === -1 || (int) $term->parent === $parent) { $level[] = $term; } } if (empty($level)) { return; } $list_class = 'king-addons-facet__list'; if ($parent > 0) { $list_class .= ' king-addons-facet__list--child'; } echo '
    '; foreach ($level as $term) { $this->render_term_item( $term, $query_id, $taxonomy, $taxonomy_label, $settings, $is_swatch, $swatch_type, $swatch_map ); if ($hierarchical) { $this->render_term_branch( $terms, (int) $term->term_id, true, $query_id, $taxonomy, $taxonomy_label, $settings, $is_swatch, $swatch_type, $swatch_map ); } echo ''; } echo '
'; } /** * One checkbox or swatch row. * * @param \WP_Term $term Term. * @param string $query_id Shop Filters ID. * @param string $taxonomy Taxonomy name. * @param string $taxonomy_label Singular label. * @param array $settings Widget settings. * @param bool $is_swatch Swatch display. * @param string $swatch_type color|image|text. * @param array $swatch_map Optional overrides. * @return void */ private function render_term_item( \WP_Term $term, string $query_id, string $taxonomy, string $taxonomy_label, array $settings, bool $is_swatch, string $swatch_type, array $swatch_map ): void { $slug = $term->slug; $term_label = $term->name; $swatch = $is_swatch ? $this->resolve_swatch($term, $swatch_type, $swatch_map) : ['type' => '', 'value' => '']; ?>
  • $map Optional slug overrides. * @return array{type: string, value: string} */ private function resolve_swatch(\WP_Term $term, string $type, array $map): array { $override = $map[$term->slug] ?? ''; if ($override !== '') { $is_image = (bool) preg_match('#^https?://#i', $override); return [ 'type' => $is_image ? 'image' : 'color', 'value' => $is_image ? $override : (sanitize_hex_color($override) ?: $override), ]; } if ('image' === $type) { $thumb_id = (int) get_term_meta($term->term_id, 'thumbnail_id', true); if ($thumb_id > 0) { $url = wp_get_attachment_image_url($thumb_id, 'thumbnail'); if (is_string($url) && $url !== '') { return ['type' => 'image', 'value' => $url]; } } } $color_raw = get_term_meta($term->term_id, 'color', true); if (is_string($color_raw) && $color_raw !== '') { $hex = sanitize_hex_color($color_raw); if ($hex) { return ['type' => 'color', 'value' => $hex]; } } return ['type' => 'letter', 'value' => '']; } /** * Get available taxonomies list. * * @return array */ private function get_taxonomy_options(): array { $options = []; $taxes = get_taxonomies(['public' => true], 'objects'); foreach ($taxes as $tax) { $options[$tax->name] = esc_html($tax->labels->singular_name ?? $tax->label ?? $tax->name); } return $options; } /** * Get term options for a taxonomy. * * @param string $taxonomy Taxonomy. * * @return array */ private function get_terms_options(string $taxonomy): array { $options = []; $terms = get_terms( [ 'taxonomy' => $taxonomy, 'hide_empty' => false, 'number' => 50, ] ); if (is_array($terms)) { foreach ($terms as $term) { $options[$term->slug] = esc_html($term->name); } } return $options; } /** * Term options for the editor select (product taxonomies + attributes). * * @return array */ private function get_editor_term_options(): array { $options = []; $taxonomies = ['product_cat', 'product_tag']; if (function_exists('wc_get_attribute_taxonomies')) { foreach (wc_get_attribute_taxonomies() as $attribute) { if (empty($attribute->attribute_name) || !function_exists('wc_attribute_taxonomy_name')) { continue; } $taxonomies[] = wc_attribute_taxonomy_name($attribute->attribute_name); } } foreach (array_unique($taxonomies) as $taxonomy) { foreach ($this->get_terms_options($taxonomy) as $slug => $name) { if (!isset($options[$slug])) { $options[$slug] = $name; } } } return $options; } /** * Get term slugs for taxonomy (fallback). * * @param string $taxonomy Taxonomy. * * @return array */ private function get_term_slugs_for_tax(string $taxonomy): array { $options = $this->get_terms_options($taxonomy); return array_keys($options); } /** * Parse swatch map textarea (slug:value). * * @param string $raw Raw input. * * @return array */ private function parse_swatch_map(string $raw): array { $lines = preg_split("/\r\n|\n|\r/", $raw); $map = []; foreach ($lines as $line) { if (strpos($line, ':') === false) { continue; } [$slug, $val] = array_map('trim', explode(':', $line, 2)); if ($slug && $val) { $map[sanitize_title($slug)] = $val; } } return $map; } }