*
*

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) What happens * * - 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 → pixels (e.g., 700 → 700px) CSS * length value → 70vw, 90%, 40rem, 32em, 720px Default: 700px Note: calc() not supported. * - **id**: unique identifier used to scope dismissal memory. Letters, numbers, underscore, hyphen * Default: auto‑generated Reusing id shares dismissal memory. * * # Inner blocks: each
controls one popup * * - **open_on** (required): when to open the popup. "exit" or "exit-intent" "idle:N" → N seconds * - inactivity (decimals allowed) "scroll:X%" * - X percent down page "scroll:Ypx" * - Y pixels down page * - **position**: top, center (default), bottom * - **dismiss**: auto‑close timer "10s", "2.5s", "5000ms", "10000" 0 disables * - **dismiss_for**: how long the popup stays dismissed "10s", "2.5s", "30m", "1h", "5000ms", bare * ms 0 disables Default: 30m * * # Full Attribute Reference (Quick Lookup) * * # [gosmartmodal] wrapper attributes * * - **width**: integer px or CSS length. Default: 700. * - **id**: unique slug used for dismissal memory. * * # Inner
attributes * * - **open_on**: exit, exit-intent, idle:N, scroll:X% or Ypx * - **position**: top | center | bottom * - **dismiss**: seconds or milliseconds * - **dismiss_for**: memory window; default 30m * * USAGE: * # Exit intent with bottom position * *
...
* * # Idle after 2.5s, auto‑close after 6s * *
...
* * # Idle after 8s, ms-based dismiss * *
...
* * # Scroll 60% * *
...
* * # Scroll 800px * *
...
* * # Behavior notes * * - **Dismissal memory**: a popup won’t open again during its dismiss_for window. * - **Accessibility**: role="dialog", aria-modal="true", ESC to close, focus sent to close button. * - **.gosmartmodalbutton** closes popup first, then navigates; hash links smooth‑scroll. * * # Styling and classes * * - `.gosmartmodalcoupon` — bold coupon code helper * - `.gosmartmodalbutton` — closes popup before navigating * - Prefixed classes avoid theme conflicts * * # Accessibility & Behavior * * - Popups include ARIA roles. * - Focus moves to the close button. * - Clicking outside closes popup. * * # Helpful tips * * - Exit intent: move cursor toward top browser chrome. * - To re-trigger a dismissed popup: wait 30m or clear site data. * - Anchor links like #pricing smooth‑scroll after closing. * - For Gravity Forms inside popup: forms activate once visible. * * # What this does not do * * - Cannot override browser Back/Close behaviors (security rule). * - Exit popup won’t fire on mobile; use idle trigger. * * # Need to customize? * * - Timings, positions, and small design tweaks can be adjusted. * - Any HTML allowed inside popup (images, headings, Gravity Forms). * * FEATURES: * - Three triggers you can use together or separately: open_on="exit" → show on exit intent * - (desktop browsers) open_on="idle:3" * - show after 3 seconds of inactivity open_on="scroll:50%" * - show after the visitor has scrolled 50% of the page * - Choose where the popup sits: position="top", "center" (default), or "bottom". * - Optional auto‑close timer: dismiss="10s" closes it after 10 seconds. * - A ready‑made Close button (×), backdrop click to close, and ESC key support. * - Any link inside the popup with class "gosmartmodalbutton" will close the popup first, then * continue to that link. If it’s a #section link, the page will smooth‑scroll there. * - Friendly with Gravity Forms: you can place a [gravityform] shortcode inside the popup. * - Respectful behavior: if a visitor closed a popup, it won’t pop again for its dismiss_for * duration (30 minutes by default). */ // 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 ); } }