| @@ -11,8 +11,9 @@ | ||
| 11 | 11 | |
| 12 | 12 | use Elementor\Plugin; |
| 13 | 13 | use RadiusTheme\SB\Helpers\Cache; |
| 14 | 14 | use RadiusTheme\SB\Helpers\Fns; |
| 15 | +use RadiusTheme\SB\Helpers\BuilderFns; | |
| 15 | 16 | use WC_Product_Query; |
| 16 | 17 | use WP_Term; |
| 17 | 18 | |
| 18 | 19 | // Do not allow directly accessing this file. |
| @@ -136,10 +137,9 @@ | ||
| 136 | 137 | |
| 137 | 138 | // Pagination. |
| 138 | 139 | 'pagination' => ! empty( $meta['show_pagination'] ), |
| 139 | 140 | 'posts_loading_type' => self::get_data( $meta, 'pagination_type', 'pagination' ), |
| 140 | - 'posts_per_page' => self::get_data( $meta, 'pagination_per_page', '' ) ?: ( ! empty( $meta['posts_per_page'] ) ? absint( $meta['posts_per_page'] ) : '' ), | |
| 141 | - 'visible_range' => max( 1, absint( self::get_data( $meta, 'pagination_visible_range', 5 ) ) ), | |
| 141 | + 'posts_per_page' => self::get_data( $meta, 'pagination_per_page', '' ), | |
| 142 | 142 | |
| 143 | 143 | // Image. |
| 144 | 144 | 'f_img' => ! empty( $meta['show_featured_image'] ), |
| 145 | 145 | 'gallery_img' => ! empty( $meta['show_product_gallery'] ), |
| @@ -263,13 +263,11 @@ | ||
| 263 | 263 | |
| 264 | 264 | $queried_obj = get_queried_object(); |
| 265 | 265 | |
| 266 | 266 | if ( is_shop() || is_product_taxonomy() ) { |
| 267 | - if ( empty( $data['posts_per_page'] ) ) { | |
| 268 | - $data['posts_per_page'] = self::get_products_per_page(); | |
| 269 | - } | |
| 270 | - $data['order_by'] = self::get_data( $data, 'rtsb_orderby', $data['order_by'] ); | |
| 271 | - $data['order'] = self::get_data( $data, 'rtsb_order', $data['order'] ); | |
| 267 | + $data['posts_per_page'] = self::get_products_per_page(); | |
| 268 | + $data['order_by'] = self::get_data( $data, 'rtsb_orderby', $data['order_by'] ); | |
| 269 | + $data['order'] = self::get_data( $data, 'rtsb_order', $data['order'] ); | |
| 272 | 270 | } |
| 273 | 271 | |
| 274 | 272 | if ( ! is_shop() && is_product_taxonomy() ) { |
| 275 | 273 | if ( ! empty( $queried_obj->taxonomy ) ) { |
| @@ -281,13 +279,9 @@ | ||
| 281 | 279 | } |
| 282 | 280 | } |
| 283 | 281 | |
| 284 | 282 | if ( is_search() ) { |
| 285 | - // Ask for the unescaped term: this dataset is data, not output. | |
| 286 | - // get_search_query() defaults to esc_attr(), and pre-escaping here | |
| 287 | - // would be encoded a second time at the output boundary and would | |
| 288 | - // corrupt the JSON payload this array is encoded into. | |
| 289 | - $search_query = get_search_query( false ); | |
| 283 | + $search_query = get_search_query(); | |
| 290 | 284 | |
| 291 | 285 | if ( $search_query ) { |
| 292 | 286 | $data['search_query'] = sanitize_text_field( $search_query ); |
| 293 | 287 | } |
| @@ -317,12 +311,8 @@ | ||
| 317 | 311 | 'comparison_button' => ! empty( $meta['comparison_button'] ), |
| 318 | 312 | 'quick_view_button' => ! empty( $meta['quick_view_button'] ), |
| 319 | 313 | 'show_rating' => ! empty( $meta['show_rating'] ), |
| 320 | 314 | 'show_pagination' => ! empty( $meta['show_pagination'] ), |
| 321 | - // The base Product Archive widget has no pagination-type control: with filters it | |
| 322 | - // uses Ajax Load More (see the widget's pagination notice). Custom-layout widgets | |
| 323 | - // pass their own pagination_type and use a different dataset. | |
| 324 | - 'posts_loading_type' => self::get_data( $meta, 'pagination_type', 'load_more' ), | |
| 325 | 315 | 'cart_icon' => self::get_data( $meta, 'cart_icon', [] ), |
| 326 | 316 | 'wishlist_icon' => self::get_data( $meta, 'wishlist_icon', [] ), |
| 327 | 317 | 'wishlist_icon_added' => self::get_data( $meta, 'wishlist_icon_added', [] ), |
| 328 | 318 | 'comparison_icon' => self::get_data( $meta, 'comparison_icon', [] ), |
| @@ -339,13 +329,9 @@ | ||
| 339 | 329 | ); |
| 340 | 330 | |
| 341 | 331 | $queried_obj = get_queried_object(); |
| 342 | 332 | |
| 343 | - // Use WooCommerce's own taxonomy check (covers product_cat, product_tag | |
| 344 | - // and product_brand) so the queried term is recorded on every product | |
| 345 | - // taxonomy archive — Ajax load-more / filters scope to it. Mirrors the | |
| 346 | - // custom-layout dataset in self::meta_dataset(). | |
| 347 | - if ( ! is_shop() && is_product_taxonomy() ) { | |
| 333 | + if ( ! is_shop() && BuilderFns::is_archive() ) { | |
| 348 | 334 | if ( ! empty( $queried_obj->taxonomy ) ) { |
| 349 | 335 | $data['queried_tax'] = esc_html( $queried_obj->taxonomy ); |
| 350 | 336 | } |
| 351 | 337 | |
| @@ -354,13 +340,9 @@ | ||
| 354 | 340 | } |
| 355 | 341 | } |
| 356 | 342 | |
| 357 | 343 | if ( is_search() ) { |
| 358 | - // Ask for the unescaped term: this dataset is data, not output. | |
| 359 | - // get_search_query() defaults to esc_attr(), and pre-escaping here | |
| 360 | - // would be encoded a second time at the output boundary and would | |
| 361 | - // corrupt the JSON payload this array is encoded into. | |
| 362 | - $search_query = get_search_query( false ); | |
| 344 | + $search_query = get_search_query(); | |
| 363 | 345 | |
| 364 | 346 | if ( $search_query ) { |
| 365 | 347 | $data['search_query'] = sanitize_text_field( $search_query ); |
| 366 | 348 | } |
| @@ -543,18 +525,9 @@ | ||
| 543 | 525 | */ |
| 544 | 526 | public static function pagination_data( $metas, $template ) { |
| 545 | 527 | $el_data = self::meta_dataset( $metas, $template ); |
| 546 | 528 | |
| 547 | - // Returned as an attribute *value*; the caller escapes it for the | |
| 548 | - // attribute context (Elementor's Utils::render_html_attributes() applies | |
| 549 | - // esc_attr()). esc_js() here only double-encoded the payload and, via | |
| 550 | - // addslashes(), corrupted the JSON escapes for apostrophes and | |
| 551 | - // non-ASCII search terms. The HTML-safe flags keep the payload free of | |
| 552 | - // characters esc_attr() would have to encode - see self::archive_data(). | |
| 553 | - return wp_json_encode( | |
| 554 | - $el_data, | |
| 555 | - JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | |
| 556 | - ); | |
| 529 | + return esc_js( wp_json_encode( $el_data, JSON_UNESCAPED_UNICODE ) ); | |
| 557 | 530 | } |
| 558 | 531 | |
| 559 | 532 | /** |
| 560 | 533 | * Container classes |
| @@ -576,9 +549,9 @@ | ||
| 576 | 549 | |
| 577 | 550 | $animation = $metas['hover_animation']; |
| 578 | 551 | |
| 579 | 552 | if ( ! $is_cat ) { |
| 580 | - $cart_txt_class = $metas['show_cart_text'] ? ' show-cart-text' : ' no-cart-text'; | |
| 553 | + $cart_txt_class = $metas['show_cart_text'] ? ' has-cart-text' : ' no-cart-text'; | |
| 581 | 554 | $animation .= ' gallery-hover-' . ( ! empty( $settings['gallery_hover_animation'] ) ? $settings['gallery_hover_animation'] : 'fade' ); |
| 582 | 555 | } |
| 583 | 556 | |
| 584 | 557 | $badge_class = [ |
| @@ -605,15 +578,11 @@ | ||
| 605 | 578 | } |
| 606 | 579 | |
| 607 | 580 | if ( ! $is_cat ) { |
| 608 | 581 | $action_btn_classes = [ |
| 609 | - 'show_compare_text' => 'show-compare-text', | |
| 610 | - 'show_quick_view_text' => 'show-quick-view-text', | |
| 611 | - 'show_wishlist_text' => 'show-wishlist-text', | |
| 612 | - // Quick checkout only had its icon entry here, so the "Show Button Text?" | |
| 613 | - // switch had nothing to emit and the label stayed hidden whatever it was | |
| 614 | - // set to. | |
| 615 | - 'show_quick_checkout_text' => 'show-quick-checkout-text', | |
| 582 | + 'show_compare_text' => 'has-compare-text', | |
| 583 | + 'show_quick_view_text' => 'has-quick-view-text', | |
| 584 | + 'show_wishlist_text' => 'has-wishlist-text', | |
| 616 | 585 | 'show_compare_icon' => 'no-compare-icon', |
| 617 | 586 | 'show_quick_view_icon' => 'no-quick-view-icon', |
| 618 | 587 | 'show_wishlist_icon' => 'no-wishlist-icon', |
| 619 | 588 | 'show_quick_checkout_icon' => 'no-quick_checkout-icon', |
| @@ -659,53 +628,15 @@ | ||
| 659 | 628 | if ( ! rtsb()->has_pro() && ! in_array( $layout, array_keys( Fns::free_layouts( $layout ) ), true ) ) { |
| 660 | 629 | $layout = self::set_default_layout( $layout ); |
| 661 | 630 | } |
| 662 | 631 | |
| 663 | - return self::resolve_existing_layout( $layout, $template ); | |
| 632 | + return $layout; | |
| 664 | 633 | } else { |
| 665 | - return ! empty( $template ) ? self::resolve_existing_layout( self::set_default_layout( $layout ), $template ) : $default; | |
| 634 | + return ! empty( $template ) ? self::set_default_layout( $layout ) : $default; | |
| 666 | 635 | } |
| 667 | 636 | } |
| 668 | 637 | |
| 669 | 638 | /** |
| 670 | - * Falls back to the default layout when the resolved template file cannot | |
| 671 | - * be found. | |
| 672 | - * | |
| 673 | - * Layouts contributed by a theme or an addon (rtsb-*, toyup-*, zilly-*) | |
| 674 | - * stay saved in the Elementor document after that theme or addon is gone. | |
| 675 | - * Without this check the loop asks for a template that no longer exists, | |
| 676 | - * so nothing renders and load_template() raises a "does not exist" notice | |
| 677 | - * for every product in the loop. | |
| 678 | - * | |
| 679 | - * @param string $layout Resolved layout name. | |
| 680 | - * @param string $template Template directory, e.g. "elementor/general/grid/". | |
| 681 | - * Empty when the layout is only used for CSS classes. | |
| 682 | - * | |
| 683 | - * @return string | |
| 684 | - */ | |
| 685 | - private static function resolve_existing_layout( $layout, $template ) { | |
| 686 | - if ( empty( $template ) || empty( $layout ) ) { | |
| 687 | - return $layout; | |
| 688 | - } | |
| 689 | - | |
| 690 | - $cache_key = 'layout_exists_' . $template . $layout; | |
| 691 | - | |
| 692 | - if ( isset( self::$cache[ $cache_key ] ) ) { | |
| 693 | - return self::$cache[ $cache_key ]; | |
| 694 | - } | |
| 695 | - | |
| 696 | - $resolved = $layout; | |
| 697 | - | |
| 698 | - if ( ! Fns::locate_template( $template . $layout ) ) { | |
| 699 | - $resolved = self::set_default_layout( $layout ); | |
| 700 | - } | |
| 701 | - | |
| 702 | - self::$cache[ $cache_key ] = $resolved; | |
| 703 | - | |
| 704 | - return $resolved; | |
| 705 | - } | |
| 706 | - | |
| 707 | - /** | |
| 708 | 639 | * Row classes |
| 709 | 640 | * |
| 710 | 641 | * @param array $settings Settings array. |
| 711 | 642 | * |
| @@ -775,12 +706,11 @@ | ||
| 775 | 706 | return $has_filters ? self::pagination_data( $settings, $template ) : ''; |
| 776 | 707 | } elseif ( $is_cat ) { |
| 777 | 708 | return ''; |
| 778 | 709 | } else { |
| 779 | - $ajax_pagination = ! empty( $settings['show_pagination'] ) && 'pagination' !== $settings['pagination_type']; | |
| 780 | - $page = Fns::product_filters_has_ajax( apply_filters( 'rtsb/builder/set/current/page/type', '' ) ); | |
| 781 | - $is_shop_or_archive = is_shop() || is_product_taxonomy(); | |
| 782 | - $condition = rtsb()->has_pro() && $ajax && ( $ajax_pagination || $has_filters || $page || $is_shop_or_archive ); | |
| 710 | + $ajax_pagination = ! empty( $settings['show_pagination'] ) && 'pagination' !== $settings['pagination_type']; | |
| 711 | + $page = Fns::product_filters_has_ajax( apply_filters( 'rtsb/builder/set/current/page/type', '' ) ); | |
| 712 | + $condition = rtsb()->has_pro() && $ajax && ( $ajax_pagination || $has_filters || $page ); | |
| 783 | 713 | |
| 784 | 714 | return $condition ? self::pagination_data( $settings, $template ) : ''; |
| 785 | 715 | } |
| 786 | 716 | } |
| @@ -795,23 +725,9 @@ | ||
| 795 | 725 | */ |
| 796 | 726 | public static function archive_data( $metas, $template ) { |
| 797 | 727 | $el_data = self::archive_meta_dataset( $metas, $template ); |
| 798 | 728 | |
| 799 | - // The return value is an HTML attribute, so it must be escaped for the | |
| 800 | - // HTML attribute context. esc_js() is a JavaScript-string escaper: it | |
| 801 | - // turns ' back into a bare apostrophe and relies on a backslash | |
| 802 | - // that means nothing to an HTML parser, which let a search term close | |
| 803 | - // the attribute and inject its own. esc_attr() encodes both quote | |
| 804 | - // characters, so the value can never terminate the attribute. | |
| 805 | - // | |
| 806 | - // The JSON is additionally encoded with the HTML-safe flags so it holds | |
| 807 | - // no <, >, &, ' or " at all. That keeps a search term that itself looks | |
| 808 | - // like an entity (e.g. ") intact: esc_attr() does not double-encode | |
| 809 | - // an existing entity, so without this the browser would decode the | |
| 810 | - // term back into a raw quote and break JSON.parse(). | |
| 811 | - $json = wp_json_encode( $el_data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ); | |
| 812 | - | |
| 813 | - return sprintf( ' data-rtsb-ajax="%s"', esc_attr( $json ) ); | |
| 729 | + return ' data-rtsb-ajax=\'' . esc_js( wp_json_encode( $el_data ) ) . '\''; | |
| 814 | 730 | } |
| 815 | 731 | |
| 816 | 732 | /** |
| 817 | 733 | * Slider options. |
| @@ -1384,188 +1300,47 @@ | ||
| 1384 | 1300 | 'pad_counts' => true, |
| 1385 | 1301 | 'hierarchical' => true, |
| 1386 | 1302 | ]; |
| 1387 | 1303 | |
| 1388 | - // Archive-scope only when the URL path itself is a term archive | |
| 1389 | - // (e.g. /product-category/clothing/). On the shop page WordPress can | |
| 1390 | - // resolve ?product_cat=X to a queried term, but in that case we want | |
| 1391 | - // the full hierarchy to remain visible for additional filtering. | |
| 1392 | - $archive_term = self::request_path_is_term_archive( $taxonomy ) ? self::resolve_archive_term_from_path( $taxonomy ) : null; | |
| 1304 | + $cache_key = 'rtsb_get_terms_filter_products' . $taxonomy; | |
| 1305 | + $taxonomy_terms = wp_cache_get( $cache_key, 'shopbuilder' ); | |
| 1393 | 1306 | |
| 1394 | - // The cache key MUST include the archive term id. The scoped list is | |
| 1395 | - // different for every term archive, so a taxonomy-only key leaks the | |
| 1396 | - // first visited category's term list into every other category archive | |
| 1397 | - // when a persistent object cache is active - the leaked terms are then | |
| 1398 | - // counted as empty in the current archive and the whole filter renders | |
| 1399 | - // blank. | |
| 1400 | - $cache_key = $archive_term instanceof WP_Term | |
| 1401 | - ? 'rtsb_get_terms_filter_products_archive_' . $taxonomy . '_' . $archive_term->term_id | |
| 1402 | - : 'rtsb_get_terms_filter_products_full_' . $taxonomy; | |
| 1307 | + if ( false === $taxonomy_terms ) { | |
| 1308 | + $queried_object = get_queried_object(); | |
| 1403 | 1309 | |
| 1404 | - $taxonomy_terms = wp_cache_get( $cache_key, Cache::group() ); | |
| 1310 | + if ( 'archive' === apply_filters( 'rtsb/builder/set/current/page/type', '' ) && $queried_object instanceof WP_Term && $queried_object->taxonomy === $taxonomy ) { | |
| 1311 | + $term_id = $queried_object->term_id; | |
| 1312 | + $taxonomy = $queried_object->taxonomy; | |
| 1313 | + $archive_cache_key = 'rtsb_get_terms_filter_products_' . $taxonomy . $term_id; | |
| 1314 | + $taxonomy_terms = wp_cache_get( $archive_cache_key, 'shopbuilder' ); | |
| 1315 | + if ( false === $taxonomy_terms ) { | |
| 1316 | + $taxonomy_terms = [ $queried_object ]; | |
| 1317 | + $child_args = [ | |
| 1318 | + 'taxonomy' => $taxonomy, | |
| 1319 | + 'child_of' => $term_id, | |
| 1320 | + 'hide_empty' => false, | |
| 1321 | + ]; | |
| 1405 | 1322 | |
| 1406 | - if ( false === $taxonomy_terms ) { | |
| 1407 | - if ( $archive_term instanceof WP_Term ) { | |
| 1408 | - // Treat the URL-derived term as the root of the visible | |
| 1409 | - // hierarchy so the recursive renderer in | |
| 1410 | - // get_product_default_filter_list_html() (which starts | |
| 1411 | - // with $parent = 0) picks it up on child-category | |
| 1412 | - // archives. Clone first to avoid mutating WP's term | |
| 1413 | - // object. | |
| 1414 | - $current_term = clone $archive_term; | |
| 1415 | - $current_term->parent = 0; | |
| 1416 | - $taxonomy_terms = [ $current_term ]; | |
| 1417 | - $child_args = [ | |
| 1418 | - 'taxonomy' => $taxonomy, | |
| 1419 | - 'child_of' => $archive_term->term_id, | |
| 1420 | - 'hide_empty' => false, | |
| 1421 | - ]; | |
| 1323 | + $child_terms = get_terms( $child_args ); | |
| 1422 | 1324 | |
| 1423 | - $child_terms = get_terms( $child_args ); | |
| 1325 | + if ( ! empty( $child_terms ) ) { | |
| 1326 | + $taxonomy_terms = array_merge( $taxonomy_terms, $child_terms ); | |
| 1327 | + } | |
| 1424 | 1328 | |
| 1425 | - if ( ! empty( $child_terms ) && ! is_wp_error( $child_terms ) ) { | |
| 1426 | - $taxonomy_terms = array_merge( $taxonomy_terms, $child_terms ); | |
| 1329 | + wp_cache_set( $archive_cache_key, $taxonomy_terms, 'shopbuilder' ); | |
| 1330 | + Cache::set_data_cache_key( $archive_cache_key ); | |
| 1427 | 1331 | } |
| 1428 | 1332 | } else { |
| 1429 | 1333 | $taxonomy_terms = get_terms( $args ); |
| 1430 | 1334 | } |
| 1431 | 1335 | |
| 1432 | - wp_cache_set( $cache_key, $taxonomy_terms, Cache::group(), 12 * HOUR_IN_SECONDS ); | |
| 1336 | + wp_cache_set( $cache_key, $taxonomy_terms, 'shopbuilder' ); | |
| 1433 | 1337 | Cache::set_data_cache_key( $cache_key ); |
| 1434 | 1338 | } |
| 1435 | 1339 | |
| 1436 | 1340 | return $taxonomy_terms; |
| 1437 | 1341 | } |
| 1438 | - | |
| 1439 | 1342 | /** |
| 1440 | - * Resolve the archive term from the request URL path. Avoids relying on | |
| 1441 | - * get_queried_object() which can return an unexpected term when | |
| 1442 | - * ?product_cat=a,b,c is also present in the request. | |
| 1443 | - * | |
| 1444 | - * @param string $taxonomy Taxonomy slug. | |
| 1445 | - * | |
| 1446 | - * @return \WP_Term|null | |
| 1447 | - */ | |
| 1448 | - private static function resolve_archive_term_from_path( $taxonomy ) { | |
| 1449 | - $rewrite_slugs = self::get_taxonomy_archive_slugs( $taxonomy ); | |
| 1450 | - | |
| 1451 | - if ( empty( $rewrite_slugs ) ) { | |
| 1452 | - return null; | |
| 1453 | - } | |
| 1454 | - | |
| 1455 | - // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Path only used for slug extraction. | |
| 1456 | - $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : ''; | |
| 1457 | - // phpcs:enable | |
| 1458 | - $path = (string) wp_parse_url( $request_uri, PHP_URL_PATH ); | |
| 1459 | - | |
| 1460 | - if ( '' === $path ) { | |
| 1461 | - return null; | |
| 1462 | - } | |
| 1463 | - | |
| 1464 | - foreach ( $rewrite_slugs as $base ) { | |
| 1465 | - $base = trim( $base, '/' ); | |
| 1466 | - $needle = '/' . $base . '/'; | |
| 1467 | - | |
| 1468 | - $pos = strpos( $path, $needle ); | |
| 1469 | - if ( false === $pos ) { | |
| 1470 | - continue; | |
| 1471 | - } | |
| 1472 | - | |
| 1473 | - $remainder = substr( $path, $pos + strlen( $needle ) ); | |
| 1474 | - $segments = array_values( | |
| 1475 | - array_filter( | |
| 1476 | - explode( '/', $remainder ), | |
| 1477 | - static function ( $segment ) { | |
| 1478 | - return '' !== $segment; | |
| 1479 | - } | |
| 1480 | - ) | |
| 1481 | - ); | |
| 1482 | - | |
| 1483 | - if ( empty( $segments ) ) { | |
| 1484 | - continue; | |
| 1485 | - } | |
| 1486 | - | |
| 1487 | - $slug = sanitize_title( end( $segments ) ); | |
| 1488 | - $term = get_term_by( 'slug', $slug, $taxonomy ); | |
| 1489 | - | |
| 1490 | - if ( $term instanceof WP_Term ) { | |
| 1491 | - return $term; | |
| 1492 | - } | |
| 1493 | - } | |
| 1494 | - | |
| 1495 | - return null; | |
| 1496 | - } | |
| 1497 | - | |
| 1498 | - /** | |
| 1499 | - * Decide whether the current REQUEST_URI path represents a real term | |
| 1500 | - * archive page for the given taxonomy. URL-based so it ignores cases | |
| 1501 | - * where WordPress merely resolves a query string into a queried term | |
| 1502 | - * on the shop page. | |
| 1503 | - * | |
| 1504 | - * @param string $taxonomy Taxonomy slug. | |
| 1505 | - * | |
| 1506 | - * @return bool | |
| 1507 | - */ | |
| 1508 | - private static function request_path_is_term_archive( $taxonomy ) { | |
| 1509 | - $rewrite_slugs = self::get_taxonomy_archive_slugs( $taxonomy ); | |
| 1510 | - | |
| 1511 | - if ( empty( $rewrite_slugs ) ) { | |
| 1512 | - return false; | |
| 1513 | - } | |
| 1514 | - | |
| 1515 | - // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Path only used in match. | |
| 1516 | - $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : ''; | |
| 1517 | - // phpcs:enable | |
| 1518 | - $path = (string) wp_parse_url( $request_uri, PHP_URL_PATH ); | |
| 1519 | - | |
| 1520 | - if ( '' === $path ) { | |
| 1521 | - return false; | |
| 1522 | - } | |
| 1523 | - | |
| 1524 | - foreach ( $rewrite_slugs as $slug ) { | |
| 1525 | - if ( '' === $slug ) { | |
| 1526 | - continue; | |
| 1527 | - } | |
| 1528 | - if ( false !== strpos( $path, '/' . trim( $slug, '/' ) . '/' ) ) { | |
| 1529 | - return true; | |
| 1530 | - } | |
| 1531 | - } | |
| 1532 | - | |
| 1533 | - return false; | |
| 1534 | - } | |
| 1535 | - | |
| 1536 | - /** | |
| 1537 | - * Resolve the URL slug(s) used by a taxonomy archive (e.g. product_cat | |
| 1538 | - * maps to "product-category"). Falls back to common WooCommerce defaults | |
| 1539 | - * when the taxonomy object is unavailable. | |
| 1540 | - * | |
| 1541 | - * @param string $taxonomy Taxonomy slug. | |
| 1542 | - * | |
| 1543 | - * @return array<string> | |
| 1544 | - */ | |
| 1545 | - private static function get_taxonomy_archive_slugs( $taxonomy ) { | |
| 1546 | - $slugs = []; | |
| 1547 | - | |
| 1548 | - if ( taxonomy_exists( $taxonomy ) ) { | |
| 1549 | - $tax_obj = get_taxonomy( $taxonomy ); | |
| 1550 | - if ( $tax_obj && ! empty( $tax_obj->rewrite['slug'] ) ) { | |
| 1551 | - $slugs[] = $tax_obj->rewrite['slug']; | |
| 1552 | - } | |
| 1553 | - } | |
| 1554 | - | |
| 1555 | - $defaults = [ | |
| 1556 | - 'product_cat' => 'product-category', | |
| 1557 | - 'product_tag' => 'product-tag', | |
| 1558 | - 'product_brand' => 'product-brand', | |
| 1559 | - ]; | |
| 1560 | - | |
| 1561 | - if ( isset( $defaults[ $taxonomy ] ) ) { | |
| 1562 | - $slugs[] = $defaults[ $taxonomy ]; | |
| 1563 | - } | |
| 1564 | - | |
| 1565 | - return array_unique( array_filter( $slugs ) ); | |
| 1566 | - } | |
| 1567 | - /** | |
| 1568 | 1343 | * Generates an error message block. |
| 1569 | 1344 | * |
| 1570 | 1345 | * @param array $title The title array of the error message block. |
| 1571 | 1346 | * @param string $wrapper Additional CSS class for the wrapper div. |
| @@ -1659,14 +1434,10 @@ | ||
| 1659 | 1434 | $term_children = in_array( $tax->taxonomy, [ 'product_cat', 'product_brand' ], true ) ? get_term_children( $tax->term_id, $tax->taxonomy ) : []; |
| 1660 | 1435 | $taxonomy = $tax->taxonomy; |
| 1661 | 1436 | $product_count = self::get_visible_term_count( $tax->taxonomy, $tax->term_id ); |
| 1662 | 1437 | |
| 1663 | - // On a product taxonomy archive (category/tag/attribute page) scope the | |
| 1664 | - // count to the current archive - only products that are in THIS archive | |
| 1665 | - // AND have this term. This makes the displayed count reflect the current | |
| 1666 | - // category context and lets terms that are empty in this archive be | |
| 1667 | - // dropped from the list. | |
| 1668 | - if ( is_product_taxonomy() || 'archive' === apply_filters( 'rtsb/builder/set/current/page/type', '' ) ) { | |
| 1438 | + // Show count. | |
| 1439 | + if ( 'archive' === apply_filters( 'rtsb/builder/set/current/page/type', '' ) && ! empty( $data['count_display'] ) && 'block' === $data['count_display'] ) { | |
| 1669 | 1440 | $product_count = self::count_tax_data( $tax, $product_count ); |
| 1670 | 1441 | } |
| 1671 | 1442 | |
| 1672 | 1443 | // Show Empty Categories. |
| @@ -1757,15 +1528,8 @@ | ||
| 1757 | 1528 | */ |
| 1758 | 1529 | public static function count_tax_data( $tax, $count ) { |
| 1759 | 1530 | $page = get_queried_object(); |
| 1760 | 1531 | |
| 1761 | - // Scoping only applies on a real taxonomy archive where the queried | |
| 1762 | - // object is a term. On other contexts (e.g. the shop page) the queried | |
| 1763 | - // object is not a term, so keep the original (global) count. | |
| 1764 | - if ( ! $page instanceof \WP_Term ) { | |
| 1765 | - return $count; | |
| 1766 | - } | |
| 1767 | - | |
| 1768 | 1532 | if ( is_array( $tax ) && strpos( $tax['taxonomy'], 'attribute_' ) !== false ) { |
| 1769 | 1533 | $taxonomy = str_replace( 'attribute_', '', $tax['taxonomy'] ); |
| 1770 | 1534 | $term_id = $tax['term_id']; |
| 1771 | 1535 | } else { |
| @@ -1772,20 +1536,8 @@ | ||
| 1772 | 1536 | $taxonomy = $tax->taxonomy; |
| 1773 | 1537 | $term_id = $tax->term_id; |
| 1774 | 1538 | } |
| 1775 | 1539 | |
| 1776 | - // A taxonomy selected through the query string is a removable choice, | |
| 1777 | - // not a fixed archive context, and the filter ORs terms within one | |
| 1778 | - // taxonomy. Scoping a term's count to that selection therefore | |
| 1779 | - // advertises a result the filter never produces - on | |
| 1780 | - // /shop/?product_cat=aliens the Movies count read 32 while choosing | |
| 1781 | - // Movies returns 279 - and it hides every sibling term whose overlap | |
| 1782 | - // with the selection is zero. Only a real term-archive URL | |
| 1783 | - // (/product-category/aliens/) is a fixed context worth scoping to. | |
| 1784 | - if ( $taxonomy === $page->taxonomy && ! self::request_path_is_term_archive( $page->taxonomy ) ) { | |
| 1785 | - return $count; | |
| 1786 | - } | |
| 1787 | - | |
| 1788 | 1540 | if ( strpos( $page->taxonomy, 'pa_' ) !== false ) { |
| 1789 | 1541 | $args['status'] = 'publish'; |
| 1790 | 1542 | $args['limit'] = -1; |
| 1791 | 1543 | $args['return'] = 'ids'; |
| @@ -1810,42 +1562,50 @@ | ||
| 1810 | 1562 | 'operator' => 'NOT IN', |
| 1811 | 1563 | ], |
| 1812 | 1564 | ]; |
| 1813 | 1565 | |
| 1814 | - return Fns::count_products_by_tax_query( $args['tax_query'] ); | |
| 1566 | + $query = new WC_Product_Query( $args ); | |
| 1567 | + $products = $query->get_products(); | |
| 1568 | + | |
| 1569 | + return count( $products ); | |
| 1815 | 1570 | } |
| 1816 | 1571 | |
| 1817 | - // Scope the count to the current archive term (category, brand, tag, etc.) | |
| 1818 | - // so it reflects only products that are in this archive AND also have the | |
| 1819 | - // filter term. Works for cross-taxonomy filters (e.g. brands on a tag | |
| 1820 | - // archive) and same-taxonomy filters (e.g. the Tags filter on a tag | |
| 1821 | - // archive shows the intersection with the current tag). | |
| 1822 | - $args = [ | |
| 1823 | - 'limit' => -1, | |
| 1824 | - 'return' => 'ids', | |
| 1825 | - 'status' => 'publish', | |
| 1826 | - 'tax_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query | |
| 1827 | - 'relation' => 'AND', | |
| 1828 | - [ | |
| 1829 | - 'taxonomy' => $taxonomy, | |
| 1830 | - 'field' => 'term_id', | |
| 1831 | - 'terms' => $term_id, | |
| 1572 | + if ( $taxonomy !== $page->taxonomy ) { | |
| 1573 | + $args = [ | |
| 1574 | + 'limit' => -1, | |
| 1575 | + 'return' => 'ids', | |
| 1576 | + 'status' => 'publish', | |
| 1577 | + 'tax_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query | |
| 1578 | + 'relation' => 'AND', | |
| 1579 | + [ | |
| 1580 | + 'taxonomy' => $taxonomy, | |
| 1581 | + 'field' => 'term_id', | |
| 1582 | + 'terms' => $term_id, | |
| 1583 | + ], | |
| 1584 | + [ | |
| 1585 | + 'taxonomy' => 'product_visibility', | |
| 1586 | + 'terms' => [ 'exclude-from-catalog' ], | |
| 1587 | + 'field' => 'name', | |
| 1588 | + 'operator' => 'NOT IN', | |
| 1589 | + ], | |
| 1832 | 1590 | ], |
| 1833 | - [ | |
| 1834 | - 'taxonomy' => $page->taxonomy, | |
| 1835 | - 'field' => 'term_id', | |
| 1836 | - 'terms' => $page->term_id, | |
| 1837 | - ], | |
| 1838 | - [ | |
| 1839 | - 'taxonomy' => 'product_visibility', | |
| 1840 | - 'terms' => [ 'exclude-from-catalog' ], | |
| 1841 | - 'field' => 'name', | |
| 1842 | - 'operator' => 'NOT IN', | |
| 1843 | - ], | |
| 1844 | - ], | |
| 1845 | - ]; | |
| 1591 | + ]; | |
| 1846 | 1592 | |
| 1847 | - return Fns::count_products_by_tax_query( $args['tax_query'] ); | |
| 1593 | + if ( 'product_cat' === $page->taxonomy ) { | |
| 1594 | + $args['category'] = [ $page->slug ]; | |
| 1595 | + } | |
| 1596 | + | |
| 1597 | + if ( 'product_tag' === $page->taxonomy ) { | |
| 1598 | + $args['tag'] = [ $page->slug ]; | |
| 1599 | + } | |
| 1600 | + | |
| 1601 | + $query = new WC_Product_Query( $args ); | |
| 1602 | + $products = $query->get_products(); | |
| 1603 | + | |
| 1604 | + $count = count( $products ); | |
| 1605 | + } | |
| 1606 | + | |
| 1607 | + return $count; | |
| 1848 | 1608 | } |
| 1849 | 1609 | |
| 1850 | 1610 | /** |
| 1851 | 1611 | * Get the count of visible (non-hidden) products for a given taxonomy term. |
| @@ -1886,9 +1646,10 @@ | ||
| 1886 | 1646 | ], |
| 1887 | 1647 | ], |
| 1888 | 1648 | ]; |
| 1889 | 1649 | |
| 1890 | - $count = Fns::count_products_by_tax_query( $args['tax_query'] ); | |
| 1650 | + $query = new WC_Product_Query( $args ); | |
| 1651 | + $count = count( $query->get_products() ); | |
| 1891 | 1652 | |
| 1892 | 1653 | $cache[ $cache_key ] = $count; |
| 1893 | 1654 | |
| 1894 | 1655 | return $count; |
| @@ -2060,9 +1821,9 @@ | ||
| 2060 | 1821 | } |
| 2061 | 1822 | |
| 2062 | 1823 | $attachment_id = absint( get_term_meta( $args['term_id'], 'product_attribute_image', true ) ); |
| 2063 | 1824 | $image_size = rtwpvs()->get_option( 'attribute_image_size' ); |
| 2064 | - $image_url = wp_get_attachment_image_url( $attachment_id, apply_filters( 'rtsb_product_attribute_image_size', $image_size ) ); | |
| 1825 | + $image_url = wp_get_attachment_image_url( $attachment_id, apply_filters( 'rtwpvs_product_attribute_image_size', $image_size ) ); | |
| 2065 | 1826 | |
| 2066 | 1827 | $html .= sprintf( |
| 2067 | 1828 | // phpcs:ignore Generic.Strings.UnnecessaryStringConcat.Found |
| 2068 | 1829 | '<input type="checkbox" id="' . esc_attr( $args['id'] ) . '" value="' . esc_attr( $args['term_slug'] ) . '" class="rtsb-attr-hidden-field" name="rtsb-filter-' . esc_attr( $args['attribute_name'] ) . '[]' . '" ' . esc_attr( $args['selected_item'] ) . '/><label for="%s" title="' . esc_attr( $args['name'] ) . '" class="rtsb-default-filter-trigger rtsb-image-filter rtsb-term-span rtsb-term-span-%s%s"><img class="rtsb-default-attr-filter" alt="%s" src="%s" /></label>', |
| @@ -2103,10 +1864,9 @@ | ||
| 2103 | 1864 | $html = ''; |
| 2104 | 1865 | |
| 2105 | 1866 | ob_start(); |
| 2106 | 1867 | |
| 2107 | - $is_editor = ! empty( \Elementor\Plugin::$instance->editor ) && \Elementor\Plugin::$instance->editor->is_edit_mode(); | |
| 2108 | - $active = ( $is_editor || ! empty( $_SERVER['QUERY_STRING'] ) ) ? ' active' : ''; | |
| 1868 | + $active = ! empty( $_SERVER['QUERY_STRING'] ) ? ' active' : ''; | |
| 2109 | 1869 | $reset = ! empty( $settings['reset_btn'] ); |
| 2110 | 1870 | $apply_text = ! empty( $settings['apply_filters_btn_text'] ) ? $settings['apply_filters_btn_text'] : esc_html__( 'Apply Filters', 'shopbuilder' ); |
| 2111 | 1871 | $apply_icon = ! empty( $settings['apply_filters_btn_icon'] ) ? $settings['apply_filters_btn_icon'] : []; |
| 2112 | 1872 | echo '<div class="default-filter-btn-wrapper">'; |