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_Cart_Table / Woo_Cart_Table.php

Woo_Cart_Table.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_Cart_Table/Woo_Cart_Table.php

543 lines 17.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Woo Cart Table widget.
4 *
5 * @package King_Addons
6 */
7
8 namespace King_Addons;
9
10 use Elementor\Controls_Manager;
11 use Elementor\Group_Control_Border;
12 use Elementor\Group_Control_Box_Shadow;
13 use Elementor\Group_Control_Typography;
14
15 if (!defined('ABSPATH')) {
16 exit;
17 }
18
19 /**
20 * Renders the WooCommerce cart table.
21 */
22 class Woo_Cart_Table extends Abstract_Cart_Widget
23 {
24 /**
25 * Widget slug.
26 *
27 * @return string
28 */
29 public function get_name(): string
30 {
31 return 'woo_cart_table';
32 }
33
34 /**
35 * Widget title.
36 *
37 * @return string
38 */
39 public function get_title(): string
40 {
41 return esc_html__('Cart Table', 'king-addons');
42 }
43
44 /**
45 * Widget icon.
46 *
47 * @return string
48 */
49 public function get_icon(): string
50 {
51 return 'king-addons-icon king-addons-woo-cart-table';
52 }
53
54 /**
55 * Categories.
56 *
57 * @return array<int, string>
58 */
59 public function get_categories(): array
60 {
61 return ['king-addons-woo-builder'];
62 }
63
64 /**
65 * Style dependencies.
66 *
67 * @return array<int, string>
68 */
69 public function get_style_depends(): array
70 {
71 return [KING_ADDONS_ASSETS_UNIQUE_KEY . '-woo-cart-table-style'];
72 }
73
74 /**
75 * Script dependencies.
76 *
77 * @return array<int, string>
78 */
79 public function get_script_depends(): array
80 {
81 return [
82 KING_ADDONS_ASSETS_UNIQUE_KEY . '-woo-cart-table-script',
83 ];
84 }
85
86 /**
87 * Register controls.
88 *
89 * @return void
90 */
91 protected function register_controls(): void
92 {
93 $this->start_controls_section(
94 'section_content',
95 [
96 'label' => esc_html__('Content', 'king-addons'),
97 'tab' => Controls_Manager::TAB_CONTENT,
98 ]
99 );
100
101 $this->add_control(
102 'show_collaterals',
103 [
104 'label' => sprintf(__('Include Totals & Cross-sells (Pro) %s', 'king-addons'), '<i class="eicon-pro-icon"></i>'),
105 'type' => Controls_Manager::SWITCHER,
106 'return_value' => 'yes',
107 ]
108 );
109
110 $this->add_control(
111 'heading_empty',
112 [
113 'label' => esc_html__('Empty title', 'king-addons'),
114 'type' => Controls_Manager::TEXT,
115 'dynamic' => ['active' => true],
116 'default' => __('Your cart is empty', 'king-addons'),
117 ]
118 );
119
120 $this->add_control(
121 'message_empty',
122 [
123 'label' => esc_html__('Empty message', 'king-addons'),
124 'type' => Controls_Manager::TEXTAREA,
125 'dynamic' => ['active' => true],
126 'default' => __('Looks like you have not added anything to your cart yet.', 'king-addons'),
127 ]
128 );
129
130 $this->add_control(
131 'primary_btn_text',
132 [
133 'label' => esc_html__('Primary button text', 'king-addons'),
134 'type' => Controls_Manager::TEXT,
135 'dynamic' => ['active' => true],
136 'default' => __('Return to shop', 'king-addons'),
137 ]
138 );
139
140 $this->add_control(
141 'primary_btn_url',
142 [
143 'label' => esc_html__('Primary button URL', 'king-addons'),
144 'type' => Controls_Manager::URL,
145 'dynamic' => ['active' => true],
146 'placeholder' => 'https://',
147 ]
148 );
149
150 $this->add_control(
151 'secondary_btn_text',
152 [
153 'label' => sprintf(__('Secondary button text %s', 'king-addons'), '<i class="eicon-pro-icon"></i>'),
154 'type' => Controls_Manager::TEXT,
155 'dynamic' => ['active' => true],
156 'default' => __('View offers', 'king-addons'),
157 ]
158 );
159
160 $this->add_control(
161 'secondary_btn_url',
162 [
163 'label' => sprintf(__('Secondary button URL %s', 'king-addons'), '<i class="eicon-pro-icon"></i>'),
164 'type' => Controls_Manager::URL,
165 'dynamic' => ['active' => true],
166 'placeholder' => 'https://',
167 ]
168 );
169
170 $this->end_controls_section();
171
172 $this->start_controls_section(
173 'section_style_table',
174 [
175 'label' => esc_html__('Table', 'king-addons'),
176 'tab' => Controls_Manager::TAB_STYLE,
177 ]
178 );
179
180 $this->add_group_control(
181 Group_Control_Typography::get_type(),
182 [
183 'name' => 'table_typography',
184 'selector' => '{{WRAPPER}} .woocommerce-cart-form',
185 ]
186 );
187
188 $this->add_control(
189 'table_text_color',
190 [
191 'label' => esc_html__('Text Color', 'king-addons'),
192 'type' => Controls_Manager::COLOR,
193 'selectors' => [
194 '{{WRAPPER}} .woocommerce-cart-form' => 'color: {{VALUE}};',
195 ],
196 ]
197 );
198
199 $this->add_group_control(
200 Group_Control_Border::get_type(),
201 [
202 'name' => 'table_border',
203 'selector' => '{{WRAPPER}} .woocommerce-cart-form table',
204 ]
205 );
206
207 $this->add_group_control(
208 Group_Control_Box_Shadow::get_type(),
209 [
210 'name' => 'table_shadow',
211 'selector' => '{{WRAPPER}} .woocommerce-cart-form table',
212 ]
213 );
214
215 $this->end_controls_section();
216 }
217
218 /**
219 * Render widget output.
220 *
221 * @return void
222 */
223 protected function render(): void
224 {
225 $settings = $this->get_settings_for_display();
226
227 if (!$this->should_render()) {
228 $this->render_missing_cart_notice();
229 return;
230 }
231
232 if (!function_exists('WC') || !WC()->cart) {
233 return;
234 }
235
236 if (WC()->cart->is_empty()) {
237 $this->render_empty_cart($settings);
238 return;
239 }
240 $can_pro = king_addons_can_use_pro();
241 $include_collaterals = (!empty($settings['show_collaterals']) && $can_pro);
242
243 $had_totals = has_action('woocommerce_cart_collaterals', 'woocommerce_cart_totals');
244 $had_cross = has_action('woocommerce_cart_collaterals', 'woocommerce_cross_sell_display');
245
246 if (!$include_collaterals) {
247 if ($had_totals) {
248 remove_action('woocommerce_cart_collaterals', 'woocommerce_cart_totals', 10);
249 }
250 if ($had_cross) {
251 remove_action('woocommerce_cart_collaterals', 'woocommerce_cross_sell_display', 10);
252 }
253 }
254
255 $ajax_nonce = wp_create_nonce('ka_cart');
256
257 // The empty-cart block after an AJAX removal used to be a hardcoded
258 // minimal version, so emptying the cart in place looked nothing like
259 // loading an empty cart. Hand the configured copy to the script.
260 $shop_id = wc_get_page_id('shop');
261 $shop_url = $shop_id > 0 ? get_permalink($shop_id) : home_url('/');
262 $empty_attrs = ' data-empty-title="' . esc_attr((string) ($settings['heading_empty'] ?? '')) . '"'
263 . ' data-empty-message="' . esc_attr((string) ($settings['message_empty'] ?? '')) . '"'
264 . ' data-empty-primary-text="' . esc_attr((string) ($settings['primary_btn_text'] ?? '')) . '"'
265 . ' data-empty-primary-url="' . esc_url(!empty($settings['primary_btn_url']['url']) ? $settings['primary_btn_url']['url'] : $shop_url) . '"'
266 . ' data-empty-secondary-text="' . esc_attr($can_pro ? (string) ($settings['secondary_btn_text'] ?? '') : '') . '"'
267 . ' data-empty-secondary-url="' . esc_url(($can_pro && !empty($settings['secondary_btn_url']['url'])) ? $settings['secondary_btn_url']['url'] : '') . '"';
268
269 echo '<div class="ka-cart-table" data-ka-cart="1" data-ajax-url="' . esc_url(admin_url('admin-ajax.php')) . '" data-nonce="' . esc_attr($ajax_nonce) . '"' . $empty_attrs . '>';
270 woocommerce_output_all_notices();
271 wc_get_template('cart/cart.php');
272 echo '</div>';
273
274 if (!$include_collaterals) {
275 if ($had_totals) {
276 add_action('woocommerce_cart_collaterals', 'woocommerce_cart_totals', 10);
277 }
278 if ($had_cross) {
279 add_action('woocommerce_cart_collaterals', 'woocommerce_cross_sell_display', 10);
280 }
281 }
282 }
283
284 /**
285 * Render simple empty cart notice.
286 *
287 * @return void
288 */
289 /**
290 * Render empty cart block.
291 *
292 * @param array<string,mixed> $settings Widget settings.
293 *
294 * @return void
295 */
296 protected function render_empty_cart(array $settings = []): void
297 {
298 $shop_id = wc_get_page_id('shop');
299 $shop_url = $shop_id > 0 ? get_permalink($shop_id) : home_url('/');
300 $can_pro = king_addons_can_use_pro();
301
302 $heading = $settings['heading_empty'] ?? __('Your cart is empty', 'king-addons');
303 $message = $settings['message_empty'] ?? __('Looks like you have not added anything to your cart yet.', 'king-addons');
304 $primary_text = $settings['primary_btn_text'] ?? __('Return to shop', 'king-addons');
305 $primary_url = !empty($settings['primary_btn_url']['url']) ? $settings['primary_btn_url']['url'] : $shop_url;
306 $secondary_text = (!empty($settings['secondary_btn_text']) && $can_pro) ? $settings['secondary_btn_text'] : '';
307 $secondary_url = (!empty($settings['secondary_btn_url']['url']) && $can_pro) ? $settings['secondary_btn_url']['url'] : '';
308
309 echo '<div class="ka-cart-empty">';
310 echo '<div class="ka-cart-empty__icon" aria-hidden="true">🛒</div>';
311 if ($heading) {
312 echo '<h3 class="ka-cart-empty__title">' . esc_html($heading) . '</h3>';
313 }
314 if ($message) {
315 echo '<p class="ka-cart-empty__text">' . esc_html($message) . '</p>';
316 }
317 echo '<div class="ka-cart-empty__actions">';
318 if ($primary_text && $primary_url) {
319 echo '<a class="ka-cart-empty__btn ka-cart-empty__btn--primary" href="' . esc_url($primary_url) . '">' . esc_html($primary_text) . '</a>';
320 }
321 if ($secondary_text && $secondary_url) {
322 echo '<a class="ka-cart-empty__btn ka-cart-empty__btn--ghost" href="' . esc_url($secondary_url) . '">' . esc_html($secondary_text) . '</a>';
323 }
324 echo '</div>';
325 /**
326 * Hook to inject additional empty cart content (e.g., Elementor template).
327 */
328 do_action('king_addons/cart/empty_after');
329 echo '</div>';
330 }
331
332 /**
333 * AJAX: Update quantity or remove item.
334 *
335 * @return void
336 */
337 public static function ajax_update(): void
338 {
339 check_ajax_referer('ka_cart', 'nonce');
340
341 if (!WC()->cart) {
342 wp_send_json_error(['message' => esc_html__('Cart unavailable.', 'king-addons')]);
343 }
344
345 $op = sanitize_text_field($_POST['op'] ?? '');
346 $key = sanitize_text_field($_POST['cart_item_key'] ?? '');
347
348 if (!$key) {
349 wp_send_json_error(['message' => esc_html__('Missing cart item.', 'king-addons')]);
350 }
351
352 if ('remove' === $op) {
353 WC()->cart->remove_cart_item($key);
354 } elseif ('qty' === $op) {
355 $qty = isset($_POST['qty']) ? (float) wc_clean(wp_unslash($_POST['qty'])) : 0;
356 WC()->cart->set_quantity($key, $qty, true);
357 } else {
358 wp_send_json_error(['message' => esc_html__('Unknown operation.', 'king-addons')]);
359 }
360
361 WC()->cart->calculate_totals();
362
363 wp_send_json_success(self::build_fragments());
364 }
365
366 /**
367 * AJAX: Apply/remove coupon.
368 *
369 * @return void
370 */
371 public static function ajax_coupon(): void
372 {
373 check_ajax_referer('ka_cart', 'nonce');
374
375 if (!WC()->cart) {
376 wp_send_json_error(['message' => esc_html__('Cart unavailable.', 'king-addons')]);
377 }
378
379 $code = isset($_POST['coupon']) ? wc_format_coupon_code(wp_unslash($_POST['coupon'])) : '';
380 $mode = sanitize_text_field($_POST['mode'] ?? 'apply');
381
382 if (!$code) {
383 wp_send_json_error(['message' => esc_html__('Coupon is empty.', 'king-addons')]);
384 }
385
386 if ('remove' === $mode) {
387 WC()->cart->remove_coupon($code);
388 } else {
389 WC()->cart->apply_coupon($code);
390 }
391
392 WC()->cart->calculate_totals();
393
394 wp_send_json_success(self::build_fragments());
395 }
396
397 /**
398 * Build fragments for AJAX responses.
399 *
400 * @return array<string, mixed>
401 */
402 private static function build_fragments(): array
403 {
404 $empty = WC()->cart->is_empty();
405
406 // Capture notices before cart HTML. Previously render_cart_html() printed
407 // them first via woocommerce_output_all_notices(), so ajax fragments
408 // returned an empty notices string and the coupon widget stayed silent.
409 $notices = function_exists('wc_print_notices') ? wc_print_notices(true) : '';
410 $cart_html = $empty ? self::render_empty_cart_html() : self::render_cart_html();
411 $totals_html = self::render_totals_html($empty);
412 $cross_html = self::render_cross_sells_html($empty);
413
414 return [
415 'cart_html' => $cart_html,
416 'totals_html' => $totals_html,
417 'cross_sells_html' => $cross_html,
418 'notices' => $notices,
419 'empty' => $empty,
420 'total' => WC()->cart->get_cart_contents_count(),
421 'cart_hash' => WC()->cart->get_cart_hash(),
422 ];
423 }
424
425 /**
426 * Render cart HTML.
427 *
428 * @return string
429 */
430 private static function render_cart_html(): string
431 {
432 ob_start();
433 $had_totals = has_action('woocommerce_cart_collaterals', 'woocommerce_cart_totals');
434 $had_cross = has_action('woocommerce_cart_collaterals', 'woocommerce_cross_sell_display');
435 if ($had_totals) {
436 remove_action('woocommerce_cart_collaterals', 'woocommerce_cart_totals', 10);
437 }
438 if ($had_cross) {
439 remove_action('woocommerce_cart_collaterals', 'woocommerce_cross_sell_display', 10);
440 }
441 wc_get_template('cart/cart.php');
442 if ($had_totals) {
443 add_action('woocommerce_cart_collaterals', 'woocommerce_cart_totals', 10);
444 }
445 if ($had_cross) {
446 add_action('woocommerce_cart_collaterals', 'woocommerce_cross_sell_display', 10);
447 }
448 $html = (string) ob_get_clean();
449 return (string) preg_replace('#<div class="cart-collaterals">\s*</div>#', '', $html);
450 }
451
452 /**
453 * Render totals HTML.
454 *
455 * @param bool $empty Is cart empty.
456 *
457 * @return string
458 */
459 private static function render_totals_html(bool $empty): string
460 {
461 if ($empty) {
462 return '';
463 }
464
465 return Woo_Cart_Totals::render_totals_block_html();
466 }
467
468 /**
469 * Render cross-sells HTML.
470 *
471 * @param bool $empty Is cart empty.
472 *
473 * @return string
474 */
475 private static function render_cross_sells_html(bool $empty): string
476 {
477 if ($empty) {
478 return '';
479 }
480
481 ob_start();
482 woocommerce_cross_sell_display();
483 return (string) ob_get_clean();
484 }
485
486 /**
487 * Render empty cart markup for AJAX.
488 *
489 * @return string
490 */
491 private static function render_empty_cart_html(): string
492 {
493 $shop_id = wc_get_page_id('shop');
494 $shop_url = $shop_id > 0 ? get_permalink($shop_id) : home_url('/');
495
496 // The copy comes from the widget, passed along by the script, so the
497 // markup matches what a full page load of an empty cart produces.
498 // phpcs:disable WordPress.Security.NonceVerification.Missing -- verified by the caller.
499 $heading = isset($_POST['empty_title']) ? sanitize_text_field(wp_unslash($_POST['empty_title'])) : __('Your cart is empty', 'king-addons');
500 $message = isset($_POST['empty_message']) ? sanitize_text_field(wp_unslash($_POST['empty_message'])) : '';
501 $primary_text = isset($_POST['empty_primary_text']) ? sanitize_text_field(wp_unslash($_POST['empty_primary_text'])) : __('Return to shop', 'king-addons');
502 $primary_url = isset($_POST['empty_primary_url']) ? esc_url_raw(wp_unslash($_POST['empty_primary_url'])) : '';
503 $secondary_text = isset($_POST['empty_secondary_text']) ? sanitize_text_field(wp_unslash($_POST['empty_secondary_text'])) : '';
504 $secondary_url = isset($_POST['empty_secondary_url']) ? esc_url_raw(wp_unslash($_POST['empty_secondary_url'])) : '';
505 // phpcs:enable WordPress.Security.NonceVerification.Missing
506
507 if ('' === $primary_url) {
508 $primary_url = $shop_url;
509 }
510
511 ob_start();
512 echo '<div class="ka-cart-empty">';
513 echo '<div class="ka-cart-empty__icon" aria-hidden="true">🛒</div>';
514 if ($heading) {
515 echo '<h3 class="ka-cart-empty__title">' . esc_html($heading) . '</h3>';
516 }
517 if ($message) {
518 echo '<p class="ka-cart-empty__text">' . esc_html($message) . '</p>';
519 }
520 echo '<div class="ka-cart-empty__actions">';
521 if ($primary_text && $primary_url) {
522 echo '<a class="ka-cart-empty__btn ka-cart-empty__btn--primary" href="' . esc_url($primary_url) . '">' . esc_html($primary_text) . '</a>';
523 }
524 if ($secondary_text && $secondary_url) {
525 echo '<a class="ka-cart-empty__btn ka-cart-empty__btn--ghost" href="' . esc_url($secondary_url) . '">' . esc_html($secondary_text) . '</a>';
526 }
527 echo '</div>';
528 do_action('king_addons/cart/empty_after');
529 echo '</div>';
530 return (string) ob_get_clean();
531 }
532 }
533
534 // Registration lives in Woo_Builder: admin-ajax.php never loads Elementor
535 // widget files, so hooking from here meant the quantity, remove and coupon
536 // requests all came back as "0".
537
538
539
540
541
542
543