Created
May 13, 2026 21:09
-
-
Save spivurno/d3ddf0745ff6d9673d9eb8d129fa42da to your computer and use it in GitHub Desktop.
Gravity Wiz Type Into Plugin
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Plugin Name: Gravity Wiz Type Into | |
| * Plugin URI: https://gravitywiz.com/ | |
| * Description: Registers window.gwTypeInto — a utility that simulates human-like typing into any input, textarea, or contenteditable element (including iframes). | |
| * Author: Gravity Wiz | |
| * Version: 1.0 | |
| * Author URI: https://gravitywiz.com | |
| * | |
| * Usage (browser console or inline script): | |
| * | |
| * // Basic | |
| * await gwTypeInto( 'input_1_3', 'Hello, world!' ); | |
| * | |
| * // Inside an iframe | |
| * await gwTypeInto( 'input_1_3', 'Hello!', { iframe: 'gform_preview_iframe' } ); | |
| * | |
| * // Fixed total duration (ignores baseDelay / jitter) | |
| * await gwTypeInto( 'input_1_3', 'Fast text', { totalTime: 2000 } ); | |
| */ | |
| // Registers the footer script only on pages where a GF form is actually rendered. | |
| // gform_enqueue_scripts fires once per form; WordPress deduplicates named callbacks so | |
| // gw_type_into_script is only added to wp_footer once even if multiple forms are present. | |
| add_action( 'gform_enqueue_scripts', function() { | |
| add_action( 'wp_footer', 'gw_type_into_script' ); | |
| } ); | |
| // The built-in GF preview page doesn't go through wp_footer. | |
| add_action( 'gform_preview_footer', 'gw_type_into_script' ); | |
| function gw_type_into_script() { | |
| ?> | |
| <script> | |
| /** | |
| * Simulate human-like typing into an input or textarea. | |
| * | |
| * @param {string} selector - Element ID, or a CSS selector (e.g. 'body') as a fallback. | |
| * @param {string} content - The text to type, supports \n for line breaks. | |
| * @param {object} [options] - Optional overrides. | |
| * @param {number} [options.baseDelay=55] - Base ms between keystrokes. | |
| * @param {number} [options.jitter=70] - Max random jitter added to base. | |
| * @param {number} [options.totalTime] - Total ms for the entire string. Overrides baseDelay/jitter. | |
| * @param {string} [options.iframe] - ID of an iframe to search inside for the target element. | |
| */ | |
| window.gwTypeInto = async function( selector, content, options ) { | |
| var opts = options || {}; | |
| var doc = document; | |
| if ( opts.iframe ) { | |
| var frame = document.getElementById( opts.iframe ); | |
| if ( ! frame || ! frame.contentDocument ) { | |
| console.error( 'gwTypeInto: iframe not found or not accessible with ID "' + opts.iframe + '"' ); | |
| return; | |
| } | |
| doc = frame.contentDocument; | |
| } | |
| // Try by ID first, then fall back to querySelector. | |
| var el = doc.getElementById( selector ) || doc.querySelector( selector ); | |
| if ( ! el ) { | |
| console.error( 'gwTypeInto: no element found for "' + selector + '"' ); | |
| return; | |
| } | |
| // Use the target element's own window for event constructors so | |
| // listeners inside an iframe receive the correct event prototypes. | |
| var win = doc.defaultView || window; | |
| var isContentEditable = el.isContentEditable; | |
| // Detect a TinyMCE editor (iframe ID convention: editorId + '_ifr'). | |
| var mceEditor = null; | |
| if ( opts.iframe && isContentEditable && typeof tinymce !== 'undefined' ) { | |
| var editorId = opts.iframe.replace( /_ifr$/, '' ); | |
| mceEditor = tinymce.get( editorId ) || null; | |
| } | |
| var chars = Array.from( content ); | |
| // Default average delay per char is ~90 ms (55 base + 70 jitter / 2). | |
| var defaultAvg = 90; | |
| var scale = 1; | |
| if ( opts.totalTime && chars.length ) { | |
| scale = ( opts.totalTime / chars.length ) / defaultAvg; | |
| } | |
| var baseDelay = ( opts.baseDelay || 55 ) * scale; | |
| var jitter = ( opts.jitter || 70 ) * scale; | |
| // If inside an iframe, focus the iframe element first. | |
| if ( opts.iframe ) { | |
| document.getElementById( opts.iframe ).focus(); | |
| } | |
| el.focus(); | |
| el.dispatchEvent( new win.Event( 'focus', { bubbles: true } ) ); | |
| for ( var i = 0; i < chars.length; i++ ) { | |
| var ch = chars[i]; | |
| var isNewline = ch === '\n'; | |
| // For contenteditable, collapse consecutive newlines into one paragraph break. | |
| if ( isContentEditable && isNewline && i > 0 && chars[ i - 1 ] === '\n' ) { | |
| continue; | |
| } | |
| /* --- natural delay ------------------------------------------------ */ | |
| var delay = baseDelay + Math.random() * jitter; | |
| // Longer pause after punctuation. | |
| if ( i > 0 && /[.,!?;:]/.test( chars[ i - 1 ] ) ) { | |
| delay += ( 40 + Math.random() * 90 ) * scale; | |
| } | |
| // Small pause after a space (word boundary). | |
| if ( i > 0 && chars[ i - 1 ] === ' ' ) { | |
| delay += ( 10 + Math.random() * 25 ) * scale; | |
| } | |
| // Occasional micro-hesitation (~6 % chance). | |
| if ( Math.random() < 0.06 ) { | |
| delay += ( 80 + Math.random() * 120 ) * scale; | |
| } | |
| await new Promise( function( r ) { setTimeout( r, delay ); } ); | |
| /* --- dispatch key events ----------------------------------------- */ | |
| if ( isContentEditable ) { | |
| if ( isNewline ) { | |
| doc.execCommand( 'insertParagraph', false, null ); | |
| } else { | |
| var keyProps = { key: ch, bubbles: true }; | |
| el.dispatchEvent( new win.KeyboardEvent( 'keydown', keyProps ) ); | |
| doc.execCommand( 'insertText', false, ch ); | |
| el.dispatchEvent( new win.KeyboardEvent( 'keyup', keyProps ) ); | |
| } | |
| // Keep TinyMCE's wpAutoResize in sync and scroll the caret into view. | |
| if ( mceEditor ) { | |
| try { | |
| mceEditor.execCommand( 'wpAutoResize' ); | |
| var sel = doc.getSelection(); | |
| if ( sel && sel.focusNode ) { | |
| var caret = sel.focusNode.nodeType === 3 ? sel.focusNode.parentNode : sel.focusNode; | |
| caret.scrollIntoView( { behavior: 'instant', block: 'nearest' } ); | |
| } | |
| } catch( e ) {} | |
| } | |
| } else if ( isNewline ) { | |
| if ( el.tagName === 'TEXTAREA' ) { | |
| el.value += '\n'; | |
| } | |
| var enterProps = { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true }; | |
| el.dispatchEvent( new win.KeyboardEvent( 'keydown', enterProps ) ); | |
| el.dispatchEvent( new win.KeyboardEvent( 'keypress', enterProps ) ); | |
| el.dispatchEvent( new win.Event( 'input', { bubbles: true } ) ); | |
| el.dispatchEvent( new win.KeyboardEvent( 'keyup', enterProps ) ); | |
| } else { | |
| el.value += ch; | |
| var keyProps = { key: ch, bubbles: true }; | |
| el.dispatchEvent( new win.KeyboardEvent( 'keydown', keyProps ) ); | |
| el.dispatchEvent( new win.KeyboardEvent( 'keypress', keyProps ) ); | |
| el.dispatchEvent( new win.Event( 'input', { bubbles: true } ) ); | |
| el.dispatchEvent( new win.KeyboardEvent( 'keyup', keyProps ) ); | |
| } | |
| } | |
| // Final change + blur so Gravity Forms (and other listeners) pick up the value. | |
| el.dispatchEvent( new win.Event( 'change', { bubbles: true } ) ); | |
| el.dispatchEvent( new win.Event( 'blur', { bubbles: true } ) ); | |
| }; | |
| </script> | |
| <?php | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment