| @@ -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 );?></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 | +} | |