*
*

Here's a gift

*

Before you go, enjoy 10% off with code GRAVITYOPS10.

*

View Plans

*
* *
*

Have a question? Ask us anything!

* [gravityform id="12" title="false"] *
* [/gosmartmodal] * * 2) That’s it. The first popup appears on exit intent. The second appears after 3 seconds * of inactivity. Clicking “View Plans” closes the popup and scrolls to the #pricing section. * * What each part means * - Shortcode wrapper: [gosmartmodal width="700" id="welcome-offer"] ... [/gosmartmodal] * • width: limits the popup’s max width. * - Number only → treated as pixels (e.g., 700 → 700px) * - CSS length value → e.g., 70vw, 90%, 40rem, 32em, 720px * - Default: 700 (px) * - Note: complex CSS functions like calc() are not supported here. * • id: unique identifier used to scope dismissal memory per popup instance. * - Allowed characters: letters, numbers, underscore, hyphen * - Default: auto‑generated unique id * - Reusing the same id across pages shares the dismissal memory. * * - Inner blocks: each
inside controls one popup instance with its own settings. * • open_on (required): when to open this popup. * - "exit" or "exit-intent" → show on exit intent (desktop only) * - "idle:N" → show after N seconds of no mouse/keyboard/scroll activity * • N can be a whole number or decimal (e.g., idle:3, idle:2.5) * - "scroll:X%" → show after visitor has scrolled X percent down the page * - "scroll:Ypx" → show after visitor has scrolled Y pixels down the page * • position: where the popup sits on screen. * - "top", "center" (default), or "bottom" * • dismiss: auto‑close timer for this popup. * - Seconds: "10s", "2.5s" * - Milliseconds with unit: "5000ms" * - Bare milliseconds: "10000" * - Omit or set to 0/empty to disable auto‑close * • dismiss_for: how long this popup stays dismissed after it opens (exit/idle). * - Seconds: "10s", "2.5s" * - Minutes: "30m", "0.5m" * - Hours: "1h", "1.5h" * - Milliseconds with unit: "5000ms" * - Bare milliseconds: "1800000" * - Use 0 to disable dismissal memory for this instance * - Default: "30m" * * Full attribute reference (quick lookup) * - [gosmartmodal] wrapper attributes * • width (string|number): max content width. Accepts integer (px) or a CSS length keyword like 70vw, 90%, 40rem, 32em, 720px. Default: 700. * • id (string): unique slug used to scope dismissal memory per popup. Allowed: A–Z, a–z, 0–9, _, -. Default: generated. * - Inner
attributes (one overlay per
) * • open_on (string, required): "exit", "exit-intent", "idle:N" (N seconds, decimals allowed), or * "scroll:X%|Ypx" (X percent of page, or Y pixels from top). * • position (string): "top" | "center" | "bottom". Default: "center". * • dismiss (string|number): "Xs" (seconds), "Yms" (milliseconds), or bare number in ms. Default: no auto‑close. * • dismiss_for (string|number): memory window after opening via exit/idle. Formats: "Xs", "Xm", "Xh", "Yms", or bare ms. 0 disables. Default: 30m. * * Examples * - Exit intent with bottom position and no auto‑close: *
* ... *
* - Idle after 2.5 seconds, auto‑close after 6 seconds, at the top: *
...
* - Idle after 8 seconds with dismiss set via milliseconds: *
...
* - Show after scrolling 60% of the page: *
...
* - Show after scrolling 800 pixels: *
...
* * Behavior notes * - Dismissal memory: When a popup opens via exit/idle, it won’t open again for the per‑popup * dismiss_for duration (default 30 minutes) for the same [gosmartmodal id] + reason ("exit", * "idle:N", or "scroll:THRESHOLD"). Use a different id to treat it as a separate campaign. * - Accessibility: role="dialog", aria-modal="true", ESC key closes. Focus moves to the * close button on open. Clicking backdrop closes. * - Links with class .gosmartmodalbutton close the popup first, then navigate. #hash links * smooth‑scroll after closing. * * Styling and classes you can reuse * - .gosmartmodalcoupon → just a bold helper for coupon codes * - .gosmartmodalbutton → any link with this class will close the popup before navigating * - The popup layout uses a small set of prefixed classes (gosmartmodal‑*) so it won’t * clash with your theme. You can add your own CSS as needed. * * Accessibility and behavior * - Popups include role="dialog", aria-modal="true", and can be closed with the ESC key. * - Focus moves to the close button when a popup opens. * - Clicking outside the popup (on the dimmed background) also closes it. * * Helpful tips * - Not seeing the exit popup? Try moving the mouse up toward the top of the browser window * (where tabs/address bar live). Exit intent doesn’t run on phones. * - If you closed a popup and want to see it again while testing, wait 30 minutes or * clear local site data for the page (the dismissal memory lives in your browser storage). * - Links to on‑page anchors like #pricing will smooth‑scroll after the popup closes. * - If you closed a popup and want to see it again while testing, either set dismiss_for="0" * on that popup or wait for its dismiss_for period to elapse (30 minutes by default). * - If you place a Gravity Form inside and it looks “frozen” until the popup opens, * that’s expected—scripts wait until the form is visible. Once the popup opens, * the form behaves normally. * * What this does not do * - It won’t block the browser’s Back/Close buttons or show a custom popup at the exact * moment a tab is closing—that’s a browser safety rule. Use the idle trigger for mobile. * * Need to customize? * - We can tweak timings, positions, or add small design changes without changing your content. * - You’re free to include any HTML in the popup content area, including images, headings, * and Gravity Forms shortcodes. */ // Security: don't load directly. if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! function_exists( 'bl_register_gosmartmodal_shortcode' ) ) { /** * Register the [gosmartmodal] shortcode. */ function bl_register_gosmartmodal_shortcode() { add_shortcode( 'gosmartmodal', 'bl_gosmartmodal_shortcode' ); } add_action( 'init', 'bl_register_gosmartmodal_shortcode' ); } if ( ! function_exists( 'bl_gosmartmodal_shortcode' ) ) { /** * Render the [gosmartmodal] shortcode. * * @param array $atts Shortcode attributes (width, id). * @param string|null $content Shortcode inner content containing
sections. * @return string */ function bl_gosmartmodal_shortcode( $atts, $content = null ) { // Defaults. $atts = shortcode_atts( [ 'width' => '700', 'id' => uniqid( 'gosm_' ), ], $atts, 'gosmartmodal' ); $base_id = preg_replace( '/[^a-zA-Z0-9_\-]/', '', (string) $atts['id'] ); $width_in = is_string( $atts['width'] ) ? trim( $atts['width'] ) : '700'; // Normalize width: allow numbers (assume px) or any CSS length keyword. if ( preg_match( '/^\d+(?:\.\d+)?$/', $width_in ) ) { $width_css = $width_in . 'px'; } else { // Very light sanitization of CSS value. $width_css = preg_replace( '/[^\w\.%\-\s]/', '', $width_in ); } // Process nested shortcodes (e.g., [gravityform]) inside content. $content = do_shortcode( (string) $content ); // Extract only sections that declare open_on="..." using DOM (handles nested DIVs). $sections = []; if ( is_string( $content ) && '' !== $content ) { $html = '' . '
' . $content . '
' . ''; $dom = new DOMDocument(); // Suppress warnings for HTML5 tags/self-closing mismatches common in WP content. $prev = libxml_use_internal_errors( true ); $dom->loadHTML( $html ); libxml_clear_errors(); libxml_use_internal_errors( $prev ); $xpath = new DOMXPath( $dom ); $nodes = $xpath->query( '//*[@id="gosm-wrapper"]//div[@open_on and not(ancestor::div[@open_on])]' ); if ( $nodes instanceof DOMNodeList ) { foreach ( $nodes as $node ) { if ( ! ( $node instanceof DOMElement ) ) { continue; } $open_on = trim( (string) $node->getAttribute( 'open_on' ) ); $position = strtolower( trim( (string) $node->getAttribute( 'position' ) ) ); $dismiss = trim( (string) $node->getAttribute( 'dismiss' ) ); $dismiss_for = trim( (string) $node->getAttribute( 'dismiss_for' ) ); // Determine trigger type and optional parameters (idle seconds or scroll threshold). $trigger = 'manual'; $idle_seconds = 0; $scroll_amount = 0; $scroll_unit = '%'; // default to percent if omitted if ( preg_match( '/^idle\s*:\s*(\d+(?:\.\d+)?)$/i', $open_on, $om ) ) { $trigger = 'idle'; $idle_seconds = (float) $om[1]; } elseif ( preg_match( '/^scroll\s*:\s*(\d+(?:\.\d+)?)(%|px)?$/i', $open_on, $sm ) ) { $trigger = 'scroll'; $scroll_amount = (float) $sm[1]; $scroll_unit = isset( $sm[2] ) && in_array( strtolower( $sm[2] ), [ '%', 'px' ], true ) ? strtolower( $sm[2] ) : '%'; } elseif ( strtolower( $open_on ) === 'exit' || strtolower( $open_on ) === 'exit-intent' ) { $trigger = 'exit'; } // Parse dismiss duration into milliseconds (supports `10s` or `10000ms` or plain ms number). $dismiss_ms = 0; if ( '' !== $dismiss ) { if ( preg_match( '/^(\d+(?:\.\d+)?)\s*s$/i', $dismiss, $dm ) ) { $dismiss_ms = (int) floor( (float) $dm[1] * 1000 ); } elseif ( preg_match( '/^(\d+)\s*ms$/i', $dismiss, $dm ) ) { $dismiss_ms = (int) $dm[1]; } elseif ( preg_match( '/^\d+$/', $dismiss ) ) { $dismiss_ms = (int) $dismiss; } } // Parse dismiss_for into milliseconds (controls localStorage memory window per popup). // Default 30 minutes when missing/invalid; 0 disables memory. $dismiss_for_ms = 30 * 60 * 1000; if ( '' !== $dismiss_for ) { if ( '0' === $dismiss_for ) { $dismiss_for_ms = 0; } elseif ( preg_match( '/^(\d+(?:\.\d+)?)\s*s$/i', $dismiss_for, $m ) ) { // Match number followed by 's' (seconds) - e.g. "10s", "2.5s" $dismiss_for_ms = (int) floor( (float) $m[1] * 1000 ); } elseif ( preg_match( '/^(\d+(?:\.\d+)?)\s*m$/i', $dismiss_for, $m ) ) { // Match number followed by 'm' (minutes) - e.g. "30m", "0.5m" $dismiss_for_ms = (int) floor( (float) $m[1] * 60 * 1000 ); } elseif ( preg_match( '/^(\d+(?:\.\d+)?)\s*h$/i', $dismiss_for, $m ) ) { // Match number followed by 'h' (hours) - e.g. "1h", "1.5h" $dismiss_for_ms = (int) floor( (float) $m[1] * 60 * 60 * 1000 ); } elseif ( preg_match( '/^(\d+(?:\.\d+)?)\s*ms$/i', $dismiss_for, $m ) ) { // Match number followed by 'ms' (milliseconds) - e.g. "5000ms" $dismiss_for_ms = (int) floor( (float) $m[1] ); } elseif ( preg_match( '/^\d+$/', $dismiss_for ) ) { // Match bare number (milliseconds) - e.g. "1800000" $dismiss_for_ms = (int) $dismiss_for; } } // Sanitize descendant elements: remove inline event handlers and unsafe protocols in href/src. // Do not strip GOSM_JS; } // Build each modal overlay. $i = 0; foreach ( $sections as $sec ) { ++$i; $overlay_id = $base_id . '-' . $i; $pos_class = 'center'; if ( in_array( $sec['position'], [ 'top', 'center', 'bottom' ], true ) ) { $pos_class = $sec['position']; } $trigger_attr = esc_attr( $sec['trigger'] ); $idle_attr = 'idle' === $sec['trigger'] ? ' data-idle-seconds="' . esc_attr( (string) $sec['idle_seconds'] ) . '"' : ''; $scroll_attr = 'scroll' === $sec['trigger'] ? ' data-scroll-amount="' . esc_attr( (string) ( $sec['scroll_amount'] ?? 0 ) ) . '" data-scroll-unit="' . esc_attr( (string) ( $sec['scroll_unit'] ?? '%' ) ) . '"' : ''; $dismiss_attr = $sec['dismiss_ms'] > 0 ? ' data-dismiss-ms="' . esc_attr( (string) $sec['dismiss_ms'] ) . '"' : ' data-dismiss-ms="0"'; $position_attr = ' data-position="' . esc_attr( $pos_class ) . '"'; $memory_attr = ' data-dismiss-for-ms="' . esc_attr( (string) ( $sec['dismiss_for_ms'] ?? ( 30 * 60 * 1000 ) ) ) . '"'; $container_style = ' style="max-width:' . esc_attr( $width_css ) . '"'; $html = ''; $out[] = $html; } return implode( "\n", $out ); } }