← All changes
|
includes/Extensions/ExitIntent/ExitIntentNotification.php
+990
-2
3.2.7
→
3.3.3
View file →
| @@ -6,10 +6,12 @@ | ||
| 6 | 6 | */ |
| 7 | 7 | |
| 8 | 8 | namespace NotificationX\Extensions\ExitIntent; |
| 9 | 9 | |
| 10 | +use NotificationX\NotificationX; | |
| 10 | 11 | use NotificationX\GetInstance; |
| 11 | 12 | use NotificationX\Core\Rules; |
| 13 | +use NotificationX\Core\Helper; | |
| 12 | 14 | use NotificationX\Extensions\GlobalFields; |
| 13 | 15 | use NotificationX\Extensions\Extension; |
| 14 | 16 | |
| 15 | 17 | /** |
| @@ -24,12 +26,571 @@ | ||
| 24 | 26 | public $doc_link = 'https://notificationx.com/docs/'; |
| 25 | 27 | public $types = 'exit_intent'; |
| 26 | 28 | public $module = 'modules_exit_intent'; |
| 27 | 29 | |
| 30 | + /** | |
| 31 | + * Elementor seed-theme registry for the "Build With Elementor" modal. | |
| 32 | + * Populated alongside the JSON files in jsons/ (see Task 07). | |
| 33 | + * | |
| 34 | + * @var array<string, array{label:string, value:string, icon:string, column:string, title:string}> | |
| 35 | + */ | |
| 36 | + public $elementor_themes = []; | |
| 37 | + | |
| 28 | 38 | public function __construct() { |
| 29 | 39 | parent::__construct(); |
| 40 | + add_action( 'init', [ $this, 'register_post_type' ] ); | |
| 41 | + add_filter( 'get_edit_post_link', [ $this, 'filter_edit_post_link' ], 10, 3 ); | |
| 42 | + add_filter( 'nx_filtered_post', [ $this, 'inject_elementor_html' ], 10, 2 ); | |
| 43 | + add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_elementor_assets' ], 20 ); | |
| 44 | + add_action( 'wp_head', [ $this, 'print_section_constraint_css' ], 99 ); | |
| 45 | + add_action( 'elementor/documents/register_controls', [ $this, 'register_popup_layout_controls' ] ); | |
| 46 | + add_action( 'elementor/editor/after_enqueue_scripts', [ $this, 'enqueue_editor_live_preview' ] ); | |
| 30 | 47 | } |
| 31 | 48 | |
| 49 | + /** | |
| 50 | + * Print the popup section's width constraint inline in <head>. | |
| 51 | + * | |
| 52 | + * Two cases: | |
| 53 | + * 1. Sections that have the `nx-exit-intent-section` class baked in via the | |
| 54 | + * seed JSON's css_classes setting — constrained by the first rule. | |
| 55 | + * 2. The Elementor editor preview iframe loads the nx_exit_intent post | |
| 56 | + * directly as its own page (body.single-nx_exit_intent). The second | |
| 57 | + * rule constrains ANY section on that page, including legacy imports | |
| 58 | + * made before css_classes was added to the seed JSON. | |
| 59 | + */ | |
| 60 | + public function print_section_constraint_css() { | |
| 61 | + // Width is governed by the Layout panel's Width control via the | |
| 62 | + // `--nx-exit-width` CSS variable; fall back to the historical 540px cap | |
| 63 | + // when it is unset (older popups / default). | |
| 64 | + $vars = ''; | |
| 65 | + if ( is_singular( 'nx_exit_intent' ) ) { | |
| 66 | + // Editor preview / direct view: resolve the variable from the saved | |
| 67 | + // Layout-panel settings on this document. | |
| 68 | + $layout = $this->get_popup_layout_settings( get_the_ID() ); | |
| 69 | + if ( ! empty( $layout['width'] ) ) { | |
| 70 | + $vars .= '--nx-exit-width:' . $layout['width'] . ';'; | |
| 71 | + } | |
| 72 | + if ( ! empty( $layout['height'] ) ) { | |
| 73 | + $vars .= '--nx-exit-height:' . $layout['height'] . ';'; | |
| 74 | + } | |
| 75 | + } | |
| 76 | + echo "<style id='nx-exit-intent-section-constraint'>\n"; | |
| 77 | + if ( '' !== $vars ) { | |
| 78 | + echo "body.single-nx_exit_intent{" . esc_html( $vars ) . "}\n"; | |
| 79 | + } | |
| 80 | + echo ".nx-exit-intent-section{width:100%!important;max-width:var(--nx-exit-width,540px)!important;margin-left:auto!important;margin-right:auto!important;}\n"; | |
| 81 | + if ( is_singular( 'nx_exit_intent' ) ) { | |
| 82 | + echo "body.single-nx_exit_intent .elementor-section,body.single-nx_exit_intent .e-con{width:100%!important;max-width:var(--nx-exit-width,540px)!important;margin-left:auto!important;margin-right:auto!important;}\n"; | |
| 83 | + } | |
| 84 | + echo "</style>\n"; | |
| 85 | + } | |
| 86 | + | |
| 87 | + /** | |
| 88 | + * Server-side: inject the Elementor-rendered HTML + a `mode` flag into the | |
| 89 | + * REST payload so the React popup shell can render it. | |
| 90 | + * | |
| 91 | + * Runs for every campaign settings array via the `nx_filtered_post` filter; | |
| 92 | + * scoped to Exit Intent campaigns that have a linked, published nx_exit_intent. | |
| 93 | + */ | |
| 94 | + public function inject_elementor_html( $settings, $params ) { | |
| 95 | + if ( empty( $settings['source'] ) || $settings['source'] !== $this->id ) { | |
| 96 | + return $settings; | |
| 97 | + } | |
| 98 | + $elementor_id = isset( $settings['elementor_id'] ) ? (int) $settings['elementor_id'] : 0; | |
| 99 | + if ( ! $elementor_id || ! class_exists( '\\Elementor\\Plugin' ) ) { | |
| 100 | + $settings['mode'] = 'built_in'; | |
| 101 | + return $settings; | |
| 102 | + } | |
| 103 | + if ( get_post_status( $elementor_id ) !== 'publish' ) { | |
| 104 | + $settings['mode'] = 'built_in'; | |
| 105 | + return $settings; | |
| 106 | + } | |
| 107 | + | |
| 108 | + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context. | |
| 109 | + $resolved_id = apply_filters( 'wpml_object_id', $elementor_id, 'nx_exit_intent', true ); | |
| 110 | + $html = \Elementor\Plugin::$instance->frontend->get_builder_content_for_display( $resolved_id, false ); | |
| 111 | + | |
| 112 | + // Surface the Layout-panel settings (registered in register_popup_layout_controls) | |
| 113 | + // so the popup runtime can honour them, and set --nx-exit-width on a wrapper so | |
| 114 | + // the section width constraint reflects the configured Width on the frontend. | |
| 115 | + $layout = $this->get_popup_layout_settings( $resolved_id ); | |
| 116 | + $wrapper_vars = ''; | |
| 117 | + if ( ! empty( $layout['width'] ) ) { | |
| 118 | + $wrapper_vars .= '--nx-exit-width:' . $layout['width'] . ';'; | |
| 119 | + } | |
| 120 | + if ( ! empty( $layout['height'] ) ) { | |
| 121 | + $wrapper_vars .= '--nx-exit-height:' . $layout['height'] . ';'; | |
| 122 | + } | |
| 123 | + if ( '' !== $wrapper_vars ) { | |
| 124 | + $html = '<div class="nx-exit-intent-elementor-wrap" style="' . esc_attr( $wrapper_vars ) . '">' . $html . '</div>'; | |
| 125 | + } | |
| 126 | + | |
| 127 | + $settings['mode'] = 'elementor'; | |
| 128 | + $settings['elementor_html'] = $html; | |
| 129 | + $settings['popup_layout'] = $layout; | |
| 130 | + return $settings; | |
| 131 | + } | |
| 132 | + | |
| 133 | + /** | |
| 134 | + * Read the Layout-panel settings off an nx_exit_intent Elementor document and | |
| 135 | + * normalise them into a flat array the popup runtime can consume. | |
| 136 | + * | |
| 137 | + * @param int $elementor_id | |
| 138 | + * @return array | |
| 139 | + */ | |
| 140 | + protected function get_popup_layout_settings( $elementor_id ) { | |
| 141 | + $defaults = [ | |
| 142 | + 'width' => '', | |
| 143 | + 'height_mode' => 'fit', | |
| 144 | + 'height' => '', | |
| 145 | + 'horizontal' => 'center', | |
| 146 | + 'vertical' => 'center', | |
| 147 | + 'overlay' => true, | |
| 148 | + 'close_button' => true, | |
| 149 | + 'entrance_animation' => '', | |
| 150 | + 'exit_animation' => '', | |
| 151 | + ]; | |
| 152 | + | |
| 153 | + if ( ! class_exists( '\\Elementor\\Plugin' ) ) { | |
| 154 | + return $defaults; | |
| 155 | + } | |
| 156 | + $document = \Elementor\Plugin::$instance->documents->get( $elementor_id ); | |
| 157 | + if ( ! $document ) { | |
| 158 | + return $defaults; | |
| 159 | + } | |
| 160 | + | |
| 161 | + $size = static function ( $value ) { | |
| 162 | + if ( is_array( $value ) && isset( $value['size'] ) && '' !== $value['size'] ) { | |
| 163 | + return $value['size'] . ( isset( $value['unit'] ) ? $value['unit'] : 'px' ); | |
| 164 | + } | |
| 165 | + return ''; | |
| 166 | + }; | |
| 167 | + | |
| 168 | + $height_mode = $document->get_settings( 'nx_popup_height' ); | |
| 169 | + | |
| 170 | + // Width precedence: the Layout-panel Width control if the user set one, | |
| 171 | + // otherwise the design's own top-level container width (boxed_width). | |
| 172 | + // Without this, every popup falls back to the CSS 540px default, which | |
| 173 | + // crushes wider designs (e.g. the two-column 896px themes). | |
| 174 | + $width = $size( $document->get_settings( 'nx_popup_width' ) ); | |
| 175 | + if ( '' === $width ) { | |
| 176 | + $width = $this->resolve_design_width( $document ); | |
| 177 | + } | |
| 178 | + | |
| 179 | + return [ | |
| 180 | + 'width' => $width, | |
| 181 | + 'height_mode' => $height_mode ? $height_mode : 'fit', | |
| 182 | + 'height' => 'custom' === $height_mode ? $size( $document->get_settings( 'nx_popup_custom_height' ) ) : '', | |
| 183 | + 'horizontal' => $document->get_settings( 'nx_popup_horizontal' ) ?: 'center', | |
| 184 | + 'vertical' => $document->get_settings( 'nx_popup_vertical' ) ?: 'center', | |
| 185 | + 'overlay' => 'yes' === $document->get_settings( 'nx_popup_overlay' ), | |
| 186 | + 'close_button' => 'yes' === $document->get_settings( 'nx_popup_close_button' ), | |
| 187 | + 'entrance_animation' => (string) $document->get_settings( 'nx_popup_entrance_animation' ), | |
| 188 | + 'exit_animation' => (string) $document->get_settings( 'nx_popup_exit_animation' ), | |
| 189 | + ]; | |
| 190 | + } | |
| 191 | + | |
| 192 | + /** | |
| 193 | + * Derive the popup's intended width from its Elementor design — the widest | |
| 194 | + * px width among the top-level elements (boxed_width / width / content_width). | |
| 195 | + * Used as the default popup width when no Layout-panel Width is configured, | |
| 196 | + * so each design keeps its own width instead of the 540px CSS fallback. | |
| 197 | + * Only top-level elements are inspected so inner column widths are ignored. | |
| 198 | + * | |
| 199 | + * @param \Elementor\Core\Base\Document $document | |
| 200 | + * @return string e.g. "896px", or '' when no explicit px width is defined. | |
| 201 | + */ | |
| 202 | + protected function resolve_design_width( $document ) { | |
| 203 | + if ( ! $document || ! method_exists( $document, 'get_elements_data' ) ) { | |
| 204 | + return ''; | |
| 205 | + } | |
| 206 | + $data = $document->get_elements_data(); | |
| 207 | + if ( empty( $data ) || ! is_array( $data ) ) { | |
| 208 | + return ''; | |
| 209 | + } | |
| 210 | + $max = 0; | |
| 211 | + foreach ( $data as $element ) { | |
| 212 | + if ( empty( $element['settings'] ) || ! is_array( $element['settings'] ) ) { | |
| 213 | + continue; | |
| 214 | + } | |
| 215 | + foreach ( [ 'boxed_width', 'width', 'content_width' ] as $key ) { | |
| 216 | + $val = isset( $element['settings'][ $key ] ) ? $element['settings'][ $key ] : null; | |
| 217 | + if ( is_array( $val ) && isset( $val['size'] ) && is_numeric( $val['size'] ) | |
| 218 | + && ( ! isset( $val['unit'] ) || 'px' === $val['unit'] ) ) { | |
| 219 | + $max = max( $max, (float) $val['size'] ); | |
| 220 | + } | |
| 221 | + } | |
| 222 | + } | |
| 223 | + return $max > 0 ? $max . 'px' : ''; | |
| 224 | + } | |
| 225 | + | |
| 226 | + /** | |
| 227 | + * Make sure Elementor's per-document CSS/JS is enqueued on every page where | |
| 228 | + * an Exit Intent campaign can fire — get_builder_content_for_display alone | |
| 229 | + * registers some assets, but not on pages other than the document's own URL. | |
| 230 | + */ | |
| 231 | + public function enqueue_elementor_assets() { | |
| 232 | + if ( ! class_exists( '\\Elementor\\Core\\Files\\CSS\\Post' ) ) { | |
| 233 | + return; | |
| 234 | + } | |
| 235 | + $ids = []; | |
| 236 | + try { | |
| 237 | + $posts = \NotificationX\Core\PostType::get_instance()->get_posts( [ 'source' => $this->id ] ); | |
| 238 | + foreach ( $posts as $post ) { | |
| 239 | + if ( ! empty( $post['elementor_id'] ) && empty( $post['enabled'] ) === false ) { | |
| 240 | + $ids[] = (int) $post['elementor_id']; | |
| 241 | + } | |
| 242 | + } | |
| 243 | + } catch ( \Exception $e ) { | |
| 244 | + return; | |
| 245 | + } | |
| 246 | + foreach ( $ids as $id ) { | |
| 247 | + try { | |
| 248 | + $css = \Elementor\Core\Files\CSS\Post::create( $id ); | |
| 249 | + if ( $css ) { | |
| 250 | + $css->enqueue(); | |
| 251 | + } | |
| 252 | + } catch ( \Exception $e ) { | |
| 253 | + // swallow — one broken doc shouldn't break the rest. | |
| 254 | + } | |
| 255 | + } | |
| 256 | + } | |
| 257 | + | |
| 258 | + /** | |
| 259 | + * Register the post type that backs Elementor-designed Exit Intent popups. | |
| 260 | + * | |
| 261 | + * Mirrors PressBar's `nx_bar`: not public, but publicly_queryable so Elementor's | |
| 262 | + * preview routes resolve. `supports[]` must include `elementor` for Elementor's | |
| 263 | + * documents API to treat this as a first-class document. | |
| 264 | + * | |
| 265 | + * @return void | |
| 266 | + */ | |
| 267 | + public static function register_post_type() { | |
| 268 | + register_post_type( 'nx_exit_intent', [ | |
| 269 | + 'label' => __( 'NotificationX Exit Intent', 'notificationx' ), | |
| 270 | + 'public' => false, | |
| 271 | + 'publicly_queryable' => true, | |
| 272 | + 'show_ui' => false, | |
| 273 | + 'show_in_menu' => true, | |
| 274 | + 'show_in_nav_menus' => false, | |
| 275 | + 'exclude_from_search' => true, | |
| 276 | + 'rewrite' => false, | |
| 277 | + 'capability_type' => 'post', | |
| 278 | + 'hierarchical' => false, | |
| 279 | + 'menu_icon' => 'dashicons-admin-page', | |
| 280 | + 'supports' => [ 'title', 'content', 'author', 'elementor' ], | |
| 281 | + ] ); | |
| 282 | + } | |
| 283 | + | |
| 284 | + /** | |
| 285 | + * Add a "Layout" section to the Elementor document-settings panel, but ONLY | |
| 286 | + * for nx_exit_intent documents. Mirrors Elementor Pro's Popup → Layout panel | |
| 287 | + * (Width / Height / Position / Overlay / Close Button / Animations). | |
| 288 | + * | |
| 289 | + * Hooked on `elementor/documents/register_controls`, which fires for every | |
| 290 | + * document right after its built-in controls register. We bail for any other | |
| 291 | + * post type so these controls never leak into normal pages/posts. | |
| 292 | + * | |
| 293 | + * Width is wired live via the `--nx-exit-width` CSS variable that | |
| 294 | + * print_section_constraint_css()/inject_elementor_html() read; the remaining | |
| 295 | + * controls persist into the document's page settings (`_elementor_page_settings`) | |
| 296 | + * so the popup runtime can consume them. | |
| 297 | + * | |
| 298 | + * @param \Elementor\Core\Base\Document $document | |
| 299 | + * @return void | |
| 300 | + */ | |
| 301 | + public function register_popup_layout_controls( $document ) { | |
| 302 | + if ( ! is_object( $document ) || ! method_exists( $document, 'get_post' ) ) { | |
| 303 | + return; | |
| 304 | + } | |
| 305 | + $post = $document->get_post(); | |
| 306 | + if ( ! $post || 'nx_exit_intent' !== $post->post_type ) { | |
| 307 | + return; | |
| 308 | + } | |
| 309 | + | |
| 310 | + $cm = '\Elementor\Controls_Manager'; | |
| 311 | + | |
| 312 | + $document->start_controls_section( | |
| 313 | + 'nx_popup_layout', | |
| 314 | + [ | |
| 315 | + 'label' => __( 'Layout', 'notificationx' ), | |
| 316 | + 'tab' => $cm::TAB_SETTINGS, | |
| 317 | + ] | |
| 318 | + ); | |
| 319 | + | |
| 320 | + $document->add_responsive_control( | |
| 321 | + 'nx_popup_width', | |
| 322 | + [ | |
| 323 | + 'label' => __( 'Width', 'notificationx' ), | |
| 324 | + 'type' => $cm::SLIDER, | |
| 325 | + 'size_units' => [ 'px', '%', 'vw' ], | |
| 326 | + 'range' => [ | |
| 327 | + 'px' => [ 'min' => 100, 'max' => 1140 ], | |
| 328 | + '%' => [ 'min' => 10, 'max' => 100 ], | |
| 329 | + 'vw' => [ 'min' => 10, 'max' => 100 ], | |
| 330 | + ], | |
| 331 | + // No default: when the user hasn't set a Width, the popup falls back | |
| 332 | + // to the design's own container width (resolve_design_width()), not a | |
| 333 | + // fixed 540px — otherwise wider designs (e.g. the 896px two-column | |
| 334 | + // themes) get crushed. An explicit value here still wins. | |
| 335 | + // | |
| 336 | + // NOTE: no `selectors` key — Elementor silently drops document-level | |
| 337 | + // controls that carry `selectors`. The value is applied server-side | |
| 338 | + // via the --nx-exit-width variable in print_section_constraint_css() | |
| 339 | + // (preview) and inject_elementor_html() (frontend) instead. | |
| 340 | + 'default' => [ 'unit' => 'px', 'size' => '' ], | |
| 341 | + ] | |
| 342 | + ); | |
| 343 | + | |
| 344 | + $document->add_control( | |
| 345 | + 'nx_popup_height', | |
| 346 | + [ | |
| 347 | + 'label' => __( 'Height', 'notificationx' ), | |
| 348 | + 'type' => $cm::SELECT, | |
| 349 | + 'default' => 'fit', | |
| 350 | + 'options' => [ | |
| 351 | + 'fit' => __( 'Fit To Content', 'notificationx' ), | |
| 352 | + 'custom' => __( 'Custom Height', 'notificationx' ), | |
| 353 | + ], | |
| 354 | + ] | |
| 355 | + ); | |
| 356 | + | |
| 357 | + $document->add_responsive_control( | |
| 358 | + 'nx_popup_custom_height', | |
| 359 | + [ | |
| 360 | + 'label' => __( 'Custom Height', 'notificationx' ), | |
| 361 | + 'type' => $cm::SLIDER, | |
| 362 | + 'size_units' => [ 'px', 'vh' ], | |
| 363 | + 'range' => [ | |
| 364 | + 'px' => [ 'min' => 100, 'max' => 1000 ], | |
| 365 | + 'vh' => [ 'min' => 10, 'max' => 100 ], | |
| 366 | + ], | |
| 367 | + 'default' => [ 'unit' => 'px', 'size' => 480 ], | |
| 368 | + 'condition' => [ 'nx_popup_height' => 'custom' ], | |
| 369 | + // No `selectors` (see nx_popup_width note); applied via --nx-exit-height. | |
| 370 | + ] | |
| 371 | + ); | |
| 372 | + | |
| 373 | + $document->add_control( | |
| 374 | + 'nx_popup_position_heading', | |
| 375 | + [ | |
| 376 | + 'label' => __( 'Position', 'notificationx' ), | |
| 377 | + 'type' => $cm::HEADING, | |
| 378 | + 'separator' => 'before', | |
| 379 | + ] | |
| 380 | + ); | |
| 381 | + | |
| 382 | + $document->add_responsive_control( | |
| 383 | + 'nx_popup_horizontal', | |
| 384 | + [ | |
| 385 | + 'label' => __( 'Horizontal', 'notificationx' ), | |
| 386 | + 'type' => $cm::CHOOSE, | |
| 387 | + 'default' => 'center', | |
| 388 | + 'options' => [ | |
| 389 | + 'start' => [ 'title' => __( 'Start', 'notificationx' ), 'icon' => 'eicon-h-align-left' ], | |
| 390 | + 'center' => [ 'title' => __( 'Center', 'notificationx' ), 'icon' => 'eicon-h-align-center' ], | |
| 391 | + 'end' => [ 'title' => __( 'End', 'notificationx' ), 'icon' => 'eicon-h-align-right' ], | |
| 392 | + ], | |
| 393 | + ] | |
| 394 | + ); | |
| 395 | + | |
| 396 | + $document->add_responsive_control( | |
| 397 | + 'nx_popup_vertical', | |
| 398 | + [ | |
| 399 | + 'label' => __( 'Vertical', 'notificationx' ), | |
| 400 | + 'type' => $cm::CHOOSE, | |
| 401 | + 'default' => 'center', | |
| 402 | + 'options' => [ | |
| 403 | + 'start' => [ 'title' => __( 'Top', 'notificationx' ), 'icon' => 'eicon-v-align-top' ], | |
| 404 | + 'center' => [ 'title' => __( 'Middle', 'notificationx' ), 'icon' => 'eicon-v-align-middle' ], | |
| 405 | + 'end' => [ 'title' => __( 'Bottom', 'notificationx' ), 'icon' => 'eicon-v-align-bottom' ], | |
| 406 | + ], | |
| 407 | + ] | |
| 408 | + ); | |
| 409 | + | |
| 410 | + $document->add_control( | |
| 411 | + 'nx_popup_overlay', | |
| 412 | + [ | |
| 413 | + 'label' => __( 'Overlay', 'notificationx' ), | |
| 414 | + 'type' => $cm::SWITCHER, | |
| 415 | + 'label_on' => __( 'Show', 'notificationx' ), | |
| 416 | + 'label_off' => __( 'Hide', 'notificationx' ), | |
| 417 | + 'return_value' => 'yes', | |
| 418 | + 'default' => 'yes', | |
| 419 | + 'separator' => 'before', | |
| 420 | + ] | |
| 421 | + ); | |
| 422 | + | |
| 423 | + $document->add_control( | |
| 424 | + 'nx_popup_close_button', | |
| 425 | + [ | |
| 426 | + 'label' => __( 'Close Button', 'notificationx' ), | |
| 427 | + 'type' => $cm::SWITCHER, | |
| 428 | + 'label_on' => __( 'Show', 'notificationx' ), | |
| 429 | + 'label_off' => __( 'Hide', 'notificationx' ), | |
| 430 | + 'return_value' => 'yes', | |
| 431 | + 'default' => 'yes', | |
| 432 | + ] | |
| 433 | + ); | |
| 434 | + | |
| 435 | + $document->add_control( | |
| 436 | + 'nx_popup_entrance_animation', | |
| 437 | + [ | |
| 438 | + 'label' => __( 'Entrance Animation', 'notificationx' ), | |
| 439 | + 'type' => $cm::ANIMATION, | |
| 440 | + 'separator' => 'before', | |
| 441 | + ] | |
| 442 | + ); | |
| 443 | + | |
| 444 | + $document->add_control( | |
| 445 | + 'nx_popup_exit_animation', | |
| 446 | + [ | |
| 447 | + 'label' => __( 'Exit Animation', 'notificationx' ), | |
| 448 | + 'type' => $cm::EXIT_ANIMATION, | |
| 449 | + ] | |
| 450 | + ); | |
| 451 | + | |
| 452 | + $document->end_controls_section(); | |
| 453 | + } | |
| 454 | + | |
| 455 | + /** | |
| 456 | + * Live-preview bridge for the Layout panel inside the Elementor editor. | |
| 457 | + * | |
| 458 | + * Document-level controls can't carry a `selectors` key (Elementor drops | |
| 459 | + * them), so Width/Height don't get Elementor's built-in live CSS injection. | |
| 460 | + * This registers page-settings change callbacks that write the | |
| 461 | + * `--nx-exit-width` / `--nx-exit-height` variables straight onto the preview | |
| 462 | + * iframe's <body> as the slider moves — so changes reflect instantly instead | |
| 463 | + * of only after a reload. | |
| 464 | + * | |
| 465 | + * Scoped to nx_exit_intent documents only. | |
| 466 | + * | |
| 467 | + * @return void | |
| 468 | + */ | |
| 469 | + public function enqueue_editor_live_preview() { | |
| 470 | + if ( ! class_exists( '\\Elementor\\Plugin' ) ) { | |
| 471 | + return; | |
| 472 | + } | |
| 473 | + $post_id = \Elementor\Plugin::$instance->editor->get_post_id(); | |
| 474 | + if ( ! $post_id || 'nx_exit_intent' !== get_post_type( $post_id ) ) { | |
| 475 | + return; | |
| 476 | + } | |
| 477 | + | |
| 478 | + $js = <<<'JS' | |
| 479 | +( function ( $ ) { | |
| 480 | + function toCss( value ) { | |
| 481 | + if ( value && '' !== value.size && null != value.size ) { | |
| 482 | + return value.size + ( value.unit || 'px' ); | |
| 483 | + } | |
| 484 | + return ''; | |
| 485 | + } | |
| 486 | + function previewBody() { | |
| 487 | + var frame = elementor.$preview && elementor.$preview[0]; | |
| 488 | + var win = frame && frame.contentWindow; | |
| 489 | + return win && win.document ? win.document.body : null; | |
| 490 | + } | |
| 491 | + function applyVar( name, value ) { | |
| 492 | + var body = previewBody(); | |
| 493 | + if ( ! body ) { return; } | |
| 494 | + var css = toCss( value ); | |
| 495 | + if ( css ) { body.style.setProperty( name, css ); } | |
| 496 | + else { body.style.removeProperty( name ); } | |
| 497 | + } | |
| 498 | + $( window ).on( 'elementor:init', function () { | |
| 499 | + if ( 'undefined' === typeof elementor || ! elementor.settings || ! elementor.settings.page ) { | |
| 500 | + return; | |
| 501 | + } | |
| 502 | + elementor.settings.page.addChangeCallback( 'nx_popup_width', function ( value ) { | |
| 503 | + applyVar( '--nx-exit-width', value ); | |
| 504 | + } ); | |
| 505 | + elementor.settings.page.addChangeCallback( 'nx_popup_custom_height', function ( value ) { | |
| 506 | + applyVar( '--nx-exit-height', value ); | |
| 507 | + } ); | |
| 508 | + } ); | |
| 509 | +} )( jQuery ); | |
| 510 | +JS; | |
| 511 | + | |
| 512 | + wp_add_inline_script( 'elementor-editor', $js ); | |
| 513 | + } | |
| 514 | + | |
| 515 | + /** | |
| 516 | + * Create a new `nx_exit_intent` Elementor document from a seed theme and | |
| 517 | + * respond to the admin with the new post ID + Elementor edit URL. | |
| 518 | + * | |
| 519 | + * Counterpart of PressBar::create_bar_of_type_bar_with_elementor(). | |
| 520 | + * | |
| 521 | + * @param array $params { theme_id: string } | |
| 522 | + * @return void Always responds via wp_send_json_{success,error}. | |
| 523 | + */ | |
| 524 | + public function create_exit_intent_with_elementor( $params ) { | |
| 525 | + if ( empty( $params['theme_id'] ) ) { | |
| 526 | + wp_send_json_error( 'missing_theme_id' ); | |
| 527 | + } | |
| 528 | + | |
| 529 | + $theme = sanitize_text_field( $params['theme_id'] ); | |
| 530 | + $importer = new Importer(); | |
| 531 | + | |
| 532 | + $ID = $importer->create_nx( [ | |
| 533 | + 'theme' => $theme, | |
| 534 | + 'post_title' => 'Design for NotificationX Exit Intent - ', | |
| 535 | + ] ); | |
| 536 | + | |
| 537 | + if ( $ID && ! is_wp_error( $ID ) ) { | |
| 538 | + // Strip theme chrome — popup HTML inlines into the host page. | |
| 539 | + update_post_meta( $ID, '_wp_page_template', 'elementor_canvas' ); | |
| 540 | + wp_send_json_success( [ | |
| 541 | + 'context' => [ | |
| 542 | + 'themes' => null, | |
| 543 | + 'elementor_id' => $ID, | |
| 544 | + 'elementor_edit_link' => \Elementor\Plugin::$instance->documents->get( $ID )->get_edit_url(), | |
| 545 | + ], | |
| 546 | + ] ); | |
| 547 | + } else { | |
| 548 | + wp_send_json_error( is_wp_error( $ID ) ? $ID->get_error_message() : 'failed' ); | |
| 549 | + } | |
| 550 | + } | |
| 551 | + | |
| 552 | + /** | |
| 553 | + * Delete the linked `nx_exit_intent` Elementor post (and its WPML translations). | |
| 554 | + * | |
| 555 | + * @param int|string $elementor_id | |
| 556 | + * @return void | |
| 557 | + */ | |
| 558 | + public function delete_elementor_post( $elementor_id ) { | |
| 559 | + if ( empty( $elementor_id ) ) { | |
| 560 | + return; | |
| 561 | + } | |
| 562 | + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context. | |
| 563 | + $languages = apply_filters( 'wpml_active_languages', null ); | |
| 564 | + if ( is_array( $languages ) ) { | |
| 565 | + foreach ( $languages as $lang => $val ) { | |
| 566 | + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context. | |
| 567 | + $translated_id = apply_filters( 'wpml_object_id', $elementor_id, 'nx_exit_intent', false, $lang ); | |
| 568 | + if ( $translated_id ) { | |
| 569 | + Helper::delete_owned_post( $translated_id, 'nx_exit_intent' ); | |
| 570 | + } | |
| 571 | + } | |
| 572 | + return; | |
| 573 | + } | |
| 574 | + Helper::delete_owned_post( $elementor_id, 'nx_exit_intent' ); | |
| 575 | + } | |
| 576 | + | |
| 577 | + /** | |
| 578 | + * Redirect WP "Edit" links for nx_exit_intent posts straight into the Elementor editor. | |
| 579 | + * | |
| 580 | + * @param string $link | |
| 581 | + * @param int $id | |
| 582 | + * @param string $context | |
| 583 | + * @return string | |
| 584 | + */ | |
| 585 | + public function filter_edit_post_link( $link, $id, $context ) { | |
| 586 | + $post = get_post( $id ); | |
| 587 | + if ( $post && 'nx_exit_intent' === $post->post_type && class_exists( '\\Elementor\\Plugin' ) ) { | |
| 588 | + return \Elementor\Plugin::$instance->documents->get( $id )->get_edit_url(); | |
| 589 | + } | |
| 590 | + return $link; | |
| 591 | + } | |
| 592 | + | |
| 32 | 593 | public function init_extension() { |
| 33 | 594 | $this->title = __( 'Exit Intent Popup', 'notificationx' ); |
| 34 | 595 | $this->module_title = __( 'Exit Intent Popup', 'notificationx' ); |
| 35 | 596 | $this->themes = [ |
| @@ -143,8 +704,77 @@ | ||
| 143 | 704 | 'column' => '5', |
| 144 | 705 | 'is_pro' => true, |
| 145 | 706 | ], |
| 146 | 707 | ]; |
| 708 | + | |
| 709 | + // Elementor seed themes for the "Build With Elementor" modal. | |
| 710 | + // Each entry has a matching JSON file under jsons/{value}.json (Task 07). | |
| 711 | + // Preview images live under assets/admin/images/extensions/themes/exit-intent-elementor/. | |
| 712 | + // Until per-theme Elementor previews are rendered, we fall back to the existing | |
| 713 | + // built-in screenshots so the modal renders with a real image. | |
| 714 | + // Order intentionally mirrors the built-in $themes preset order above | |
| 715 | + // (one, four, three, six, two, seven, five) so the Elementor modal lists | |
| 716 | + // the same designs in the same sequence as the Default-theme picker. | |
| 717 | + // | |
| 718 | + // Unlike $themes (processed by Extension::__nx_themes), this array is fed | |
| 719 | + // straight to the radio-card field, so we must resolve the pro badge here: | |
| 720 | + // only flag a theme as pro when the Pro plugin is NOT active. | |
| 721 | + $is_pro_badge = ! NotificationX::is_pro(); | |
| 722 | + $this->elementor_themes = [ | |
| 723 | + 'theme-one' => [ | |
| 724 | + 'label' => 'theme-one', | |
| 725 | + 'value' => 'theme-one', | |
| 726 | + 'icon' => NOTIFICATIONX_ADMIN_URL . 'images/extensions/themes/exit-intent/exit-intent-theme-one.png', | |
| 727 | + 'column' => '6', | |
| 728 | + 'title' => 'Exit Intent Theme One', | |
| 729 | + ], | |
| 730 | + 'theme-four' => [ | |
| 731 | + 'label' => 'theme-four', | |
| 732 | + 'value' => 'theme-four', | |
| 733 | + 'icon' => NOTIFICATIONX_ADMIN_URL . 'images/extensions/themes/exit-intent/exit-intent-theme-four.png', | |
| 734 | + 'column' => '6', | |
| 735 | + 'title' => 'Exit Intent Theme Four', | |
| 736 | + ], | |
| 737 | + 'theme-three' => [ | |
| 738 | + 'label' => 'theme-three', | |
| 739 | + 'value' => 'theme-three', | |
| 740 | + 'icon' => NOTIFICATIONX_ADMIN_URL . 'images/extensions/themes/exit-intent/exit-intent-theme-three.png', | |
| 741 | + 'column' => '6', | |
| 742 | + 'title' => 'Exit Intent Theme Three', | |
| 743 | + ], | |
| 744 | + 'theme-six' => [ | |
| 745 | + 'label' => 'theme-six', | |
| 746 | + 'value' => 'theme-six', | |
| 747 | + 'icon' => NOTIFICATIONX_ADMIN_URL . 'images/extensions/themes/exit-intent/exit-intent-theme-six.png', | |
| 748 | + 'column' => '6', | |
| 749 | + 'title' => 'Exit Intent Theme Six', | |
| 750 | + 'is_pro' => $is_pro_badge, | |
| 751 | + ], | |
| 752 | + 'theme-two' => [ | |
| 753 | + 'label' => 'theme-two', | |
| 754 | + 'value' => 'theme-two', | |
| 755 | + 'icon' => NOTIFICATIONX_ADMIN_URL . 'images/extensions/themes/exit-intent/exit-intent-theme-two.png', | |
| 756 | + 'column' => '6', | |
| 757 | + 'title' => 'Exit Intent Theme Two', | |
| 758 | + 'is_pro' => $is_pro_badge, | |
| 759 | + ], | |
| 760 | + 'theme-seven' => [ | |
| 761 | + 'label' => 'theme-seven', | |
| 762 | + 'value' => 'theme-seven', | |
| 763 | + 'icon' => NOTIFICATIONX_ADMIN_URL . 'images/extensions/themes/exit-intent/exit-intent-theme-seven.png', | |
| 764 | + 'column' => '6', | |
| 765 | + 'title' => 'Exit Intent Theme Seven', | |
| 766 | + 'is_pro' => $is_pro_badge, | |
| 767 | + ], | |
| 768 | + 'theme-five' => [ | |
| 769 | + 'label' => 'theme-five', | |
| 770 | + 'value' => 'theme-five', | |
| 771 | + 'icon' => NOTIFICATIONX_ADMIN_URL . 'images/extensions/themes/exit-intent/exit-intent-theme-five.png', | |
| 772 | + 'column' => '6', | |
| 773 | + 'title' => 'Exit Intent Theme Five', | |
| 774 | + 'is_pro' => $is_pro_badge, | |
| 775 | + ], | |
| 776 | + ]; | |
| 147 | 777 | } |
| 148 | 778 | |
| 149 | 779 | public function init_fields() { |
| 150 | 780 | parent::init_fields(); |
| @@ -149,12 +779,361 @@ | ||
| 149 | 779 | public function init_fields() { |
| 150 | 780 | parent::init_fields(); |
| 151 | 781 | add_filter( 'nx_content_fields', [ $this, 'content_fields' ], 999 ); |
| 152 | 782 | add_filter( 'nx_design_tab_fields', [ $this, 'design_fields' ], 99 ); |
| 783 | + add_filter( 'nx_design_tab_fields', [ $this, 'builder_tabs_fields' ], 100 ); | |
| 153 | 784 | add_filter( 'nx_customize_fields', [ $this, 'customize_fields' ], 999 ); |
| 154 | 785 | add_filter( 'nx_display_fields', [ $this, 'display_fields' ], 999 ); |
| 786 | + | |
| 787 | + // Hide per-theme content sections when an Elementor doc is linked (Task 06). | |
| 788 | + // We deliberately do NOT hide the themes radio-card itself — keeping it | |
| 789 | + // visible on the Default tab lets users switch back to a built-in theme | |
| 790 | + // after they've imported an Elementor design. | |
| 791 | + add_filter( 'nx_content_fields', [ $this, 'suppress_when_elementor' ], 1001 ); | |
| 792 | + | |
| 793 | + // Hide the whole Content wizard step when an Elementor design is linked — | |
| 794 | + // suppress_when_elementor() empties every content section in that case, so | |
| 795 | + // the step would otherwise render as a blank tab. | |
| 796 | + add_filter( 'nx_metabox_tabs', [ $this, 'hide_content_tab_for_elementor' ] ); | |
| 155 | 797 | } |
| 156 | 798 | |
| 799 | + /** | |
| 800 | + * Remove the Content step from the wizard for Exit Intent campaigns that are | |
| 801 | + * built with Elementor. Elementor owns all of the popup's content there (every | |
| 802 | + * `exit_intent_*_section` is suppressed by suppress_when_elementor()), leaving | |
| 803 | + * the Content tab empty. Built-in themes keep the Content step, and all other | |
| 804 | + * sources are untouched. | |
| 805 | + * | |
| 806 | + * The tab stays visible UNLESS (source is Exit Intent AND elementor_id is a | |
| 807 | + * number), expressed below as the OR'd "show when" rule. | |
| 808 | + */ | |
| 809 | + public function hide_content_tab_for_elementor( $tabs ) { | |
| 810 | + if ( empty( $tabs['content_tab'] ) ) { | |
| 811 | + return $tabs; | |
| 812 | + } | |
| 813 | + $show_rule = Rules::_logicalRule( [ | |
| 814 | + Rules::_is( 'source', $this->id, true ), | |
| 815 | + Rules::_isOfType( 'elementor_id', 'number', true ), | |
| 816 | + ], 'or' ); | |
| 817 | + $tabs['content_tab'] = Rules::add( $show_rule, $tabs['content_tab'] ); | |
| 818 | + return $tabs; | |
| 819 | + } | |
| 820 | + | |
| 821 | + /** | |
| 822 | + * Hide every per-theme content section once an Elementor design is linked. | |
| 823 | + * Elementor owns all of these fields; showing them would let users edit | |
| 824 | + * values with no visible effect on the rendered popup. | |
| 825 | + */ | |
| 826 | + public function suppress_when_elementor( $fields ) { | |
| 827 | + foreach ( $fields as $key => $field ) { | |
| 828 | + if ( strpos( $key, 'exit_intent_' ) === 0 && substr( $key, -8 ) === '_section' ) { | |
| 829 | + $fields[ $key ] = Rules::isOfType( 'elementor_id', 'number', true, $field ); | |
| 830 | + } | |
| 831 | + } | |
| 832 | + return $fields; | |
| 833 | + } | |
| 834 | + | |
| 835 | + /** | |
| 836 | + * @deprecated Kept for back-compat only; the filter that called this has | |
| 837 | + * been removed so the themes radio stays visible on the Default tab even | |
| 838 | + * when an Elementor design is linked (lets users switch back). | |
| 839 | + */ | |
| 840 | + public function suppress_themes_radio( $fields ) { | |
| 841 | + if ( ! isset( $fields['themes']['fields']['themes']['rules'] ) ) { | |
| 842 | + return $fields; | |
| 843 | + } | |
| 844 | + $existing = $fields['themes']['fields']['themes']['rules']; | |
| 845 | + // Additional clause: either we're NOT on Exit Intent (so this rule | |
| 846 | + // shouldn't change anything for other sources), OR elementor_id is | |
| 847 | + // not yet set. Combined via AND with the existing tab-gate so other | |
| 848 | + // sources (e.g. PressBar) keep their tab-based hiding intact. | |
| 849 | + $additional = Rules::logicalRule( [ | |
| 850 | + Rules::is( 'source', $this->id, true ), | |
| 851 | + Rules::isOfType( 'elementor_id', 'number', true ), | |
| 852 | + ], 'or' ); | |
| 853 | + $fields['themes']['fields']['themes']['rules'] = Rules::logicalRule( [ | |
| 854 | + $existing, | |
| 855 | + $additional, | |
| 856 | + ], 'and' ); | |
| 857 | + return $fields; | |
| 858 | + } | |
| 859 | + | |
| 860 | + /** | |
| 861 | + * Restructures the Themes section into two tabs: | |
| 862 | + * 1. Default — original 7 React themes grid (the `themes` radio-card, | |
| 863 | + * already gated to themes_tab='for_desktop' in GlobalFields). | |
| 864 | + * 2. Custom — Build With Elementor button + modal, plus Edit/Remove | |
| 865 | + * and Install fallback after a doc is linked. | |
| 866 | + * | |
| 867 | + * Matches Notification Bar's Default/Custom tab structure. | |
| 868 | + */ | |
| 869 | + public function builder_tabs_fields( $fields ) { | |
| 870 | + if ( empty( $fields['themes']['fields']['themes_section']['fields']['themes_tab']['fields'] ) ) { | |
| 871 | + return $fields; | |
| 872 | + } | |
| 873 | + $tab =& $fields['themes']['fields']['themes_section']['fields']['themes_tab']['fields']; | |
| 874 | + | |
| 875 | + // Hide the global "For Desktop" / "For Mobile" tabs for Exit Intent only — we | |
| 876 | + // ship our own Default + Custom tabs below. | |
| 877 | + foreach ( [ 'for_desktop', 'for_mobile' ] as $key ) { | |
| 878 | + if ( isset( $tab[ $key ] ) ) { | |
| 879 | + $tab[ $key ] = Rules::is( 'source', $this->id, true, $tab[ $key ] ); | |
| 880 | + } | |
| 881 | + } | |
| 882 | + | |
| 883 | + // Expand the global themes radio-card so it ALSO renders when our new | |
| 884 | + // Default tab is active. Without this, the radio would never appear for | |
| 885 | + // Exit Intent — its existing rule is hard-coded to themes_tab='for_desktop'. | |
| 886 | + if ( isset( $fields['themes']['fields']['themes'] ) ) { | |
| 887 | + $fields['themes']['fields']['themes']['rules'] = Rules::logicalRule( [ | |
| 888 | + Rules::is( 'themes_tab', 'for_desktop' ), | |
| 889 | + Rules::is( 'themes_tab', 'exit_intent_default_tab' ), | |
| 890 | + ], 'or' ); | |
| 891 | + } | |
| 892 | + | |
| 893 | + $is_installed = Helper::is_plugin_installed( 'elementor/elementor.php' ); | |
| 894 | + $install_activate_text = $is_installed | |
| 895 | + ? __( 'Activate Elementor', 'notificationx' ) | |
| 896 | + : __( 'Install Elementor', 'notificationx' ); | |
| 897 | + | |
| 898 | + // Default tab — unique name (no collision with global for_desktop). | |
| 899 | + $tab[] = [ | |
| 900 | + 'label' => __( 'Default', 'notificationx' ), | |
| 901 | + 'name' => 'exit_intent_default_tab', | |
| 902 | + 'id' => 'exit_intent_default_tab', | |
| 903 | + 'type' => 'section', | |
| 904 | + 'icon' => NOTIFICATIONX_ADMIN_URL . 'images/icons/nxbar-presets-icon.svg', | |
| 905 | + 'rules' => Rules::is( 'source', $this->id ), | |
| 906 | + ]; | |
| 907 | + | |
| 908 | + // The Custom tab — only renders for Exit Intent campaigns. | |
| 909 | + $tab[] = [ | |
| 910 | + 'label' => __( 'Custom', 'notificationx' ), | |
| 911 | + 'name' => 'exit_intent_custom_tab', | |
| 912 | + 'id' => 'exit_intent_custom_tab', | |
| 913 | + 'type' => 'section', | |
| 914 | + 'icon' => NOTIFICATIONX_ADMIN_URL . 'images/icons/nxbar-custom-tab.svg', | |
| 915 | + 'rules' => Rules::is( 'source', $this->id ), | |
| 916 | + 'fields' => [ | |
| 917 | + | |
| 918 | + // Preview image / empty-state illustration (reuses NxBarPresets). | |
| 919 | + 'exit_intent_custom_preview' => [ | |
| 920 | + 'name' => 'exit_intent_custom_preview', | |
| 921 | + 'type' => 'exit-intent-custom', | |
| 922 | + 'label' => __( 'Exit Intent', 'notificationx' ), | |
| 923 | + 'priority' => 5, | |
| 924 | + ], | |
| 925 | + | |
| 926 | + // Edit With Elementor — shown when an Elementor doc is linked. | |
| 927 | + 'elementor_edit_link' => [ | |
| 928 | + 'name' => 'elementor_edit_link', | |
| 929 | + 'type' => 'button', | |
| 930 | + 'text' => __( 'Edit With Elementor', 'notificationx' ), | |
| 931 | + 'href' => -1, | |
| 932 | + 'priority' => 10, | |
| 933 | + 'target' => '_blank', | |
| 934 | + 'rules' => Rules::logicalRule( [ | |
| 935 | + Rules::is( 'elementor_edit_link', false, true ), | |
| 936 | + Rules::isOfType( 'elementor_edit_link', 'string' ), | |
| 937 | + Rules::is( 'elementor_id', false, true ), | |
| 938 | + ] ), | |
| 939 | + ], | |
| 940 | + | |
| 941 | + // Remove — deletes the Elementor doc and resets state. | |
| 942 | + 'nx-exit-intent_with_elementor-remove' => [ | |
| 943 | + 'name' => 'nx-exit-intent_with_elementor-remove', | |
| 944 | + 'type' => 'button', | |
| 945 | + 'text' => __( 'Remove', 'notificationx' ), | |
| 946 | + 'priority' => 15, | |
| 947 | + 'rules' => Rules::logicalRule( [ | |
| 948 | + Rules::is( 'elementor_id', false, true ), | |
| 949 | + Rules::is( 'is_elementor', true ), | |
| 950 | + Rules::is( 'source', $this->id ), | |
| 951 | + ] ), | |
| 952 | + 'ajax' => [ | |
| 953 | + 'on' => 'click', | |
| 954 | + 'api' => '/notificationx/v1/exit-intent/elementor/remove', | |
| 955 | + 'data' => [ 'elementor_id' => '@elementor_id' ], | |
| 956 | + 'hideSwal' => true, | |
| 957 | + ], | |
| 958 | + 'trigger' => [ | |
| 959 | + [ | |
| 960 | + 'type' => 'setFieldValue', | |
| 961 | + 'action' => [ | |
| 962 | + 'elementor_id' => false, | |
| 963 | + 'elementor_edit_link' => '', | |
| 964 | + 'elementor_exit_theme' => 'theme-one', | |
| 965 | + 'is_confirmed' => false, | |
| 966 | + 'themes' => $this->id . '_theme-one', | |
| 967 | + ], | |
| 968 | + ], | |
| 969 | + ], | |
| 970 | + ], | |
| 971 | + | |
| 972 | + // Build With Elementor — modal with seed-theme picker. | |
| 973 | + 'nx-exit-intent_with_elementor' => [ | |
| 974 | + 'name' => 'nx-exit-intent_with_elementor', | |
| 975 | + 'type' => 'modal', | |
| 976 | + 'priority' => 20, | |
| 977 | + 'button' => [ | |
| 978 | + 'name' => 'build_with_elementor', | |
| 979 | + 'text' => __( 'Build With Elementor', 'notificationx' ), | |
| 980 | + 'trigger' => [ | |
| 981 | + [ | |
| 982 | + 'type' => 'setFieldValue', | |
| 983 | + 'action' => [ 'import_elementor_theme' => false ], | |
| 984 | + ], | |
| 985 | + ], | |
| 986 | + ], | |
| 987 | + 'confirm_button' => [ | |
| 988 | + 'type' => 'button', | |
| 989 | + 'name' => 'import_elementor_theme', | |
| 990 | + 'group' => true, | |
| 991 | + 'fields' => [ | |
| 992 | + [ | |
| 993 | + 'type' => 'button', | |
| 994 | + 'name' => 'import_elementor_theme', | |
| 995 | + 'default' => false, | |
| 996 | + 'text' => [ | |
| 997 | + 'normal' => __( 'Import', 'notificationx' ), | |
| 998 | + 'saved' => __( 'Import', 'notificationx' ), | |
| 999 | + 'loading' => __( 'Importing...', 'notificationx' ), | |
| 1000 | + ], | |
| 1001 | + 'ajax' => [ | |
| 1002 | + 'on' => 'click', | |
| 1003 | + 'api' => '/notificationx/v1/exit-intent/elementor/import', | |
| 1004 | + 'data' => [ 'theme_id' => '@elementor_exit_theme' ], | |
| 1005 | + 'trigger' => '@is_confirmed:true', | |
| 1006 | + 'hideSwal' => true, | |
| 1007 | + ], | |
| 1008 | + 'rules' => Rules::is( 'is_confirmed', true, true ), | |
| 1009 | + ], | |
| 1010 | + [ | |
| 1011 | + 'type' => 'button', | |
| 1012 | + 'name' => 'import_elementor_theme_next', | |
| 1013 | + 'default' => false, | |
| 1014 | + 'text' => __( 'Next', 'notificationx' ), | |
| 1015 | + 'rules' => Rules::is( 'is_confirmed', true ), | |
| 1016 | + 'trigger' => [ | |
| 1017 | + [ | |
| 1018 | + 'type' => 'setContext', | |
| 1019 | + 'action' => [ 'config.active' => 'display_tab' ], | |
| 1020 | + ], | |
| 1021 | + [ | |
| 1022 | + 'type' => 'setFieldValue', | |
| 1023 | + 'action' => [ 'import_elementor_theme_next' => true ], | |
| 1024 | + ], | |
| 1025 | + ], | |
| 1026 | + ], | |
| 1027 | + ], | |
| 1028 | + ], | |
| 1029 | + 'cancel' => 'import_elementor_theme_next', | |
| 1030 | + 'body' => [ | |
| 1031 | + 'header' => __( 'Choose Your ', 'notificationx' ), | |
| 1032 | + 'fields' => [ | |
| 1033 | + 'themes' => [ | |
| 1034 | + 'type' => 'radio-card', | |
| 1035 | + 'name' => 'elementor_exit_theme', | |
| 1036 | + 'style' => [ 'label' => [ 'position' => 'top' ] ], | |
| 1037 | + 'rules' => Rules::is( 'is_confirmed', true, true ), | |
| 1038 | + 'default' => 'theme-one', | |
| 1039 | + 'options' => $this->elementor_themes, | |
| 1040 | + ], | |
| 1041 | + ], | |
| 1042 | + ], | |
| 1043 | + 'rules' => Rules::logicalRule( [ | |
| 1044 | + Rules::is( 'elementor_id', false ), | |
| 1045 | + Rules::is( 'is_elementor', true ), | |
| 1046 | + Rules::is( 'is_confirmed', true, true ), | |
| 1047 | + Rules::is( 'source', $this->id ), | |
| 1048 | + ] ), | |
| 1049 | + ], | |
| 1050 | + | |
| 1051 | + // Install / Activate Elementor — fallback when Elementor isn't active. | |
| 1052 | + 'nx-exit-intent_with_elementor_install' => [ | |
| 1053 | + 'name' => 'nx-exit-intent_with_elementor_install', | |
| 1054 | + 'type' => 'button', | |
| 1055 | + 'priority' => 25, | |
| 1056 | + 'text' => [ | |
| 1057 | + 'normal' => $install_activate_text, | |
| 1058 | + 'saved' => $is_installed ? __( 'Activated Elementor', 'notificationx' ) : __( 'Installed Elementor', 'notificationx' ), | |
| 1059 | + 'loading' => $is_installed ? __( 'Activating Elementor...', 'notificationx' ) : __( 'Installing Elementor...', 'notificationx' ), | |
| 1060 | + ], | |
| 1061 | + 'rules' => Rules::logicalRule( [ | |
| 1062 | + Rules::is( 'is_elementor', false ), | |
| 1063 | + Rules::is( 'source', $this->id ), | |
| 1064 | + ] ), | |
| 1065 | + 'ajax' => [ | |
| 1066 | + 'on' => 'click', | |
| 1067 | + 'api' => '/notificationx/v1/core-install', | |
| 1068 | + 'data' => [ | |
| 1069 | + 'source' => $this->id, | |
| 1070 | + 'slug' => 'elementor', | |
| 1071 | + 'file' => 'elementor.php', | |
| 1072 | + 'is_installed' => $is_installed, | |
| 1073 | + ], | |
| 1074 | + 'swal' => [ | |
| 1075 | + 'icon' => 'success', | |
| 1076 | + 'text' => __( 'Successfully Activated', 'notificationx' ), | |
| 1077 | + ], | |
| 1078 | + 'trigger' => '@is_elementor:true', | |
| 1079 | + ], | |
| 1080 | + ], | |
| 1081 | + | |
| 1082 | + // Hidden state fields (Task 05). | |
| 1083 | + 'is_elementor' => [ | |
| 1084 | + 'name' => 'is_elementor', | |
| 1085 | + 'type' => 'hidden', | |
| 1086 | + 'default' => class_exists( '\\Elementor\\Plugin' ), | |
| 1087 | + 'rules' => Rules::is( 'source', $this->id ), | |
| 1088 | + ], | |
| 1089 | + 'elementor_id' => [ | |
| 1090 | + 'name' => 'elementor_id', | |
| 1091 | + 'type' => 'hidden', | |
| 1092 | + 'default' => false, | |
| 1093 | + 'rules' => Rules::is( 'source', $this->id ), | |
| 1094 | + ], | |
| 1095 | + 'is_confirmed' => [ | |
| 1096 | + 'name' => 'is_confirmed', | |
| 1097 | + 'type' => 'hidden', | |
| 1098 | + 'default' => false, | |
| 1099 | + ], | |
| 1100 | + ], | |
| 1101 | + ]; | |
| 1102 | + | |
| 1103 | + // Build With AI tab — Pro feature. Free plugin only registers the tab | |
| 1104 | + // shell + field type; the Pro plugin supplies the actual builder via | |
| 1105 | + // the `nx_build_ai_render` filter (see PressBar's `nxbar_build_with_ai` | |
| 1106 | + // for the same pattern already shipped for Notification Bar). | |
| 1107 | + $tab[] = [ | |
| 1108 | + 'label' => __( 'Build With AI', 'notificationx' ), | |
| 1109 | + 'name' => 'exit_intent_ai_tab', | |
| 1110 | + 'id' => 'exit_intent_ai_tab', | |
| 1111 | + 'type' => 'section', | |
| 1112 | + 'icon' => NOTIFICATIONX_ADMIN_URL . 'images/icons/build-with-ai-icon.svg', | |
| 1113 | + 'rules' => Rules::is( 'source', $this->id ), | |
| 1114 | + 'fields' => [ | |
| 1115 | + 'exit_intent_ai_tab' => [ | |
| 1116 | + 'label' => __( 'Build With AI', 'notificationx' ), | |
| 1117 | + 'name' => 'exit_intent_ai_tab', | |
| 1118 | + 'id' => 'exit_intent_ai_tab', | |
| 1119 | + 'type' => 'section', | |
| 1120 | + 'icon' => NOTIFICATIONX_ADMIN_URL . 'images/responsive/desktop.svg', | |
| 1121 | + 'fields' => [ | |
| 1122 | + 'exit_intent_ai_tab_fields' => [ | |
| 1123 | + 'name' => 'exit_intent_ai_tab_fields', | |
| 1124 | + 'type' => 'exit-intent-build_with_ai', | |
| 1125 | + 'label' => __( 'Exit Intent', 'notificationx' ), | |
| 1126 | + 'priority' => 10, | |
| 1127 | + ], | |
| 1128 | + ], | |
| 1129 | + ], | |
| 1130 | + ], | |
| 1131 | + ]; | |
| 1132 | + | |
| 1133 | + return $fields; | |
| 1134 | + } | |
| 1135 | + | |
| 157 | 1136 | public function display_fields( $fields ) { |
| 158 | 1137 | if ( isset( $fields['image-section'] ) ) { |
| 159 | 1138 | $fields['image-section'] = Rules::is( 'source', $this->id, true, $fields['image-section'] ); |
| 160 | 1139 | } |
| @@ -769,8 +1748,9 @@ | ||
| 769 | 1748 | 'name' => 'exit_intent_name_label', |
| 770 | 1749 | 'type' => 'text', |
| 771 | 1750 | 'default' => __( 'Name *', 'notificationx' ), |
| 772 | 1751 | 'priority' => 75, |
| 1752 | + 'is_pro' => true, | |
| 773 | 1753 | 'rules' => Rules::is( 'exit_intent_show_name', true ), |
| 774 | 1754 | ], |
| 775 | 1755 | [ |
| 776 | 1756 | 'label' => __( 'Show Email Field', 'notificationx' ), |
| @@ -785,8 +1765,9 @@ | ||
| 785 | 1765 | 'name' => 'exit_intent_email_label', |
| 786 | 1766 | 'type' => 'text', |
| 787 | 1767 | 'default' => __( 'Enter Your Email *', 'notificationx' ), |
| 788 | 1768 | 'priority' => 85, |
| 1769 | + 'is_pro' => true, | |
| 789 | 1770 | 'rules' => Rules::is( 'exit_intent_show_email', true ), |
| 790 | 1771 | ], |
| 791 | 1772 | [ |
| 792 | 1773 | 'label' => __( 'Show Message Field', 'notificationx' ), |
| @@ -1227,11 +2208,17 @@ | ||
| 1227 | 2208 | 'rules' => Rules::is( 'source', $this->id ), |
| 1228 | 2209 | ]; |
| 1229 | 2210 | } |
| 1230 | 2211 | |
| 1231 | - $fields['exit_intent_settings'] = [ | |
| 2212 | + // NOTE: keep this key unique. The Announcement extension (PopupNotification) | |
| 2213 | + // also registers a section literally named `exit_intent_settings` for its | |
| 2214 | + // "Convert to Exit Intent" feature; sharing the key here let Popup's filter | |
| 2215 | + // (same `nx_customize_fields` priority, registered later) overwrite this | |
| 2216 | + // section with a `source == popup_notification` rule, hiding it for the | |
| 2217 | + // Exit Intent type and leaving the whole Customize tab blank. | |
| 2218 | + $fields['exit_intent_popup_settings'] = [ | |
| 1232 | 2219 | 'label' => __( 'Exit Intent Settings', 'notificationx' ), |
| 1233 | - 'name' => 'exit_intent_settings', | |
| 2220 | + 'name' => 'exit_intent_popup_settings', | |
| 1234 | 2221 | 'type' => 'section', |
| 1235 | 2222 | 'priority' => 15, |
| 1236 | 2223 | 'rules' => Rules::is( 'source', $this->id ), |
| 1237 | 2224 | 'fields' => [ |
| @@ -1285,8 +2272,9 @@ | ||
| 1285 | 2272 | return $fields; |
| 1286 | 2273 | } |
| 1287 | 2274 | |
| 1288 | 2275 | public function doc() { |
| 2276 | + /* translators: %1$s: documentation link URL */ | |
| 1289 | 2277 | return sprintf(__(' |
| 1290 | 2278 | <p>Show a targeted message at the exact moment someone is about to close your tab & bring them back into the funnel. Need help? Check out our <a target="_blank" href="%1$s">documentation</a>.</p>', |
| 1291 | 2279 | 'notificationx'), |
| 1292 | 2280 | 'https://notificationx.com/docs/how-to-configure-exit-intent-popup/', |