Skip to content

Instantly share code, notes, and snippets.

@theronic
Created February 14, 2026 20:04
Show Gist options
  • Select an option

  • Save theronic/3308727d3acb7129e95cb120fdbc086f to your computer and use it in GitHub Desktop.

Select an option

Save theronic/3308727d3acb7129e95cb120fdbc086f to your computer and use it in GitHub Desktop.
Save Greenhouse Form Inputs
// Greenhouse Job Application Form Saver
// Saves and restores form data on job-boards.greenhouse.io across page reloads.
// Handles React-controlled inputs, textareas, and React-Select dropdowns.
//
// Usage:
// 1. Fill out the form as usual
// 2. Open browser console (Cmd+Option+J) and paste this entire script
// 3. Run: GH.save() — copies form data JSON to clipboard
// 4. Save the JSON somewhere (notes, file, etc.)
// 5. Reboot / reload the page, paste this script again
// 6. Run: GH.restore() — paste JSON when prompted, restores everything
// 7. Run: GH.restoreSelects() — retry dropdowns if they failed
const GH = (() => {
// ── Helpers ──
function getReactFiberKey(el) {
return Object.keys(el).find(k => k.startsWith('__reactFiber$'));
}
function findSelectFiber(el) {
const fk = getReactFiberKey(el);
if (!fk) return null;
let cur = el[fk];
for (let i = 0; i < 30 && cur; i++) {
const p = cur.memoizedProps;
if (p && p.selectOption && p.options) return p;
cur = cur.return;
}
return null;
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
function setFieldValue(el, value) {
const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
el.focus();
el.dispatchEvent(new FocusEvent('focus', { bubbles: true }));
setter.call(el, value);
el.dispatchEvent(new InputEvent('input', { bubbles: true, data: value, inputType: 'insertText' }));
el.dispatchEvent(new Event('change', { bubbles: true }));
}
// ── Save ──
async function save() {
const data = {
_meta: { url: location.href, saved: new Date().toISOString() },
fields: {},
selects: {}
};
// Text inputs and textareas
document.querySelectorAll('input[id], textarea[id]').forEach(el => {
if (el.type === 'hidden' || el.type === 'file' || el.type === 'search') return;
if (el.classList.contains('select__input')) return;
if (el.id.startsWith('iti-') || el.id.startsWith('g-recaptcha')) return;
if (el.value) data.fields[el.id] = el.value;
});
// React-Select dropdowns (needs React to be hydrated)
document.querySelectorAll('.select').forEach(sel => {
const input = sel.querySelector('input.select__input');
if (!input || !input.id) return;
const fiber = findSelectFiber(input);
if (fiber) {
const val = fiber.getValue();
if (val && val.length > 0) {
data.selects[input.id] = { label: val[0].label, value: val[0].value };
}
}
});
const json = JSON.stringify(data, null, 2);
// Open a new tab with the JSON so user can easily copy/save it
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
window.open(url, '_blank');
const fc = Object.keys(data.fields).length;
const sc = Object.keys(data.selects).length;
console.log(`%c✓ Saved ${fc} fields + ${sc} dropdowns — opened in new tab!`, 'color: green; font-weight: bold');
if (sc === 0) console.log(' Note: Dropdowns only save when React is hydrated (interact with the page first).');
console.log('Save that JSON somewhere, then use GH.restore(`json`) after reload.');
return data;
}
// ── Restore ──
let _lastData = null;
async function restore(jsonOrData) {
let data;
if (typeof jsonOrData === 'string') {
data = JSON.parse(jsonOrData);
} else if (jsonOrData && typeof jsonOrData === 'object') {
data = jsonOrData;
} else {
console.log('%c✗ Pass the saved JSON: GH.restore(`paste json here`)', 'color: red');
return;
}
if (!data || !data.fields) {
console.log('%c✗ Invalid data — expected {fields: {...}, selects: {...}}', 'color: red');
return;
}
_lastData = data;
console.log(`Restoring from save made at ${data._meta?.saved || 'unknown time'}...`);
// Phase 1: Restore text fields immediately (works with or without React)
let fieldOk = 0;
for (const [id, value] of Object.entries(data.fields)) {
const el = document.getElementById(id);
if (!el) continue;
setFieldValue(el, value);
fieldOk++;
await sleep(30);
}
document.activeElement?.blur();
console.log(`%c✓ Restored ${fieldOk} text fields`, 'color: green; font-weight: bold');
// Phase 2: Restore dropdowns (needs React hydration)
const selectKeys = Object.keys(data.selects || {});
if (selectKeys.length === 0) return;
console.log(`Waiting for React to hydrate (for ${selectKeys.length} dropdowns)...`);
console.log(' Tip: click on the form if this takes more than ~10s.');
const hydrated = await waitForHydration(30000);
if (!hydrated) {
console.log('%c⚠ React did not hydrate — dropdowns not restored.', 'color: orange; font-weight: bold');
console.log(' Click anywhere on the form, then run: GH.restoreSelects()');
return;
}
await restoreSelectsInner(data);
}
async function restoreSelects() {
if (!_lastData) {
console.log('%c✗ No data loaded. Run GH.restore() first.', 'color: red');
return;
}
const el = document.getElementById('first_name');
if (!el || !getReactFiberKey(el)) {
console.log('%c✗ React not hydrated yet. Click on the form first.', 'color: red');
return;
}
await restoreSelectsInner(_lastData);
// Re-apply text fields too (hydration may have wiped them)
for (const [id, value] of Object.entries(_lastData.fields || {})) {
const field = document.getElementById(id);
if (field && field.value !== value) {
setFieldValue(field, value);
await sleep(30);
}
}
document.activeElement?.blur();
}
async function restoreSelectsInner(data) {
let ok = 0, fail = 0;
for (const [id, sel] of Object.entries(data.selects || {})) {
const el = document.getElementById(id);
if (!el) { fail++; continue; }
const fiber = findSelectFiber(el);
if (!fiber) { fail++; console.log(` ⚠ Dropdown ${id}: fiber not found`); continue; }
const option = fiber.options.find(o => o.value === sel.value)
|| fiber.options.find(o => o.label === sel.label);
if (!option) { fail++; console.log(` ⚠ Dropdown ${id}: option "${sel.label}" not found`); continue; }
fiber.selectOption(option);
ok++;
}
console.log(`%c✓ Restored ${ok} dropdowns` + (fail ? ` (${fail} failed)` : ''),
fail ? 'color: orange; font-weight: bold' : 'color: green; font-weight: bold');
}
async function waitForHydration(maxWait) {
const start = Date.now();
document.getElementById('first_name')?.scrollIntoView({ block: 'center' });
while (Date.now() - start < maxWait) {
const el = document.getElementById('first_name');
if (el && getReactFiberKey(el)) return true;
await sleep(500);
}
return false;
}
// ── Init ──
console.log('%cGreenhouse Form Saver loaded', 'color: #6B46C1; font-weight: bold');
console.log(' GH.save() — capture form data → clipboard');
console.log(' GH.restore() — restore from clipboard (or pass JSON string)');
console.log(' GH.restoreSelects() — retry dropdowns after clicking on the form');
return { save, restore, restoreSelects };
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment