require_helpers(); $this->repository = new Repository(); $this->resolver = new Resolver($this->repository); // Use priority 15 to ensure the parent menu exists before adding this submenu add_action('admin_menu', [$this, 'register_admin_menu'], 15); add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']); add_action('add_meta_boxes', [$this, 'register_meta_boxes']); add_action('save_post', [$this, 'handle_save_post'], 10, 2); add_action('delete_post', [$this, 'handle_delete_post']); add_action('transition_post_status', [$this, 'handle_status_transition'], 10, 3); add_action('admin_post_ka_theme_builder_create', [$this, 'handle_create_template']); add_action('admin_post_ka_theme_builder_quick_update', [$this, 'handle_quick_update']); add_action('admin_post_ka_theme_builder_duplicate', [$this, 'handle_duplicate_template']); add_action('elementor/preview/enqueue_scripts', [$this, 'enqueue_preview_handler']); add_action('elementor/documents/register_controls', [$this, 'register_document_controls']); add_action('elementor/document/after_save', [$this, 'handle_document_after_save'], 10, 2); add_filter('template_include', [$this, 'handle_template_include'], 99); add_filter('king_addons/theme_builder/current_template_id', [$this, 'filter_current_template_id']); add_filter('body_class', [$this, 'filter_body_class']); } /** * Require helper files for Theme Builder. * * @return void */ private function require_helpers(): void { require_once KING_ADDONS_PATH . 'includes/helpers/Theme_Builder/Meta_Keys.php'; require_once KING_ADDONS_PATH . 'includes/helpers/Theme_Builder/Context.php'; require_once KING_ADDONS_PATH . 'includes/helpers/Theme_Builder/Conditions.php'; require_once KING_ADDONS_PATH . 'includes/helpers/Theme_Builder/Repository.php'; require_once KING_ADDONS_PATH . 'includes/helpers/Theme_Builder/Resolver.php'; require_once KING_ADDONS_PATH . 'includes/helpers/Theme_Builder/Admin_List_Table.php'; } /** * Register admin menu entry. * * @return void */ public function register_admin_menu(): void { add_menu_page( esc_html__('Theme Builder', 'king-addons'), esc_html__('Theme Builder', 'king-addons'), 'manage_options', $this->menu_slug, [$this, 'render_admin_page'], 'dashicons-layout', 54.6 ); } /** * Enqueue admin assets for Theme Builder pages. * * @param string $hook Current admin page hook. * * @return void */ public function enqueue_admin_assets(string $hook): void { if (false === strpos($hook, $this->menu_slug)) { return; } wp_enqueue_style( KING_ADDONS_ASSETS_UNIQUE_KEY . '-theme-builder-admin', KING_ADDONS_URL . 'includes/extensions/Theme_Builder/style.css', [], KING_ADDONS_VERSION ); wp_enqueue_script( KING_ADDONS_ASSETS_UNIQUE_KEY . '-theme-builder-admin', KING_ADDONS_URL . 'includes/extensions/Theme_Builder/script.js', ['jquery'], KING_ADDONS_VERSION, true ); } /** * Register meta boxes for Elementor templates. * * @return void */ public function register_meta_boxes(): void { add_meta_box( 'king-addons-theme-builder', esc_html__('Theme Builder', 'king-addons'), [$this, 'render_meta_box'], 'elementor_library', 'side', 'default' ); } /** * Render Theme Builder meta box. * * @param WP_Post $post Current post. * * @return void */ public function render_meta_box(WP_Post $post): void { wp_nonce_field('ka_theme_builder_meta_box', 'ka_theme_builder_meta_box_nonce'); $location = get_post_meta($post->ID, Meta_Keys::LOCATION, true); $sub_location = get_post_meta($post->ID, Meta_Keys::SUB_LOCATION, true); $priority = (int) get_post_meta($post->ID, Meta_Keys::PRIORITY, true); $enabled = '1' === (string) get_post_meta($post->ID, Meta_Keys::ENABLED, true); echo '

' . esc_html__('Location', 'king-addons') . ': ' . esc_html($sub_location ?: '-') . '

'; echo '

' . esc_html__('Type', 'king-addons') . ': ' . esc_html($location ?: '-') . '

'; echo '

' . esc_html__('Priority', 'king-addons') . ': ' . esc_html((string) ($priority ?: 10)) . '

'; echo '

' . esc_html__('Status', 'king-addons') . ': ' . ($enabled ? esc_html__('Enabled', 'king-addons') : esc_html__('Disabled', 'king-addons')) . '

