PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.86
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.86
51.1.86 51.1.84 51.1.85 51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 All 40 releases
king-addons / includes / widgets / Facet_Taxonomy / Facet_Taxonomy.php

Facet_Taxonomy.php in King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder 51.1.86, at includes/widgets/Facet_Taxonomy/Facet_Taxonomy.php

677 lines 21.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Facet Taxonomy widget.
4 *
5 * @package King_Addons
6 */
7
8 namespace King_Addons;
9
10 use Elementor\Controls_Manager;
11 use Elementor\Widget_Base;
12
13 if (!defined('ABSPATH')) {
14 exit;
15 }
16
17 /**
18 * Checkbox taxonomy filter widget.
19 */
20 class Facet_Taxonomy extends Widget_Base
21 {
22 /**
23 * Widget slug.
24 *
25 * @return string
26 */
27 public function get_name(): string
28 {
29 return 'king-addons-facet-taxonomy';
30 }
31
32 /**
33 * Widget title.
34 *
35 * @return string
36 */
37 public function get_title(): string
38 {
39 return esc_html__('Taxonomy Filter', 'king-addons');
40 }
41
42 /**
43 * Keywords.
44 *
45 * @return array<int, string>
46 */
47 public function get_keywords(): array
48 {
49 return ['shop', 'product', 'filter', 'filters', 'ajax', 'woocommerce', 'category', 'attribute', 'faceted', 'smart filters'];
50 }
51
52 /**
53 * Widget icon.
54 *
55 * @return string
56 */
57 public function get_icon(): string
58 {
59 return 'king-addons-icon king-addons-facet-taxonomy';
60 }
61
62 /**
63 * Categories.
64 *
65 * @return array<int, string>
66 */
67 public function get_categories(): array
68 {
69 return ['king-addons'];
70 }
71
72 /**
73 * Style dependencies.
74 *
75 * @return array<int, string>
76 */
77 public function get_style_depends(): array
78 {
79 return [
80 KING_ADDONS_ASSETS_UNIQUE_KEY . '-facet-taxonomy-style',
81 ];
82 }
83
84 /**
85 * Script dependencies.
86 *
87 * @return array<int, string>
88 */
89 public function get_script_depends(): array
90 {
91 return [
92 KING_ADDONS_ASSETS_UNIQUE_KEY . '-facet-taxonomy-script',
93 ];
94 }
95
96 public function get_custom_help_url()
97 {
98 return 'mailto:[email protected]?subject=Bug Report - King Addons&body=Please describe the issue';
99 }
100
101 /**
102 * Register controls.
103 *
104 * @return void
105 */
106 public function register_controls(): void
107 {
108 $this->start_controls_section(
109 'kng_facet_section',
110 [
111 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Taxonomy Filter', 'king-addons'),
112 'tab' => Controls_Manager::TAB_CONTENT,
113 ]
114 );
115
116 $this->add_control(
117 'kng_filters_query_id',
118 [
119 'label' => esc_html__('Shop Filters ID', 'king-addons'),
120 'type' => Controls_Manager::TEXT,
121 'placeholder' => esc_html__('shop_grid_1', 'king-addons'),
122 'description' => esc_html__('Must match the Shop Filters ID on the product grid.', 'king-addons'),
123 ]
124 );
125
126 $this->add_control(
127 'kng_taxonomy',
128 [
129 'label' => esc_html__('Taxonomy', 'king-addons'),
130 'type' => Controls_Manager::SELECT,
131 'options' => $this->get_taxonomy_options(),
132 'default' => 'product_cat',
133 ]
134 );
135
136 $this->add_control(
137 'kng_terms_mode',
138 [
139 'label' => esc_html__('Terms Source', 'king-addons'),
140 'type' => Controls_Manager::SELECT,
141 'options' => [
142 'auto' => esc_html__('Auto (all terms)', 'king-addons'),
143 'manual' => esc_html__('Manual list', 'king-addons'),
144 ],
145 'default' => 'auto',
146 ]
147 );
148
149 $this->add_control(
150 'kng_terms_select',
151 [
152 'label' => esc_html__('Select Terms', 'king-addons'),
153 'type' => Controls_Manager::SELECT2,
154 'multiple' => true,
155 'options' => $this->get_editor_term_options(),
156 'condition' => [
157 'kng_terms_mode' => 'auto',
158 ],
159 ]
160 );
161
162 $this->add_control(
163 'kng_terms',
164 [
165 'label' => esc_html__('Terms (slugs, one per line)', 'king-addons'),
166 'type' => Controls_Manager::TEXTAREA,
167 'placeholder' => "shoes\nboots\nsneakers",
168 'condition' => [
169 'kng_terms_mode' => 'manual',
170 ],
171 ]
172 );
173
174 $this->add_control(
175 'kng_show_counts',
176 [
177 'label' => esc_html__('Show Counts', 'king-addons'),
178 'type' => Controls_Manager::SWITCHER,
179 'return_value' => 'yes',
180 'default' => '',
181 ]
182 );
183
184 $this->add_control(
185 'kng_hide_zero',
186 [
187 'label' => esc_html__('Hide Zero Count', 'king-addons'),
188 'type' => Controls_Manager::SWITCHER,
189 'return_value' => 'yes',
190 'default' => '',
191 'condition' => [
192 'kng_show_counts' => 'yes',
193 ],
194 ]
195 );
196
197 $this->add_control(
198 'kng_hierarchy',
199 [
200 'label' => sprintf(__('Show child terms %s', 'king-addons'), '<i class="eicon-pro-icon"></i>'),
201 'type' => Controls_Manager::SWITCHER,
202 'return_value' => 'yes',
203 'default' => '',
204 'description' => esc_html__('Indent child categories under their parent. Applies to hierarchical taxonomies such as product categories.', 'king-addons'),
205 ]
206 );
207
208 Core::renderUpgradeProNotice($this, Controls_Manager::RAW_HTML, 'facet-taxonomy', 'kng_hierarchy', 'yes');
209
210 $this->add_control(
211 'kng_display_mode',
212 [
213 'label' => esc_html__('Display Mode', 'king-addons'),
214 'type' => Controls_Manager::SELECT,
215 'options' => [
216 'text' => esc_html__('Text', 'king-addons'),
217 'swatch' => sprintf(__('Swatch %s', 'king-addons'), '<i class="eicon-pro-icon"></i>'),
218 ],
219 'default' => 'text',
220 ]
221 );
222
223 Core::renderUpgradeProNotice($this, Controls_Manager::RAW_HTML, 'facet-taxonomy', 'kng_display_mode', 'swatch');
224
225 $this->add_control(
226 'kng_swatch_type',
227 [
228 'label' => sprintf(__('Swatch Type %s', 'king-addons'), '<i class="eicon-pro-icon"></i>'),
229 'type' => Controls_Manager::SELECT,
230 'options' => [
231 'color' => esc_html__('Color', 'king-addons'),
232 'image' => esc_html__('Image URL', 'king-addons'),
233 'text' => esc_html__('Text (fallback)', 'king-addons'),
234 ],
235 'default' => 'color',
236 'condition' => [
237 'kng_display_mode' => 'swatch',
238 ],
239 ]
240 );
241
242 $this->add_control(
243 'kng_swatch_map',
244 [
245 'label' => sprintf(__('Swatch Map %s', 'king-addons'), '<i class="eicon-pro-icon"></i>'),
246 'type' => Controls_Manager::TEXTAREA,
247 'placeholder' => "red:#ff0000\nblue:#0000ff\npattern:https://example.com/pattern.jpg",
248 'description' => esc_html__('Optional. A line here overrides the color or image saved on the WooCommerce attribute.', 'king-addons'),
249 'condition' => [
250 'kng_display_mode' => 'swatch',
251 ],
252 ]
253 );
254
255 $this->end_controls_section();
256
257 require_once KING_ADDONS_PATH . 'includes/helpers/Faceted/Style_Controls.php';
258 Facet_Style_Controls::list($this);
259 }
260
261 /**
262 * Render widget output.
263 *
264 * @return void
265 */
266 public function render(): void
267 {
268 $settings = $this->get_settings_for_display();
269 $query_id = sanitize_title($settings['kng_filters_query_id'] ?? '');
270 $taxonomy = sanitize_key($settings['kng_taxonomy'] ?? '');
271 if ('' === $query_id || '' === $taxonomy) {
272 return;
273 }
274
275 $terms = $this->resolve_terms($taxonomy, $settings);
276 if (empty($terms)) {
277 return;
278 }
279
280 $can_pro = function_exists('king_addons_can_use_pro') && king_addons_can_use_pro();
281 $hierarchical = $can_pro
282 && ($settings['kng_hierarchy'] ?? '') === 'yes'
283 && is_taxonomy_hierarchical($taxonomy);
284 if ($hierarchical) {
285 $terms = $this->with_ancestors($terms, $taxonomy);
286 }
287
288 $can_swatch = $can_pro;
289 $swatch_map = $can_swatch ? $this->parse_swatch_map($settings['kng_swatch_map'] ?? '') : [];
290 $is_swatch = $can_swatch && ($settings['kng_display_mode'] ?? 'text') === 'swatch';
291 $swatch_type = $settings['kng_swatch_type'] ?? 'color';
292 $taxonomy_obj = get_taxonomy($taxonomy);
293 $taxonomy_label = ($taxonomy_obj && !empty($taxonomy_obj->labels->singular_name))
294 ? (string) $taxonomy_obj->labels->singular_name
295 : $taxonomy;
296
297 echo '<div class="king-addons-facet king-addons-facet--taxonomy">';
298 $this->render_term_branch(
299 $terms,
300 $hierarchical ? 0 : -1,
301 $hierarchical,
302 $query_id,
303 $taxonomy,
304 $taxonomy_label,
305 $settings,
306 $is_swatch,
307 $swatch_type,
308 $swatch_map
309 );
310 echo '</div>';
311 }
312
313 /**
314 * Terms chosen for this filter.
315 *
316 * @param string $taxonomy Taxonomy name.
317 * @param array<string, mixed> $settings Widget settings.
318 * @return array<int, \WP_Term>
319 */
320 private function resolve_terms(string $taxonomy, array $settings): array
321 {
322 if (($settings['kng_terms_mode'] ?? 'auto') === 'manual') {
323 $terms_raw = $settings['kng_terms'] ?? '';
324 $slugs = array_filter(array_map('sanitize_title', preg_split("/\r\n|\n|\r/", (string) $terms_raw)));
325 $terms = [];
326 foreach ($slugs as $slug) {
327 $term = get_term_by('slug', $slug, $taxonomy);
328 if ($term instanceof \WP_Term) {
329 $terms[] = $term;
330 }
331 }
332 return $terms;
333 }
334
335 $selected = $settings['kng_terms_select'] ?? [];
336 $args = [
337 'taxonomy' => $taxonomy,
338 'hide_empty' => false,
339 'number' => 50,
340 'orderby' => 'name',
341 'order' => 'ASC',
342 ];
343 if (!empty($selected) && is_array($selected)) {
344 $args['slug'] = array_map('sanitize_title', $selected);
345 unset($args['number']);
346 }
347
348 $terms = get_terms($args);
349 return is_array($terms) ? $terms : [];
350 }
351
352 /**
353 * Include missing parents so a child is not rendered as a root term.
354 *
355 * @param array<int, \WP_Term> $terms Terms already selected.
356 * @param string $taxonomy Taxonomy name.
357 * @return array<int, \WP_Term>
358 */
359 private function with_ancestors(array $terms, string $taxonomy): array
360 {
361 $by_id = [];
362 foreach ($terms as $term) {
363 $by_id[$term->term_id] = $term;
364 $parent_id = (int) $term->parent;
365 while ($parent_id > 0 && !isset($by_id[$parent_id])) {
366 $parent = get_term($parent_id, $taxonomy);
367 if (!$parent instanceof \WP_Term) {
368 break;
369 }
370 $by_id[$parent->term_id] = $parent;
371 $parent_id = (int) $parent->parent;
372 }
373 }
374
375 return array_values($by_id);
376 }
377
378 /**
379 * Render one level of the term list. Parent -1 means a flat list.
380 *
381 * @param array<int, \WP_Term> $terms All terms.
382 * @param int $parent Parent term id, or -1 for a flat list.
383 * @param bool $hierarchical Whether to nest children.
384 * @param string $query_id Shop Filters ID.
385 * @param string $taxonomy Taxonomy name.
386 * @param string $taxonomy_label Singular taxonomy label.
387 * @param array<string, mixed> $settings Widget settings.
388 * @param bool $is_swatch Swatch display.
389 * @param string $swatch_type color|image|text.
390 * @param array<string, string> $swatch_map Optional overrides.
391 * @return void
392 */
393 private function render_term_branch(
394 array $terms,
395 int $parent,
396 bool $hierarchical,
397 string $query_id,
398 string $taxonomy,
399 string $taxonomy_label,
400 array $settings,
401 bool $is_swatch,
402 string $swatch_type,
403 array $swatch_map
404 ): void {
405 $level = [];
406 foreach ($terms as $term) {
407 if ($parent === -1 || (int) $term->parent === $parent) {
408 $level[] = $term;
409 }
410 }
411 if (empty($level)) {
412 return;
413 }
414
415 $list_class = 'king-addons-facet__list';
416 if ($parent > 0) {
417 $list_class .= ' king-addons-facet__list--child';
418 }
419
420 echo '<ul class="' . esc_attr($list_class) . '" data-ka-filters-query-id="' . esc_attr($query_id) . '">';
421 foreach ($level as $term) {
422 $this->render_term_item(
423 $term,
424 $query_id,
425 $taxonomy,
426 $taxonomy_label,
427 $settings,
428 $is_swatch,
429 $swatch_type,
430 $swatch_map
431 );
432 if ($hierarchical) {
433 $this->render_term_branch(
434 $terms,
435 (int) $term->term_id,
436 true,
437 $query_id,
438 $taxonomy,
439 $taxonomy_label,
440 $settings,
441 $is_swatch,
442 $swatch_type,
443 $swatch_map
444 );
445 }
446 echo '</li>';
447 }
448 echo '</ul>';
449 }
450
451 /**
452 * One checkbox or swatch row.
453 *
454 * @param \WP_Term $term Term.
455 * @param string $query_id Shop Filters ID.
456 * @param string $taxonomy Taxonomy name.
457 * @param string $taxonomy_label Singular label.
458 * @param array<string, mixed> $settings Widget settings.
459 * @param bool $is_swatch Swatch display.
460 * @param string $swatch_type color|image|text.
461 * @param array<string, string> $swatch_map Optional overrides.
462 * @return void
463 */
464 private function render_term_item(
465 \WP_Term $term,
466 string $query_id,
467 string $taxonomy,
468 string $taxonomy_label,
469 array $settings,
470 bool $is_swatch,
471 string $swatch_type,
472 array $swatch_map
473 ): void {
474 $slug = $term->slug;
475 $term_label = $term->name;
476 $swatch = $is_swatch ? $this->resolve_swatch($term, $swatch_type, $swatch_map) : ['type' => '', 'value' => ''];
477 ?>
478 <li class="king-addons-facet__item<?php echo (int) $term->parent > 0 ? ' king-addons-facet__item--child' : ''; ?>">
479 <label class="king-addons-facet__label">
480 <input
481 type="checkbox"
482 class="king-addons-facet__input"
483 data-ka-filter-type="taxonomy"
484 data-ka-filters-query-id="<?php echo esc_attr($query_id); ?>"
485 data-ka-taxonomy="<?php echo esc_attr($taxonomy); ?>"
486 data-ka-taxonomy-label="<?php echo esc_attr($taxonomy_label); ?>"
487 data-ka-term="<?php echo esc_attr($slug); ?>"
488 data-ka-term-label="<?php echo esc_attr($term_label); ?>"
489 data-ka-show-counts="<?php echo esc_attr(($settings['kng_show_counts'] ?? '') === 'yes' ? '1' : '0'); ?>"
490 data-ka-swatch="<?php echo $is_swatch ? '1' : '0'; ?>"
491 data-ka-count-key="<?php echo esc_attr($taxonomy . ':' . $slug); ?>"
492 />
493 <?php if ($is_swatch) : ?>
494 <?php
495 $class = 'king-addons-facet__swatch';
496 $style_attr = '';
497 if ('image' === $swatch['type'] && $swatch['value'] !== '') {
498 $style_attr = ' style="background-image:url(' . esc_url($swatch['value']) . ');"';
499 $class .= ' king-addons-facet__swatch--image';
500 } elseif ('color' === $swatch['type'] && $swatch['value'] !== '') {
501 $style_attr = ' style="background-color:' . esc_attr($swatch['value']) . ';"';
502 } else {
503 $class .= ' king-addons-facet__swatch--letter';
504 }
505 ?>
506 <span class="<?php echo esc_attr($class); ?>"<?php echo $style_attr; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>><?php echo 'letter' === $swatch['type'] || $swatch['value'] === '' ? esc_html(mb_substr($term_label, 0, 1)) : ''; ?></span>
507 <span class="king-addons-facet__text"><?php echo esc_html($term_label); ?></span>
508 <?php else : ?>
509 <span class="king-addons-facet__box" aria-hidden="true"></span>
510 <span class="king-addons-facet__text"><?php echo esc_html($term_label); ?></span>
511 <?php endif; ?>
512 <?php if (($settings['kng_show_counts'] ?? '') === 'yes') : ?>
513 <span
514 class="king-addons-facet__count"
515 data-ka-count="<?php echo esc_attr($taxonomy . ':' . $slug); ?>"
516 data-ka-hide-zero="<?php echo esc_attr(($settings['kng_hide_zero'] ?? '') === 'yes' ? '1' : '0'); ?>"
517 ></span>
518 <?php endif; ?>
519 </label>
520 <?php
521 }
522
523 /**
524 * Color or image for a swatch. The textarea overrides attribute data.
525 *
526 * @param \WP_Term $term Term.
527 * @param string $type color|image|text.
528 * @param array<string, string> $map Optional slug overrides.
529 * @return array{type: string, value: string}
530 */
531 private function resolve_swatch(\WP_Term $term, string $type, array $map): array
532 {
533 $override = $map[$term->slug] ?? '';
534 if ($override !== '') {
535 $is_image = (bool) preg_match('#^https?://#i', $override);
536 return [
537 'type' => $is_image ? 'image' : 'color',
538 'value' => $is_image ? $override : (sanitize_hex_color($override) ?: $override),
539 ];
540 }
541
542 if ('image' === $type) {
543 $thumb_id = (int) get_term_meta($term->term_id, 'thumbnail_id', true);
544 if ($thumb_id > 0) {
545 $url = wp_get_attachment_image_url($thumb_id, 'thumbnail');
546 if (is_string($url) && $url !== '') {
547 return ['type' => 'image', 'value' => $url];
548 }
549 }
550 }
551
552 $color_raw = get_term_meta($term->term_id, 'color', true);
553 if (is_string($color_raw) && $color_raw !== '') {
554 $hex = sanitize_hex_color($color_raw);
555 if ($hex) {
556 return ['type' => 'color', 'value' => $hex];
557 }
558 }
559
560 return ['type' => 'letter', 'value' => ''];
561 }
562
563 /**
564 * Get available taxonomies list.
565 *
566 * @return array<string,string>
567 */
568 private function get_taxonomy_options(): array
569 {
570 $options = [];
571 $taxes = get_taxonomies(['public' => true], 'objects');
572 foreach ($taxes as $tax) {
573 $options[$tax->name] = esc_html($tax->labels->singular_name ?? $tax->label ?? $tax->name);
574 }
575 return $options;
576 }
577
578 /**
579 * Get term options for a taxonomy.
580 *
581 * @param string $taxonomy Taxonomy.
582 *
583 * @return array<string,string>
584 */
585 private function get_terms_options(string $taxonomy): array
586 {
587 $options = [];
588 $terms = get_terms(
589 [
590 'taxonomy' => $taxonomy,
591 'hide_empty' => false,
592 'number' => 50,
593 ]
594 );
595
596 if (is_array($terms)) {
597 foreach ($terms as $term) {
598 $options[$term->slug] = esc_html($term->name);
599 }
600 }
601
602 return $options;
603 }
604
605 /**
606 * Term options for the editor select (product taxonomies + attributes).
607 *
608 * @return array<string,string>
609 */
610 private function get_editor_term_options(): array
611 {
612 $options = [];
613 $taxonomies = ['product_cat', 'product_tag'];
614
615 if (function_exists('wc_get_attribute_taxonomies')) {
616 foreach (wc_get_attribute_taxonomies() as $attribute) {
617 if (empty($attribute->attribute_name) || !function_exists('wc_attribute_taxonomy_name')) {
618 continue;
619 }
620 $taxonomies[] = wc_attribute_taxonomy_name($attribute->attribute_name);
621 }
622 }
623
624 foreach (array_unique($taxonomies) as $taxonomy) {
625 foreach ($this->get_terms_options($taxonomy) as $slug => $name) {
626 if (!isset($options[$slug])) {
627 $options[$slug] = $name;
628 }
629 }
630 }
631
632 return $options;
633 }
634
635 /**
636 * Get term slugs for taxonomy (fallback).
637 *
638 * @param string $taxonomy Taxonomy.
639 *
640 * @return array<int,string>
641 */
642 private function get_term_slugs_for_tax(string $taxonomy): array
643 {
644 $options = $this->get_terms_options($taxonomy);
645 return array_keys($options);
646 }
647
648 /**
649 * Parse swatch map textarea (slug:value).
650 *
651 * @param string $raw Raw input.
652 *
653 * @return array<string,string>
654 */
655 private function parse_swatch_map(string $raw): array
656 {
657 $lines = preg_split("/\r\n|\n|\r/", $raw);
658 $map = [];
659 foreach ($lines as $line) {
660 if (strpos($line, ':') === false) {
661 continue;
662 }
663 [$slug, $val] = array_map('trim', explode(':', $line, 2));
664 if ($slug && $val) {
665 $map[sanitize_title($slug)] = $val;
666 }
667 }
668 return $map;
669 }
670 }
671
672
673
674
675
676
677