template settings keys (meta). */ public const SETTING_KEYS = ['tpl_sticky' => 'sticky', 'tpl_transparent' => 'transparent', 'tpl_layout' => 'layout', 'tpl_preview_post' => 'preview_post']; private TemplateRenderer $renderer; private Renderer $elements; private Locations $locations; private ThemeSupport $support; private Assets $assets; private EditorLoader $editor; public function __construct(Renderer $renderer, Assets $assets, EditorLoader $editor) { $this->renderer = new TemplateRenderer($renderer); $this->elements = $renderer; $this->locations = new Locations(); $this->support = new ThemeSupport($this->renderer); $this->assets = $assets; $this->editor = $editor; self::$instance = $this; } public static function instance(): ThemeBuilder { if (self::$instance === null) { $plugin = \Zadex\Plugin::instance(); new self($plugin->renderer, $plugin->assets, $plugin->editor); } return self::$instance; } public function renderer(): TemplateRenderer { return $this->renderer; } public function locations(): Locations { return $this->locations; } public function register(): void { add_action('init', [TemplatePost::class, 'register'], 5); add_filter('zadex/editor/post_types', [$this, 'editable_post_types']); add_filter('zadex/editor/config', [$this, 'editor_config'], 10, 2); add_action('zadex/document/saved', [$this, 'document_saved'], 10, 1); add_action('template_redirect', [$this, 'guard_single'], 0); add_filter('template_include', [$this, 'template_include'], 1000); add_action('wp_enqueue_scripts', [$this, 'enqueue'], 20); add_action('wp_enqueue_scripts', [$this, 'enqueue_template_styles'], 30); (new RestTemplates())->register(); $this->support->register(); $this->locations->register(); if (is_admin()) { (new ThemeBuilderPage($this->editor))->register(); } } public function editable_post_types($types): array { $types = is_array($types) ? $types : []; $types[] = TemplatePost::POST_TYPE; return array_values(array_unique($types)); } /** Template post shown as "the current post" in the editor (single templates) — `previewPostId`. */ public function preview_post_id(int $template_id): int { return TemplatePost::preview_post($template_id); } public function editor_config(array $config, int $post_id): array { if (!TemplatePost::is_template($post_id)) { return $config; } $type = TemplatePost::type($post_id); $settings = TemplatePost::settings($post_id); $rules = TemplatePost::conditions($post_id); $config['themeTemplate'] = [ 'type' => $type, 'label' => TemplatePost::type_labels()[$type] ?? $type, 'conditions' => $rules, 'summary' => Conditions::summary($rules), 'settings' => $settings, ]; $config['previewPostId'] = $this->preview_post_id($post_id); $config['exitUrl'] = ThemeBuilderPage::url($type); // The Page panel edits the template settings as page settings `tpl_*`: seed them from the meta. $page = is_array($config['pageSettings'] ?? null) ? $config['pageSettings'] : []; $has_envelope = isset($page['settings']) && is_array($page['settings']); $inner = $has_envelope ? $page['settings'] : $page; foreach (self::SETTING_KEYS as $page_key => $key) { if (!array_key_exists($page_key, $inner)) { $inner[$page_key] = $key === 'preview_post' ? (string) $settings[$key] : $settings[$key]; } } $config['pageSettings'] = $has_envelope ? array_merge($page, ['settings' => $inner]) : $inner; $config['data'] = (is_array($config['data'] ?? null) ? $config['data'] : []) + ['previewPosts' => self::preview_post_options($type)]; return $config; } /** * Options for the "Preview with" select (latest posts and pages). Filter `zadex/theme/preview_post_options` * (array $options, string $type) replaces them per template type (product templates list products). */ public static function preview_post_options(string $type = ''): array { $filtered = apply_filters('zadex/theme/preview_post_options', null, $type); if (is_array($filtered)) { return $filtered; } // String values: select options compare as strings in the editor. $out = [['value' => '0', 'label' => __('Latest post', 'zadex')]]; $posts = get_posts(['post_type' => ['post', 'page'], 'post_status' => 'publish', 'numberposts' => 30, 'orderby' => 'date', 'order' => 'DESC']); foreach ($posts as $post) { $out[] = ['value' => (string) $post->ID, 'label' => ($post->post_title !== '' ? $post->post_title : __('(no title)', 'zadex')) . ' (#' . (int) $post->ID . ')']; } return $out; } /** Keeps `_zadex_template_settings` in sync with the template settings saved as page settings. */ public function document_saved($post_id): void { $post_id = (int) $post_id; if (!TemplatePost::is_template($post_id)) { return; } $settings = TemplatePost::settings($post_id); foreach (self::SETTING_KEYS as $page_key => $key) { $value = Document::page_setting($post_id, $page_key, null); if ($value !== null) { $settings[$key] = $value; } } TemplatePost::set_settings($post_id, $settings); Resolver::reset(); } /** Visitors never see a template on its own URL (editors can: preview / view). */ public function guard_single(): void { if (!is_singular(TemplatePost::POST_TYPE) || EditorLoader::mode() !== '') { return; } if (current_user_can('edit_post', (int) get_queried_object_id())) { nocache_headers(); return; } global $wp_query; $wp_query->set_404(); status_header(404); nocache_headers(); } /** Singular `zadex_template` requests (editor canvas and editors' view) use the template preview page. */ public function template_include($template) { if (!is_singular(TemplatePost::POST_TYPE)) { return $template; } add_filter('wp_robots', static fn(array $r): array => array_merge($r, ['noindex' => true, 'nofollow' => true])); return __DIR__ . '/views/template-preview.php'; } /** * Queues the CSS of the templates this request renders (header, footer, the location template) and of the * documents they reference, so it prints in (see \Zadex\Frontend\Styles). The editor preview renders * the header / footer around the canvas too. */ public function enqueue_template_styles(): void { $mode = EditorLoader::mode(); if ($mode === 'edit' || is_singular(TemplatePost::POST_TYPE)) { return; } $types = ['header', 'footer']; $location = $mode === '' ? Locations::location() : ''; $single_with_layout = $location === 'single' && \Zadex\Frontend\Templates::get((int) get_queried_object_id()) !== ''; if ($location !== '' && !$single_with_layout) { foreach (Locations::candidates($location) as $candidate) { if (Resolver::for_location($candidate)) { $types[] = $candidate; break; } } } foreach ($types as $type) { $id = Resolver::for_location($type); if ($id) { if ($type === 'header') { \Zadex\Frontend\Styles::add('zadex-template-base', TemplateRenderer::base_css()); } \Zadex\Frontend\Styles::document_tree((int) $id, $this->elements); } } } /** The frontend bundle (widget CSS + runtime) whenever a template renders on this request. */ public function enqueue(): void { if (EditorLoader::mode() !== '' || is_singular(TemplatePost::POST_TYPE)) { if (is_singular(TemplatePost::POST_TYPE)) { $this->assets->enqueue_entry(Assets::ENTRY_FRONTEND, 'zadex-frontend'); } return; } $location = Locations::location(); foreach (array_merge(['header', 'footer'], Locations::candidates($location)) as $type) { if (Resolver::for_location($type)) { $this->assets->enqueue_entry(Assets::ENTRY_FRONTEND, 'zadex-frontend'); return; } } } }