Skip to content

Instantly share code, notes, and snippets.

@spivurno
Created May 13, 2026 21:09
Show Gist options
  • Select an option

  • Save spivurno/d3ddf0745ff6d9673d9eb8d129fa42da to your computer and use it in GitHub Desktop.

Select an option

Save spivurno/d3ddf0745ff6d9673d9eb8d129fa42da to your computer and use it in GitHub Desktop.
Gravity Wiz Type Into Plugin
<?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