| @@ -27,8 +27,9 @@ | ||
| 27 | 27 | use WPDeveloper\BetterDocs\Editors\Elementor\Widget\Basic\FAQ; |
| 28 | 28 | use WPDeveloper\BetterDocs\Editors\Elementor\Widget\Reactions; |
| 29 | 29 | use WPDeveloper\BetterDocs\Editors\Elementor\Widget\Navigation; |
| 30 | 30 | use WPDeveloper\BetterDocs\Editors\Elementor\Widget\CodeSnippet; |
| 31 | +use WPDeveloper\BetterDocs\Editors\Elementor\Widget\CodeSnippetTab; | |
| 31 | 32 | use WPDeveloper\BetterDocs\Editors\Elementor\Widget\ArchiveList; |
| 32 | 33 | use WPDeveloper\BetterDocs\Editors\Elementor\Widget\PrintButton; |
| 33 | 34 | use WPDeveloper\BetterDocs\Editors\Elementor\Widget\Breadcrumbs; |
| 34 | 35 | use WPDeveloper\BetterDocs\Editors\Elementor\Widget\ReadingTime; |
| @@ -204,9 +205,11 @@ | ||
| 204 | 205 | } else if ( $wb->get_name() !== 'betterdocs-sidebar' ) { |
| 205 | 206 | $wb->add_control( |
| 206 | 207 | 'box_per_page', |
| 207 | 208 | [ |
| 208 | - 'label' => __( $wb->get_name() == 'betterdocs-tab-view-list' ? 'Tabs Per Page' : 'Box Per Page', 'betterdocs' ), | |
| 209 | + 'label' => $wb->get_name() == 'betterdocs-tab-view-list' | |
| 210 | + ? __( 'Tabs Per Page', 'betterdocs' ) | |
| 211 | + : __( 'Box Per Page', 'betterdocs' ), | |
| 209 | 212 | 'type' => Controls_Manager::NUMBER, |
| 210 | 213 | 'default' => '8' |
| 211 | 214 | ] |
| 212 | 215 | ); |
| @@ -405,8 +408,21 @@ | ||
| 405 | 408 | 'nested_subcategory' => 'true' |
| 406 | 409 | ] |
| 407 | 410 | ] |
| 408 | 411 | ); |
| 412 | + | |
| 413 | + $wb->add_control( | |
| 414 | + 'lazy_load', | |
| 415 | + [ | |
| 416 | + 'label' => __( 'Lazy Load Docs & Subcategories', 'betterdocs' ), | |
| 417 | + 'description' => __( 'Render only the active category upfront; fetch the rest on click. Recommended for large knowledge bases when this widget is used as a sidebar.', 'betterdocs' ), | |
| 418 | + 'type' => Controls_Manager::SWITCHER, | |
| 419 | + 'label_on' => __( 'Yes', 'betterdocs' ), | |
| 420 | + 'label_off' => __( 'No', 'betterdocs' ), | |
| 421 | + 'return_value' => 'true', | |
| 422 | + 'default' => '' | |
| 423 | + ] | |
| 424 | + ); | |
| 409 | 425 | } |
| 410 | 426 | |
| 411 | 427 | if ( $wb->get_name() === 'betterdocs-category-box' ) { |
| 412 | 428 | $wb->add_control( |
| @@ -454,8 +470,21 @@ | ||
| 454 | 470 | 'return_value' => 'true', |
| 455 | 471 | 'default' => ( $this->settings->get( 'archive_nested_subcategory' ) == 1 ) ? 'true' : '' |
| 456 | 472 | ] |
| 457 | 473 | ); |
| 474 | + | |
| 475 | + $wb->add_control( | |
| 476 | + 'lazy_load', | |
| 477 | + [ | |
| 478 | + 'label' => __( 'Lazy Load Docs & Subcategories', 'betterdocs' ), | |
| 479 | + 'description' => __( 'Render only the active category upfront; fetch the rest on click (or on scroll for the Memphis sidebar layout).', 'betterdocs' ), | |
| 480 | + 'type' => Controls_Manager::SWITCHER, | |
| 481 | + 'label_on' => __( 'Yes', 'betterdocs' ), | |
| 482 | + 'label_off' => __( 'No', 'betterdocs' ), | |
| 483 | + 'return_value' => 'true', | |
| 484 | + 'default' => ( $this->settings->get( 'archive_lazy_load_descendants' ) == 1 ) ? 'true' : '' | |
| 485 | + ] | |
| 486 | + ); | |
| 458 | 487 | } |
| 459 | 488 | |
| 460 | 489 | if ( $wb->get_name() === 'betterdocs-tab-view-list' ) { |
| 461 | 490 | $wb->add_control( |
| @@ -593,17 +622,25 @@ | ||
| 593 | 622 | return ''; |
| 594 | 623 | } |
| 595 | 624 | |
| 596 | 625 | public function get_kb_terms( $prettify = false, $term_id = true ) { |
| 597 | - $args = [ | |
| 598 | - 'taxonomy' => 'knowledge_base', | |
| 599 | - 'hide_empty' => true, | |
| 600 | - 'parent' => 0 | |
| 601 | - ]; | |
| 626 | + // This list populates the widget "Knowledge Bases" selector — an admin-side | |
| 627 | + // config picker, not a front-end listing. It must show EVERY KB regardless of | |
| 628 | + // whether it currently has content or is access-restricted for the editing | |
| 629 | + // user's role. Previously it ran through terms_query()/get_terms(), so on a | |
| 630 | + // regular page the Pro access-control terms_clauses filter + hide_empty pruned | |
| 631 | + // the list to a subset, while the theme-builder context showed them all. | |
| 632 | + // hide_empty:false keeps empty KBs; suppress_filters:true signals AccessControl | |
| 633 | + // (get_terms_args) to skip its role-based terms_clauses filter, so the picker is | |
| 634 | + // consistent everywhere. | |
| 635 | + $terms = get_terms( [ | |
| 636 | + 'taxonomy' => 'knowledge_base', | |
| 637 | + 'hide_empty' => false, | |
| 638 | + 'parent' => 0, | |
| 639 | + 'orderby' => 'name', | |
| 640 | + 'suppress_filters' => true, | |
| 641 | + ] ); | |
| 602 | 642 | |
| 603 | - $terms_query_args = betterdocs()->query->terms_query( $args ); | |
| 604 | - $terms = betterdocs()->query->get_terms( $terms_query_args ); | |
| 605 | - | |
| 606 | 643 | if ( is_wp_error( $terms ) ) { |
| 607 | 644 | return []; |
| 608 | 645 | } |
| 609 | 646 | |
| @@ -610,9 +647,11 @@ | ||
| 610 | 647 | if ( $prettify ) { |
| 611 | 648 | $pretty_taxonomies = []; |
| 612 | 649 | |
| 613 | 650 | foreach ( $terms as $term ) { |
| 614 | - $pretty_taxonomies[$term_id ? $term->term_id : $term->slug] = $term->name; | |
| 651 | + // Decode HTML entities (e.g. "&" → "&") so the select2 control | |
| 652 | + // shows the real KB name instead of the escaped entity. | |
| 653 | + $pretty_taxonomies[$term_id ? $term->term_id : $term->slug] = wp_specialchars_decode( $term->name, ENT_QUOTES ); | |
| 615 | 654 | } |
| 616 | 655 | |
| 617 | 656 | return $pretty_taxonomies; |
| 618 | 657 | } |
| @@ -659,9 +698,10 @@ | ||
| 659 | 698 | 'betterdocs-elementor-category-grid' => CategoryGrid::class, |
| 660 | 699 | 'betterdocs-elementor-category-box' => CategoryBox::class, |
| 661 | 700 | 'betterdocs-elementor-category-slate' => CategorySlateLayout::class, |
| 662 | 701 | 'betterdocs-faq-widget' => FAQ::class, |
| 663 | - 'betterdocs-code-snippet' => CodeSnippet::class | |
| 702 | + 'betterdocs-code-snippet' => CodeSnippet::class, | |
| 703 | + 'betterdocs-code-snippet-tab' => CodeSnippetTab::class | |
| 664 | 704 | ]; |
| 665 | 705 | |
| 666 | 706 | return $widget_arr; |
| 667 | 707 | } |
| @@ -722,8 +762,9 @@ | ||
| 722 | 762 | $assets->register( 'betterdocs-el-category-box', 'elementor/css/category-box.css' ); |
| 723 | 763 | $assets->register( 'betterdocs-el-articles-list', 'elementor/css/articles-list.css' ); |
| 724 | 764 | $assets->register( 'betterdocs-el-navigation', 'elementor/css/navigation.css' ); |
| 725 | 765 | $assets->register( 'betterdocs-code-snippet', 'public/css/code-snippet.css' ); |
| 766 | + $assets->register( 'betterdocs-code-snippet-tab', 'public/css/code-snippet-tab.css' ); | |
| 726 | 767 | |
| 727 | 768 | /** |
| 728 | 769 | * Widget's JS |
| 729 | 770 | */ |
| @@ -728,14 +769,22 @@ | ||
| 728 | 769 | * Widget's JS |
| 729 | 770 | */ |
| 730 | 771 | $assets->register( 'betterdocs-el-category-grid', 'elementor/js/category-grid.js', ['jquery', 'betterdocs-category-toggler'] ); |
| 731 | 772 | $assets->register( 'betterdocs-code-snippet', 'public/js/code-snippet.js', ['jquery'] ); |
| 773 | + $assets->register( 'betterdocs-code-snippet-tab', 'public/js/code-snippet-tab.js', ['jquery'] ); | |
| 732 | 774 | |
| 733 | 775 | /** |
| 734 | 776 | * Localize This In Order To Know If This Block Is Arriving From Betterdocs Templates Or Not |
| 735 | 777 | */ |
| 736 | 778 | betterdocs()->assets->localize( 'betterdocs-el-category-grid', 'betterdocsCategoryGridConfig', [ |
| 737 | - 'is_betterdocs_templates' => betterdocs()->helper->is_templates() ? true : false | |
| 779 | + 'is_betterdocs_templates' => betterdocs()->helper->is_templates() ? true : false, | |
| 780 | + // Must also carry the lazy-load endpoint. wp_localize_script concatenates | |
| 781 | + // every `var betterdocsCategoryGridConfig = …` for this object name and the | |
| 782 | + // last assignment wins; shipping only is_betterdocs_templates here clobbered | |
| 783 | + // Scripts.php's full config on Elementor pages, leaving ajax_url undefined so | |
| 784 | + // loadLazyBody() bailed and sidebar lazy-load never fired. | |
| 785 | + 'ajax_url' => admin_url( 'admin-ajax.php' ), | |
| 786 | + 'lazy_load_action' => 'betterdocs_lazy_category_body', | |
| 738 | 787 | ] ); |
| 739 | 788 | |
| 740 | 789 | if ( betterdocs()->helper->is_el_templates() == true ) { |
| 741 | 790 | $assets->enqueue( 'betterdocs-elementor-editor', 'elementor/css/betterdocs-el-edit.css' ); |
| @@ -888,17 +937,22 @@ | ||
| 888 | 937 | * @return array|mixed |
| 889 | 938 | * @since 2.0.0 |
| 890 | 939 | */ |
| 891 | 940 | public function get_templates() { |
| 892 | - $_cache_key = 'betterdocs_template_library_' . betterdocs()->version; | |
| 893 | - $templates = get_transient( $_cache_key ); | |
| 941 | + $_cache_key = 'betterdocs_template_library_' . betterdocs()->version; | |
| 942 | + $source = new TemplateSource; | |
| 943 | + $local_ids = array_column( $source->get_local_templates(), 'template_id' ); | |
| 944 | + $templates = get_transient( $_cache_key ); | |
| 894 | 945 | |
| 895 | 946 | if ( ! $templates ) { |
| 896 | - $source = new TemplateSource; | |
| 897 | - $templates = $source->get_items(); | |
| 947 | + // Only cache remote templates — local templates are always appended fresh below | |
| 948 | + $all_templates = $source->get_items(); | |
| 949 | + $remote_only = array_values( array_filter( $all_templates, function ( $t ) use ( $local_ids ) { | |
| 950 | + return ! in_array( $t['template_id'], $local_ids, true ); | |
| 951 | + } ) ); | |
| 898 | 952 | |
| 899 | - if ( ! empty( $templates ) ) { | |
| 900 | - $templates = array_map( function ( $template ) { | |
| 953 | + if ( ! empty( $remote_only ) ) { | |
| 954 | + $remote_only = array_map( function ( $template ) { | |
| 901 | 955 | $template['id'] = $template['template_id']; |
| 902 | 956 | $template['tmpl_created'] = $template['date']; |
| 903 | 957 | $template['tags'] = json_encode( $template['tags'] ); |
| 904 | 958 | $template['is_pro'] = $template['isPro']; |
| @@ -907,25 +961,52 @@ | ||
| 907 | 961 | $template['trend_index'] = $template['trendIndex']; |
| 908 | 962 | $template['has_page_settings'] = $template['hasPageSettings']; |
| 909 | 963 | |
| 910 | 964 | return $template; |
| 911 | - }, $templates ); | |
| 965 | + }, $remote_only ); | |
| 912 | 966 | |
| 913 | - set_transient( $_cache_key, $templates, WEEK_IN_SECONDS ); | |
| 967 | + set_transient( $_cache_key, $remote_only, WEEK_IN_SECONDS ); | |
| 968 | + $templates = $remote_only; | |
| 914 | 969 | } else { |
| 915 | 970 | $templates = []; |
| 916 | 971 | } |
| 972 | + } else { | |
| 973 | + // Strip any local templates that may have been cached in a previous version | |
| 974 | + $templates = array_values( array_filter( $templates, function ( $t ) use ( $local_ids ) { | |
| 975 | + $id = isset( $t['template_id'] ) ? $t['template_id'] : ( isset( $t['id'] ) ? $t['id'] : '' ); | |
| 976 | + return ! in_array( $id, $local_ids, true ); | |
| 977 | + } ) ); | |
| 917 | 978 | } |
| 918 | 979 | |
| 919 | - return $templates; | |
| 980 | + // Always append local templates fresh so they are never stale-cached | |
| 981 | + $local_templates = array_map( function ( $template ) { | |
| 982 | + $template['id'] = $template['template_id']; | |
| 983 | + $template['tmpl_created'] = $template['date']; | |
| 984 | + $template['tags'] = json_encode( $template['tags'] ); | |
| 985 | + $template['is_pro'] = $template['isPro']; | |
| 986 | + $template['access_level'] = $template['accessLevel']; | |
| 987 | + $template['popularity_index'] = $template['popularityIndex']; | |
| 988 | + $template['trend_index'] = $template['trendIndex']; | |
| 989 | + $template['has_page_settings'] = $template['hasPageSettings']; | |
| 990 | + | |
| 991 | + return $template; | |
| 992 | + }, $source->get_local_templates() ); | |
| 993 | + | |
| 994 | + return array_merge( $templates, $local_templates ); | |
| 920 | 995 | } |
| 921 | 996 | |
| 922 | 997 | public function modified_ajax_action( $ajax ) { |
| 998 | + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Elementor's own ajax handler verifies nonce upstream. | |
| 923 | 999 | if ( ! isset( $_REQUEST['actions'] ) ) { |
| 924 | 1000 | return; |
| 925 | 1001 | } |
| 926 | 1002 | |
| 927 | - $actions = json_decode( stripslashes( $_REQUEST['actions'] ), true ); | |
| 1003 | + // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Elementor's ajax handler verifies nonce; raw JSON is structure-validated below. | |
| 1004 | + $raw_actions = wp_unslash( $_REQUEST['actions'] ); | |
| 1005 | + $actions = json_decode( $raw_actions, true ); | |
| 1006 | + if ( ! is_array( $actions ) ) { | |
| 1007 | + return; | |
| 1008 | + } | |
| 928 | 1009 | $data = false; |
| 929 | 1010 | |
| 930 | 1011 | foreach ( $actions as $id => $action_data ) { |
| 931 | 1012 | if ( ! isset( $action_data['get_template_data'] ) ) { |