PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.2
4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 All 200 releases
← All changes | includes/Editors/Elementor.php +103 -22 4.5.04.9.2 View file →
@@ -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'] ) ) {