PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.2
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 3.5.1 All 200 releases
← All changes | includes/Shortcodes/CategoryGrid.php +107 -3 4.5.24.9.2 View file →
@@ -1,10 +1,17 @@
1 1 <?php
2 +// phpcs:disable WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- shortcode exposes user-driven exclusion attribute.
3 +namespace WPDeveloper\BetterDocs\Shortcodes;
2 4
3 -namespace WPDeveloper\BetterDocs\Shortcodes;
5 +if ( ! defined( 'ABSPATH' ) ) {
6 + exit;
7 +}
4 8
9 +use WPDeveloper\BetterDocs\Core\Query;
5 10 use WPDeveloper\BetterDocs\Utils\Helper;
11 +use WPDeveloper\BetterDocs\Core\Settings;
6 12 use WPDeveloper\BetterDocs\Core\Shortcode;
13 +use WPDeveloper\BetterDocs\Admin\Customizer\Defaults;
7 14
8 15 class CategoryGrid extends Shortcode {
9 16 protected $layout_class = 'layout-1';
10 17
@@ -18,8 +25,15 @@
18 25 'icon' => 'show_icon',
19 26 'post_counter' => 'show_count'
20 27 ];
21 28
29 + public function __construct( Settings $settings, Query $query, Helper $helper, Defaults $defaults ) {
30 + parent::__construct( $settings, $query, $helper, $defaults );
31 +
32 + add_action( 'wp_ajax_betterdocs_lazy_category_body', [ $this, 'ajax_lazy_category_body' ] );
33 + add_action( 'wp_ajax_nopriv_betterdocs_lazy_category_body', [ $this, 'ajax_lazy_category_body' ] );
34 + }
35 +
22 36 public function get_name() {
23 37 return 'betterdocs_category_grid';
24 38 }
25 39
@@ -65,8 +79,9 @@
65 79 'list_icon_url' => '',
66 80 'list_icon_name' => 'list',
67 81 'show_list_icon' => true,
68 82 'sidebar_layout' => '',
83 + 'lazy_load' => false,
69 84 'wrapper_class' => []
70 85 ];
71 86 }
72 87
@@ -143,9 +158,9 @@
143 158
144 159 $terms_query = $this->query->terms_query(
145 160 [
146 161 'taxonomy' => $this->attributes['taxonomy'],
147 - 'multiple_kb' => $this->attributes['multiple_knowledge_base'],
162 + 'multiple_kb' => ( $this->attributes['multiple_knowledge_base'] && ! empty( $this->attributes['kb_slug'] ) ) ? true : false,
148 163 'kb_slug' => $this->attributes['kb_slug'],
149 164 'terms' => $this->attributes['terms'],
150 165 'order' => $this->attributes['terms_order'],
151 166 'orderby' => $this->attributes['terms_orderby'],
@@ -222,8 +237,97 @@
222 237 'category_title_link' => $category_title_link,
223 238 'layout_type' => $this->attributes['layout_type'] ?? '',
224 239 'list_icon_url' => $this->attributes['list_icon_url'] ?? '',
225 240 'show_list_icon' => $this->attributes['show_list_icon'] ?? true,
226 - 'sidebar_layout' => $this->attributes['sidebar_layout'] ?? ''
241 + 'sidebar_layout' => $this->attributes['sidebar_layout'] ?? '',
242 + 'lazy_load' => ! empty( $this->attributes['lazy_load'] )
227 243 ];
244 + }
245 +
246 + /**
247 + * AJAX handler for lazy-loaded category bodies. Returns the inner HTML of
248 + * a single .betterdocs-body (docs list + nested subcategories) for one term.
249 + * Reuses the same template (`template-parts/category-list`) that the full
250 + * page render uses, which in turn hits the nested-categories fragment cache.
251 + *
252 + * Public read, intentionally no nonce — matches the SearchForm/SearchModal
253 + * nopriv pattern and stays compatible with full-page caching (a per-request
254 + * nonce would go stale on cached anonymous pages and silently break the
255 + * lazy expand). This endpoint only ever returns data that is already public:
256 + * WP_Query runs publish-only and Query::get_doc_ids_by_term applies the same
257 + * capability filtering as the on-page render, so private/draft docs never
258 + * leak. The response is the published doc titles for a term — the same set a
259 + * visitor sees by navigating to that category — so there is nothing to gate
260 + * beyond what the normal archive already exposes.
261 + */
262 + public function ajax_lazy_category_body() {
263 + $term_id = isset( $_POST['term_id'] ) ? (int) $_POST['term_id'] : 0;
264 + $kb_slug = isset( $_POST['kb_slug'] ) ? sanitize_text_field( wp_unslash( $_POST['kb_slug'] ) ) : '';
265 + $multiple_kb = ! empty( $_POST['multiple_kb'] ) && ! empty( $kb_slug );
266 + // category_icon comes from the client when the body is inside a
267 + // sidebar layout that uses folder icons (Sleek / layout-7). Allowed
268 + // values match what nested-categories.php branches on.
269 + $category_icon = '';
270 + if ( isset( $_POST['category_icon'] ) ) {
271 + $raw = sanitize_text_field( wp_unslash( $_POST['category_icon'] ) );
272 + if ( in_array( $raw, array( 'folder', 'folder-open' ), true ) ) {
273 + $category_icon = $raw;
274 + }
275 + }
276 +
277 + if ( $term_id <= 0 ) {
278 + wp_send_json_error( [ 'message' => 'invalid term_id' ], 400 );
279 + }
280 +
281 + $term = get_term( $term_id, 'doc_category' );
282 + if ( ! $term instanceof \WP_Term ) {
283 + wp_send_json_error( [ 'message' => 'term not found' ], 404 );
284 + }
285 +
286 + $docs_query = [
287 + 'orderby' => $this->settings->get( 'alphabetically_order_post', 'betterdocs_order' ),
288 + 'order' => $this->settings->get( 'docs_order', 'ASC' ),
289 + 'posts_per_page' => -1,
290 + 'term_id' => $term->term_id,
291 + 'term_slug' => $term->slug,
292 + 'multiple_kb' => $multiple_kb,
293 + 'kb_slug' => $kb_slug,
294 + ];
295 +
296 + $view_object = betterdocs()->views;
297 +
298 + ob_start();
299 + $view_object->get(
300 + 'template-parts/category-list',
301 + [
302 + 'view_object' => $view_object,
303 + 'term' => $term,
304 + 'query_args' => betterdocs()->query->docs_query_args( $docs_query ),
305 + 'nested_subcategory' => (bool) $this->settings->get( 'archive_nested_subcategory' ),
306 + 'nested_docs_query_args' => [
307 + 'multiple_kb' => $multiple_kb,
308 + 'kb_slug' => $kb_slug,
309 + 'posts_per_page' => -1,
310 + ],
311 + 'nested_terms_query' => [
312 + 'orderby' => $this->settings->get( 'terms_orderby', 'betterdocs_order' ),
313 + 'order' => $this->settings->get( 'terms_order', 'ASC' ),
314 + ],
315 + 'show_list_icon' => true,
316 + 'list_icon_name' => 'list',
317 + 'list_icon_url' => '',
318 + 'layout_type' => '',
319 + 'widget_type' => 'category-grid',
320 + 'sidebar_layout' => '',
321 + 'category_icon' => $category_icon,
322 + 'multiple_knowledge_base' => $multiple_kb,
323 + 'kb_slug' => $kb_slug,
324 + ]
325 + );
326 + $html = ob_get_clean();
327 +
328 + wp_send_json_success( [
329 + 'term_id' => $term->term_id,
330 + 'html' => $html,
331 + ] );
228 332 }
229 333 }