PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.87
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.87
51.1.87 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 All 41 releases
king-addons / includes / widgets / Woo_Product_Tabs / Woo_Product_Tabs.php

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

490 lines 18.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Woo Product Tabs widget.
4 *
5 * @package King_Addons
6 */
7
8 namespace King_Addons;
9
10 use Elementor\Controls_Manager;
11 use Elementor\Group_Control_Typography;
12 use Elementor\Repeater;
13
14 if (!defined('ABSPATH')) {
15 exit;
16 }
17
18 /**
19 * Renders WooCommerce product tabs.
20 */
21 class Woo_Product_Tabs extends Abstract_Single_Widget
22 {
23 public function get_name(): string
24 {
25 return 'woo_product_tabs';
26 }
27
28 public function get_title(): string
29 {
30 return esc_html__('Product Tabs', 'king-addons');
31 }
32
33 public function get_icon(): string
34 {
35 return 'king-addons-icon king-addons-woo-product-tabs';
36 }
37
38 public function get_categories(): array
39 {
40 return ['king-addons-woo-builder'];
41 }
42
43 public function get_style_depends(): array
44 {
45 return [KING_ADDONS_ASSETS_UNIQUE_KEY . '-woo-product-tabs-style'];
46 }
47
48 public function get_script_depends(): array
49 {
50 return [KING_ADDONS_ASSETS_UNIQUE_KEY . '-woo-product-tabs-script'];
51 }
52
53 protected function register_controls(): void
54 {
55 $this->start_controls_section(
56 'section_content',
57 [
58 'label' => esc_html__('Content', 'king-addons'),
59 'tab' => Controls_Manager::TAB_CONTENT,
60 ]
61 );
62
63 $this->add_control(
64 'layout',
65 [
66 'label' => esc_html__('Layout', 'king-addons'),
67 'type' => Controls_Manager::SELECT,
68 'options' => [
69 'horizontal' => esc_html__('Horizontal', 'king-addons'),
70 'vertical' => sprintf(__('Vertical %s', 'king-addons'), '<i class="eicon-pro-icon"></i>'),
71 'accordion' => sprintf(__('Accordion %s', 'king-addons'), '<i class="eicon-pro-icon"></i>'),
72 ],
73 'default' => 'horizontal',
74 ]
75 );
76
77 $this->add_control(
78 'tabs_selection',
79 [
80 'label' => sprintf(__('Tabs to show %s', 'king-addons'), '<i class="eicon-pro-icon"></i>'),
81 'type' => Controls_Manager::SELECT2,
82 'multiple' => true,
83 'label_block' => true,
84 'options' => [
85 'description' => esc_html__('Description', 'king-addons'),
86 'additional_information' => esc_html__('Additional Information', 'king-addons'),
87 'reviews' => esc_html__('Reviews', 'king-addons'),
88 ],
89 'default' => ['description', 'additional_information', 'reviews'],
90 ]
91 );
92
93 $repeater = new Repeater();
94 $repeater->add_control(
95 'tab_key',
96 [
97 'label' => esc_html__('Tab', 'king-addons'),
98 'type' => Controls_Manager::SELECT,
99 'options' => [
100 'description' => esc_html__('Description', 'king-addons'),
101 'additional_information' => esc_html__('Additional Information', 'king-addons'),
102 'reviews' => esc_html__('Reviews', 'king-addons'),
103 ],
104 'default' => 'description',
105 ]
106 );
107
108 $repeater->add_control(
109 'custom_label',
110 [
111 'label' => esc_html__('Custom label', 'king-addons'),
112 'type' => Controls_Manager::TEXT,
113 ]
114 );
115
116 $repeater->add_control(
117 'enabled',
118 [
119 'label' => esc_html__('Show tab', 'king-addons'),
120 'type' => Controls_Manager::SWITCHER,
121 'return_value' => 'yes',
122 'default' => 'yes',
123 ]
124 );
125
126 $this->add_control(
127 'tabs_manager',
128 [
129 'label' => sprintf(__('Tabs manager %s', 'king-addons'), '<i class="eicon-pro-icon"></i>'),
130 'type' => Controls_Manager::REPEATER,
131 'fields' => $repeater->get_controls(),
132 'default' => [
133 ['tab_key' => 'description', 'enabled' => 'yes'],
134 ['tab_key' => 'additional_information', 'enabled' => 'yes'],
135 ['tab_key' => 'reviews', 'enabled' => 'yes'],
136 ],
137 'title_field' => '{{ tab_key }}',
138 ]
139 );
140
141 $this->add_control(
142 'active_tab',
143 [
144 'label' => esc_html__('Default Active Tab', 'king-addons'),
145 'type' => Controls_Manager::SELECT,
146 'options' => [
147 'description' => esc_html__('Description', 'king-addons'),
148 'additional_information' => esc_html__('Additional Information', 'king-addons'),
149 'reviews' => esc_html__('Reviews', 'king-addons'),
150 ],
151 'default' => 'description',
152 ]
153 );
154
155 $this->add_control(
156 'ajax_load',
157 [
158 'label' => sprintf(__('AJAX load content %s', 'king-addons'), '<i class="eicon-pro-icon"></i>'),
159 'type' => Controls_Manager::SWITCHER,
160 'return_value' => 'yes',
161 ]
162 );
163
164 $this->end_controls_section();
165
166 $this->start_controls_section(
167 'section_style_tabs',
168 [
169 'label' => esc_html__('Tabs', 'king-addons'),
170 'tab' => Controls_Manager::TAB_STYLE,
171 ]
172 );
173
174 $this->add_group_control(
175 Group_Control_Typography::get_type(),
176 [
177 'name' => 'tabs_typo',
178 'selector' => '{{WRAPPER}} .ka-woo-tabs__nav button',
179 ]
180 );
181
182 $this->add_control(
183 'tabs_color',
184 [
185 'label' => esc_html__('Text Color', 'king-addons'),
186 'type' => Controls_Manager::COLOR,
187 'selectors' => [
188 '{{WRAPPER}} .ka-woo-tabs__nav button' => 'color: {{VALUE}};',
189 ],
190 ]
191 );
192
193 $this->add_control(
194 'tabs_color_active',
195 [
196 'label' => esc_html__('Active Text Color', 'king-addons'),
197 'type' => Controls_Manager::COLOR,
198 'selectors' => [
199 '{{WRAPPER}} .ka-woo-tabs__nav button.is-active' => 'color: {{VALUE}};',
200 ],
201 ]
202 );
203
204 $this->add_control(
205 'tabs_border_active',
206 [
207 'label' => esc_html__('Active Border Color', 'king-addons'),
208 'type' => Controls_Manager::COLOR,
209 'selectors' => [
210 '{{WRAPPER}} .ka-woo-tabs__nav button.is-active' => 'border-color: {{VALUE}};',
211 ],
212 ]
213 );
214
215 $this->add_responsive_control(
216 'tabs_gap',
217 [
218 'label' => esc_html__('Tabs Gap', 'king-addons'),
219 'type' => Controls_Manager::SLIDER,
220 'range' => [
221 'px' => ['min' => 0, 'max' => 30],
222 ],
223 'selectors' => [
224 '{{WRAPPER}} .ka-woo-tabs__nav' => 'gap: {{SIZE}}{{UNIT}};',
225 ],
226 ]
227 );
228
229 $this->end_controls_section();
230
231 $this->start_controls_section(
232 'section_style_content',
233 [
234 'label' => esc_html__('Content', 'king-addons'),
235 'tab' => Controls_Manager::TAB_STYLE,
236 ]
237 );
238
239 $this->add_group_control(
240 Group_Control_Typography::get_type(),
241 [
242 'name' => 'content_typo',
243 'selector' => '{{WRAPPER}} .ka-woo-tabs__panel',
244 ]
245 );
246
247 $this->add_control(
248 'content_color',
249 [
250 'label' => esc_html__('Content Color', 'king-addons'),
251 'type' => Controls_Manager::COLOR,
252 'selectors' => [
253 '{{WRAPPER}} .ka-woo-tabs__panel' => 'color: {{VALUE}};',
254 ],
255 ]
256 );
257
258 $this->add_responsive_control(
259 'content_padding',
260 [
261 'label' => esc_html__('Content Padding', 'king-addons'),
262 'type' => Controls_Manager::DIMENSIONS,
263 'size_units' => ['px', 'em', '%'],
264 'selectors' => [
265 '{{WRAPPER}} .ka-woo-tabs__panel' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
266 ],
267 ]
268 );
269
270 $this->end_controls_section();
271 }
272
273 protected function render(): void
274 {
275 $product = $this->get_product();
276 if (!$product) {
277 $this->render_missing_product_notice();
278 return;
279 }
280
281 // WooCommerce assembles the tab list - and runs every tab callback -
282 // off the globals: woocommerce_default_product_tabs() reads
283 // $post->post_content for the description tab and comments_open() for
284 // the reviews tab, and the callbacks themselves call the_content().
285 // Point both globals at the product this widget resolved and put them
286 // back afterwards. Declaring `global $product` here instead would
287 // rebind the variable to the global and throw away the product
288 // resolved above, which is null on any page that reaches the widget
289 // without the global set; and leaving $post alone left the widget
290 // completely blank in the editor, where $post is the template.
291 $previous_product = $GLOBALS['product'] ?? null;
292 $previous_post = $GLOBALS['post'] ?? null;
293 $product_post = get_post($product->get_id());
294
295 $GLOBALS['product'] = $product;
296 if ($product_post instanceof \WP_Post) {
297 $GLOBALS['post'] = $product_post;
298 setup_postdata($product_post);
299 }
300
301 try {
302 $this->render_tabs($product);
303 } finally {
304 $GLOBALS['product'] = $previous_product;
305 $GLOBALS['post'] = $previous_post;
306 if ($previous_post instanceof \WP_Post) {
307 setup_postdata($previous_post);
308 }
309 }
310 }
311
312 /**
313 * Insert a merged / third-party tab among already-ordered tabs by Woo priority.
314 *
315 * Product Tabs rebuilds the list from the manager or “Tabs to show”, then
316 * used to append Custom Tabs after every native row. That ignored the
317 * Custom Tabs Priority control (qa-526).
318 *
319 * @param array<string, array<string, mixed>> $ordered Already ordered tabs.
320 * @param string $key Tab key to insert.
321 * @param array<string, mixed> $tab Tab payload from woocommerce_product_tabs.
322 *
323 * @return array<string, array<string, mixed>>
324 */
325 private static function insert_tab_by_priority(array $ordered, string $key, array $tab): array
326 {
327 if (isset($ordered[$key])) {
328 return $ordered;
329 }
330 $pri = isset($tab['priority']) ? (int) $tab['priority'] : 80;
331 $out = [];
332 $inserted = false;
333 foreach ($ordered as $existing_key => $existing) {
334 $existing_pri = isset($existing['priority']) ? (int) $existing['priority'] : 80;
335 if (!$inserted && $pri < $existing_pri) {
336 $out[$key] = $tab;
337 $inserted = true;
338 }
339 $out[$existing_key] = $existing;
340 }
341 if (!$inserted) {
342 $out[$key] = $tab;
343 }
344 return $out;
345 }
346
347 /**
348 * Render the tab nav and panels for a product.
349 *
350 * @param \WC_Product $product Product to render tabs for.
351 * @return void
352 */
353 private function render_tabs($product): void
354 {
355 $settings = $this->get_settings_for_display();
356 $can_pro = king_addons_can_use_pro();
357
358 // The value lands in a class name, so anything outside the three known
359 // layouts has to be dropped rather than passed through.
360 $layout = (string) ($settings['layout'] ?? 'horizontal');
361 if (!in_array($layout, ['horizontal', 'vertical', 'accordion'], true)) {
362 $layout = 'horizontal';
363 }
364 if (in_array($layout, ['vertical', 'accordion'], true) && !$can_pro) {
365 $layout = 'horizontal';
366 }
367
368 $manager_rows = $settings['tabs_manager'] ?? [];
369 $allowed_tabs = $settings['tabs_selection'] ?? ['description', 'additional_information', 'reviews'];
370 if (!$can_pro || empty($allowed_tabs)) {
371 $allowed_tabs = ['description', 'additional_information', 'reviews'];
372 }
373
374 $active_tab = $settings['active_tab'] ?? 'description';
375 $ajax = !empty($settings['ajax_load']) && $can_pro;
376
377 $tabs = apply_filters('woocommerce_product_tabs', []);
378 $all_tabs = $tabs;
379 if (empty($tabs)) {
380 // Nothing to show, but an editor that renders nothing at all looks
381 // like the widget is broken - say why instead.
382 if (\Elementor\Plugin::$instance->editor->is_edit_mode()) {
383 echo '<div class="king-addons-woo-builder-notice">'
384 . esc_html__('This product has no description, attributes or reviews, so there are no tabs to show.', 'king-addons')
385 . '</div>';
386 }
387 return;
388 }
389
390 // Use manager order if enabled and Pro, otherwise fallback to selection list.
391 if ($can_pro && !empty($manager_rows)) {
392 $ordered_tabs = [];
393 foreach ($manager_rows as $row) {
394 if (empty($row['tab_key'])) {
395 continue;
396 }
397 $key = sanitize_key($row['tab_key']);
398 $enabled = !isset($row['enabled']) || 'yes' === $row['enabled'];
399 if (!$enabled || !isset($tabs[$key])) {
400 continue;
401 }
402 if (!empty($row['custom_label'])) {
403 // Titles go out through wp_kses_post() below, which is what
404 // makes them safe. Escaping here as well turned a label like
405 // "<b>New</b>" into visible &lt;b&gt; markup on the page.
406 $tabs[$key]['title'] = $row['custom_label'];
407 }
408 $ordered_tabs[$key] = $tabs[$key];
409 }
410 // If manager removed everything, keep original.
411 if (!empty($ordered_tabs)) {
412 $tabs = $ordered_tabs;
413 }
414 // Manager only lists native Woo tabs. Keep merged / third-party keys
415 // (Product Custom Tabs merge_wc_tabs, extra plugins) ordered by
416 // their Woo priority so Custom Tabs Priority is not a no-op.
417 $native_keys = ['description', 'additional_information', 'reviews'];
418 foreach ($all_tabs as $key => $tab) {
419 if (isset($tabs[$key]) || in_array($key, $native_keys, true)) {
420 continue;
421 }
422 $tabs = self::insert_tab_by_priority($tabs, $key, $tab);
423 }
424 } else {
425 $ordered_tabs = [];
426 foreach ($allowed_tabs as $key) {
427 if (isset($tabs[$key])) {
428 $ordered_tabs[$key] = $tabs[$key];
429 }
430 }
431 // Keep third-party tabs, but honor “Tabs to show” for Woo’s own keys.
432 $native_keys = ['description', 'additional_information', 'reviews'];
433 foreach ($tabs as $key => $tab) {
434 if (isset($ordered_tabs[$key])) {
435 continue;
436 }
437 if (in_array($key, $native_keys, true)) {
438 continue;
439 }
440 $ordered_tabs = self::insert_tab_by_priority($ordered_tabs, $key, $tab);
441 }
442 $tabs = $ordered_tabs;
443 }
444
445 if (!isset($tabs[$active_tab])) {
446 $active_tab = array_key_first($tabs);
447 }
448
449 $wrapper_classes = ['ka-woo-tabs', 'ka-woo-tabs--' . $layout];
450 echo '<div class="' . esc_attr(implode(' ', $wrapper_classes)) . '" data-active="' . esc_attr($active_tab) . '" data-ajax="' . ($ajax ? 'yes' : 'no') . '" data-ajax-url="' . esc_url(admin_url('admin-ajax.php')) . '" data-nonce="' . esc_attr(wp_create_nonce(Woo_Product_Tabs_Ajax::NONCE)) . '" data-product-id="' . esc_attr($product->get_id()) . '" data-error-text="' . esc_attr__('Could not load content.', 'king-addons') . '">';
451
452 echo '<div class="ka-woo-tabs__nav" role="tablist">';
453 foreach ($tabs as $key => $tab) {
454 $is_active = $key === $active_tab ? ' is-active' : '';
455 echo '<button type="button" role="tab" class="ka-woo-tabs__tab' . $is_active . '" data-tab="' . esc_attr($key) . '" aria-selected="' . ($is_active ? 'true' : 'false') . '">' . wp_kses_post($tab['title']) . '</button>';
456 }
457 echo '</div>';
458
459 echo '<div class="ka-woo-tabs__panels">';
460 foreach ($tabs as $key => $tab) {
461 $is_active = $key === $active_tab ? ' is-active' : '';
462 echo '<div class="ka-woo-tabs__panel' . $is_active . '" data-tab="' . esc_attr($key) . '">';
463 if ('accordion' === $layout) {
464 echo '<button type="button" class="ka-woo-tabs__accordion-toggle" data-tab="' . esc_attr($key) . '" aria-expanded="' . ($is_active ? 'true' : 'false') . '">' . wp_kses_post($tab['title']) . '</button>';
465 echo '<div class="ka-woo-tabs__accordion-body">';
466 }
467 // Native Woo tabs can be deferred. Merged / third-party tabs
468 // (Custom Tabs merge_wc_tabs) are not in the AJAX handler's
469 // default list unless the builder template is already current,
470 // so they must render inline or the panel shows the error string.
471 $native_keys = ['description', 'additional_information', 'reviews'];
472 $defer = $ajax && !$is_active && in_array($key, $native_keys, true);
473 if ($defer) {
474 echo '<div class="ka-woo-tabs__placeholder"></div>';
475 } else {
476 if (isset($tab['callback'])) {
477 call_user_func($tab['callback'], $key, $tab);
478 }
479 }
480 if ('accordion' === $layout) {
481 echo '</div>';
482 }
483 echo '</div>';
484 }
485 echo '</div>';
486
487 echo '</div>';
488 }
489 }
490