| @@ -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 | +} | |