PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.3
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.3
4.9.3 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 All 201 releases
← All changes | views/template-parts/nested-categories.php +97 -19 4.4.1 → 4.9.3 View file →
@@ -1,6 +1,11 @@
1 1 <?php
2 +// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- view template receives variables via extract(); prefixing is impractical.
2 3
4 +
5 +if ( ! defined( 'ABSPATH' ) ) {
6 + exit;
7 +}
3 8 use WPDeveloper\BetterDocs\Utils\Helper;
4 9
5 10 if ( ! $nested_subcategory ) {
6 11 return;
@@ -10,13 +15,16 @@
10 15 'parent' => $term_id,
11 16 'hide_empty' => true
12 17 ];
13 18
19 +// User-controlled exclusion list for nested category rendering — exclude is required UX.
20 +// phpcs:disable WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude
14 21 if ( isset( $terms_exclude ) ) {
15 22 $_terms_args['exclude'] = $terms_exclude;
16 23 } elseif ( isset( $exclude ) ) {
17 24 $_terms_args['exclude'] = $exclude;
18 25 }
26 +// phpcs:enable WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude
19 27
20 28 $nested_terms_query = isset( $nested_terms_query ) ? array_merge( $_terms_args, $nested_terms_query ) : $_terms_args;
21 29
22 30 $_nested_categories = get_terms( betterdocs()->query->terms_query( apply_filters( 'betterdocs_nested_terms_args', $nested_terms_query ) ) );
@@ -24,8 +32,55 @@
24 32 if ( empty( $_nested_categories ) ) {
25 33 return;
26 34 }
27 35
36 +$_nested_term_ids = wp_list_pluck( $_nested_categories, 'term_id' );
37 +if ( ! empty( $_nested_term_ids ) ) {
38 + update_termmeta_cache( $_nested_term_ids );
39 +}
40 +
41 +// Fragment cache: cache the rendered HTML of the entire nested subtree at the
42 +// outermost invocation. The active-branch highlighting is baked into the
43 +// rendered HTML server-side (see the $classes / inline-style computation
44 +// below), so the cache key must vary by the current page's identity as well as
45 +// the top-level term, caps, and kb (see the key composition further down).
46 +// (`static` at file scope doesn't persist across the recursive include, so
47 +// the depth tracker has to live on a global.)
48 +global $bd_nested_depth;
49 +if ( ! isset( $bd_nested_depth ) ) {
50 + $bd_nested_depth = 0;
51 +}
52 +$bd_is_outermost = ( $bd_nested_depth === 0 );
53 +$bd_cache_key = '';
54 +
55 +if ( $bd_is_outermost ) {
56 + $bd_can_priv = current_user_can( 'read_private_docs' ) ? 1 : 0;
57 + $bd_multi_kb = isset( $multiple_knowledge_base ) && $multiple_knowledge_base ? 1 : 0;
58 + $bd_kb_slug = isset( $kb_slug ) ? $kb_slug : '';
59 + $bd_cat_icon = isset( $category_icon ) ? (string) $category_icon : '';
60 + // Active-state highlighting is now baked into the rendered HTML (see
61 + // $classes / inline style computation below). That means the cache key
62 + // must also vary by the current page's identity, otherwise a fragment
63 + // rendered while viewing page A would be served unchanged to page B with
64 + // the wrong .active branch.
65 + $bd_queried = (int) get_queried_object_id();
66 + $bd_is_single = is_singular( 'docs' ) ? 1 : 0;
67 + $bd_version = betterdocs()->database->get_cache_version( 'betterdocs_term_counts' );
68 + $bd_icon_disc = md5( wp_json_encode( isset( $list_icon_name ) ? $list_icon_name : "" ) );
69 + $bd_show_icon = ( isset( $show_list_icon ) && $show_list_icon === false ) ? 0 : 1;
70 + $bd_order_disc = md5( wp_json_encode( array( isset( $nested_terms_query ) ? $nested_terms_query : array(), isset( $nested_docs_query_args ) ? $nested_docs_query_args : array() ) ) );
71 + $bd_cache_key = 'bd_nested_frag_' . md5( "v{$bd_version}_term{$term_id}_m{$bd_multi_kb}_k{$bd_kb_slug}_p{$bd_can_priv}_i{$bd_cat_icon}_q{$bd_queried}_s{$bd_is_single}_ic{$bd_icon_disc}_si{$bd_show_icon}_o{$bd_order_disc}" );
72 +
73 + $bd_cached = get_transient( $bd_cache_key );
74 + if ( false !== $bd_cached ) {
75 + echo $bd_cached; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
76 + return;
77 + }
78 +
79 + ob_start();
80 +}
81 +$bd_nested_depth++;
82 +
28 83 // Ensure $layout_type is set
29 84 if ( ! isset( $layout_type ) ) {
30 85 $layout_type = '';
31 86 }
@@ -38,14 +93,8 @@
38 93 )
39 94 );
40 95 }
41 96
42 -$_page_id = null;
43 -$_category_ids = [];
44 -$_is_single = false;
45 -$_is_doc_category = false;
46 -$_current_doc_category = null;
47 -
48 97 // Check if list icon should be shown
49 98 $_show_list_icon = true;
50 99 if ( isset( $show_list_icon ) && $show_list_icon === false ) {
51 100 $_show_list_icon = false;
@@ -52,8 +101,19 @@
52 101 }
53 102
54 103 $_icon = $_show_list_icon ? betterdocs()->template_helper->icon( isset( $list_icon_name ) ? $list_icon_name : 'list' ) : '';
55 104
105 +// Active-branch detection (mirrors master). Used to set .active class +
106 +// display:block on each nested-category-list <ul> that's in the user's
107 +// current branch, so the parent's body opens with the right state on
108 +// initial paint and Sleek's CSS (.betterdocs-current-category /
109 +// .betterdocs-nested-category-list.active) gets to apply its styling.
110 +$_page_id = null;
111 +$_category_ids = [];
112 +$_is_single = false;
113 +$_is_doc_category = false;
114 +$_current_doc_category = null;
115 +
56 116 if ( is_single() ) {
57 117 $_is_single = true;
58 118 $_page_id = get_the_ID();
59 119 $_category_ids = wp_get_post_terms( $_page_id, 'doc_category', [ 'fields' => 'ids' ] );
@@ -86,9 +146,14 @@
86 146
87 147 $_nested_docs_args = apply_filters( 'betterdocs_nested_docs_args', $nested_docs_query_args );
88 148
89 149 foreach ( $_nested_categories as $_nested_category ) :
90 - $classes = $_is_single && in_array( $_nested_category->term_id, $_category_ids ) || ( $_is_doc_category && in_array( $_nested_category->term_id, $_category_ids ) ) ? 'betterdocs-nested-category-list betterdocs-current-category active' : 'betterdocs-nested-category-list';
150 + $_is_in_active_branch = ( $_is_single && in_array( $_nested_category->term_id, $_category_ids ) )
151 + || ( $_is_doc_category && in_array( $_nested_category->term_id, $_category_ids ) );
152 + $_ul_classes = $_is_in_active_branch
153 + ? 'betterdocs-nested-category-list betterdocs-current-category active'
154 + : 'betterdocs-nested-category-list';
155 + $_ul_style = $_is_in_active_branch ? 'display:block;' : 'display:none;';
91 156
92 157 $_counts = betterdocs()->query->get_docs_count(
93 158 $_nested_category,
94 159 $nested_subcategory,
@@ -102,9 +167,9 @@
102 167 continue;
103 168 }
104 169
105 170 ?>
106 - <li class="betterdocs-nested-category-wrapper">
171 + <li class="betterdocs-nested-category-wrapper" data-bd-term-id="<?php echo (int) $_nested_category->term_id; ?>">
107 172 <span class="betterdocs-nested-category-title">
108 173 <?php
109 174 if ( isset( $category_icon ) && $category_icon == 'folder' ) {
110 175 betterdocs()->template_helper->icon( 'folder', true );
@@ -112,16 +177,12 @@
112 177 } else {
113 178 betterdocs()->template_helper->icon( 'arrow-right', true );
114 179 betterdocs()->template_helper->icon( 'arrow-down', true );
115 180 }
116 - /**
117 - * Icons
118 - */
119 -
120 181 ?>
121 182 <a href="#"><?php echo esc_html( $_nested_category->name ); ?></a>
122 183 </span>
123 - <ul class="<?php echo esc_attr( $classes ); ?>" style="<?php echo $_is_single && in_array( $_nested_category->term_id, $_category_ids ) || ( $_is_doc_category && in_array( $_nested_category->term_id, $_category_ids ) ) ? 'display:block;' : 'display:none;'; ?>">
184 + <ul class="<?php echo esc_attr( $_ul_classes ); ?>" style="<?php echo esc_attr( $_ul_style ); ?>">
124 185 <?php
125 186 $_nested_docs_args['term_id'] = $_nested_category->term_id;
126 187 $_nested_docs_args['term_slug'] = $_nested_category->slug;
127 188
@@ -131,22 +192,23 @@
131 192
132 193 if ( $_docs_query->have_posts() ) {
133 194 while ( $_docs_query->have_posts() ) :
134 195 $_docs_query->the_post();
135 - $_attributes = [
136 - 'href' => esc_url( get_the_permalink() )
196 + $_attrs = [
197 + 'href' => esc_url( get_the_permalink() ),
198 + 'data-bd-doc-id' => (string) get_the_ID(),
137 199 ];
138 200 if ( $_page_id === get_the_ID() && Helper::get_tax() != 'doc_category' ) {
139 - $_attributes['class'] = 'active';
201 + $_attrs['class'] = 'active';
140 202 }
203 + $_link_attributes = betterdocs()->template_helper->get_html_attributes( $_attrs );
141 204
142 - $_link_attributes = betterdocs()->template_helper->get_html_attributes( $_attributes );
143 -
144 205 echo wp_sprintf(
145 206 '<li>%s<a %s>%s</a></li>',
146 207 $_icon, //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
147 208 $_link_attributes, //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
148 - betterdocs()->template_helper->kses( get_the_title() ) //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
209 + // Same per-item filter as category-list.php (API method badges etc.).
210 + apply_filters( 'betterdocs_docs_list_item_title', betterdocs()->template_helper->kses( get_the_title() ), get_the_ID() ) //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
149 211 );
150 212 endwhile;
151 213 }
152 214
@@ -167,4 +229,20 @@
167 229 </ul>
168 230 </li>
169 231 <?php
170 232 endforeach;
233 +
234 +$bd_nested_depth--;
235 +
236 +if ( $bd_is_outermost ) {
237 + $bd_html = ob_get_clean();
238 + // Never cache an empty render. An empty buffer means every child term was
239 + // skipped ( $_counts <= 0 ), which is usually a transient data state — stale
240 + // counts mid-import, docs not yet published. The read guard treats '' as a hit
241 + // ( '' !== false ), so caching it would pin a blank nested block for 6 hours
242 + // after the data is already correct. Re-rendering once per request while empty
243 + // is what lets it self-heal. (#165)
244 + if ( '' !== $bd_html ) {
245 + set_transient( $bd_cache_key, $bd_html, HOUR_IN_SECONDS * 6 );
246 + }
247 + echo $bd_html; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
248 +}