assets
1 week ago
BuilderPanel.php
1 week ago
Compiler.php
1 week ago
Engine.php
1 week ago
FrontendEnqueue.php
1 week ago
Migrator.php
1 week ago
Palettes.php
1 week ago
PreviewDraft.php
1 week ago
RestController.php
1 week ago
Sanitizer.php
1 week ago
Schema.php
1 week ago
Templates.php
1 week ago
BuilderPanel.php
193 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Style Customizer v2 — builder "Style" tab. |
| 4 | * |
| 5 | * Registers a "Style" tab in the form builder, mounts the React panel into it, and enqueues |
| 6 | * the panel bundle. Wired only when `Engine::enabled()`. |
| 7 | * |
| 8 | * @package EverestForms\Addons\StyleCustomizer\V2 |
| 9 | * @since x.x.x |
| 10 | */ |
| 11 | |
| 12 | namespace EverestForms\Addons\StyleCustomizer\V2; |
| 13 | |
| 14 | defined( 'ABSPATH' ) || exit; |
| 15 | |
| 16 | /** |
| 17 | * Builder tab + panel enqueue. |
| 18 | */ |
| 19 | final class BuilderPanel { |
| 20 | |
| 21 | /** |
| 22 | * Builder tab slug (drives the `&tab=style` URL and the `everest-forms-panel-style` wrapper). |
| 23 | */ |
| 24 | const TAB = 'style'; |
| 25 | |
| 26 | /** |
| 27 | * Builder screen id. |
| 28 | */ |
| 29 | const SCREEN = 'everest-forms_page_evf-builder'; |
| 30 | |
| 31 | /** |
| 32 | * Wire the builder hooks. Called from {@see Engine::boot()}. |
| 33 | */ |
| 34 | public static function register() { |
| 35 | add_filter( 'everest_forms_builder_tabs_array', array( __CLASS__, 'add_tab' ), 30 ); |
| 36 | add_action( 'everest_forms_builder_sidebar_' . self::TAB, array( __CLASS__, 'render_sidebar' ) ); |
| 37 | add_action( 'everest_forms_builder_content_' . self::TAB, array( __CLASS__, 'render_content' ) ); |
| 38 | add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue' ) ); |
| 39 | add_action( 'admin_head', array( __CLASS__, 'tab_icon_styles' ) ); |
| 40 | } |
| 41 | |
| 42 | /** |
| 43 | * Add the "Style" tab to the builder nav, positioned right after "Fields". |
| 44 | * |
| 45 | * @param array $tabs Tabs (slug => [label, sidebar]). |
| 46 | * @return array |
| 47 | */ |
| 48 | public static function add_tab( $tabs ) { |
| 49 | $entry = array( |
| 50 | self::TAB => array( |
| 51 | 'label' => __( 'Style', 'everest-forms' ), |
| 52 | 'sidebar' => true, |
| 53 | ), |
| 54 | ); |
| 55 | |
| 56 | if ( ! isset( $tabs['fields'] ) ) { |
| 57 | return array_merge( $tabs, $entry ); |
| 58 | } |
| 59 | |
| 60 | $out = array(); |
| 61 | foreach ( $tabs as $slug => $tab ) { |
| 62 | $out[ $slug ] = $tab; |
| 63 | if ( 'fields' === $slug ) { |
| 64 | $out = array_merge( $out, $entry ); |
| 65 | } |
| 66 | } |
| 67 | return $out; |
| 68 | } |
| 69 | |
| 70 | /** |
| 71 | * Render the controls mount point inside the builder's native sidebar. |
| 72 | */ |
| 73 | public static function render_sidebar() { |
| 74 | echo '<div id="evf-scv2-controls" class="evf-scv2-controls"></div>'; |
| 75 | } |
| 76 | |
| 77 | /** |
| 78 | * Render the live-preview mount point inside the tab's content panel. |
| 79 | */ |
| 80 | public static function render_content() { |
| 81 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 82 | $form_id = isset( $_GET['form_id'] ) ? absint( wp_unslash( $_GET['form_id'] ) ) : 0; |
| 83 | printf( '<div id="evf-scv2-preview" class="evf-scv2-preview" data-form-id="%d"></div>', (int) $form_id ); |
| 84 | } |
| 85 | |
| 86 | /** |
| 87 | * Paint the "Style" builder tab's icon with a masked SVG (no icon-font glyph exists for it). |
| 88 | */ |
| 89 | public static function tab_icon_styles() { |
| 90 | $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; |
| 91 | if ( ! $screen || self::SCREEN !== $screen->id ) { |
| 92 | return; |
| 93 | } |
| 94 | // Hardcoded, fully URL-encoded SVG constant with no external input. |
| 95 | $svg = "data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M3%2021l3-1%2011-11a2.8%202.8%200%200%200-4-4L2%2016l-1%205'/%3E%3Cpath%20d='m15%205%204%204'/%3E%3C/svg%3E"; |
| 96 | $mask = 'url("' . $svg . '") no-repeat center / contain'; |
| 97 | |
| 98 | echo '<style id="evf-scv2-tab-icon">' |
| 99 | . '.everest-forms nav.evf-nav-tab-wrapper a.evf-panel-style-button .evf-nav-icon.style::before{' |
| 100 | . 'content:"";display:inline-block;width:16px;height:16px;background-color:currentColor;' |
| 101 | . '-webkit-mask:' . $mask . ';mask:' . $mask . ';}' |
| 102 | . '</style>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static developer-controlled CSS constant. |
| 103 | } |
| 104 | |
| 105 | /** |
| 106 | * Mtime of a bundled V2 asset (cache-bust component), or '0' if unreadable. |
| 107 | * |
| 108 | * @param string $relative Path relative to the V2 directory. |
| 109 | * @return string |
| 110 | */ |
| 111 | protected static function asset_mtime( $relative ) { |
| 112 | $path = plugin_dir_path( __FILE__ ) . ltrim( $relative, '/' ); |
| 113 | return file_exists( $path ) ? (string) filemtime( $path ) : '0'; |
| 114 | } |
| 115 | |
| 116 | /** |
| 117 | * Enqueue the panel bundle on the builder screen. |
| 118 | * |
| 119 | * @param string $hook Current admin page hook (unused; we check the screen id). |
| 120 | */ |
| 121 | public static function enqueue( $hook ) { |
| 122 | $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; |
| 123 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 124 | if ( ! $screen || self::SCREEN !== $screen->id || ! isset( $_GET['form_id'] ) ) { |
| 125 | return; |
| 126 | } |
| 127 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 128 | $form_id = absint( wp_unslash( $_GET['form_id'] ) ); |
| 129 | |
| 130 | wp_enqueue_media(); |
| 131 | |
| 132 | // Cache-bust by the built file's mtime; fall back to the plugin version. |
| 133 | $bundle_path = evf()->plugin_path() . '/dist/styleCustomizerV2.min.js'; |
| 134 | $bundle_ver = file_exists( $bundle_path ) ? (string) filemtime( $bundle_path ) : ( defined( 'EVF_VERSION' ) ? EVF_VERSION : false ); |
| 135 | |
| 136 | wp_enqueue_script( |
| 137 | 'evf-style-v2-panel', |
| 138 | evf()->plugin_url() . '/dist/styleCustomizerV2.min.js', |
| 139 | array( 'wp-element', 'wp-api-fetch', 'wp-i18n', 'react', 'react-dom' ), |
| 140 | $bundle_ver, |
| 141 | true |
| 142 | ); |
| 143 | |
| 144 | if ( function_exists( 'wp_set_script_translations' ) ) { |
| 145 | wp_set_script_translations( 'evf-style-v2-panel', 'everest-forms' ); |
| 146 | } |
| 147 | |
| 148 | // Per-page-load token scoping the live-preview draft to this builder session. |
| 149 | $preview_session = wp_generate_password( 12, false ); |
| 150 | |
| 151 | wp_localize_script( |
| 152 | 'evf-style-v2-panel', |
| 153 | 'evfStyleV2', |
| 154 | array( |
| 155 | 'restBase' => '/everest-forms/v1/styles', |
| 156 | 'formId' => $form_id, |
| 157 | 'formTitle' => get_the_title( $form_id ), |
| 158 | // Scheme forced to match the admin request to avoid a mixed-content iframe block. |
| 159 | 'previewUrl' => set_url_scheme( |
| 160 | add_query_arg( |
| 161 | array( |
| 162 | 'form_id' => $form_id, |
| 163 | 'evf_preview' => 'true', |
| 164 | PreviewDraft::PREVIEW_FLAG => '1', |
| 165 | PreviewDraft::SESSION_ARG => $preview_session, |
| 166 | ), |
| 167 | home_url( '/' ) |
| 168 | ), |
| 169 | is_ssl() ? 'https' : 'http' |
| 170 | ), |
| 171 | // Rule template URL, version-stamped to avoid a stale browser cache. |
| 172 | 'frontendCssUrl' => add_query_arg( 'ver', (string) Schema::version() . '.' . self::asset_mtime( 'assets/css/frontend.css' ), evf()->plugin_url() . '/addons/StyleCustomizer/V2/assets/css/frontend.css' ), |
| 173 | 'wrapperId' => 'evf-' . $form_id, |
| 174 | 'markerClass' => FrontendEnqueue::MARKER_CLASS, |
| 175 | 'previewSession' => $preview_session, |
| 176 | // AI launcher visibility: present whenever the integration class exists — so on a |
| 177 | // local/staging site it still renders, greyed-out and non-functional, exactly like |
| 178 | // the Fields-tab AI Form Assistant (rather than vanishing). `aiDisabled` carries the |
| 179 | // local-site check the button uses to grey itself out and explain why on hover. |
| 180 | 'aiEnabled' => class_exists( 'EVF_AI_Registration' ), |
| 181 | 'aiDisabled' => class_exists( 'EVF_AI_Registration' ) && \EVF_AI_Registration::is_local_site(), |
| 182 | // Discovery-hint dismissal — per-user (not per-browser), shared AJAX action/nonce |
| 183 | // with the Fields-tab AI assistant (see EVF_AI_Ajax::dismiss_hint()). |
| 184 | 'ajaxUrl' => admin_url( 'admin-ajax.php' ), |
| 185 | 'aiNonce' => wp_create_nonce( 'evf_ai_nonce' ), |
| 186 | 'aiHintDismissed' => (bool) get_user_meta( get_current_user_id(), 'evf_ai_style_hint_dismissed', true ), |
| 187 | // Full initial REST payload, computed here so the panel initializes without a follow-up fetch. |
| 188 | 'payload' => RestController::build_payload( $form_id ), |
| 189 | ) |
| 190 | ); |
| 191 | } |
| 192 | } |
| 193 |