PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.8.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.8.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 3.5.2 All 199 releases
betterdocs / includes / Shortcodes / CategoryGrid.php

CategoryGrid.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.8.2, at includes/Shortcodes/CategoryGrid.php

334 lines 12.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // phpcs:disable WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- shortcode exposes user-driven exclusion attribute.
3 namespace WPDeveloper\BetterDocs\Shortcodes;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit;
7 }
8
9 use WPDeveloper\BetterDocs\Core\Query;
10 use WPDeveloper\BetterDocs\Utils\Helper;
11 use WPDeveloper\BetterDocs\Core\Settings;
12 use WPDeveloper\BetterDocs\Core\Shortcode;
13 use WPDeveloper\BetterDocs\Admin\Customizer\Defaults;
14
15 class CategoryGrid extends Shortcode {
16 protected $layout_class = 'layout-1';
17
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 ];
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
36 public function get_name() {
37 return 'betterdocs_category_grid';
38 }
39
40 public function get_style_depends() {
41 return [ 'betterdocs-category-grid' ];
42 }
43
44 public function get_script_depends() {
45 return [ 'betterdocs-category-grid' ];
46 }
47
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 }
87
88 public function generate_attributes() {
89 $attributes = [
90 'class' => [
91 'betterdocs-category-grid-inner-wrapper',
92 $this->layout_class
93 ]
94 ];
95
96 $masonry = (bool) $this->settings->get( 'masonry_layout', false );
97 if ( $this->has( 'masonry' ) ) {
98 $masonry = (bool) $this->attributes['masonry'];
99 }
100
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 }
117
118 $attributes['class'][] = 'docs-col-' . $_column_val;
119 $attributes['data-column_desktop'] = esc_html( $_column_val );
120 $attributes['style'] = "--column: $_column_val;";
121
122 if ( $this->isset( 'disable_customizer_style', false ) ) {
123 $attributes['class'][] = 'single-kb';
124 }
125 }
126
127
128 return $attributes;
129 }
130
131 public function header_layout_sequence( $sequence, $layout, $widget_type, $args ) {
132 return [ 'category_icon', 'category_title', 'category_counts', 'collapse_icon' ];
133 }
134
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 }
139
140 $this->views( 'layouts/base' );
141
142 if ( (bool) $this->attributes['sidebar_list'] ) {
143 remove_filter( 'betterdocs_header_layout_sequence', [ $this, 'header_layout_sequence' ], 10 );
144 }
145 }
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'] : '';
151
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 }
158
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 );
170
171 if ( $this->attributes['terms_include'] ) {
172 $terms_query['include'] = $this->attributes['terms_include'];
173 }
174
175 if ( $this->attributes['terms_exclude'] ) {
176 $terms_query['exclude'] = $this->attributes['terms_exclude'];
177 }
178
179 if ( $this->attributes['terms_offset'] ) {
180 $terms_query['offset'] = (int) $this->attributes['terms_offset'];
181 }
182
183 $inner_wrapper_attr = $this->generate_attributes();
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 }
333 }
334