*/ private array $options = []; public static function instance(): Docs_KB { if (is_null(self::$instance)) { self::$instance = new self(); } return self::$instance; } /* ───────── Bootstrap ───────── */ public function __construct() { $this->options = $this->get_options(); // Activation register_activation_hook(KING_ADDONS_PATH . 'king-addons.php', [$this, 'handle_activation']); // Init add_action('init', [$this, 'register_post_type']); add_action('init', [$this, 'register_taxonomy']); add_action('init', [$this, 'register_tag_taxonomy']); add_action('init', [$this, 'add_rewrite_rules']); // Admin add_filter('parent_file', [$this, 'fix_admin_parent_file']); add_filter('submenu_file', [$this, 'fix_admin_submenu_file']); add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']); add_action('admin_post_king_addons_docs_kb_save', [$this, 'handle_save_settings']); add_filter('manage_' . self::POST_TYPE . '_posts_columns', [$this, 'add_admin_columns']); add_action('manage_' . self::POST_TYPE . '_posts_custom_column', [$this, 'render_admin_columns'], 10, 2); // Frontend add_action('wp_enqueue_scripts', [$this, 'enqueue_frontend_assets']); add_filter('template_include', [$this, 'template_loader']); // REST add_action('rest_api_init', [$this, 'register_rest_routes']); // AJAX – reactions & feedback add_action('wp_ajax_king_docs_reaction', [$this, 'ajax_save_reaction']); add_action('wp_ajax_nopriv_king_docs_reaction', [$this, 'ajax_save_reaction']); // Track views (Pro) add_action('wp', [$this, 'track_view']); } /* ═══════════════════════════════════════════ ACTIVATION / OPTIONS ═══════════════════════════════════════════ */ public function handle_activation(): void { if (!get_option(self::OPTION_NAME)) { add_option(self::OPTION_NAME, $this->get_default_options()); } $this->register_post_type(); $this->register_taxonomy(); $this->register_tag_taxonomy(); $this->add_rewrite_rules(); flush_rewrite_rules(); } /** @return array */ private function get_default_options(): array { return [ // General 'enabled' => true, 'docs_slug' => 'docs', 'main_page_id' => 0, 'docs_per_page' => 12, 'archive_title' => __('How can we help?', 'king-addons'), 'archive_subtitle' => __('Find guides, tutorials and answers to your questions.', 'king-addons'), // Layout 'layout' => 'glass-card', // glass-card | glass-list | glass-grid 'columns' => 3, 'show_article_count' => true, 'show_category_icon' => true, 'dark_mode' => 'auto', // light | dark | auto // Single article 'toc_enabled' => true, 'toc_sticky' => true, 'toc_headings' => 'h2,h3', 'sidebar_enabled' => true, 'navigation_enabled' => true, 'print_button' => true, 'reading_time' => true, 'social_share' => true, 'reactions_enabled' => true, // Search 'search_enabled' => true, 'search_placeholder' => __('Search documentation…', 'king-addons'), 'search_min_chars' => 2, // Pro: Multiple KBs 'multiple_kb_enabled' => false, // Pro: Internal docs 'internal_docs_enabled' => false, 'internal_docs_roles' => ['administrator'], // Pro: Feedback 'feedback_enabled' => false, 'feedback_question' => __('Was this article helpful?', 'king-addons'), 'feedback_thanks' => __('Thank you for your feedback!', 'king-addons'), 'empty_text' => __('No articles yet', 'king-addons'), // Pro: Related 'related_enabled' => true, 'related_count' => 3, // Pro: Analytics 'analytics_enabled' => false, 'analytics_email_report' => false, 'analytics_email' => get_option('admin_email'), // Appearance 'primary_color' => '#0071e3', 'category_icon_color' => '#0071e3', 'link_color' => '#0071e3', ]; } /** @return array */ public function get_options(): array { $saved = get_option(self::OPTION_NAME, []); return wp_parse_args($saved, $this->get_default_options()); } public function is_premium(): bool { return function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code__premium_only(); } /* ═══════════════════════════════════════════ POST TYPE / TAXONOMIES ═══════════════════════════════════════════ */ public function register_post_type(): void { $slug = sanitize_title($this->options['docs_slug'] ?? 'docs'); register_post_type(self::POST_TYPE, [ 'labels' => [ 'name' => __('Docs', 'king-addons'), 'singular_name' => __('Doc', 'king-addons'), 'add_new' => __('Add New Doc', 'king-addons'), 'add_new_item' => __('Add New Doc', 'king-addons'), 'edit_item' => __('Edit Doc', 'king-addons'), 'new_item' => __('New Doc', 'king-addons'), 'view_item' => __('View Doc', 'king-addons'), 'search_items' => __('Search Docs', 'king-addons'), 'not_found' => __('No docs found', 'king-addons'), 'not_found_in_trash' => __('No docs found in Trash', 'king-addons'), 'menu_name' => __('Docs', 'king-addons'), ], 'public' => true, 'publicly_queryable' => true, 'show_ui' => true, 'show_in_menu' => false, 'show_in_rest' => true, 'query_var' => true, 'rewrite' => ['slug' => $slug, 'with_front' => false], 'capability_type' => 'post', 'has_archive' => $slug, 'hierarchical' => false, 'menu_position' => 25, 'menu_icon' => 'dashicons-book-alt', 'supports' => ['title', 'editor', 'excerpt', 'thumbnail', 'revisions', 'custom-fields', 'page-attributes'], ]); } public function register_taxonomy(): void { $slug = sanitize_title($this->options['docs_slug'] ?? 'docs'); register_taxonomy(self::TAXONOMY, self::POST_TYPE, [ 'labels' => [ 'name' => __('Doc Categories', 'king-addons'), 'singular_name' => __('Doc Category', 'king-addons'), 'search_items' => __('Search Categories', 'king-addons'), 'all_items' => __('All Categories', 'king-addons'), 'parent_item' => __('Parent Category', 'king-addons'), 'edit_item' => __('Edit Category', 'king-addons'), 'update_item' => __('Update Category', 'king-addons'), 'add_new_item' => __('Add New Category', 'king-addons'), 'menu_name' => __('Categories', 'king-addons'), ], 'hierarchical' => true, 'public' => true, 'show_ui' => true, 'show_admin_column' => true, 'show_in_rest' => true, 'query_var' => true, 'rewrite' => ['slug' => $slug . '/category', 'with_front' => false, 'hierarchical' => true], ]); // Custom meta fields add_action(self::TAXONOMY . '_add_form_fields', [$this, 'add_category_fields']); add_action(self::TAXONOMY . '_edit_form_fields', [$this, 'edit_category_fields'], 10, 2); add_action('created_' . self::TAXONOMY, [$this, 'save_category_fields']); add_action('edited_' . self::TAXONOMY, [$this, 'save_category_fields']); } public function register_tag_taxonomy(): void { $slug = sanitize_title($this->options['docs_slug'] ?? 'docs'); register_taxonomy(self::TAG_TAXONOMY, self::POST_TYPE, [ 'labels' => [ 'name' => __('Doc Tags', 'king-addons'), 'singular_name' => __('Doc Tag', 'king-addons'), 'search_items' => __('Search Tags', 'king-addons'), 'all_items' => __('All Tags', 'king-addons'), 'edit_item' => __('Edit Tag', 'king-addons'), 'update_item' => __('Update Tag', 'king-addons'), 'add_new_item' => __('Add New Tag', 'king-addons'), 'menu_name' => __('Tags', 'king-addons'), ], 'hierarchical' => false, 'public' => true, 'show_ui' => true, 'show_admin_column' => true, 'show_in_rest' => true, 'rewrite' => ['slug' => $slug . '/tag', 'with_front' => false], ]); } /* ── Category form fields ── */ public function add_category_fields(): void { $icons = self::get_available_icons(); ?>
term_id, 'kng_doc_cat_icon', true) ?: 'book'; $order = get_term_meta($term->term_id, 'kng_doc_cat_order', true) ?: 0; $icons = self::get_available_icons(); ?> options['docs_slug'] ?? 'docs'); add_rewrite_rule('^' . $slug . '/?$', 'index.php?post_type=' . self::POST_TYPE, 'top'); add_rewrite_rule('^' . $slug . '/category/([^/]+)/?$', 'index.php?' . self::TAXONOMY . '=$matches[1]', 'top'); add_rewrite_rule('^' . $slug . '/([^/]+)/?$', 'index.php?' . self::POST_TYPE . '=$matches[1]', 'top'); if (get_option('king_addons_docs_kb_rewrite_flushed') !== $slug) { flush_rewrite_rules(); update_option('king_addons_docs_kb_rewrite_flushed', $slug); } } /* ═══════════════════════════════════════════ ADMIN COLUMNS ═══════════════════════════════════════════ */ public function add_admin_columns(array $columns): array { $new = []; foreach ($columns as $k => $v) { $new[$k] = $v; if ($k === 'title') { $new['doc_category'] = __('Category', 'king-addons'); } } $new['doc_views'] = __('Views', 'king-addons'); $new['doc_reactions'] = __('Reactions', 'king-addons'); return $new; } public function render_admin_columns(string $column, int $post_id): void { switch ($column) { case 'doc_category': $terms = get_the_terms($post_id, self::TAXONOMY); echo ($terms && !is_wp_error($terms)) ? esc_html(implode(', ', wp_list_pluck($terms, 'name'))) : '—'; break; case 'doc_views': echo esc_html(number_format_i18n(get_post_meta($post_id, '_kng_doc_views', true) ?: 0)); break; case 'doc_reactions': $happy = get_post_meta($post_id, '_kng_doc_react_happy', true) ?: 0; $neutral = get_post_meta($post_id, '_kng_doc_react_neutral', true) ?: 0; $sad = get_post_meta($post_id, '_kng_doc_react_sad', true) ?: 0; echo '😊 ' . esc_html($happy) . ' '; echo '😐 ' . esc_html($neutral) . ' '; echo '😞 ' . esc_html($sad) . ''; break; } } /* ═══════════════════════════════════════════ ASSETS ═══════════════════════════════════════════ */ public function enqueue_admin_assets(string $hook): void { if ($hook !== 'king-addons_page_king-addons-docs-kb') { return; } wp_enqueue_style('wp-color-picker'); wp_enqueue_script('wp-color-picker'); wp_enqueue_style( 'king-addons-v3-styles', KING_ADDONS_URL . 'includes/admin/layouts/shared/admin-v3-styles.css', [], KING_ADDONS_VERSION ); } public function enqueue_frontend_assets(): void { if (!$this->options['enabled']) { return; } if ( !is_post_type_archive(self::POST_TYPE) && !is_singular(self::POST_TYPE) && !is_tax(self::TAXONOMY) && !is_tax(self::TAG_TAXONOMY) && !$this->is_docs_main_page() ) { return; } wp_enqueue_style( 'king-addons-docs-kb', KING_ADDONS_URL . 'includes/extensions/Docs_KB/assets/frontend.css', [], KING_ADDONS_VERSION ); wp_enqueue_script( 'king-addons-docs-kb', KING_ADDONS_URL . 'includes/extensions/Docs_KB/assets/frontend.js', [], KING_ADDONS_VERSION, true ); wp_localize_script('king-addons-docs-kb', 'kingDocsKB', [ 'restUrl' => rest_url(self::API_NAMESPACE . '/docs/'), 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('wp_rest'), 'reactionNonce' => wp_create_nonce('king_docs_reaction'), 'searchEnabled' => !empty($this->options['search_enabled']), 'searchMinChars' => intval($this->options['search_min_chars'] ?? 2), 'tocEnabled' => !empty($this->options['toc_enabled']), 'tocSticky' => !empty($this->options['toc_sticky']), 'tocHeadings' => $this->options['toc_headings'] ?? 'h2,h3', 'reactionsEnabled' => !empty($this->options['reactions_enabled']), 'darkMode' => $this->options['dark_mode'] ?? 'auto', 'i18n' => [ 'searchPlaceholder' => $this->options['search_placeholder'] ?? __('Search documentation…', 'king-addons'), 'noResults' => __('No results found', 'king-addons'), 'searching' => __('Searching…', 'king-addons'), 'tocTitle' => __('On this page', 'king-addons'), 'reactionThanks' => __('Thanks for your feedback!', 'king-addons'), 'copied' => __('Link copied!', 'king-addons'), ], ]); // Inline custom-property overrides wp_add_inline_style('king-addons-docs-kb', $this->get_custom_css()); } private function get_custom_css(): string { $o = $this->options; $primary = sanitize_hex_color($o['primary_color'] ?? '#0071e3'); $icon = sanitize_hex_color($o['category_icon_color'] ?? '#0071e3'); $link = sanitize_hex_color($o['link_color'] ?? '#0071e3'); return ":root{--kng-docs-primary:{$primary};--kng-docs-icon-color:{$icon};--kng-docs-link-color:{$link};}"; } private function is_docs_main_page(): bool { $id = intval($this->options['main_page_id'] ?? 0); return $id > 0 && is_page($id); } /* ═══════════════════════════════════════════ TEMPLATE LOADER ═══════════════════════════════════════════ */ public function template_loader(string $template): string { if (!$this->options['enabled']) { return $template; } $base = __DIR__ . '/templates/'; if ($this->is_docs_main_page() || is_post_type_archive(self::POST_TYPE)) { $t = $base . 'archive-docs.php'; return file_exists($t) ? $t : $template; } if (is_tax(self::TAXONOMY) || is_tax(self::TAG_TAXONOMY)) { $t = $base . 'taxonomy-docs.php'; return file_exists($t) ? $t : $template; } if (is_singular(self::POST_TYPE)) { $t = $base . 'single-doc.php'; return file_exists($t) ? $t : $template; } return $template; } /* ═══════════════════════════════════════════ REST API ═══════════════════════════════════════════ */ public function register_rest_routes(): void { register_rest_route(self::API_NAMESPACE, '/docs/search', [ 'methods' => 'GET', 'callback' => [$this, 'rest_search'], 'permission_callback' => '__return_true', 'args' => [ 'q' => ['required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field'], 'cat' => ['required' => false, 'type' => 'integer', 'default' => 0], ], ]); register_rest_route(self::API_NAMESPACE, '/docs/categories', [ 'methods' => 'GET', 'callback' => [$this, 'rest_get_categories'], 'permission_callback' => '__return_true', ]); register_rest_route(self::API_NAMESPACE, '/docs/category/(?P\d+)', [ 'methods' => 'GET', 'callback' => [$this, 'rest_get_category_articles'], 'permission_callback' => '__return_true', ]); } public function rest_search(\WP_REST_Request $request): \WP_REST_Response { $q = $request->get_param('q'); $cat = $request->get_param('cat'); if (strlen($q) < intval($this->options['search_min_chars'] ?? 2)) { return new \WP_REST_Response(['results' => []], 200); } $args = [ 'post_type' => self::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => 10, 's' => $q, 'orderby' => 'relevance', ]; if ($cat) { $args['tax_query'] = [['taxonomy' => self::TAXONOMY, 'field' => 'term_id', 'terms' => $cat]]; } if ($this->should_limit_to_public_docs()) { $args['meta_query'] = $this->get_public_docs_meta_query(); } $wp = new \WP_Query($args); $results = []; foreach ($wp->posts as $post) { $cats = get_the_terms($post->ID, self::TAXONOMY); $cat_name = ($cats && !is_wp_error($cats)) ? $cats[0]->name : ''; $excerpt = $post->post_excerpt ?: wp_trim_words($post->post_content, 20, '…'); $results[] = [ 'id' => $post->ID, 'title' => $post->post_title, 'h_title' => $this->highlight($post->post_title, $q), 'url' => get_permalink($post->ID), 'excerpt' => $excerpt, 'h_excerpt' => $this->highlight($excerpt, $q), 'category' => $cat_name, 'date' => get_the_date('', $post), 'reading_time' => $this->estimate_reading_time($post->post_content), ]; } // Pro: analytics log if ($this->is_premium() && !empty($this->options['analytics_enabled'])) { $this->log_search($q, count($results)); } return new \WP_REST_Response(['results' => $results], 200); } public function rest_get_categories(): \WP_REST_Response { $args = [ 'taxonomy' => self::TAXONOMY, 'hide_empty' => true, 'orderby' => 'meta_value_num', 'meta_key' => 'kng_doc_cat_order', 'order' => 'ASC', ]; if ($this->should_limit_to_public_docs()) { $public_doc_ids = get_posts([ 'post_type' => self::POST_TYPE, 'post_status' => 'publish', 'fields' => 'ids', 'posts_per_page' => -1, 'meta_query' => $this->get_public_docs_meta_query(), ]); if (empty($public_doc_ids)) { return new \WP_REST_Response([], 200); } $args['object_ids'] = $public_doc_ids; } $cats = get_terms($args); if (is_wp_error($cats)) { return new \WP_REST_Response([], 200); } $out = []; foreach ($cats as $c) { if (is_wp_error($c)) continue; $out[] = [ 'id' => $c->term_id, 'name' => $c->name, 'slug' => $c->slug, 'desc' => $c->description, 'count' => $c->count, 'icon' => get_term_meta($c->term_id, 'kng_doc_cat_icon', true) ?: 'book', 'url' => get_term_link($c), ]; } return new \WP_REST_Response($out, 200); } public function rest_get_category_articles(\WP_REST_Request $request): \WP_REST_Response { $cat_id = absint($request->get_param('id')); $args = [ 'post_type' => self::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => -1, 'tax_query' => [['taxonomy' => self::TAXONOMY, 'field' => 'term_id', 'terms' => $cat_id]], 'orderby' => 'menu_order title', 'order' => 'ASC', ]; if ($this->should_limit_to_public_docs()) { $args['meta_query'] = $this->get_public_docs_meta_query(); } $wp = new \WP_Query($args); $out = []; foreach ($wp->posts as $p) { $out[] = [ 'id' => $p->ID, 'title' => $p->post_title, 'url' => get_permalink($p->ID), 'excerpt' => $p->post_excerpt ?: wp_trim_words($p->post_content, 15, '…'), ]; } return new \WP_REST_Response($out, 200); } private function should_limit_to_public_docs(): bool { if (!$this->is_premium() || empty($this->options['internal_docs_enabled'])) { return false; } if (!is_user_logged_in()) { return true; } $user = wp_get_current_user(); $allowed_roles = array_map('sanitize_key', (array)($this->options['internal_docs_roles'] ?? ['administrator'])); return empty(array_intersect($allowed_roles, (array)$user->roles)); } private function get_public_docs_meta_query(): array { return [['key' => '_kng_doc_visibility', 'value' => 'public']]; } /* ═══════════════════════════════════════════ REACTIONS (AJAX) ═══════════════════════════════════════════ */ public function ajax_save_reaction(): void { check_ajax_referer('king_docs_reaction', 'nonce'); $post_id = intval($_POST['post_id'] ?? 0); $reaction = sanitize_text_field($_POST['reaction'] ?? ''); if (!$post_id || !in_array($reaction, ['happy', 'neutral', 'sad'], true)) { wp_send_json_error('Invalid'); } $cookie = 'kng_docs_reaction_' . $post_id; if (isset($_COOKIE[$cookie])) { wp_send_json_error('Already voted'); } $key = '_kng_doc_react_' . $reaction; $count = get_post_meta($post_id, $key, true) ?: 0; update_post_meta($post_id, $key, $count + 1); setcookie($cookie, $reaction, time() + YEAR_IN_SECONDS, '/'); wp_send_json_success([ 'happy' => get_post_meta($post_id, '_kng_doc_react_happy', true) ?: 0, 'neutral' => get_post_meta($post_id, '_kng_doc_react_neutral', true) ?: 0, 'sad' => get_post_meta($post_id, '_kng_doc_react_sad', true) ?: 0, ]); } /* ═══════════════════════════════════════════ VIEW TRACKING (Pro) ═══════════════════════════════════════════ */ public function track_view(): void { if (!is_singular(self::POST_TYPE)) return; if (empty($this->options['analytics_enabled']) || !$this->is_premium()) return; $id = get_the_ID(); update_post_meta($id, '_kng_doc_views', (get_post_meta($id, '_kng_doc_views', true) ?: 0) + 1); $ck = 'kng_doc_viewed_' . $id; if (!isset($_COOKIE[$ck])) { update_post_meta($id, '_kng_doc_unique_views', (get_post_meta($id, '_kng_doc_unique_views', true) ?: 0) + 1); setcookie($ck, '1', time() + DAY_IN_SECONDS, '/'); } } /* ═══════════════════════════════════════════ HELPERS ═══════════════════════════════════════════ */ private function highlight(string $text, string $q): string { if (!$q) return $text; foreach (explode(' ', $q) as $w) { if (strlen($w) >= 2) { $text = preg_replace('/(' . preg_quote($w, '/') . ')/iu', '$1', $text); } } return $text; } private function log_search(string $q, int $cnt): void { $logs = get_option('king_addons_docs_search_logs', []); $logs[] = ['query' => $q, 'results' => $cnt, 'ts' => current_time('mysql')]; if (count($logs) > 1000) $logs = array_slice($logs, -1000); update_option('king_addons_docs_search_logs', $logs); } public static function estimate_reading_time(string $content): string { $min = max(1, (int)ceil(str_word_count(strip_tags($content)) / 200)); return sprintf(_n('%d min read', '%d min read', $min, 'king-addons'), $min); } /** * Get all docs grouped by category — used by archive template. * * @return array */ public function get_categories_with_docs(int $limit = 5): array { $cats = get_terms([ 'taxonomy' => self::TAXONOMY, 'hide_empty' => true, 'parent' => 0, 'orderby' => 'meta_value_num', 'meta_key' => 'kng_doc_cat_order', 'order' => 'ASC', ]); $out = []; foreach ($cats as $c) { if (is_wp_error($c)) continue; $docs = get_posts([ 'post_type' => self::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => $limit, 'tax_query' => [['taxonomy' => self::TAXONOMY, 'field' => 'term_id', 'terms' => $c->term_id]], 'orderby' => 'menu_order title', 'order' => 'ASC', ]); $doc_items = []; foreach ($docs as $d) { $doc_items[] = ['id' => $d->ID, 'title' => $d->post_title, 'url' => get_permalink($d->ID)]; } $subcats = get_terms([ 'taxonomy' => self::TAXONOMY, 'hide_empty' => true, 'parent' => $c->term_id, 'orderby' => 'meta_value_num', 'meta_key' => 'kng_doc_cat_order', 'order' => 'ASC', ]); $sub_items = []; foreach ($subcats as $sc) { if (is_wp_error($sc)) continue; $sub_items[] = [ 'id' => $sc->term_id, 'name' => $sc->name, 'count' => $sc->count, 'url' => get_term_link($sc), ]; } $out[] = [ 'id' => $c->term_id, 'name' => $c->name, 'slug' => $c->slug, 'description' => $c->description, 'count' => $c->count, 'icon' => get_term_meta($c->term_id, 'kng_doc_cat_icon', true) ?: 'book', 'url' => get_term_link($c), 'docs' => $doc_items, 'subcategories' => $sub_items, ]; } return $out; } public function get_related_docs(int $post_id, int $count = 3): array { $cats = wp_get_post_terms($post_id, self::TAXONOMY, ['fields' => 'ids']); if (empty($cats) || is_wp_error($cats)) return []; $q = new \WP_Query([ 'post_type' => self::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => $count, 'post__not_in' => [$post_id], 'tax_query' => [['taxonomy' => self::TAXONOMY, 'field' => 'term_id', 'terms' => $cats]], 'orderby' => 'rand', ]); $out = []; foreach ($q->posts as $p) { $out[] = [ 'id' => $p->ID, 'title' => $p->post_title, 'url' => get_permalink($p->ID), 'excerpt' => $p->post_excerpt ?: wp_trim_words($p->post_content, 15, '…'), ]; } return $out; } public function get_article_navigation(int $post_id): array { $cats = wp_get_post_terms($post_id, self::TAXONOMY, ['fields' => 'ids']); if (empty($cats) || is_wp_error($cats)) return ['prev' => null, 'next' => null]; $all = get_posts([ 'post_type' => self::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => -1, 'tax_query' => [['taxonomy' => self::TAXONOMY, 'field' => 'term_id', 'terms' => $cats[0]]], 'orderby' => 'menu_order title', 'order' => 'ASC', 'fields' => 'ids', ]); $idx = array_search($post_id, $all); $prev = $next = null; if ($idx !== false) { if ($idx > 0) { $pid = $all[$idx - 1]; $prev = ['id' => $pid, 'title' => get_the_title($pid), 'url' => get_permalink($pid)]; } if ($idx < count($all) - 1) { $nid = $all[$idx + 1]; $next = ['id' => $nid, 'title' => get_the_title($nid), 'url' => get_permalink($nid)]; } } return ['prev' => $prev, 'next' => $next]; } /* ═══════════════════════════════════════════ ADMIN PAGE ═══════════════════════════════════════════ */ /** * Highlight King Addons parent menu when editing docs. */ public function fix_admin_parent_file(string $parent_file): string { $screen = get_current_screen(); if ($screen && ($screen->post_type === self::POST_TYPE || in_array($screen->taxonomy, [self::TAXONOMY, self::TAG_TAXONOMY], true))) { return 'king-addons'; } return $parent_file; } /** * Highlight correct submenu item when editing docs. */ public function fix_admin_submenu_file(?string $submenu_file): ?string { $screen = get_current_screen(); if (!$screen) return $submenu_file; // Keep the single “Builder” page highlighted for any Docs KB admin screens. if ( ($screen->post_type === self::POST_TYPE) || (!empty($screen->taxonomy) && in_array($screen->taxonomy, [self::TAXONOMY, self::TAG_TAXONOMY], true)) ) { return 'king-addons-docs-kb'; } return $submenu_file; } public function render_admin_page(): void { if (!current_user_can('manage_options')) return; $this->options = $this->get_options(); $options = $this->options; $is_premium = $this->is_premium(); include __DIR__ . '/templates/admin-page.php'; } public function handle_save_settings(): void { if (!current_user_can('manage_options')) wp_die('Unauthorized'); check_admin_referer('king_addons_docs_kb_save', 'king_docs_kb_nonce'); $old_slug = $this->options['docs_slug'] ?? 'docs'; $o = []; // General $o['enabled'] = !empty($_POST['enabled']); $o['docs_slug'] = sanitize_title($_POST['docs_slug'] ?? 'docs'); $o['main_page_id'] = intval($_POST['main_page_id'] ?? 0); $o['docs_per_page'] = max(1, intval($_POST['docs_per_page'] ?? 12)); $o['archive_title'] = sanitize_text_field($_POST['archive_title'] ?? ''); $o['archive_subtitle'] = sanitize_text_field($_POST['archive_subtitle'] ?? ''); // Layout $valid_layouts = ['glass-card', 'glass-list', 'glass-grid']; $o['layout'] = in_array($_POST['layout'] ?? 'glass-card', $valid_layouts) ? sanitize_text_field($_POST['layout']) : 'glass-card'; $o['columns'] = max(1, min(4, intval($_POST['columns'] ?? 3))); $o['show_article_count'] = !empty($_POST['show_article_count']); $o['show_category_icon'] = !empty($_POST['show_category_icon']); $o['dark_mode'] = in_array($_POST['dark_mode'] ?? 'auto', ['light', 'dark', 'auto']) ? sanitize_text_field($_POST['dark_mode']) : 'auto'; // Single $o['toc_enabled'] = !empty($_POST['toc_enabled']); $o['toc_sticky'] = !empty($_POST['toc_sticky']); $o['toc_headings'] = sanitize_text_field($_POST['toc_headings'] ?? 'h2,h3'); $o['sidebar_enabled'] = !empty($_POST['sidebar_enabled']); $o['navigation_enabled'] = !empty($_POST['navigation_enabled']); $o['print_button'] = !empty($_POST['print_button']); $o['reading_time'] = !empty($_POST['reading_time']); $o['social_share'] = !empty($_POST['social_share']); $o['reactions_enabled'] = !empty($_POST['reactions_enabled']); // Search $o['search_enabled'] = !empty($_POST['search_enabled']); $o['search_placeholder'] = sanitize_text_field($_POST['search_placeholder'] ?? ''); $o['search_min_chars'] = max(1, intval($_POST['search_min_chars'] ?? 2)); // Pro $o['multiple_kb_enabled'] = !empty($_POST['multiple_kb_enabled']); $o['internal_docs_enabled'] = !empty($_POST['internal_docs_enabled']); $o['internal_docs_roles'] = isset($_POST['internal_docs_roles']) ? array_map('sanitize_text_field', (array)$_POST['internal_docs_roles']) : ['administrator']; $o['feedback_enabled'] = !empty($_POST['feedback_enabled']); $o['feedback_question'] = sanitize_text_field($_POST['feedback_question'] ?? ''); $o['feedback_thanks'] = sanitize_text_field($_POST['feedback_thanks'] ?? ''); $o['empty_text'] = sanitize_text_field($_POST['empty_text'] ?? ''); $o['related_enabled'] = !empty($_POST['related_enabled']); $o['related_count'] = max(1, intval($_POST['related_count'] ?? 3)); $o['analytics_enabled'] = !empty($_POST['analytics_enabled']); $o['analytics_email_report'] = !empty($_POST['analytics_email_report']); $o['analytics_email'] = sanitize_email($_POST['analytics_email'] ?? ''); // Colors $o['primary_color'] = sanitize_hex_color($_POST['primary_color'] ?? '#0071e3'); $o['category_icon_color'] = sanitize_hex_color($_POST['category_icon_color'] ?? '#0071e3'); $o['link_color'] = sanitize_hex_color($_POST['link_color'] ?? '#0071e3'); update_option(self::OPTION_NAME, $o); if ($old_slug !== $o['docs_slug']) { delete_option('king_addons_docs_kb_rewrite_flushed'); } wp_redirect(admin_url('admin.php?page=king-addons-docs-kb&saved=1')); exit; } /* ═══════════════════════════════════════════ ICONS ═══════════════════════════════════════════ */ /** @return array */ public static function get_available_icons(): array { return [ 'book' => 'Book', 'lightbulb' => 'Lightbulb', 'gear' => 'Gear', 'rocket' => 'Rocket', 'star' => 'Star', 'code' => 'Code', 'help' => 'Help', 'video' => 'Video', 'layers' => 'Layers', 'shield' => 'Shield', 'palette' => 'Palette', 'globe' => 'Globe', ]; } public static function get_icon_svg(string $name): string { $icons = [ 'book' => '', 'book-open' => '', 'home' => '', 'calendar' => '', 'clock' => '', 'eye' => '', 'list' => '', 'file-text' => '', 'printer' => '', 'lightbulb' => '', 'gear' => '', 'rocket' => '', 'star' => '', 'code' => '', 'help' => '', 'video' => '', 'layers' => '', 'shield' => '', 'palette' => '', 'globe' => '', 'search' => '', 'folder' => '', 'arrow-right' => '', 'arrow-left' => '', 'print' => '', 'share' => '', 'link' => '', ]; return $icons[$name] ?? $icons['book']; } }