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(); ?>
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);
}
}