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
betterdocs / views / template-parts / nested-categories.php

nested-categories.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.3, at views/template-parts/nested-categories.php

249 lines 9.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- view template receives variables via extract(); prefixing is impractical.
3
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit;
7 }
8 use WPDeveloper\BetterDocs\Utils\Helper;
9
10 if ( ! $nested_subcategory ) {
11 return;
12 }
13
14 $_terms_args = [
15 'parent' => $term_id,
16 'hide_empty' => true
17 ];
18
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;
25 }
26 // phpcs:enable WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude
27
28 $nested_terms_query = isset( $nested_terms_query ) ? array_merge( $_terms_args, $nested_terms_query ) : $_terms_args;
29
30 $_nested_categories = get_terms( betterdocs()->query->terms_query( apply_filters( 'betterdocs_nested_terms_args', $nested_terms_query ) ) );
31
32 if ( empty( $_nested_categories ) ) {
33 return;
34 }
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
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
116 if ( is_single() ) {
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 }
124 }
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
135 $_multiple_kb = isset( $multiple_knowledge_base ) ? $multiple_knowledge_base : false;
136 $_kb_slug = isset( $kb_slug ) ? $kb_slug : '';
137
138 $_default_nested_docs_query_args = [
139 'multiple_kb' => $_multiple_kb,
140 'posts_per_page' => -1
141 ];
142
143 $nested_docs_query_args = isset( $nested_docs_query_args ) ?
144 array_merge( $_default_nested_docs_query_args, $nested_docs_query_args ) :
145 $_default_nested_docs_query_args;
146
147 $_nested_docs_args = apply_filters( 'betterdocs_nested_docs_args', $nested_docs_query_args );
148
149 foreach ( $_nested_categories as $_nested_category ) :
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;';
156
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 );
165
166 if ( $_counts <= 0 ) {
167 continue;
168 }
169
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;
188
189 $_docs_query = betterdocs()->query->docs_query_args( $_nested_docs_args );
190
191 $_docs_query = new WP_Query( $_docs_query );
192
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 );
204
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 }
214
215 wp_reset_postdata();
216
217 $_params = get_defined_vars();
218 $_params = isset( $_params['params'] ) ? $_params['params'] : [];
219
220 $_params = wp_parse_args(
221 [
222 'term_id' => $_nested_category->term_id
223 ],
224 $_params
225 );
226
227 betterdocs()->views->get( 'template-parts/nested-categories', $_params );
228 ?>
229 </ul>
230 </li>
231 <?php
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 }
249