'; echo '

' . esc_html__('Manage in Theme Builder', 'king-addons') . '

'; } /** * Handle meta save for Elementor Library posts. * * @param int $post_id Post ID. * @param WP_Post $post Post object. * * @return void */ public function handle_save_post(int $post_id, WP_Post $post): void { if ('elementor_library' !== $post->post_type) { return; } if (!isset($_POST['ka_theme_builder_meta_box_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['ka_theme_builder_meta_box_nonce'])), 'ka_theme_builder_meta_box')) { // phpcs:ignore WordPress.Security.NonceVerification.Missing return; } // Keep cache fresh if any relevant meta changes. $this->repository->clear_cache(); } /** * Clear cache on delete. * * @return void */ public function handle_delete_post(): void { $this->repository->clear_cache(); } /** * Clear cache on status transition. * * @param string $new_status New status. * @param string $old_status Old status. * @param WP_Post $post Post object. * * @return void */ public function handle_status_transition(string $new_status, string $old_status, WP_Post $post): void { if ('elementor_library' !== $post->post_type || $new_status === $old_status) { return; } $this->repository->clear_cache(); } /** * Render admin page for Theme Builder. * * @return void */ public function render_admin_page(): void { if (!current_user_can('manage_options')) { return; } $this->handle_inline_actions(); $templates = $this->prepare_admin_templates(); $table = new Admin_List_Table($templates, admin_url('admin.php?page=' . $this->menu_slug)); $table->prepare_items(); // Enqueue shared V3 styles wp_enqueue_style( 'king-addons-admin-v3', KING_ADDONS_URL . 'includes/admin/layouts/shared/admin-v3-styles.css', [], KING_ADDONS_VERSION ); $this->render_modern_styles(); // Theme mode is per-user $theme_mode = get_user_meta(get_current_user_id(), 'king_addons_theme_mode', true); $allowed_theme_modes = ['dark', 'light', 'auto']; if (!in_array($theme_mode, $allowed_theme_modes, true)) { $theme_mode = 'dark'; } ?>

render_add_new_modal(); ?> render_quick_edit_modal(); ?>

views(); ?> display(); ?>
get_main_id(); if ('elementor_library' !== get_post_type($post_id)) { return; } $location = get_post_meta($post_id, Meta_Keys::LOCATION, true); if (!$location) { return; } $sub_location = get_post_meta($post_id, Meta_Keys::SUB_LOCATION, true); $preview_post = (int) get_post_meta($post_id, Meta_Keys::PREVIEW_POST_ID, true); $preview_term = (int) get_post_meta($post_id, Meta_Keys::PREVIEW_TERM_ID, true); $preview_author = (int) get_post_meta($post_id, Meta_Keys::PREVIEW_AUTHOR_ID, true); $preview_query = get_post_meta($post_id, Meta_Keys::PREVIEW_QUERY, true); $document->start_controls_section( 'ka_theme_builder_settings', [ 'label' => esc_html__('Theme Builder', 'king-addons'), 'tab' => Controls_Manager::TAB_SETTINGS, ] ); $document->add_control( 'ka_tb_readonly_type', [ 'label' => esc_html__('Template Type', 'king-addons'), 'type' => Controls_Manager::RAW_HTML, 'raw' => '' . esc_html($location) . '', 'content_classes' => 'ka-theme-builder-readonly', ] ); $document->add_control( 'ka_tb_readonly_location', [ 'label' => esc_html__('Location', 'king-addons'), 'type' => Controls_Manager::RAW_HTML, 'raw' => '' . esc_html($sub_location) . '', 'content_classes' => 'ka-theme-builder-readonly', ] ); $document->add_control( 'ka_tb_preview_post', [ 'label' => esc_html__('Preview Post ID', 'king-addons'), 'type' => Controls_Manager::NUMBER, 'default' => $preview_post, ] ); $document->add_control( 'ka_tb_preview_term', [ 'label' => esc_html__('Preview Term ID', 'king-addons'), 'type' => Controls_Manager::NUMBER, 'default' => $preview_term, ] ); $document->add_control( 'ka_tb_preview_author', [ 'label' => esc_html__('Preview Author ID', 'king-addons'), 'type' => Controls_Manager::NUMBER, 'default' => $preview_author, ] ); $document->add_control( 'ka_tb_preview_query', [ 'label' => esc_html__('Preview Search Query', 'king-addons'), 'type' => Controls_Manager::TEXT, 'default' => $preview_query, ] ); if ($this->is_pro_location($sub_location)) { $document->add_control( 'ka_tb_pro_notice', [ 'label' => esc_html__('Pro', 'king-addons'), 'type' => Controls_Manager::RAW_HTML, 'raw' => esc_html__('Advanced conditions require Pro.', 'king-addons'), 'content_classes' => 'ka-theme-builder-readonly', ] ); } $document->end_controls_section(); } /** * Persist preview meta from Elementor document settings. * * @param \Elementor\Core\Base\Document $document Elementor document. * @param array $data Saved data. * * @return void */ public function handle_document_after_save($document, array $data): void { if (!is_object($document) || !method_exists($document, 'get_main_id')) { return; } $post_id = (int) $document->get_main_id(); if ('elementor_library' !== get_post_type($post_id)) { return; } if (!get_post_meta($post_id, Meta_Keys::LOCATION, true)) { return; } $preview_post = (int) $document->get_settings('ka_tb_preview_post'); $preview_term = (int) $document->get_settings('ka_tb_preview_term'); $preview_author = (int) $document->get_settings('ka_tb_preview_author'); $preview_query = sanitize_text_field((string) $document->get_settings('ka_tb_preview_query')); update_post_meta($post_id, Meta_Keys::PREVIEW_POST_ID, $preview_post); update_post_meta($post_id, Meta_Keys::PREVIEW_TERM_ID, $preview_term); update_post_meta($post_id, Meta_Keys::PREVIEW_AUTHOR_ID, $preview_author); update_post_meta($post_id, Meta_Keys::PREVIEW_QUERY, $preview_query); $this->repository->clear_cache(); } /** * Handle "Add New" submission. * * @return void */ public function handle_create_template(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to perform this action.', 'king-addons')); } if (!isset($_POST['ka_theme_builder_create_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['ka_theme_builder_create_nonce'])), 'ka_theme_builder_create')) { // phpcs:ignore WordPress.Security.NonceVerification.Missing wp_die(esc_html__('Invalid nonce.', 'king-addons')); } $title = isset($_POST['ka_tb_title']) ? sanitize_text_field(wp_unslash($_POST['ka_tb_title'])) : esc_html__('New Theme Template', 'king-addons'); $type = isset($_POST['ka_tb_type']) ? sanitize_text_field(wp_unslash($_POST['ka_tb_type'])) : 'single'; $sub_location = isset($_POST['ka_tb_location']) ? sanitize_text_field(wp_unslash($_POST['ka_tb_location'])) : ''; $type = $this->normalize_type($type, $sub_location); // Check Free tier limit: 1 active template per primary type if (!$this->can_use_pro()) { $primary_type = $this->normalize_type($type, $sub_location); if ($this->count_enabled_templates_by_type($primary_type) >= 1) { wp_die( sprintf( /* translators: %s: template type name */ esc_html__('Free version allows only 1 active %s template. Disable an existing template or upgrade to Pro for unlimited templates.', 'king-addons'), esc_html($primary_type) ) ); } } $post_id = wp_insert_post([ 'post_type' => 'elementor_library', 'post_status' => 'publish', 'post_title' => $title, ]); if (is_wp_error($post_id)) { wp_die(esc_html__('Unable to create template.', 'king-addons')); } $conditions = $this->build_default_conditions($sub_location); $is_pro = Conditions::is_pro_only($conditions) || $this->is_pro_location($sub_location); $enabled = $is_pro && !$this->can_use_pro() ? '0' : '1'; update_post_meta($post_id, Meta_Keys::ENABLED, $enabled); update_post_meta($post_id, Meta_Keys::LOCATION, $type); update_post_meta($post_id, Meta_Keys::SUB_LOCATION, $sub_location); update_post_meta($post_id, Meta_Keys::PRIORITY, 10); update_post_meta($post_id, Meta_Keys::IS_PRO_ONLY, $is_pro ? '1' : '0'); update_post_meta($post_id, Meta_Keys::CONDITIONS, wp_json_encode($conditions)); $this->repository->clear_cache(); wp_safe_redirect(admin_url('post.php?post=' . $post_id . '&action=elementor')); exit; } /** * Handle quick update submission. * * @return void */ public function handle_quick_update(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to perform this action.', 'king-addons')); } if (!isset($_POST['ka_theme_builder_quick_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['ka_theme_builder_quick_nonce'])), 'ka_theme_builder_quick')) { // phpcs:ignore WordPress.Security.NonceVerification.Missing wp_die(esc_html__('Invalid nonce.', 'king-addons')); } $template_id = isset($_POST['template_id']) ? (int) $_POST['template_id'] : 0; $priority = isset($_POST['priority']) ? (int) $_POST['priority'] : 10; $enabled = isset($_POST['enabled']) && '1' === $_POST['enabled']; // phpcs:ignore WordPress.Security.NonceVerification.Missing if ($template_id > 0) { update_post_meta($template_id, Meta_Keys::PRIORITY, $priority); update_post_meta($template_id, Meta_Keys::ENABLED, $enabled ? '1' : '0'); $this->repository->clear_cache(); } wp_safe_redirect(admin_url('admin.php?page=' . $this->menu_slug)); exit; } /** * Handle template duplication (Pro feature). * * @return void */ public function handle_duplicate_template(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to perform this action.', 'king-addons')); } if (!$this->can_use_pro()) { wp_die(esc_html__('Template duplication requires Pro.', 'king-addons')); } $template_id = isset($_GET['template_id']) ? (int) $_GET['template_id'] : 0; if (!$template_id || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'] ?? '')), 'ka_theme_builder_duplicate_' . $template_id)) { wp_die(esc_html__('Invalid request.', 'king-addons')); } $original = get_post($template_id); if (!$original || 'elementor_library' !== $original->post_type) { wp_die(esc_html__('Template not found.', 'king-addons')); } // Clone the post $new_post_id = wp_insert_post([ 'post_type' => 'elementor_library', 'post_status' => 'draft', 'post_title' => $original->post_title . ' ' . esc_html__('(Copy)', 'king-addons'), 'post_content' => $original->post_content, ]); if (is_wp_error($new_post_id)) { wp_die(esc_html__('Failed to duplicate template.', 'king-addons')); } // Copy all meta data $meta_keys = [ Meta_Keys::ENABLED, Meta_Keys::LOCATION, Meta_Keys::SUB_LOCATION, Meta_Keys::CONDITIONS, Meta_Keys::PRIORITY, Meta_Keys::IS_PRO_ONLY, Meta_Keys::PREVIEW_POST_ID, Meta_Keys::PREVIEW_TERM_ID, Meta_Keys::PREVIEW_AUTHOR_ID, Meta_Keys::PREVIEW_QUERY, ]; foreach ($meta_keys as $key) { $value = get_post_meta($template_id, $key, true); if ('' !== $value) { update_post_meta($new_post_id, $key, $value); } } // Disable the duplicate by default to avoid conflicts update_post_meta($new_post_id, Meta_Keys::ENABLED, '0'); // Copy Elementor data $elementor_data = get_post_meta($template_id, '_elementor_data', true); if ($elementor_data) { update_post_meta($new_post_id, '_elementor_data', $elementor_data); } $elementor_edit_mode = get_post_meta($template_id, '_elementor_edit_mode', true); if ($elementor_edit_mode) { update_post_meta($new_post_id, '_elementor_edit_mode', $elementor_edit_mode); } $this->repository->clear_cache(); wp_safe_redirect(admin_url('admin.php?page=' . $this->menu_slug . '&duplicated=1')); exit; } /** * Handle template include override. * * @param string $template Current template path. * * @return string */ public function handle_template_include(string $template): string { if (is_admin() || wp_doing_ajax() || (defined('REST_REQUEST') && REST_REQUEST)) { return $template; } if (!did_action('elementor/loaded')) { return $template; } if (is_singular('elementor_library')) { return $template; } if ($this->is_woo_context()) { return $template; } $context = Context::from_request(); $resolved = $this->resolver->resolve($context); if (!$resolved) { return $template; } $this->current_template_id = $resolved; return KING_ADDONS_PATH . 'includes/extensions/Theme_Builder/templates/theme-builder.php'; } /** * Provide current template ID to filters. * * @param int $template_id Existing template id. * * @return int */ public function filter_current_template_id(int $template_id): int { if ($this->current_template_id) { return (int) $this->current_template_id; } return (int) $template_id; } /** * Append body class for resolved template. * * @param array $classes Body classes. * * @return array */ public function filter_body_class(array $classes): array { if ($this->current_template_id) { $classes[] = 'king-addons-theme-builder-active'; } return $classes; } /** * Render add-new modal markup. * * @return void */ private function render_add_new_modal(): void { ?> */ private function build_default_conditions(string $sub_location): array { $groups = []; switch ($sub_location) { case 'single_post': $groups[] = [ 'relation' => 'AND', 'rules' => [ [ 'type' => 'include', 'target' => 'post_type', 'operator' => 'in', 'value' => ['post'], ], ], ]; break; case 'single_page': $groups[] = [ 'relation' => 'AND', 'rules' => [ [ 'type' => 'include', 'target' => 'post_type', 'operator' => 'in', 'value' => ['page'], ], ], ]; break; case 'search_results': $groups[] = [ 'relation' => 'AND', 'rules' => [ [ 'type' => 'include', 'target' => 'search', 'operator' => 'in', 'value' => ['search'], ], ], ]; break; case 'not_found': $groups[] = [ 'relation' => 'AND', 'rules' => [ [ 'type' => 'include', 'target' => '404', 'operator' => 'in', 'value' => ['404'], ], ], ]; break; default: $groups[] = [ 'relation' => 'AND', 'rules' => [], ]; break; } return ['groups' => $groups]; } /** * Normalize template type based on selected sub-location. * * @param string $type Base type from UI. * @param string $sub_location Selected sub-location. * * @return string */ private function normalize_type(string $type, string $sub_location): string { if (strpos($sub_location, 'archive') === 0) { return 'archive'; } if ('search_results' === $sub_location) { return 'search'; } if ('not_found' === $sub_location) { return 'not_found'; } if (strpos($sub_location, 'author') === 0) { return 'author'; } return $type ?: 'single'; } /** * Determine if sub-location is Pro-only. * * @param string $sub_location Sub-location identifier. * * @return bool */ private function is_pro_location(string $sub_location): bool { if (in_array($sub_location, ['single_cpt', 'archive_tax', 'author_all', 'author_specific'], true)) { return true; } if (strpos($sub_location, 'single_') === 0 && !in_array($sub_location, ['single_post', 'single_page'], true)) { return true; } if (strpos($sub_location, 'archive_') === 0 && !in_array($sub_location, ['archive_blog', 'archive_category', 'archive_tag'], true)) { return true; } return false; } /** * Check whether Pro license is available. * * @return bool */ private function can_use_pro(): bool { return function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code__premium_only(); } /** * Count enabled templates of a given primary type. * * @param string $type Primary type (single, archive, search, not_found, author). * * @return int */ private function count_enabled_templates_by_type(string $type): int { $templates = $this->repository->get_active_templates(); $count = 0; foreach ($templates as $template) { if (empty($template['enabled'])) { continue; } $template_location = $template['location'] ?? ''; if ($template_location === $type) { $count++; } } return $count; } /** * Prepare data for admin list table. * * @return array> */ private function prepare_admin_templates(): array { $templates = $this->repository->get_active_templates(); $prepared = []; foreach ($templates as $template) { $post = get_post($template['id']); if (!$post instanceof WP_Post) { continue; } $prepared[] = [ 'id' => $template['id'], 'title' => get_the_title($template['id']), 'type' => $template['location'] ?? '', 'sub_location' => $template['sub_location'] ?? '', 'conditions' => $template['conditions'] ?? [], 'priority' => $template['priority'] ?? 10, 'enabled' => !empty($template['enabled']), 'is_pro_only' => !empty($template['is_pro_only']), ]; } return $prepared; } /** * Handle inline toggle/delete actions. * * @return void */ private function handle_inline_actions(): void { $action = isset($_GET['action']) ? sanitize_text_field(wp_unslash($_GET['action'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $template_id = isset($_GET['template_id']) ? (int) $_GET['template_id'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if (!$template_id || empty($action)) { return; } if ('delete_template' === $action) { if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'] ?? '')), 'ka_theme_builder_delete_' . $template_id)) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } wp_trash_post($template_id); $this->repository->clear_cache(); } if ('disable_template' === $action || 'enable_template' === $action) { if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'] ?? '')), 'ka_theme_builder_toggle_' . $template_id)) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } update_post_meta($template_id, Meta_Keys::ENABLED, 'enable_template' === $action ? '1' : '0'); $this->repository->clear_cache(); } } /** * Check if Woo Builder should own the context. * * @return bool */ private function is_woo_context(): bool { if (!class_exists(Woo_Context::class)) { return false; } $context = Woo_Context::detect_context(); return in_array($context, ['single_product', 'product_archive', 'cart', 'checkout', 'my_account'], true); } }