Created
February 14, 2026 20:04
-
-
Save theronic/3308727d3acb7129e95cb120fdbc086f to your computer and use it in GitHub Desktop.
Save Greenhouse Form Inputs
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
| // 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