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 +299 -151 3.5.04.9.2 View file →
@@ -1,185 +1,333 @@
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 - protected $layout_class = 'layout-1';
16 + protected $layout_class = 'layout-1';
10 17
11 - /**
12 - * A list of deprecated attributes.
13 - * @var array<string, string>
14 - */
15 - protected $deprecated_attributes = [
16 - 'category' => 'taxonomy',
17 - 'posts_per_grid' => 'posts_per_page',
18 - 'icon' => 'show_icon',
19 - 'post_counter' => 'show_count'
20 - ];
18 + /**
19 + * A list of deprecated attributes.
20 + * @var array<string, string>
21 + */
22 + protected $deprecated_attributes = [
23 + 'category' => 'taxonomy',
24 + 'posts_per_grid' => 'posts_per_page',
25 + 'icon' => 'show_icon',
26 + 'post_counter' => 'show_count'
27 + ];
21 28
22 - public function get_name() {
23 - return 'betterdocs_category_grid';
24 - }
29 + public function __construct( Settings $settings, Query $query, Helper $helper, Defaults $defaults ) {
30 + parent::__construct( $settings, $query, $helper, $defaults );
25 31
26 - public function get_style_depends() {
27 - return ['betterdocs-category-grid'];
28 - }
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 + }
29 35
30 - public function get_script_depends() {
31 - return ['betterdocs-category-grid'];
32 - }
36 + public function get_name() {
37 + return 'betterdocs_category_grid';
38 + }
33 39
34 - /**
35 - * Summary of default_attributes
36 - * @return array
37 - */
38 - public function default_attributes() {
39 - return [
40 - 'sidebar_list' => false,
41 - 'taxonomy' => 'doc_category',
42 - 'show_icon' => true,
43 - 'masonry' => false,
44 - 'posts_per_page' => $this->settings->get( 'posts_number', 0 ),
45 - 'orderby' => $this->settings->get( 'alphabetically_order_post', 'betterdocs_order' ),
46 - 'order' => $this->settings->get( 'docs_order', 'ASC' ),
47 - 'show_count' => $this->settings->get( 'post_count' ),
48 - 'column' => $this->settings->get( 'column_number' ),
49 - 'nested_subcategory' => $this->settings->get( 'nested_subcategory' ),
50 - 'terms' => '',
51 - 'terms_orderby' => '',
52 - 'terms_order' => '',
53 - 'terms_include' => '',
54 - 'terms_exclude' => '',
55 - 'terms_offset' => '',
56 - 'kb_slug' => '',
57 - 'multiple_knowledge_base' => false,
58 - 'disable_customizer_style' => false,
59 - 'title_tag' => 'h2',
60 - 'category_title_link' => false
61 - ];
62 - }
40 + public function get_style_depends() {
41 + return [ 'betterdocs-category-grid' ];
42 + }
63 43
64 - public function generate_attributes() {
65 - $attributes = [
66 - 'class' => [
67 - 'betterdocs-category-grid-inner-wrapper',
68 - $this->layout_class
69 - ]
70 - ];
44 + public function get_script_depends() {
45 + return [ 'betterdocs-category-grid' ];
46 + }
71 47
72 - $masonry = (bool) $this->settings->get( 'masonry_layout', false );
73 - if ( $this->has( 'masonry' ) ) {
74 - $masonry = (bool) $this->attributes['masonry'];
75 - }
48 + /**
49 + * Summary of default_attributes
50 + * @return array
51 + */
52 + public function default_attributes() {
53 + return [
54 + 'sidebar_list' => false,
55 + 'taxonomy' => 'doc_category',
56 + 'show_icon' => true,
57 + 'category_icon' => '',
58 + 'masonry' => false,
59 + 'posts_per_page' => $this->settings->get( 'posts_number', 0 ),
60 + 'orderby' => $this->settings->get( 'alphabetically_order_post', 'betterdocs_order' ),
61 + 'order' => $this->settings->get( 'docs_order', 'ASC' ),
62 + 'show_count' => $this->settings->get( 'post_count' ),
63 + 'count_suffix' => '',
64 + 'count_suffix_singular' => '',
65 + 'column' => $this->settings->get( 'column_number' ),
66 + 'nested_subcategory' => $this->settings->get( 'nested_subcategory' ),
67 + 'terms' => '',
68 + 'terms_orderby' => '',
69 + 'terms_order' => '',
70 + 'terms_include' => '',
71 + 'terms_exclude' => '',
72 + 'terms_offset' => '',
73 + 'kb_slug' => '',
74 + 'multiple_knowledge_base' => false,
75 + 'disable_customizer_style' => false,
76 + 'title_tag' => 'h2',
77 + 'category_title_link' => false,
78 + 'layout_type' => '',
79 + 'list_icon_url' => '',
80 + 'list_icon_name' => 'list',
81 + 'show_list_icon' => true,
82 + 'sidebar_layout' => '',
83 + 'lazy_load' => false,
84 + 'wrapper_class' => []
85 + ];
86 + }
76 87
77 - if ( ! is_singular( 'docs' ) && ! is_tax( 'doc_category' ) && ! is_tax( 'doc_tag' ) ) {
78 - if ( $this->attributes['sidebar_list'] == true ) {
79 - $attributes['class'][] = 'layout-flex';
80 - } elseif ( $masonry == true ) {
81 - wp_enqueue_script( 'masonry' );
82 - $attributes['class'][] = 'masonry';
83 - } else {
84 - $attributes['class'][] = 'layout-flex';
85 - }
86 - if ( $this->attributes['sidebar_list'] == true ) {
87 - $_column_val = 1;
88 - } elseif ( $this->isset( 'column' ) ) {
89 - $_column_val = $this->attributes['column'];
90 - } else {
91 - $_column_val = $this->settings->get( 'column_number' );
92 - }
88 + public function generate_attributes() {
89 + $attributes = [
90 + 'class' => [
91 + 'betterdocs-category-grid-inner-wrapper',
92 + $this->layout_class
93 + ]
94 + ];
93 95
94 - $attributes['class'][] = 'docs-col-' . $_column_val;
95 - $attributes['data-column_desktop'] = esc_html( $_column_val );
96 - $attributes['style'] = "--column: $_column_val;";
96 + $masonry = (bool) $this->settings->get( 'masonry_layout', false );
97 + if ( $this->has( 'masonry' ) ) {
98 + $masonry = (bool) $this->attributes['masonry'];
99 + }
97 100
98 - if ( $this->isset( 'disable_customizer_style', false ) ) {
99 - $attributes['class'][] = 'single-kb';
100 - }
101 - }
101 + if ( ! is_singular( 'docs' ) && ! is_tax( 'doc_category' ) && ! is_tax( 'doc_tag' ) ) {
102 + if ( $this->attributes['sidebar_list'] == true ) {
103 + $attributes['class'][] = 'layout-flex';
104 + } elseif ( $masonry == true ) {
105 + wp_enqueue_script( 'masonry' );
106 + $attributes['class'][] = 'masonry';
107 + } else {
108 + $attributes['class'][] = 'layout-flex';
109 + }
110 + if ( $this->attributes['sidebar_list'] == true ) {
111 + $_column_val = 1;
112 + } elseif ( $this->isset( 'column' ) ) {
113 + $_column_val = $this->attributes['column'];
114 + } else {
115 + $_column_val = $this->settings->get( 'column_number' );
116 + }
102 117
103 - return $attributes;
104 - }
118 + $attributes['class'][] = 'docs-col-' . $_column_val;
119 + $attributes['data-column_desktop'] = esc_html( $_column_val );
120 + $attributes['style'] = "--column: $_column_val;";
105 121
106 - public function header_layout_sequence( $sequence, $layout, $widget_type, $args ) {
107 - return ['category_icon', 'category_title', 'category_counts', 'collapse_icon'];
108 - }
122 + if ( $this->isset( 'disable_customizer_style', false ) ) {
123 + $attributes['class'][] = 'single-kb';
124 + }
125 + }
109 126
110 - public function render( $atts, $content = null ) {
111 - if ( (bool) $this->attributes['sidebar_list'] ) {
112 - add_filter( 'betterdocs_header_layout_sequence', [$this, 'header_layout_sequence'], 10, 4 );
113 - }
114 127
115 - $this->views( 'layouts/base' );
128 + return $attributes;
129 + }
116 130
117 - if ( (bool) $this->attributes['sidebar_list'] ) {
118 - remove_filter( 'betterdocs_header_layout_sequence', [$this, 'header_layout_sequence'], 10 );
119 - }
120 - }
131 + public function header_layout_sequence( $sequence, $layout, $widget_type, $args ) {
132 + return [ 'category_icon', 'category_title', 'category_counts', 'collapse_icon' ];
133 + }
121 134
122 - public function view_params() {
123 - $exploremore_btn = $this->settings->get( 'exploremore_btn' );
124 - $button_text = $this->settings->get( 'exploremore_btn_txt' );
125 - $category_title_link = isset( $this->attributes['category_title_link'] ) ? $this->attributes['category_title_link'] : '';
135 + public function render( $atts, $content = null ) {
136 + if ( (bool) $this->attributes['sidebar_list'] ) {
137 + add_filter( 'betterdocs_header_layout_sequence', [ $this, 'header_layout_sequence' ], 10, 4 );
138 + }
126 139
127 - $show_button = false;
128 - if ( $this->attributes['posts_per_page'] == -1 ) {
129 - $show_button = false;
130 - } elseif ( $exploremore_btn && ! is_singular( 'docs' ) && Helper::get_tax() != 'doc_category' && ! is_tax( 'doc_tag' ) ) {
131 - $show_button = true;
132 - }
140 + $this->views( 'layouts/base' );
133 141
134 - $terms_query = $this->query->terms_query( [
135 - 'taxonomy' => $this->attributes['taxonomy'],
136 - 'multiple_kb' => $this->attributes['multiple_knowledge_base'],
137 - 'kb_slug' => $this->attributes['kb_slug'],
138 - 'terms' => $this->attributes['terms'],
139 - 'order' => $this->attributes['terms_order'],
140 - 'orderby' => $this->attributes['terms_orderby'],
141 - 'nested_subcategory' => $this->attributes['nested_subcategory']
142 - ] );
142 + if ( (bool) $this->attributes['sidebar_list'] ) {
143 + remove_filter( 'betterdocs_header_layout_sequence', [ $this, 'header_layout_sequence' ], 10 );
144 + }
145 + }
143 146
144 - if ( $this->attributes['terms_include'] ) {
145 - $terms_query['include'] = $this->attributes['terms_include'];
146 - }
147 + public function view_params() {
148 + $exploremore_btn = $this->settings->get( 'exploremore_btn' );
149 + $button_text = $this->settings->get( 'exploremore_btn_txt' );
150 + $category_title_link = isset( $this->attributes['category_title_link'] ) ? $this->attributes['category_title_link'] : '';
147 151
148 - if ( $this->attributes['terms_exclude'] ) {
149 - $terms_query['exclude'] = $this->attributes['terms_exclude'];
150 - }
152 + $show_button = false;
153 + if ( $this->attributes['posts_per_page'] == -1 ) {
154 + $show_button = false;
155 + } elseif ( $exploremore_btn && ! is_singular( 'docs' ) && Helper::get_tax() != 'doc_category' && ! is_tax( 'doc_tag' ) ) {
156 + $show_button = true;
157 + }
151 158
152 - if ( $this->attributes['terms_offset'] ) {
153 - $terms_query['offset'] = (int) $this->attributes['terms_offset'];
154 - }
159 + $terms_query = $this->query->terms_query(
160 + [
161 + 'taxonomy' => $this->attributes['taxonomy'],
162 + 'multiple_kb' => ( $this->attributes['multiple_knowledge_base'] && ! empty( $this->attributes['kb_slug'] ) ) ? true : false,
163 + 'kb_slug' => $this->attributes['kb_slug'],
164 + 'terms' => $this->attributes['terms'],
165 + 'order' => $this->attributes['terms_order'],
166 + 'orderby' => $this->attributes['terms_orderby'],
167 + 'nested_subcategory' => $this->attributes['nested_subcategory']
168 + ]
169 + );
155 170
156 - $inner_wrapper_attr = $this->generate_attributes();
171 + if ( $this->attributes['terms_include'] ) {
172 + $terms_query['include'] = $this->attributes['terms_include'];
173 + }
157 174
158 - $docs_query = [
159 - 'orderby' => $this->attributes['orderby'],
160 - 'order' => $this->attributes['order'],
161 - 'posts_per_page' => $this->attributes['posts_per_page']
162 - ];
175 + if ( $this->attributes['terms_exclude'] ) {
176 + $terms_query['exclude'] = $this->attributes['terms_exclude'];
177 + }
163 178
164 - return [
165 - 'wrapper_attr' => ['class' => ['betterdocs-category-grid-wrapper']],
166 - 'inner_wrapper_attr' => $inner_wrapper_attr,
167 - 'layout' => 'default',
168 - 'widget_type' => 'category-grid',
179 + if ( $this->attributes['terms_offset'] ) {
180 + $terms_query['offset'] = (int) $this->attributes['terms_offset'];
181 + }
169 182
170 - 'terms_query_args' => $terms_query,
171 - 'docs_query_args' => $docs_query,
172 - 'nested_docs_query_args' => $docs_query,
183 + $inner_wrapper_attr = $this->generate_attributes();
173 184
174 - 'show_header' => true,
175 - 'show_list' => true,
176 - 'show_title' => true,
177 - 'show_button' => $show_button,
178 - 'button_text' => $button_text,
179 - 'show_button_icon' => true,
180 - 'button_icon_position' => true,
181 - 'button_icon' => true,
182 - 'category_title_link' => $category_title_link
183 - ];
184 - }
185 + $docs_query = [
186 + 'orderby' => $this->attributes['orderby'],
187 + 'order' => $this->attributes['order'],
188 + 'posts_per_page' => $this->attributes['posts_per_page']
189 + ];
190 +
191 + /**
192 + * Add This Attribute When Using Outside Betterdocs Templates Only
193 + */
194 + if ( $this->attributes['multiple_knowledge_base'] && ( ! empty( $this->attributes['kb_slug'] ) ) && ( ! betterdocs()->helper->is_templates() ) ) {
195 + $inner_wrapper_attr['data-mkb-slug'] = $this->attributes['kb_slug'];
196 + }
197 +
198 + // Prepare wrapper attributes with custom classes
199 + $wrapper_attr_classes = [ 'betterdocs-category-grid-wrapper' ];
200 +
201 + // Add custom wrapper classes if provided
202 + if ( $this->isset( 'wrapper_class' ) ) {
203 + $wrapper_classes = $this->attributes['wrapper_class'];
204 +
205 + // Handle both array and string formats
206 + if ( is_string( $wrapper_classes ) ) {
207 + // Split by comma or space and clean up
208 + $wrapper_classes = preg_split( '/[,\s]+/', $wrapper_classes );
209 + $wrapper_classes = array_filter( array_map( 'trim', $wrapper_classes ) );
210 + } elseif ( is_array( $wrapper_classes ) ) {
211 + $wrapper_classes = array_filter( $wrapper_classes );
212 + }
213 +
214 + if ( ! empty( $wrapper_classes ) ) {
215 + $wrapper_attr_classes = array_merge( $wrapper_attr_classes, $wrapper_classes );
216 + }
217 + }
218 +
219 + return [
220 + 'wrapper_attr' => [ 'class' => $wrapper_attr_classes ],
221 + 'inner_wrapper_attr' => $inner_wrapper_attr,
222 + 'layout' => 'default',
223 + 'widget_type' => 'category-grid',
224 + 'terms_query_args' => $terms_query,
225 + 'docs_query_args' => $docs_query,
226 + 'nested_docs_query_args' => $docs_query,
227 + 'list_icon_name' => ($this->attributes['list_icon_name'] ?? 'list') == 'list' ? 'list' : [ 'value' => $this->attributes['list_icon_name'] ?? 'list' ],
228 + 'show_header' => true,
229 + 'show_list' => true,
230 + 'show_title' => true,
231 + 'show_button' => $show_button,
232 + 'button_text' => $button_text,
233 + 'show_button_icon' => true,
234 + 'button_icon_position' => true,
235 + 'title_tag' => $this->attributes['title_tag'] ?? 'h2',
236 + 'button_icon' => true,
237 + 'category_title_link' => $category_title_link,
238 + 'layout_type' => $this->attributes['layout_type'] ?? '',
239 + 'list_icon_url' => $this->attributes['list_icon_url'] ?? '',
240 + 'show_list_icon' => $this->attributes['show_list_icon'] ?? true,
241 + 'sidebar_layout' => $this->attributes['sidebar_layout'] ?? '',
242 + 'lazy_load' => ! empty( $this->attributes['lazy_load'] )
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 + ] );
332 + }
185 333 }