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 +201 -79 3.4.0 → 4.9.3 View file →
@@ -1,22 +1,30 @@
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 - return;
11 + return;
7 12 }
8 13
9 14 $_terms_args = [
10 - 'parent' => $term_id,
11 - 'hide_empty' => true
15 + 'parent' => $term_id,
16 + 'hide_empty' => true
12 17 ];
13 18
14 -if ( isset ($terms_exclude) ) {
15 - $_terms_args['exclude'] = $terms_exclude;
16 -} else if ( isset ($exclude) ) {
17 - $_terms_args['exclude'] = $exclude;
19 +// User-controlled exclusion list for nested category rendering — exclude is required UX.
20 +// phpcs:disable WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude
21 +if ( isset( $terms_exclude ) ) {
22 + $_terms_args['exclude'] = $terms_exclude;
23 +} elseif ( isset( $exclude ) ) {
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 ) ) );
@@ -21,106 +29,220 @@
21 29
22 30 $_nested_categories = get_terms( betterdocs()->query->terms_query( apply_filters( 'betterdocs_nested_terms_args', $nested_terms_query ) ) );
23 31
24 32 if ( empty( $_nested_categories ) ) {
25 - return;
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 +}
28 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 = '';
29 54
30 -$_page_id = null;
31 -$_category_ids = [];
32 -$_is_single = false;
33 -$_icon = betterdocs()->template_helper->icon( isset( $list_icon_name ) ? $list_icon_name : 'list' );
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}" );
34 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 +
83 +// Ensure $layout_type is set
84 +if ( ! isset( $layout_type ) ) {
85 + $layout_type = '';
86 +}
87 +
88 +// if there have list icon url from customizer or shortcodes attribites format it to $list_icon_name
89 +if ( $layout_type == 'template' && isset( $list_icon_url ) && $list_icon_url ) {
90 + $list_icon_name = array(
91 + 'value' => array(
92 + 'url' => $list_icon_url
93 + )
94 + );
95 +}
96 +
97 +// Check if list icon should be shown
98 +$_show_list_icon = true;
99 +if ( isset( $show_list_icon ) && $show_list_icon === false ) {
100 + $_show_list_icon = false;
101 +}
102 +
103 +$_icon = $_show_list_icon ? betterdocs()->template_helper->icon( isset( $list_icon_name ) ? $list_icon_name : 'list' ) : '';
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 +
35 116 if ( is_single() ) {
36 - $_is_single = true;
37 - $_page_id = get_the_ID();
38 - $_category_ids = wp_get_post_terms( $_page_id, 'doc_category', ["fields" => "ids"] );
39 - if ( !empty ($_category_ids) && ! is_wp_error( $_category_ids ) ) {
40 - $ancestors = get_ancestors( $_category_ids[0], 'doc_category' );
41 - $_category_ids = array_merge( $_category_ids, $ancestors );
42 - }
117 + $_is_single = true;
118 + $_page_id = get_the_ID();
119 + $_category_ids = wp_get_post_terms( $_page_id, 'doc_category', [ 'fields' => 'ids' ] );
120 + if ( ! empty( $_category_ids ) && ! is_wp_error( $_category_ids ) ) {
121 + $ancestors = get_ancestors( $_category_ids[0], 'doc_category' );
122 + $_category_ids = array_merge( $_category_ids, $ancestors );
123 + }
43 124 }
44 125
126 +if ( is_tax( 'doc_category' ) ) {
127 + $_is_doc_category = true;
128 + $_current_doc_category = get_queried_object() != null ? get_queried_object()->term_id : '';
129 + $parent_id = Helper::get_the_top_most_parent( $_current_doc_category );
130 + $_category_ids = get_term_children( $parent_id, 'doc_category' );
131 + $current_category_index = array_search( $_current_doc_category, $_category_ids );
132 + $_category_ids = ! is_bool( $current_category_index ) ? array_slice( $_category_ids, 0, ( $current_category_index + 1 ), true ) : []; // get the range from start to current
133 +}
134 +
45 135 $_multiple_kb = isset( $multiple_knowledge_base ) ? $multiple_knowledge_base : false;
46 -$_kb_slug = isset( $kb_slug ) ? $kb_slug : '';
136 +$_kb_slug = isset( $kb_slug ) ? $kb_slug : '';
47 137
48 138 $_default_nested_docs_query_args = [
49 - 'multiple_kb' => $_multiple_kb,
50 - 'posts_per_page' => -1
139 + 'multiple_kb' => $_multiple_kb,
140 + 'posts_per_page' => -1
51 141 ];
52 142
53 143 $nested_docs_query_args = isset( $nested_docs_query_args ) ?
54 - array_merge($_default_nested_docs_query_args, $nested_docs_query_args) :
55 - $_default_nested_docs_query_args;
144 + array_merge( $_default_nested_docs_query_args, $nested_docs_query_args ) :
145 + $_default_nested_docs_query_args;
56 146
57 147 $_nested_docs_args = apply_filters( 'betterdocs_nested_docs_args', $nested_docs_query_args );
58 148
59 149 foreach ( $_nested_categories as $_nested_category ) :
60 - $classes = $_is_single && 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;';
61 156
62 - $_counts = betterdocs()->query->get_docs_count( $_nested_category, $nested_subcategory, [
63 - 'multiple_knowledge_base' => $_multiple_kb,
64 - 'kb_slug' => $_kb_slug,
65 - ] );
157 + $_counts = betterdocs()->query->get_docs_count(
158 + $_nested_category,
159 + $nested_subcategory,
160 + [
161 + 'multiple_knowledge_base' => $_multiple_kb,
162 + 'kb_slug' => $_kb_slug,
163 + ]
164 + );
66 165
67 - if( $_counts <= 0 ) {
68 - continue;
69 - }
166 + if ( $_counts <= 0 ) {
167 + continue;
168 + }
70 169
71 - ?>
72 - <li class="betterdocs-nested-category-wrapper">
73 - <span class="betterdocs-nested-category-title">
74 - <?php
75 - /**
76 - * Icons
77 - */
78 - betterdocs()->template_helper->icon( 'arrow-right', true );
79 - betterdocs()->template_helper->icon( 'arrow-down', true );
80 - ?>
81 - <a href="#"><?php _e( $_nested_category->name, 'betterdocs' );?></a>
82 - </span>
83 - <ul class="<?php esc_attr_e( $classes );?>">
84 - <?php
85 - $_nested_docs_args['term_id'] = $_nested_category->term_id;
86 - $_nested_docs_args['term_slug'] = $_nested_category->slug;
170 + ?>
171 + <li class="betterdocs-nested-category-wrapper" data-bd-term-id="<?php echo (int) $_nested_category->term_id; ?>">
172 + <span class="betterdocs-nested-category-title">
173 + <?php
174 + if ( isset( $category_icon ) && $category_icon == 'folder' ) {
175 + betterdocs()->template_helper->icon( 'folder', true );
176 + betterdocs()->template_helper->icon( 'folder-open', true );
177 + } else {
178 + betterdocs()->template_helper->icon( 'arrow-right', true );
179 + betterdocs()->template_helper->icon( 'arrow-down', true );
180 + }
181 + ?>
182 + <a href="#"><?php echo esc_html( $_nested_category->name ); ?></a>
183 + </span>
184 + <ul class="<?php echo esc_attr( $_ul_classes ); ?>" style="<?php echo esc_attr( $_ul_style ); ?>">
185 + <?php
186 + $_nested_docs_args['term_id'] = $_nested_category->term_id;
187 + $_nested_docs_args['term_slug'] = $_nested_category->slug;
87 188
88 - $_docs_query = betterdocs()->query->docs_query_args( $_nested_docs_args );
189 + $_docs_query = betterdocs()->query->docs_query_args( $_nested_docs_args );
89 190
90 - $_docs_query = new WP_Query( $_docs_query );
191 + $_docs_query = new WP_Query( $_docs_query );
91 192
92 - if ( $_docs_query->have_posts() ) {
93 - while ( $_docs_query->have_posts() ): $_docs_query->the_post();
94 - $_attributes = [
95 - 'href' => esc_url( get_the_permalink() )
96 - ];
97 - if ( $_page_id === get_the_ID() && Helper::get_tax() != 'doc_category' ) {
98 - $_attributes['class'] = 'active';
99 - }
193 + if ( $_docs_query->have_posts() ) {
194 + while ( $_docs_query->have_posts() ) :
195 + $_docs_query->the_post();
196 + $_attrs = [
197 + 'href' => esc_url( get_the_permalink() ),
198 + 'data-bd-doc-id' => (string) get_the_ID(),
199 + ];
200 + if ( $_page_id === get_the_ID() && Helper::get_tax() != 'doc_category' ) {
201 + $_attrs['class'] = 'active';
202 + }
203 + $_link_attributes = betterdocs()->template_helper->get_html_attributes( $_attrs );
100 204
101 - $_link_attributes = betterdocs()->template_helper->get_html_attributes( $_attributes );
205 + echo wp_sprintf(
206 + '<li>%s<a %s>%s</a></li>',
207 + $_icon, //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
208 + $_link_attributes, //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
211 + );
212 + endwhile;
213 + }
102 214
103 - echo wp_sprintf(
104 - '<li>%s<a %s>%s</a></li>',
105 - $_icon,
106 - $_link_attributes,
107 - betterdocs()->template_helper->kses( get_the_title() )
108 - );
109 - endwhile;
110 - }
111 - wp_reset_query();
112 - wp_reset_postdata();
215 + wp_reset_postdata();
113 216
114 - $_params = get_defined_vars();
115 - $_params = isset( $_params['params'] ) ? $_params['params'] : [];
217 + $_params = get_defined_vars();
218 + $_params = isset( $_params['params'] ) ? $_params['params'] : [];
116 219
117 - $_params = wp_parse_args( [
118 - 'term_id' => $_nested_category->term_id
119 - ], $_params );
220 + $_params = wp_parse_args(
221 + [
222 + 'term_id' => $_nested_category->term_id
223 + ],
224 + $_params
225 + );
120 226
121 - betterdocs()->views->get( 'template-parts/nested-categories', $_params );
122 - ?>
123 - </ul>
124 - </li>
125 -<?php
227 + betterdocs()->views->get( 'template-parts/nested-categories', $_params );
228 + ?>
229 + </ul>
230 + </li>
231 + <?php
126 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 +}