Last active
March 25, 2026 17:24
-
-
Save minanagehsalalma/22178e2b2dc914235b848b357f3b5b1e to your computer and use it in GitHub Desktop.
a bookmarklet that will extract all values from dropdown menus on a page.
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
| javascript:(function(){ | |
| const delay = ms => new Promise(r => setTimeout(r, ms)); | |
| // ── Selectors ────────────────────────────────────────────────────────────── | |
| const triggerSelectors = [ | |
| '[aria-haspopup="listbox"]','[aria-haspopup="true"]','[aria-expanded]', | |
| 'button[class*="select"]','div[class*="select"]', | |
| '[class*="dropdown"] > button','[class*="trigger"]','select', | |
| ]; | |
| const optionSelectors = [ | |
| '[role="option"]','[role="menuitem"]','[role="listbox"] li','[data-value]','[aria-selected]', | |
| ]; | |
| // ── Helpers ──────────────────────────────────────────────────────────────── | |
| function getLabel(el) { | |
| if (el.id) { | |
| const lbl = document.querySelector(`label[for="${el.id}"]`); | |
| if (lbl) return lbl.textContent.trim(); | |
| } | |
| return el.getAttribute('aria-label') || el.getAttribute('placeholder') || | |
| el.getAttribute('name') || | |
| el.querySelector('span,label,p')?.textContent?.trim() || | |
| el.textContent.trim().replace(/\s+/g,' ').slice(0,40) || | |
| `Dropdown #${Math.random().toString(36).slice(2,6)}`; | |
| } | |
| function readNativeOptions(sel) { | |
| return Array.from(sel.options).map(o => ({ text: o.textContent.trim(), value: o.value })); | |
| } | |
| function isPlaceholder(opt) { | |
| return opt.value === '' || opt.value === '-1' || opt.value === '0' || opt.text === '--' || opt.text === '-'; | |
| } | |
| // Trigger a native select change (works with Vue/Angular/React-ish) | |
| function setNativeValue(sel, value) { | |
| const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLSelectElement.prototype, 'value'); | |
| if (nativeSetter && nativeSetter.set) nativeSetter.set.call(sel, value); | |
| else sel.value = value; | |
| sel.dispatchEvent(new Event('input', { bubbles: true })); | |
| sel.dispatchEvent(new Event('change', { bubbles: true })); | |
| } | |
| // Collect all visible triggers once | |
| const allTriggers = []; | |
| const seenEls = new Set(); | |
| triggerSelectors.forEach(sel => { | |
| document.querySelectorAll(sel).forEach(el => { | |
| if (!seenEls.has(el) && el.offsetParent !== null) { seenEls.add(el); allTriggers.push(el); } | |
| }); | |
| }); | |
| const nativeSelects = allTriggers.filter(el => el.tagName === 'SELECT'); | |
| const jsDropdowns = allTriggers.filter(el => el.tagName !== 'SELECT'); | |
| // ── Highlight helpers ────────────────────────────────────────────────────── | |
| const ORIG = new WeakMap(); | |
| function hl(el, color) { | |
| if (!ORIG.has(el)) ORIG.set(el, { outline: el.style.outline, outlineOffset: el.style.outlineOffset, boxShadow: el.style.boxShadow }); | |
| el.style.outline = `2px solid ${color}`; el.style.outlineOffset = '2px'; el.style.boxShadow = `0 0 0 4px ${color}33`; | |
| } | |
| function unhl(el) { | |
| if (ORIG.has(el)) { const s = ORIG.get(el); el.style.outline = s.outline; el.style.outlineOffset = s.outlineOffset; el.style.boxShadow = s.boxShadow; } | |
| } | |
| function refreshHL(selected) { | |
| allTriggers.forEach(el => selected.has(el) ? hl(el, '#22c55e') : unhl(el)); | |
| } | |
| // ── Core export logic ────────────────────────────────────────────────────── | |
| async function exportTargets(targets, statusEl, exportBtn) { | |
| const allFound = {}; | |
| const nativeTargets = targets.filter(el => el.tagName === 'SELECT'); | |
| const jsTargets = targets.filter(el => el.tagName !== 'SELECT'); | |
| // --- Pass 1: snapshot all native selects directly --- | |
| for (const sel of nativeTargets) { | |
| const label = getLabel(sel); | |
| const opts = readNativeOptions(sel); | |
| const realOpts = opts.filter(o => !isPlaceholder(o)); | |
| if (realOpts.length > 0) { | |
| allFound[label] = realOpts.map(o => `${o.text} [value="${o.value}"]`); | |
| } else { | |
| // Seems dependent — mark for pass 2 | |
| allFound[label] = null; | |
| } | |
| } | |
| // --- Pass 2: detect & expand dependent native selects --- | |
| // For each "empty" select, try every other native select as the potential parent | |
| const emptySelects = nativeTargets.filter(s => allFound[getLabel(s)] === null); | |
| for (const depSel of emptySelects) { | |
| const depLabel = getLabel(depSel); | |
| const mapping = {}; // parentOptionText -> [child options] | |
| let foundParent = false; | |
| for (const parentSel of nativeTargets) { | |
| if (parentSel === depSel) continue; | |
| const parentOpts = readNativeOptions(parentSel).filter(o => !isPlaceholder(o)); | |
| if (parentOpts.length === 0) continue; | |
| const savedParentVal = parentSel.value; | |
| let anyChildOpts = false; | |
| for (const pOpt of parentOpts) { | |
| statusEl.textContent = `Probing "${getLabel(depSel)}" ← "${pOpt.text}"…`; | |
| setNativeValue(parentSel, pOpt.value); | |
| await delay(350); | |
| const childOpts = readNativeOptions(depSel).filter(o => !isPlaceholder(o)); | |
| if (childOpts.length > 0) { | |
| mapping[pOpt.text] = childOpts.map(o => `${o.text} [value="${o.value}"]`); | |
| anyChildOpts = true; | |
| } | |
| } | |
| // Restore parent | |
| setNativeValue(parentSel, savedParentVal); | |
| await delay(200); | |
| if (anyChildOpts) { foundParent = true; break; } | |
| } | |
| if (foundParent && Object.keys(mapping).length > 0) { | |
| allFound[depLabel] = mapping; // store as object = dependent map | |
| } else { | |
| allFound[depLabel] = []; // truly empty | |
| } | |
| } | |
| // --- Pass 3: JS dropdowns --- | |
| for (let i = 0; i < jsTargets.length; i++) { | |
| const trigger = jsTargets[i]; | |
| const label = getLabel(trigger); | |
| statusEl.textContent = `Scanning JS dropdown ${i+1}/${jsTargets.length}: "${label.slice(0,22)}"…`; | |
| try { | |
| const before = new Set( | |
| [...document.querySelectorAll(optionSelectors.join(','))] | |
| .filter(e => e.offsetParent !== null).map(e => e.textContent.trim()) | |
| ); | |
| trigger.click(); | |
| trigger.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); | |
| trigger.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); | |
| await delay(450); | |
| const seen = new Set(); const opts = []; | |
| optionSelectors.forEach(sel => { | |
| document.querySelectorAll(sel).forEach(el => { | |
| if (!el.offsetParent) return; | |
| const text = el.textContent.trim().replace(/\s+/g,' '); | |
| if (text && text.length > 1 && !seen.has(text) && !before.has(text)) { seen.add(text); opts.push(text); } | |
| }); | |
| }); | |
| if (!opts.length) { | |
| optionSelectors.forEach(sel => { | |
| document.querySelectorAll(sel).forEach(el => { | |
| if (!el.offsetParent) return; | |
| const text = el.textContent.trim().replace(/\s+/g,' '); | |
| if (text && text.length > 1 && !seen.has(text)) { seen.add(text); opts.push(text); } | |
| }); | |
| }); | |
| } | |
| if (opts.length) allFound[label] = opts; | |
| document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); | |
| trigger.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true })); | |
| document.body.click(); | |
| await delay(200); | |
| } catch(e) {} | |
| } | |
| return allFound; | |
| } | |
| // ── Format output ────────────────────────────────────────────────────────── | |
| function formatOutput(allFound, scannedCount) { | |
| let totalItems = 0; | |
| Object.values(allFound).forEach(v => { | |
| if (!v) return; | |
| if (Array.isArray(v)) totalItems += v.length; | |
| else Object.values(v).forEach(arr => totalItems += arr.length); | |
| }); | |
| let out = `=== DROPDOWN EXPORT ===\nScanned: ${scannedCount} | Found: ${totalItems} options\n${'='.repeat(40)}\n\n`; | |
| Object.entries(allFound).forEach(([label, val]) => { | |
| if (!val) return; | |
| out += `📋 ${label}\n`; | |
| if (Array.isArray(val)) { | |
| if (val.length === 0) { out += ` (no options)\n`; } | |
| else val.forEach((item, i) => out += ` ${i+1}. ${item}\n`); | |
| } else { | |
| // dependent map | |
| out += ` ⚠️ Options depend on parent selection:\n`; | |
| Object.entries(val).forEach(([parentOpt, items]) => { | |
| out += ` ▸ When parent = "${parentOpt}":\n`; | |
| items.forEach((item, i) => out += ` ${i+1}. ${item}\n`); | |
| }); | |
| } | |
| out += '\n'; | |
| }); | |
| return { out, totalItems }; | |
| } | |
| async function runExport(targets, statusEl, exportBtn) { | |
| const allFound = await exportTargets(targets, statusEl, exportBtn); | |
| const { out, totalItems } = formatOutput(allFound, targets.length); | |
| exportBtn.textContent = exportBtn.dataset.label; | |
| exportBtn.disabled = false; | |
| navigator.clipboard.writeText(out).then(() => { | |
| statusEl.textContent = `✅ Copied ${totalItems} options to clipboard!`; | |
| statusEl.style.color = '#22c55e'; | |
| }).catch(() => { | |
| // Fallback textarea | |
| cleanup(); | |
| const ta = document.createElement('textarea'); | |
| ta.value = out; | |
| ta.style.cssText = 'position:fixed;top:5%;left:5%;width:90%;height:90%;z-index:9999999;padding:12px;font:13px monospace;background:#1e1e1e;color:#d4d4d4;border:2px solid #444;border-radius:8px;'; | |
| document.body.appendChild(ta); ta.select(); | |
| const cb = document.createElement('button'); | |
| cb.textContent = '✕ Close'; | |
| cb.style.cssText = 'position:fixed;top:6%;right:6%;z-index:9999999;padding:6px 14px;background:#c0392b;color:white;border:none;border-radius:6px;cursor:pointer;'; | |
| cb.onclick = () => { ta.remove(); cb.remove(); }; | |
| document.body.appendChild(cb); | |
| }); | |
| } | |
| // ── Build UI ─────────────────────────────────────────────────────────────── | |
| const overlay = document.createElement('div'); | |
| overlay.style.cssText = `position:fixed;top:16px;right:16px;z-index:2147483647;width:330px;max-height:82vh;background:#1a1a2e;color:#e0e0e0;border:1px solid #444;border-radius:12px;font:13px/1.5 system-ui,sans-serif;box-shadow:0 8px 32px rgba(0,0,0,.6);display:flex;flex-direction:column;user-select:none;`; | |
| overlay.innerHTML = ` | |
| <div id="__ddh__" style="padding:12px 16px;background:#16213e;border-radius:12px 12px 0 0;cursor:move;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #333;"> | |
| <span style="font-weight:700;font-size:14px;">🗂️ Dropdown Exporter</span> | |
| <button id="__ddc__" style="background:none;border:none;color:#aaa;font-size:18px;cursor:pointer;line-height:1;padding:0 2px;">×</button> | |
| </div> | |
| <!-- Mode switcher --> | |
| <div style="display:flex;border-bottom:1px solid #2a2a3e;"> | |
| <button id="__mode_all__" data-mode="all" style="flex:1;padding:9px 0;border:none;background:#22c55e;color:#000;font-weight:700;cursor:pointer;font-size:12px;border-radius:0;">⚡ Export All</button> | |
| <button id="__mode_sel__" data-mode="sel" style="flex:1;padding:9px 0;border:none;background:#2d2d4e;color:#9ca3af;font-weight:600;cursor:pointer;font-size:12px;border-radius:0;">🖱️ Select Mode</button> | |
| </div> | |
| <!-- Export All panel --> | |
| <div id="__panel_all__" style="padding:14px;display:flex;flex-direction:column;gap:10px;"> | |
| <div style="font-size:12px;color:#9ca3af;line-height:1.6;"> | |
| Exports <b style="color:#e0e0e0;">all ${allTriggers.length} dropdowns</b> found on this page.<br> | |
| Dependent sub-menus are automatically probed. | |
| </div> | |
| <button id="__dde_all__" data-label="⬇️ Export All Dropdowns" style="padding:10px;border-radius:8px;border:none;background:#22c55e;color:#000;font-weight:700;cursor:pointer;font-size:13px;">⬇️ Export All Dropdowns</button> | |
| </div> | |
| <!-- Select Mode panel --> | |
| <div id="__panel_sel__" style="display:none;flex-direction:column;flex:1;overflow:hidden;"> | |
| <div style="padding:8px 14px;background:#0f3460;font-size:12px;color:#93c5fd;"><b>Hover</b> to highlight · <b>Click</b> to toggle</div> | |
| <div style="padding:8px 14px;display:flex;gap:8px;border-bottom:1px solid #2a2a3e;"> | |
| <button id="__dda__" style="flex:1;padding:5px;border-radius:6px;border:1px solid #555;background:#2d2d4e;color:#e0e0e0;cursor:pointer;font-size:11px;">✅ All</button> | |
| <button id="__ddn__" style="flex:1;padding:5px;border-radius:6px;border:1px solid #555;background:#2d2d4e;color:#e0e0e0;cursor:pointer;font-size:11px;">❌ Clear</button> | |
| </div> | |
| <div id="__ddl__" style="overflow-y:auto;flex:1;padding:8px 6px;min-height:80px;max-height:calc(80vh - 260px);"></div> | |
| <div style="padding:10px 14px;border-top:1px solid #2a2a3e;"> | |
| <button id="__dde_sel__" data-label="⬇️ Export Selected" style="width:100%;padding:9px;border-radius:8px;border:none;background:#22c55e;color:#000;font-weight:700;cursor:pointer;font-size:13px;">⬇️ Export Selected</button> | |
| </div> | |
| </div> | |
| <div id="__dds__" style="padding:4px 14px 10px;font-size:11px;color:#6b7280;text-align:center;min-height:20px;"></div> | |
| `; | |
| document.body.appendChild(overlay); | |
| const statusEl = overlay.querySelector('#__dds__'); | |
| const listEl = overlay.querySelector('#__ddl__'); | |
| const panelAll = overlay.querySelector('#__panel_all__'); | |
| const panelSel = overlay.querySelector('#__panel_sel__'); | |
| const btnModeAll = overlay.querySelector('#__mode_all__'); | |
| const btnModeSel = overlay.querySelector('#__mode_sel__'); | |
| const exportAll = overlay.querySelector('#__dde_all__'); | |
| const exportSel = overlay.querySelector('#__dde_sel__'); | |
| const selectedTriggers = new Set(); | |
| // Mode switching | |
| function setMode(mode) { | |
| const isAll = mode === 'all'; | |
| panelAll.style.display = isAll ? 'flex' : 'none'; | |
| panelSel.style.display = isAll ? 'none' : 'flex'; | |
| btnModeAll.style.background = isAll ? '#22c55e' : '#2d2d4e'; | |
| btnModeAll.style.color = isAll ? '#000' : '#9ca3af'; | |
| btnModeSel.style.background = isAll ? '#2d2d4e' : '#22c55e'; | |
| btnModeSel.style.color = isAll ? '#9ca3af' : '#000'; | |
| statusEl.textContent = ''; | |
| statusEl.style.color = '#6b7280'; | |
| if (!isAll) buildList(); | |
| } | |
| btnModeAll.addEventListener('click', () => setMode('all')); | |
| btnModeSel.addEventListener('click', () => setMode('sel')); | |
| // ── Select mode list ─────────────────────────────────────────────────────── | |
| function updateStatus() { | |
| statusEl.textContent = `${selectedTriggers.size} / ${allTriggers.length} selected`; | |
| exportSel.style.opacity = selectedTriggers.size > 0 ? '1' : '0.45'; | |
| exportSel.disabled = selectedTriggers.size === 0; | |
| } | |
| function buildList() { | |
| listEl.innerHTML = ''; | |
| if (!allTriggers.length) { listEl.innerHTML = '<div style="padding:16px;color:#6b7280;text-align:center;">No dropdowns detected.</div>'; return; } | |
| allTriggers.forEach(el => { | |
| const label = getLabel(el); | |
| const isNative = el.tagName === 'SELECT'; | |
| const row = document.createElement('div'); | |
| row.style.cssText = `display:flex;align-items:center;gap:8px;padding:7px 8px;border-radius:7px;cursor:pointer;transition:background .15s;margin-bottom:2px;`; | |
| const cb = document.createElement('input'); | |
| cb.type = 'checkbox'; cb.style.cssText = 'width:15px;height:15px;accent-color:#22c55e;cursor:pointer;flex-shrink:0;'; | |
| const lbl = document.createElement('span'); | |
| lbl.textContent = label.length > 28 ? label.slice(0,28)+'…' : label; | |
| lbl.style.cssText = 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;'; | |
| const badge = document.createElement('span'); | |
| badge.textContent = isNative ? 'native' : 'js'; | |
| badge.style.cssText = `font-size:10px;padding:1px 5px;border-radius:4px;flex-shrink:0;background:${isNative?'#1d4ed8':'#7c3aed'};color:${isNative?'#bfdbfe':'#ddd6fe'};`; | |
| row.append(cb, lbl, badge); | |
| listEl.appendChild(row); | |
| function syncRow() { | |
| const on = selectedTriggers.has(el); | |
| cb.checked = on; row.style.background = on ? '#14532d55' : 'transparent'; | |
| refreshHL(selectedTriggers); updateStatus(); | |
| } | |
| row.addEventListener('click', () => { selectedTriggers.has(el)?selectedTriggers.delete(el):selectedTriggers.add(el); syncRow(); }); | |
| row.addEventListener('mouseenter', () => { row.style.background = selectedTriggers.has(el)?'#14532d88':'#2d2d4e'; if(!selectedTriggers.has(el)){hl(el,'#3b82f6');el.scrollIntoView({behavior:'smooth',block:'center'});} }); | |
| row.addEventListener('mouseleave', () => { row.style.background = selectedTriggers.has(el)?'#14532d55':'transparent'; if(!selectedTriggers.has(el))unhl(el); }); | |
| syncRow(); | |
| }); | |
| updateStatus(); | |
| } | |
| overlay.querySelector('#__dda__').addEventListener('click', () => { allTriggers.forEach(el=>selectedTriggers.add(el)); buildList(); refreshHL(selectedTriggers); }); | |
| overlay.querySelector('#__ddn__').addEventListener('click', () => { selectedTriggers.clear(); allTriggers.forEach(el=>unhl(el)); buildList(); }); | |
| // Page-level interception (select mode only) | |
| let hoverTarget = null; | |
| function onPageMouseover(e) { | |
| if (overlay.contains(e.target)) return; | |
| const el = allTriggers.find(t => t===e.target||t.contains(e.target)); | |
| if (!el) { if(hoverTarget&&!selectedTriggers.has(hoverTarget))unhl(hoverTarget); hoverTarget=null; return; } | |
| if(hoverTarget&&hoverTarget!==el&&!selectedTriggers.has(hoverTarget))unhl(hoverTarget); | |
| hoverTarget=el; if(!selectedTriggers.has(el))hl(el,'#3b82f6'); | |
| } | |
| function onPageClick(e) { | |
| if(overlay.contains(e.target))return; | |
| const el=allTriggers.find(t=>t===e.target||t.contains(e.target)); | |
| if(!el)return; | |
| e.preventDefault(); e.stopImmediatePropagation(); | |
| selectedTriggers.has(el)?selectedTriggers.delete(el):selectedTriggers.add(el); | |
| refreshHL(selectedTriggers); buildList(); updateStatus(); | |
| } | |
| document.addEventListener('mouseover', onPageMouseover, true); | |
| document.addEventListener('click', onPageClick, true); | |
| // ── Export buttons ───────────────────────────────────────────────────────── | |
| exportAll.addEventListener('click', async () => { | |
| exportAll.textContent = '⏳ Scanning…'; exportAll.disabled = true; | |
| statusEl.style.color = '#6b7280'; | |
| await runExport(allTriggers, statusEl, exportAll); | |
| }); | |
| exportSel.addEventListener('click', async () => { | |
| const targets = [...selectedTriggers]; | |
| if (!targets.length) return; | |
| exportSel.textContent = '⏳ Scanning…'; exportSel.disabled = true; | |
| statusEl.style.color = '#6b7280'; | |
| await runExport(targets, statusEl, exportSel); | |
| }); | |
| // ── Close ────────────────────────────────────────────────────────────────── | |
| function cleanup() { | |
| document.removeEventListener('mouseover', onPageMouseover, true); | |
| document.removeEventListener('click', onPageClick, true); | |
| allTriggers.forEach(el => unhl(el)); | |
| overlay.remove(); | |
| } | |
| overlay.querySelector('#__ddc__').addEventListener('click', cleanup); | |
| // ── Draggable ────────────────────────────────────────────────────────────── | |
| const hdr = overlay.querySelector('#__ddh__'); | |
| let drag=false,ox=0,oy=0; | |
| hdr.addEventListener('mousedown', e=>{drag=true;const r=overlay.getBoundingClientRect();ox=e.clientX-r.left;oy=e.clientY-r.top;e.preventDefault();}); | |
| document.addEventListener('mousemove', e=>{if(!drag)return;overlay.style.right='auto';overlay.style.left=(e.clientX-ox)+'px';overlay.style.top=(e.clientY-oy)+'px';}); | |
| document.addEventListener('mouseup', ()=>drag=false); | |
| })(); |
Author
Author
A more versatile version that handles complex react dropmenus :
Author
(function(){
const OID = '__ddx__';
const VERSION = '2.3.0';
const SHOW_VALUES = true;
const MAX_VIRTUAL_SCROLL_STEPS = 28;
const SCAN_STATUS_TRIM = 34;
const existing = document.getElementById(OID);
if (existing && typeof existing.__cleanup === 'function') {
existing.__cleanup();
return;
}
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
const norm = value => String(value || '').replace(/\s+/g, ' ').trim();
const quoted = value => JSON.stringify(String(value));
const escAttr = value => String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
const cssEsc = value => {
try {
if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(String(value));
} catch (_) {}
return String(value).replace(/[^a-zA-Z0-9_-]/g, ch => `\\${ch}`);
};
let box = null;
let scanCache = { triggers: [], inputs: [] };
let labelCache = new WeakMap();
let docOrder = new WeakMap();
const TRIG = [
'select',
'[role="combobox"]',
'button[aria-haspopup]',
'button[aria-expanded]',
'button[data-bs-toggle="dropdown"]',
'button[class*="dropdown-toggle"]',
'[aria-controls][aria-expanded]',
'[role="button"][aria-haspopup]',
'[role="button"][aria-expanded]',
'[class*="dropdown-toggle"]',
'[class*="select"]',
'[class*="combobox"]',
'[data-slot*="trigger"]',
'[data-slot*="select"]',
'[data-testid*="select"]',
'[data-testid*="dropdown-trigger"]'
];
const POP = [
'[role="listbox"]',
'[role="menu"]',
'[data-radix-popper-content-wrapper]',
'[data-state="open"]',
'[data-headlessui-state="open"]',
'[data-slot*="content"]',
'[class*="popover"]',
'[class*="dropdown-menu"]',
'[class*="select-menu"]'
].join(',');
const OPT = [
'[role="option"]',
'[role="menuitem"]',
'[data-value]',
'li[aria-selected]',
'[data-radix-collection-item]',
'[data-slot*="option"]',
'[data-testid*="option"]'
].join(',');
const APP_DATA_KEYS = ['__NEXT_DATA__', '__INITIAL_STATE__', '__PRELOADED_STATE__', '__APOLLO_STATE__', '__NUXT__'];
const LABEL_KEYS = /^(label|question|title|name|text|prompt|fieldLabel|displayName|legend)$/i;
const OPTION_KEYS = /^(options|choices|answers|values|items|allowedValues|enum|selections)$/i;
const STOP_WORDS = new Set(['a', 'an', 'and', 'are', 'at', 'be', 'by', 'do', 'else', 'for', 'how', 'if', 'in', 'is', 'it', 'of', 'on', 'or', 'per', 'the', 'to', 'us', 'we', 'what', 'when', 'which', 'with', 'would', 'you', 'your']);
const appOptionCache = new Map();
const ql = (sel, root = document) => {
try {
return Array.from(root.querySelectorAll(sel));
} catch (_) {
return [];
}
};
function walkRoots(visitor) {
const seen = new Set();
const walk = root => {
if (!root || seen.has(root)) return;
seen.add(root);
visitor(root);
const base = root instanceof Document ? root.documentElement : root;
if (!base) return;
const nodes = [base, ...ql('*', base)];
for (const el of nodes) {
if (!el || el === box || (box && box.contains(el))) continue;
if (el.shadowRoot) walk(el.shadowRoot);
if (el.tagName === 'IFRAME') {
try {
const frameDoc = el.contentDocument;
if (frameDoc && frameDoc.documentElement) walk(frameDoc);
} catch (_) {}
}
}
};
walk(document);
}
function q(sel, root) {
if (root) return ql(sel, root);
const out = [];
const seen = new Set();
walkRoots(searchRoot => {
ql(sel, searchRoot).forEach(el => {
if (!seen.has(el)) {
seen.add(el);
out.push(el);
}
});
});
return out;
}
function ownerDoc(el) {
return (el && el.ownerDocument) || document;
}
function ownerWin(el) {
const doc = ownerDoc(el);
return doc.defaultView || window;
}
function refreshDocOrder() {
docOrder = new WeakMap();
let index = 0;
walkRoots(root => {
const doc = root instanceof Document ? root : root.ownerDocument;
if (doc && !docOrder.has(doc)) docOrder.set(doc, index++);
});
}
function order(a, b) {
if (a === b) return 0;
const da = docOrder.get(ownerDoc(a)) || 0;
const db = docOrder.get(ownerDoc(b)) || 0;
if (da !== db) return da - db;
try {
return a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
} catch (_) {
return 0;
}
}
function vis(el) {
if (!el || !el.isConnected) return false;
try {
const styles = ownerWin(el).getComputedStyle(el);
const rect = el.getBoundingClientRect();
return styles.display !== 'none' && styles.visibility !== 'hidden' && +styles.opacity !== 0 && rect.width > 0 && rect.height > 0 && el.getClientRects().length > 0;
} catch (_) {
return false;
}
}
function txt(el) {
return norm(el && (el.innerText || el.textContent));
}
function area(el) {
const rect = el.getBoundingClientRect();
return rect.width * rect.height;
}
function parentLike(node) {
if (!node) return null;
if (node.parentElement) return node.parentElement;
try {
const root = node.getRootNode && node.getRootNode();
if (root instanceof ShadowRoot) return root.host || null;
} catch (_) {}
return null;
}
function ancestorChain(el, depth = 6) {
const out = [];
let cur = el;
for (let i = 0; i < depth; i += 1) {
cur = parentLike(cur);
if (!cur) break;
out.push(cur);
}
return out;
}
function closestMatch(el, selector) {
let cur = el;
while (cur) {
try {
if (cur.matches && cur.matches(selector)) return cur;
} catch (_) {}
cur = parentLike(cur);
}
return null;
}
function deepGetById(id, preferredDoc) {
if (!id) return null;
const findInRoot = root => {
try {
if (root instanceof Document) return root.getElementById(id);
return root.querySelector(`#${cssEsc(id)}`) || root.querySelector(`[id="${escAttr(id)}"]`);
} catch (_) {
return null;
}
};
if (preferredDoc) {
const direct = findInRoot(preferredDoc);
if (direct) return direct;
}
let found = null;
walkRoots(root => {
if (found) return;
const hit = findInRoot(root);
if (hit) found = hit;
});
return found;
}
function fallbackLabel(el) {
const rect = el.getBoundingClientRect();
const name = norm(el.getAttribute && (el.getAttribute('name') || el.getAttribute('id') || ''));
const tag = (el.tagName || 'Field').toLowerCase();
return name || `${tag} @ ${Math.round(rect.top)},${Math.round(rect.left)}`;
}
function reactLike(el) {
if (!el) return false;
const raw = `${el.className || ''} ${el.id || ''}`.toLowerCase();
return raw.includes('react-select') || /^react-select-\d+/.test(String(el.id || ''));
}
function badTrigger(el) {
if (!el) return true;
const role = String(el.getAttribute('role') || '').toLowerCase();
const raw = `${el.className || ''} ${el.id || ''}`.toLowerCase();
if (role === 'option' || role === 'menuitem') return true;
if (/^react-select-\d+-(option|group|listbox|placeholder|input|value)/.test(String(el.id || ''))) return true;
if (reactLike(el) && /(placeholder|value-container|single-value|input-container|menu-list|indicator|group)/.test(raw) && !/control/.test(raw)) return true;
if (/(^|\s)(option|menu-item|dropdown-item)(\s|$)/.test(raw) && !/control|trigger/.test(raw)) return true;
return false;
}
function normalizeTrigger(el) {
if (!el) return el;
if (el.tagName === 'SELECT') return el;
const shell = closestMatch(el, '.react-select,[class*="react-select"],[id^="dropdown"]');
if (badTrigger(el) || shell) {
const control = closestMatch(el, '.react-select__control,[class*="__control"],[class*="-control"]');
if (control && vis(control)) return control;
if (shell) {
const pick = ql('.react-select__control,[class*="__control"],[class*="-control"],[role="combobox"],button[aria-haspopup],button[aria-expanded]', shell).find(vis);
if (pick) return pick;
if (vis(shell)) return shell;
}
}
return el;
}
function labelFor(el) {
const cached = labelCache.get(el);
if (cached) return cached;
const doc = ownerDoc(el);
const aria = norm(el.getAttribute('aria-label'));
if (aria) {
labelCache.set(el, aria);
return aria;
}
const labelledBy = norm(el.getAttribute('aria-labelledby'));
if (labelledBy) {
const text = labelledBy
.split(/\s+/)
.map(id => doc.getElementById(id) || doc.querySelector(`[id="${escAttr(id)}"]`))
.filter(Boolean)
.map(txt)
.filter(Boolean)
.join(' ');
if (text) {
labelCache.set(el, text);
return text;
}
}
if (el.labels && el.labels[0] && txt(el.labels[0])) {
const label = txt(el.labels[0]);
labelCache.set(el, label);
return label;
}
if (el.id) {
const explicit = ql('label', doc).find(node => node.htmlFor === el.id);
if (explicit && txt(explicit)) {
const label = txt(explicit);
labelCache.set(el, label);
return label;
}
}
const wrap = closestMatch(el, 'label');
if (wrap) {
const label = txt(wrap).replace(txt(el), '').trim();
if (label) {
labelCache.set(el, label);
return label;
}
}
const rect = el.getBoundingClientRect();
let best = '';
let bestScore = -1;
let parent = parentLike(el);
for (let depth = 0; depth < 5 && parent; depth += 1, parent = parentLike(parent)) {
ql('label,legend,p,span,div,strong,h1,h2,h3,h4,h5,h6', parent).forEach(candidate => {
if (candidate === el || candidate.contains(el) || el.contains(candidate) || !vis(candidate)) return;
const text = txt(candidate);
if (!text || text.length > 160 || /^(select|choose|submit|continue|next|back)$/i.test(text)) return;
const cRect = candidate.getBoundingClientRect();
const overlap = Math.min(rect.right, cRect.right) - Math.max(rect.left, cRect.left);
const gap = rect.top - cRect.bottom;
const beside = cRect.left < rect.left && Math.abs(cRect.top - rect.top) < 22;
const above = gap >= -8 && gap < 96 && overlap > 18;
if (!(above || beside)) return;
let score = 80 - depth * 10 - Math.min(Math.abs(gap), 60) + Math.min(overlap, 180) / 8;
if (/LABEL|LEGEND|H\d/.test(candidate.tagName)) score += 18;
if (text.endsWith('?')) score += 8;
if (score > bestScore) {
best = text;
bestScore = score;
}
});
if (best) break;
}
const raw = rawAttrs(el);
const selfText = txt(el);
if (/\bdropdown-toggle-split\b/.test(raw) && !selfText) {
const group = parentLike(el);
const siblingText = group
? ql('button,a,[role="button"]', group)
.filter(node => node !== el)
.map(txt)
.find(text => text && text.length <= 120 && !/^(toggle dropdown|copy|search|result|live editor)$/i.test(text))
: '';
if (siblingText) {
const label = `${siblingText} (toggle)`;
labelCache.set(el, label);
return label;
}
}
if ((/\bdropdown-toggle\b/.test(raw) || (el.getAttribute('data-bs-toggle') || '').toLowerCase() === 'dropdown') && selfText && !placeholderLike(selfText) && selfText.length <= 120) {
labelCache.set(el, selfText);
return selfText;
}
const label = best || norm(el.getAttribute('placeholder')) || norm(el.getAttribute('name')) || norm(el.id) || selfText.slice(0, 80) || fallbackLabel(el);
labelCache.set(el, label);
return label;
}
function nearbySelect(el) {
if (!el) return null;
if (el.tagName === 'SELECT') return el;
const target = el.getBoundingClientRect();
const scopes = [el, ...ancestorChain(el, 5)];
for (const scope of scopes) {
const picks = ql('select', scope)
.filter(node => node.options)
.sort((a, b) => Math.abs(a.getBoundingClientRect().top - target.top) - Math.abs(b.getBoundingClientRect().top - target.top));
if (picks[0]) return picks[0];
}
return null;
}
function rawAttrs(el) {
return `${el.className || ''} ${el.id || ''} ${Array.from(el.attributes || []).map(attr => `${attr.name} ${attr.value}`).join(' ')}`.toLowerCase();
}
function classHints(el) {
const raw = rawAttrs(el);
let score = 0;
if (/dropdown-toggle|react-select|select|combobox|listbox|trigger|menu-button/.test(raw)) score += 18;
if (/option|item|dropdown-menu|menu__item/.test(raw)) score -= 8;
return score;
}
function disabledish(el) {
if (!el) return false;
const raw = `${el.className || ''} ${el.getAttribute('aria-disabled') || ''} ${el.getAttribute('data-disabled') || ''}`.toLowerCase();
return !!el.disabled || el.getAttribute('aria-disabled') === 'true' || raw.includes('is-disabled') || raw.includes('disabled');
}
function popupTarget(el) {
const ids = [el.getAttribute('aria-controls'), el.getAttribute('aria-owns')]
.filter(Boolean)
.flatMap(value => norm(value).split(/\s+/).filter(Boolean));
for (const id of ids) {
const hit = deepGetById(id, ownerDoc(el));
if (!hit) continue;
const raw = rawAttrs(hit);
try {
if ((hit.matches && hit.matches(POP)) || /dropdown-menu|listbox|popover|menu/.test(raw)) return hit;
} catch (_) {
if (/dropdown-menu|listbox|popover|menu/.test(raw)) return hit;
}
}
return null;
}
function ancestorRaw(el, depth = 7) {
return [el, ...ancestorChain(el, depth)].map(node => rawAttrs(node)).join(' ');
}
function chromeContext(el) {
if (!el) return false;
if (closestMatch(el, 'nav,[role="navigation"],header,footer,aside,.navbar,.theme-doc-sidebar-container,.tableOfContents_bqdL,.DocSearch,.breadcrumbs,.pagination')) return true;
const raw = ancestorRaw(el, 8);
return /navbar|theme-doc-sidebar|docsidebar|sidebar|tableofcontents|table-of-contents|docsearch|breadcrumb|pagination|skiptocontent|backtotop|colormode|navbarsearchcontainer|menu__|footer|header/.test(raw);
}
function editorContext(el) {
if (!el) return false;
if (closestMatch(el, 'pre,code,.theme-code-block,[class*="codeBlock"],[class*="playgroundEditor"],[class*="editorToolbar"],[class*="copyButton"],.buttonGroup_wSGZ,.npm__react-simple-code-editor__textarea,.CodeMirror,.monaco-editor,.ace_editor')) return true;
const raw = ancestorRaw(el, 8);
return /playgroundeditor|editortoolbar|copybutton|codeblock|theme-code-block|simple-code-editor|codemirror|monaco|ace_editor|buttongroup_wsgz/.test(raw);
}
function insidePopupContent(el) {
const popup = closestMatch(el, '[role="menu"],[role="listbox"],.dropdown-menu,[class*="dropdown-menu"],[class*="popover"],[class*="select-menu"]');
return !!popup && popup !== el;
}
function weakButtonText(text) {
return /^(search(?:\s*\(.*\))?|copy(?:\s+code(?:\s+to\s+clipboard)?)?|result|live editor|toggle navigation bar|switch between dark and light mode.*|back to top)$/i.test(norm(text));
}
function strongTriggerSignal(el) {
if (!el) return false;
const raw = rawAttrs(el);
const role = norm(el.getAttribute('role')).toLowerCase();
const dataToggle = String(el.getAttribute('data-bs-toggle') || '').toLowerCase();
if (el.tagName === 'SELECT') return true;
if (role === 'combobox') return true;
if (el.hasAttribute('aria-haspopup')) return true;
if (dataToggle === 'dropdown') return true;
if (/\bdropdown-toggle\b/.test(raw)) return true;
if (reactLike(el)) return true;
if (!!nearbySelect(el)) return true;
if (popupTarget(el)) return true;
if (el.hasAttribute('aria-expanded') && el.tagName !== 'A' && (el.matches('button,[role="button"],[role="combobox"],input[readonly],div[tabindex]') || classHints(el) > 12)) return true;
return false;
}
function selectish(el) {
if (!el) return false;
const role = norm(el.getAttribute('role')).toLowerCase();
return el.tagName === 'SELECT' || role === 'combobox' || strongTriggerSignal(el) || classHints(el) > 0 || reactLike(el);
}
function placeholderLike(text) {
return /^(select|choose|pick)(\.\.\.|\u2026)?$/i.test(text) || /^(select|choose|pick)\b/i.test(text);
}
function fieldish(el, text) {
const rect = el.getBoundingClientRect();
const styles = ownerWin(el).getComputedStyle(el);
const border = ['Top', 'Right', 'Bottom', 'Left'].some(side => parseFloat(styles[`border${side}Width`]) > 0);
const hint = classHints(el);
const hasChevron = !!el.querySelector('svg,[class*="chevron"],[class*="caret"],[data-icon],[data-testid*="chevron"]');
const hasNative = !!nearbySelect(el);
const hasHidden = !!el.querySelector('input[type="hidden"],select');
const signal = (placeholderLike(text) ? 1 : 0) + (hasChevron ? 1 : 0) + (hasNative ? 1 : 0) + (hasHidden ? 1 : 0) + (hint > 0 ? 1 : 0);
let score = 0;
if (rect.width >= 180 && rect.width <= 1200 && rect.height >= 30 && rect.height <= 72) score += 28;
else if (rect.width >= 140 && rect.height >= 28 && rect.height <= 96) score += 12;
else if (rect.height > 120) score -= 40;
if (border) score += 12;
if (parseFloat(styles.borderRadius || '0') >= 4) score += 6;
if (styles.cursor === 'pointer') score += 8;
if (hasChevron) score += 14;
if (placeholderLike(text)) score += 30;
if (hasNative) score += 16;
if (hasHidden) score += 10;
const label = labelFor(el);
if (label && label !== text) score += 12;
if (el.children.length > 0 && el.children.length <= 6) score += 4;
if (ql('button,a,input:not([type="hidden"]),textarea,[contenteditable="true"]', el).length > 1) score -= 24;
if (text && text.length > 80) score -= 16;
return score + hint - (signal ? 0 : 35);
}
function score(el) {
el = normalizeTrigger(el);
if (!el || !vis(el) || badTrigger(el) || closestMatch(el, `#${OID}`)) return -1;
const disabled = disabledish(el);
const role = norm(el.getAttribute('role')).toLowerCase();
const text = txt(el);
const raw = rawAttrs(el);
const strong = strongTriggerSignal(el);
if (disabled && !selectish(el)) return -1;
if (insidePopupContent(el) && !strong) return -1;
if (editorContext(el) && !strong) return -1;
if (chromeContext(el) && !strong) return -1;
if (el.tagName === 'A' && role === 'button' && !strong) return -1;
if ((weakButtonText(text) || weakButtonText(el.getAttribute('aria-label') || '')) && !strong) return -1;
if (el.tagName === 'SELECT') return 220 + (disabled ? 12 : 0);
if (el.tagName === 'INPUT' && !el.readOnly && el.type !== 'hidden') return -1;
const rect = el.getBoundingClientRect();
if (rect.height > 140) return -1;
let s = fieldish(el, text);
if (role === 'combobox') s += 85;
if (role === 'button') s += 18;
if (el.hasAttribute('aria-haspopup')) s += 35;
if (el.hasAttribute('aria-expanded')) s += 25;
if (popupTarget(el)) s += 20;
if (el.getAttribute('data-bs-toggle') === 'dropdown') s += 28;
if (/\bdropdown-toggle\b/.test(raw)) s += 26;
if (el.hasAttribute('aria-controls') || el.hasAttribute('aria-owns')) s += 16;
if (el.tagName === 'BUTTON') s += 20;
if (el.tagName === 'INPUT' && el.readOnly) s += 16;
if (el.tagName === 'DIV' && el.tabIndex >= 0) s += 10;
if (rect.width < 90 || rect.height < 24) s -= 18;
if (placeholderLike(text)) s += 18;
if (disabled) s += 14;
if (!strong) s -= 18;
if (/^(submit|continue|next|back|close|cancel|save)$/i.test(text)) s -= 70;
return s;
}
function collectTriggers() {
const candidates = [];
const seenRaw = new Set();
const seenNormalized = new Set();
const strictSel = TRIG.join(',');
const broadSel = [...TRIG, '[class*="dropdown"]', '[data-testid*="dropdown"]', 'div[tabindex]', 'input[readonly]', 'button', '[role="button"]'].join(',');
const passes = [
{ sel: strictSel, min: 54 },
{ sel: broadSel, min: 72, gate: () => candidates.length < 8 }
];
passes.forEach(pass => {
if (pass.gate && !pass.gate()) return;
q(pass.sel).forEach(node => {
if (seenRaw.has(node)) return;
seenRaw.add(node);
const el = normalizeTrigger(node);
if (!el || seenNormalized.has(el)) return;
const s = score(el);
if (s >= pass.min) {
seenNormalized.add(el);
candidates.push({ el, s });
}
});
});
candidates.sort((a, b) => b.s - a.s || area(b.el) - area(a.el));
const keep = [];
candidates.forEach(candidate => {
const nested = keep.findIndex(item => item.el === candidate.el || item.el.contains(candidate.el) || candidate.el.contains(item.el));
if (nested === -1) {
keep.push(candidate);
return;
}
const current = keep[nested];
const preferNew = candidate.s > current.s + 6 || (candidate.s >= current.s - 2 && area(candidate.el) > area(current.el) * 1.1);
if (preferNew) keep[nested] = candidate;
});
return keep.map(item => normalizeTrigger(item.el)).filter((el, idx, arr) => el && arr.indexOf(el) === idx).sort(order);
}
function isSensitiveInput(el) {
if (!el || el.tagName !== 'INPUT') return false;
const type = String(el.type || 'text').toLowerCase();
if (type === 'password') return true;
const autocomplete = String(el.getAttribute('autocomplete') || '').toLowerCase();
return autocomplete.includes('password');
}
function isTextField(el) {
if (!el || !vis(el) || closestMatch(el, `#${OID}`)) return false;
if (el.getAttribute('role') === 'combobox' || el.hasAttribute('aria-haspopup')) return false;
if (closestMatch(el, '.react-select,[class*="react-select"]')) return false;
if (editorContext(el)) return false;
if (el.tagName === 'TEXTAREA') return true;
if (el.tagName !== 'INPUT') return false;
if (isSensitiveInput(el)) return false;
const type = String(el.type || 'text').toLowerCase();
if (['hidden', 'button', 'submit', 'reset', 'checkbox', 'radio', 'file', 'image', 'range', 'color'].includes(type)) return false;
return true;
}
function inputDetails(el) {
const type = el.tagName === 'TEXTAREA' ? 'textarea' : String(el.getAttribute('type') || 'text').toLowerCase();
const placeholder = norm(el.getAttribute('placeholder')) || '(none)';
const value = norm(el.value) || '(empty)';
const maxLength = Number.isFinite(el.maxLength) && el.maxLength >= 0 ? String(el.maxLength) : '(none)';
return [
`type: ${quoted(type)}`,
`placeholder: ${quoted(placeholder)}`,
`value: ${quoted(value)}`,
`maxlength: ${maxLength}`
];
}
function collectInputs() {
return q('input,textarea').filter(isTextField).sort(order);
}
function formatOption(label, value) {
const text = norm(label);
const val = norm(value);
if (!text) return '';
if (!SHOW_VALUES || !val || text === val) return text;
return `${text} [value="${escAttr(val)}"]`;
}
function nativeOptions(sel) {
return Array.from(sel.options || []).map(opt => ({ text: norm(opt.textContent || opt.label), value: String(opt.value ?? '') }));
}
function isPlaceholderOpt(opt) {
return !opt || !opt.text || opt.value === '' || opt.value === '-1' || opt.text === '--' || opt.text === '-' || placeholderLike(opt.text);
}
function optionish(item) {
return !!item && typeof item === 'object' && (Array.isArray(item.options) || item.label != null || item.value != null || item.name != null || item.text != null);
}
function scalarish(value) {
return typeof value === 'string' || typeof value === 'number';
}
function flattenOptionData(items, out = []) {
(items || []).forEach(item => {
if (!item) return;
if (Array.isArray(item.options)) {
flattenOptionData(item.options, out);
return;
}
if (typeof item !== 'object') return;
const label = norm(item.label ?? item.text ?? item.name ?? item.value);
const value = item.value ?? item.id ?? item.key ?? '';
if (!label || placeholderLike(label)) return;
out.push(formatOption(label, value));
});
return [...new Set(out)];
}
function optionArrayValue(value) {
if (!Array.isArray(value) || !value.length) return [];
if (value.every(optionish)) return flattenOptionData(value);
if (value.every(scalarish)) return flattenOptionData(value.map(item => ({ label: String(item), value: String(item) })));
return [];
}
function optionItem(el) {
const text = txt(el);
if (!text || text.length > 220 || placeholderLike(text) || /^(search|type|loading|no options)(\.\.\.|\u2026)?$/i.test(text)) return '';
const value = norm(el.getAttribute('data-value') || el.getAttribute('value'));
return formatOption(text, value);
}
function looseOpts(root) {
const out = [];
const seen = new Set();
ql(`${OPT},li,[class*="option"],[class*="menu-item"],[class*="dropdown-item"]`, root).forEach(el => {
if (!vis(el) || closestMatch(el, `#${OID}`)) return;
const item = optionItem(el);
if (!item || seen.has(item)) return;
seen.add(item);
out.push(item);
});
return out;
}
function labelWords(text) {
return norm(text).toLowerCase().replace(/[^a-z0-9\s]+/g, ' ').split(/\s+/).filter(word => word && !STOP_WORDS.has(word));
}
function labelScore(a, b) {
const aa = norm(a).toLowerCase();
const bb = norm(b).toLowerCase();
if (!aa || !bb) return 0;
if (aa === bb) return 140;
if (aa.includes(bb) || bb.includes(aa)) return 105;
const aw = new Set(labelWords(aa));
const bw = new Set(labelWords(bb));
if (!aw.size || !bw.size) return 0;
let shared = 0;
aw.forEach(word => {
if (bw.has(word)) shared += 1;
});
if (!shared) return 0;
return Math.round((shared / Math.max(aw.size, bw.size)) * 90 + (shared / Math.min(aw.size, bw.size)) * 30);
}
function appDataRoots() {
const roots = [];
const add = value => {
if (value && typeof value === 'object' && !roots.includes(value)) roots.push(value);
};
APP_DATA_KEYS.forEach(key => {
try {
add(window[key]);
} catch (_) {}
});
const next = document.getElementById('__NEXT_DATA__');
if (next && !roots.length) {
try {
add(JSON.parse(next.textContent || '{}'));
} catch (_) {}
}
return roots;
}
function walkable(value) {
return !!value && typeof value === 'object' && value !== window && value !== document && !(typeof Element !== 'undefined' && value instanceof Element);
}
function appDataOptions(label) {
const cacheKey = norm(label);
if (appOptionCache.has(cacheKey)) return appOptionCache.get(cacheKey);
const roots = appDataRoots();
if (!roots.length) {
appOptionCache.set(cacheKey, []);
return [];
}
const seen = new WeakSet();
const queue = roots.map(value => ({ value, score: 0 }));
let best = [];
let bestScore = 0;
let steps = 0;
while (queue.length && steps < 14000) {
steps += 1;
const current = queue.shift();
const node = current && current.value;
const baseScore = (current && current.score) || 0;
if (!walkable(node) || seen.has(node)) continue;
seen.add(node);
if (Array.isArray(node)) {
const arr = optionArrayValue(node);
if (arr.length && baseScore > bestScore) {
best = arr;
bestScore = baseScore;
}
node.slice(0, 20).forEach(child => {
if (walkable(child)) queue.push({ value: child, score: Math.max(baseScore - 2, 0) });
});
continue;
}
const keys = Object.keys(node).slice(0, 60);
let scoreValue = baseScore;
keys.forEach(key => {
const value = node[key];
if (scalarish(value) && LABEL_KEYS.test(key)) scoreValue = Math.max(scoreValue, labelScore(label, String(value)));
});
keys.forEach(key => {
const arr = OPTION_KEYS.test(key) ? optionArrayValue(node[key]) : [];
if (arr.length && (scoreValue > bestScore || (scoreValue === bestScore && arr.length > best.length))) {
best = arr;
bestScore = scoreValue;
}
});
keys.forEach(key => {
if (/^(ownerDocument|document|window|parentNode|offsetParent|children|childNodes|nextSibling|previousSibling|nextElementSibling|previousElementSibling|firstChild|lastChild|_owner)$/i.test(key)) return;
const value = node[key];
if (walkable(value)) queue.push({ value, score: Math.max(scoreValue - 1, 0) });
});
}
const result = bestScore >= 60 ? best : [];
appOptionCache.set(cacheKey, result);
return result;
}
function reactAttachments(el) {
if (!el) return [];
return Object.keys(el)
.filter(key => key.startsWith('__reactProps$') || key.startsWith('__reactFiber$') || key.startsWith('__reactEventHandlers$'))
.map(key => el[key])
.filter(Boolean);
}
function reactBase(trigger) {
const roots = [
trigger,
activator(trigger),
trigger && closestMatch(trigger, '[id^="dropdown"]'),
trigger && closestMatch(trigger, '.react-select'),
trigger && closestMatch(trigger, '[class*="react-select"]')
].filter(Boolean);
for (const root of roots) {
const nodes = [root, ...ql('[id]', root).slice(0, 12)];
for (const node of nodes) {
const match = String(node.id || '').match(/^(react-select-\d+)/);
if (match) return match[1];
}
}
return '';
}
function matchesReactBase(obj, base) {
if (!base || !obj || typeof obj !== 'object') return false;
const short = base.replace('react-select-', '');
const values = [];
['id', 'inputId', 'instanceId', 'instancePrefix', 'controlId', 'menuId', 'placeholderId', 'name', 'htmlFor'].forEach(key => {
if (obj[key] != null) values.push(String(obj[key]));
});
['aria-controls', 'aria-labelledby'].forEach(key => {
if (obj[key] != null) values.push(String(obj[key]));
});
return values.some(value => value === base || value.startsWith(base) || value.includes(base) || value === short);
}
function objectContextScore(obj, label, base) {
if (!obj || typeof obj !== 'object') return 0;
let scoreValue = 0;
[obj, obj.selectProps, obj.props, obj.memoizedProps, obj.pendingProps].forEach(node => {
if (matchesReactBase(node, base)) scoreValue = Math.max(scoreValue, 90);
});
Object.keys(obj).slice(0, 24).forEach(key => {
const value = obj[key];
if (scalarish(value) && LABEL_KEYS.test(key)) scoreValue = Math.max(scoreValue, labelScore(label, String(value)) + 20);
});
return scoreValue;
}
function chooseBestOptions(best, flat, scoreValue) {
if (!flat.length) return best;
if (!best.items.length || scoreValue > best.score || (scoreValue === best.score && scoreValue > 0 && flat.length < best.items.length) || (scoreValue === best.score && scoreValue <= 0 && flat.length > best.items.length)) {
best.items = flat;
best.score = scoreValue;
}
return best;
}
function internalOptionSets(seed, label, base) {
const queue = [{ value: seed, score: 0 }];
const seen = new WeakSet();
let steps = 0;
const best = { items: [], score: -1 };
while (queue.length && steps < 3000) {
const current = queue.shift();
steps += 1;
const node = current && current.value;
const inheritedScore = (current && current.score) || 0;
if (!walkable(node) || seen.has(node)) continue;
seen.add(node);
const scoreValue = Math.max(inheritedScore, objectContextScore(node, label, base));
if (Array.isArray(node)) {
chooseBestOptions(best, optionArrayValue(node), inheritedScore);
node.slice(0, 12).forEach(child => {
if (walkable(child)) queue.push({ value: child, score: Math.max(inheritedScore - 8, 0) });
});
continue;
}
chooseBestOptions(best, optionArrayValue(node.options), scoreValue + 10);
chooseBestOptions(best, optionArrayValue(node.selectProps && node.selectProps.options), scoreValue + 16);
chooseBestOptions(best, optionArrayValue(node.props && node.props.options), scoreValue + 12);
chooseBestOptions(best, optionArrayValue(node.memoizedProps && node.memoizedProps.options), scoreValue + 10);
chooseBestOptions(best, optionArrayValue(node.pendingProps && node.pendingProps.options), scoreValue + 8);
['selectProps', 'props', 'memoizedProps', 'pendingProps', 'return', 'child', 'sibling', 'stateNode'].forEach(key => {
const value = node[key];
if (walkable(value)) queue.push({ value, score: Math.max(scoreValue - 6, 0) });
});
Object.keys(node).slice(0, 30).forEach(key => {
if (key.startsWith('__')) return;
const value = node[key];
if (walkable(value)) queue.push({ value, score: Math.max(scoreValue - 10, 0) });
});
}
return best.items.length ? [best.items] : [];
}
function isReactSelectLike(el) {
if (!el) return false;
const raw = `${el.className || ''} ${el.id || ''}`.toLowerCase();
return raw.includes('react-select');
}
function internalRoots(trigger) {
const roots = [];
const add = el => {
if (el && !roots.includes(el)) roots.push(el);
};
const act = activator(trigger);
const shell = closestMatch(trigger, '.react-select,[class*="react-select"],[id^="dropdown"]') || (act && closestMatch(act, '.react-select,[class*="react-select"],[id^="dropdown"]'));
const control = closestMatch(trigger, '.react-select__control,[class*="__control"],[class*="-control"]') || (act && closestMatch(act, '.react-select__control,[class*="__control"],[class*="-control"]'));
add(trigger);
add(act);
add(shell);
add(control);
const scope = shell || control || parentLike(trigger) || ownerDoc(trigger).body;
ql('[id^="react-select-"],.react-select__control,[class*="__control"],[class*="-control"],[class*="react-select"]', scope).slice(0, 24).forEach(add);
const id = (trigger.id && /^react-select-\d+/.test(trigger.id) ? trigger.id : '') || (act && act.id && /^react-select-\d+/.test(act.id) ? act.id : '') || (scope && scope.querySelector && scope.querySelector('[id^="react-select-"]') && scope.querySelector('[id^="react-select-"]').id) || '';
if (id) {
const prefix = id.replace(/-(input|placeholder|listbox|option).*$/, '');
ql(`[id^="${escAttr(prefix)}"]`, scope).forEach(add);
}
if (!shell) {
ancestorChain(trigger, 4).forEach(add);
}
return roots;
}
function internalOptions(trigger, label) {
const base = reactBase(trigger) || reactBase(activator(trigger));
let best = [];
internalRoots(trigger).forEach(root => {
reactAttachments(root).forEach(obj => {
const hit = internalOptionSets(obj, label, base)[0] || [];
if (hit.length && (!best.length || hit.length < best.length)) best = hit;
});
});
if (!best.length) best = appDataOptions(label);
return best;
}
function safeDispatch(target, event) {
try {
target.dispatchEvent(event);
} catch (_) {}
}
function focusish(el) {
if (!el) return;
try {
el.focus({ preventScroll: true });
return;
} catch (_) {}
try {
el.focus();
} catch (_) {}
}
function clickish(el) {
if (!el) return;
const view = ownerWin(el);
const opts = { bubbles: true, cancelable: true, view };
focusish(el);
if (typeof view.PointerEvent === 'function') {
safeDispatch(el, new view.PointerEvent('pointerdown', opts));
safeDispatch(el, new view.PointerEvent('pointerup', opts));
}
safeDispatch(el, new view.MouseEvent('mousedown', opts));
safeDispatch(el, new view.MouseEvent('mouseup', opts));
safeDispatch(el, new view.MouseEvent('click', opts));
try {
el.click();
} catch (_) {}
}
function keyCodeFor(key) {
const map = {
Enter: 13,
Escape: 27,
' ': 32,
ArrowDown: 40,
ArrowUp: 38,
Tab: 9
};
return map[key] || 0;
}
function keyEvent(target, key) {
if (!target) return;
const view = ownerWin(target);
const code = keyCodeFor(key);
const init = { key, code: key === ' ' ? 'Space' : key, keyCode: code, which: code, bubbles: true, cancelable: true };
safeDispatch(target, new view.KeyboardEvent('keydown', init));
safeDispatch(target, new view.KeyboardEvent('keyup', init));
}
function activator(trigger) {
trigger = normalizeTrigger(trigger);
if (!trigger) return null;
if (trigger.matches && trigger.matches('button,[role="button"],[role="combobox"],input,select,textarea')) return trigger;
const picks = ql('button,[role="button"],[role="combobox"],input,select,[class*="control"],[class*="trigger"]', trigger).filter(vis);
if (picks[0]) return picks.sort((a, b) => score(b) - score(a) || area(b) - area(a))[0];
return trigger;
}
function popups(doc) {
const out = [];
const seen = new Set();
const add = el => {
if (!el || !vis(el) || seen.has(el)) return;
if (doc && ownerDoc(el) !== doc) return;
seen.add(el);
out.push(el);
};
q(POP).forEach(add);
q(OPT).forEach(el => add(closestMatch(el, '[role="listbox"],[role="menu"],ul,div') || parentLike(el)));
return out;
}
function optionSnapshot(doc) {
const seen = new Set();
popups(doc).forEach(popup => {
looseOpts(popup).forEach(item => seen.add(item));
});
q(OPT).forEach(el => {
if (doc && ownerDoc(el) !== doc) return;
if (vis(el)) {
const item = optionItem(el);
if (item) seen.add(item);
}
});
return seen;
}
function popupIdsFor(el) {
if (!el) return [];
return norm(`${el.getAttribute('aria-controls') || ''} ${el.getAttribute('aria-owns') || ''}`).split(/\s+/).filter(Boolean);
}
function popupFromAria(el) {
for (const id of popupIdsFor(el)) {
const node = deepGetById(id, ownerDoc(el));
if (!node) continue;
if (vis(node)) return node;
const wrap = closestMatch(node, '[role="listbox"],[role="menu"],[data-state="open"],[data-headlessui-state="open"],[class*="dropdown-menu"]');
if (wrap && vis(wrap)) return wrap;
}
return null;
}
function popupFromLabelledBy(el) {
if (!el || !el.id) return null;
const doc = ownerDoc(el);
const selector = `[aria-labelledby~="${cssEsc(el.id)}"]`;
const direct = q(selector).find(node => ownerDoc(node) === doc && vis(node));
if (direct) return direct;
const scoped = q('[aria-labelledby]').find(node => {
if (ownerDoc(node) !== doc || !vis(node)) return false;
const ids = norm(node.getAttribute('aria-labelledby')).split(/\s+/).filter(Boolean);
return ids.includes(el.id);
});
if (!scoped) return null;
if (scoped.matches && scoped.matches('[role="listbox"],[role="menu"],[class*="dropdown-menu"],[data-state="open"],[data-headlessui-state="open"]')) return scoped;
const wrap = closestMatch(scoped, '[role="listbox"],[role="menu"],[class*="dropdown-menu"],[data-state="open"],[data-headlessui-state="open"]');
return wrap && vis(wrap) ? wrap : scoped;
}
function dist(a, b) {
const ax = a.left + a.width / 2;
const ay = a.top + a.height / 2;
const bx = b.left + b.width / 2;
const by = b.top + b.height / 2;
return Math.hypot(ax - bx, ay - by);
}
function popupFor(trigger, before) {
const doc = ownerDoc(trigger);
const direct = popupFromAria(trigger) || popupFromAria(activator(trigger)) || popupFromLabelledBy(trigger) || popupFromLabelledBy(activator(trigger));
if (direct) return direct;
const base = reactBase(trigger) || reactBase(activator(trigger));
if (base) {
const react = deepGetById(`${base}-listbox`, doc) || q(`[id^="${escAttr(base)}-listbox"]`).find(el => ownerDoc(el) === doc && vis(el));
if (react && vis(react)) return react;
}
const current = popups(doc);
let list = current.filter(popup => !(before || []).includes(popup));
if (!list.length) list = current.length ? current : popups();
if (!list.length) return null;
const rect = trigger.getBoundingClientRect();
list.sort((a, b) => dist(rect, a.getBoundingClientRect()) - dist(rect, b.getBoundingClientRect()));
return list[0] || null;
}
function scrollContainers(root) {
const nodes = [root, ...ql('*', root)];
return nodes
.filter(el => {
if (!(el instanceof Element) || !vis(el)) return false;
const styles = ownerWin(el).getComputedStyle(el);
const overflow = `${styles.overflowY || ''} ${styles.overflow || ''}`;
return el.scrollHeight > el.clientHeight + 16 && /(auto|scroll)/.test(overflow);
})
.sort((a, b) => (b.scrollHeight - b.clientHeight) - (a.scrollHeight - a.clientHeight));
}
async function harvestPopupOptions(root) {
const found = new Set();
const addCurrent = () => {
looseOpts(root).forEach(item => found.add(item));
};
addCurrent();
const scroller = scrollContainers(root)[0];
if (scroller) {
const start = scroller.scrollTop;
const max = Math.max(scroller.scrollHeight - scroller.clientHeight, 0);
let stagnant = 0;
let lastCount = found.size;
scroller.scrollTop = 0;
await delay(70);
addCurrent();
for (let step = 0; step < MAX_VIRTUAL_SCROLL_STEPS; step += 1) {
const ratio = MAX_VIRTUAL_SCROLL_STEPS <= 1 ? 1 : step / (MAX_VIRTUAL_SCROLL_STEPS - 1);
scroller.scrollTop = Math.round(max * ratio);
await delay(70);
addCurrent();
if (found.size === lastCount) stagnant += 1;
else stagnant = 0;
lastCount = found.size;
if (ratio === 1 && stagnant >= 2) break;
}
scroller.scrollTop = start;
await delay(20);
}
return [...found];
}
async function openReactSelect(trigger, base) {
const root = normalizeTrigger(trigger);
const doc = ownerDoc(root);
clickish(root);
await delay(120);
const input = deepGetById(`${base}-input`, doc) || q(`[id^="${escAttr(base)}-input"]`).find(el => ownerDoc(el) === doc && vis(el));
if (input) {
focusish(input);
keyEvent(input, 'ArrowDown');
} else {
keyEvent(root, 'ArrowDown');
keyEvent(root, 'Enter');
}
await delay(220);
return deepGetById(`${base}-listbox`, doc) || q(`[id^="${escAttr(base)}-listbox"]`).find(el => ownerDoc(el) === doc && vis(el)) || null;
}
async function openJS(trigger, before) {
const root = normalizeTrigger(trigger);
const act = activator(root) || root;
const candidates = [act, root].filter((node, idx, arr) => node && arr.indexOf(node) === idx);
for (const candidate of candidates) {
clickish(candidate);
await delay(120);
let popup = popupFor(candidate, before);
if (popup) return popup;
keyEvent(candidate, 'ArrowDown');
await delay(140);
popup = popupFor(candidate, before);
if (popup) return popup;
keyEvent(candidate, 'Enter');
await delay(180);
popup = popupFor(candidate, before);
if (popup) return popup;
keyEvent(candidate, ' ');
await delay(140);
popup = popupFor(candidate, before);
if (popup) return popup;
}
return popupFor(act, before) || null;
}
async function closeJS(trigger) {
const root = normalizeTrigger(trigger);
const act = activator(root) || root;
[act, root, ownerDoc(act || root)].forEach(target => {
if (target) keyEvent(target, 'Escape');
});
await delay(60);
const body = ownerDoc(act || root).body;
if (body) {
try {
body.click();
} catch (_) {}
}
await delay(100);
}
function setNative(sel, value) {
const view = ownerWin(sel);
const desc = Object.getOwnPropertyDescriptor(view.HTMLSelectElement.prototype, 'value');
if (desc && desc.set) desc.set.call(sel, value);
else sel.value = value;
safeDispatch(sel, new view.Event('input', { bubbles: true }));
safeDispatch(sel, new view.Event('change', { bubbles: true }));
}
function keyName(obj, base) {
const seed = base || 'Field';
let out = seed;
let n = 2;
while (Object.prototype.hasOwnProperty.call(obj, out)) out = `${seed} (${n++})`;
return out;
}
function activeTargets(includeInputs) {
return [...new Set(includeInputs ? [...scanCache.triggers, ...scanCache.inputs] : [...scanCache.triggers])].sort(order);
}
function everyTarget() {
return [...new Set([...scanCache.triggers, ...scanCache.inputs])].sort(order);
}
function refreshScan() {
labelCache = new WeakMap();
appOptionCache.clear();
refreshDocOrder();
scanCache.triggers = collectTriggers();
scanCache.inputs = collectInputs();
}
function decodeSnippet(text) {
const doc = document;
const ta = doc.createElement('textarea');
ta.innerHTML = String(text || '');
return ta.value;
}
function snippetOptionsFromText(text) {
const source = decodeSnippet(text);
const out = [];
const patterns = [
/<Dropdown\.Item\b[^>]*>([\s\S]*?)<\/Dropdown\.Item>/gi,
/<option\b[^>]*>([\s\S]*?)<\/option>/gi,
/<MenuItem\b[^>]*>([\s\S]*?)<\/MenuItem>/gi,
/label\s*:\s*['"`]([^'"`]+)['"`]/gi
];
patterns.forEach(pattern => {
let match;
while ((match = pattern.exec(source))) {
const clean = norm(String(match[1]).replace(/<[^>]+>/g, ' '));
if (!clean || clean.length > 180 || placeholderLike(clean)) continue;
if (/^(dropdown\.?item|menuitem|option)$/i.test(clean)) continue;
out.push(clean);
}
});
return [...new Set(out)];
}
function codeSnippetOptions(trigger) {
const scopes = [
closestMatch(trigger, '.playgroundContainer_TGbA,[class*="playgroundContainer"],[class*="codeBlock"],.theme-code-block'),
...ancestorChain(trigger, 4)
].filter(Boolean);
const seen = new Set();
for (const scope of scopes) {
const texts = [];
ql('textarea,[code],pre code,code,[class*="token-line"]', scope).forEach(node => {
const text = ('value' in node && node.value) ? String(node.value) : norm(node.getAttribute && node.getAttribute('code')) || txt(node);
if (text && text.length > 40) texts.push(text);
});
const combined = texts.join('\n');
if (!combined || seen.has(combined)) continue;
seen.add(combined);
const opts = snippetOptionsFromText(combined);
if (opts.length) return opts;
}
return [];
}
function allDocs() {
const docs = [];
const seen = new Set();
walkRoots(root => {
const doc = root instanceof Document ? root : root.ownerDocument;
if (doc && !seen.has(doc)) {
seen.add(doc);
docs.push(doc);
}
});
return docs;
}
async function exportTargets(targets, statusNode) {
const out = {};
const ordered = [...targets].sort(order).map(trigger => normalizeTrigger(trigger)).filter(Boolean);
const records = [];
ordered.forEach(trigger => {
const input = isTextField(trigger);
const rawLabel = labelFor(trigger);
const label = keyName(out, input ? `${rawLabel} [INPUT]` : rawLabel);
out[label] = null;
records.push({ trigger, label, rawLabel, isInput: input });
});
const nativeRecords = [];
const nativeParents = [];
for (let i = 0; i < records.length; i += 1) {
const record = records[i];
const trigger = record.trigger;
if (record.isInput) {
out[record.label] = inputDetails(trigger);
continue;
}
const sel = trigger.tagName === 'SELECT' ? trigger : nearbySelect(trigger);
if (sel) {
record.sel = sel;
nativeRecords.push(record);
if (!nativeParents.includes(sel)) nativeParents.push(sel);
const opts = nativeOptions(sel).filter(opt => !isPlaceholderOpt(opt));
out[record.label] = opts.length ? opts.map(opt => formatOption(opt.text, opt.value)) : null;
continue;
}
statusNode.textContent = `Scanning ${i + 1}/${records.length}: "${record.label.slice(0, SCAN_STATUS_TRIM)}"...`;
const act = activator(trigger);
const base = reactBase(trigger) || reactBase(act);
const likelyReact = !!base || isReactSelectLike(trigger) || isReactSelectLike(act);
let internal = likelyReact ? internalOptions(trigger, record.rawLabel) : [];
if (!internal.length) internal = appDataOptions(record.rawLabel);
const preferDirect = likelyReact && (disabledish(trigger) || disabledish(act));
if (preferDirect && internal.length) {
out[record.label] = internal;
continue;
}
try {
let opts = [];
if (likelyReact && base) {
const list = await openReactSelect(trigger, base);
opts = list ? await harvestPopupOptions(list) : [];
}
if (!opts.length) {
const beforePopups = popups(ownerDoc(trigger));
let popup = await openJS(trigger, beforePopups);
opts = popup ? await harvestPopupOptions(popup) : [];
if (!opts.length) {
await delay(250);
popup = popup || popupFor(normalizeTrigger(trigger), beforePopups);
opts = popup ? await harvestPopupOptions(popup) : [];
}
}
if (!opts.length && internal.length) opts = internal;
if (!opts.length) opts = appDataOptions(record.rawLabel);
if (!opts.length) opts = codeSnippetOptions(trigger);
out[record.label] = [...new Set((opts || []).filter(Boolean))];
await closeJS(trigger);
} catch (err) {
console.error('Field Exporter: JS dropdown scan failed', err);
const fallback = internal.length ? internal : appDataOptions(record.rawLabel);
out[record.label] = fallback.length ? fallback : codeSnippetOptions(trigger);
}
}
for (const record of nativeRecords.filter(item => out[item.label] === null)) {
const mapping = {};
let foundParent = false;
for (const parentSel of nativeParents) {
if (parentSel === record.sel) continue;
const parentOpts = nativeOptions(parentSel).filter(opt => !isPlaceholderOpt(opt));
if (!parentOpts.length) continue;
const savedValue = parentSel.value;
let anyChild = false;
for (const pOpt of parentOpts) {
statusNode.textContent = `Probing "${record.label.slice(0, SCAN_STATUS_TRIM)}" <- "${pOpt.text}"...`;
setNative(parentSel, pOpt.value);
await delay(320);
const childOpts = nativeOptions(record.sel).filter(opt => !isPlaceholderOpt(opt));
if (childOpts.length) {
mapping[pOpt.text] = childOpts.map(opt => formatOption(opt.text, opt.value));
anyChild = true;
}
}
setNative(parentSel, savedValue);
await delay(160);
if (anyChild) {
foundParent = true;
break;
}
}
out[record.label] = foundParent && Object.keys(mapping).length ? mapping : [];
}
records.forEach(record => {
if (out[record.label] === null) out[record.label] = [];
});
return out;
}
function formatOutput(allFound, scannedCount, includeInputs) {
let inputFields = 0;
let dropdownItems = 0;
Object.entries(allFound).forEach(([label, value]) => {
if (value == null) return;
if (/\s\[INPUT\]$/.test(label)) {
inputFields += 1;
return;
}
if (Array.isArray(value)) dropdownItems += value.length;
else Object.values(value).forEach(arr => {
dropdownItems += arr.length;
});
});
const title = includeInputs ? 'FIELD EXPORT' : 'DROPDOWN EXPORT';
const summary = includeInputs ? `Scanned: ${scannedCount} | Inputs: ${inputFields} | Dropdown options: ${dropdownItems}` : `Scanned: ${scannedCount} | Found: ${dropdownItems} options`;
let text = `=== ${title} ===\n${summary}\n${'='.repeat(40)}\n\n`;
Object.entries(allFound).forEach(([label, value]) => {
if (value == null) return;
text += `${label}\n`;
if (Array.isArray(value)) {
if (!value.length) text += ' (no items)\n';
else value.forEach((item, index) => {
text += ` ${index + 1}. ${item}\n`;
});
} else {
const entries = Object.entries(value);
if (!entries.length) {
text += ' (no items)\n';
} else {
text += ' Options depend on parent selection:\n';
entries.forEach(([parentOpt, items]) => {
text += ` When parent = ${quoted(parentOpt)}:\n`;
items.forEach((item, index) => {
text += ` ${index + 1}. ${item}\n`;
});
});
}
}
text += '\n';
});
return { text, total: includeInputs ? inputFields + dropdownItems : dropdownItems };
}
const orig = new WeakMap();
let selecting = false;
let exporting = false;
let hover = null;
let includeInputs = false;
let selectionDocs = [];
const hl = (el, color) => {
if (!orig.has(el)) {
orig.set(el, { outline: el.style.outline, outlineOffset: el.style.outlineOffset, boxShadow: el.style.boxShadow });
}
el.style.outline = `2px solid ${color}`;
el.style.outlineOffset = '2px';
el.style.boxShadow = `0 0 0 4px ${color}33`;
};
const unhl = el => {
const state = orig.get(el);
if (!state) return;
el.style.outline = state.outline;
el.style.outlineOffset = state.outlineOffset;
el.style.boxShadow = state.boxShadow;
};
function refreshHighlights(chosen) {
const active = new Set(activeTargets(includeInputs));
everyTarget().forEach(el => {
if (chosen.has(el) && active.has(el)) hl(el, '#22c55e');
else unhl(el);
});
}
refreshScan();
box = document.createElement('div');
box.id = OID;
box.style.cssText = 'position:fixed;top:16px;right:16px;z-index:2147483647;width:360px;max-height:84vh;background:#111827;color:#e5e7eb;border:1px solid #374151;border-radius:12px;font:13px/1.45 system-ui,sans-serif;box-shadow:0 16px 48px rgba(0,0,0,.55);display:flex;flex-direction:column;user-select:none;';
box.innerHTML = `
<div id="h" style="padding:12px 14px;background:#0f172a;border-radius:12px 12px 0 0;cursor:move;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #1f2937;gap:8px;">
<div style="display:flex;flex-direction:column;gap:2px;min-width:0;">
<span style="font-weight:700;font-size:14px;">Field Exporter Pro</span>
<span style="font-size:11px;color:#94a3b8;">v${VERSION}</span>
</div>
<div style="display:flex;align-items:center;gap:6px;flex-shrink:0;">
<button id="rs" title="Rescan page" style="background:#1f2937;border:1px solid #374151;color:#cbd5e1;font-size:13px;cursor:pointer;line-height:1;padding:5px 8px;border-radius:7px;">Rescan</button>
<button id="x" title="Close" style="background:none;border:none;color:#9ca3af;font-size:18px;cursor:pointer;line-height:1;padding:0 2px;">×</button>
</div>
</div>
<div style="display:flex;border-bottom:1px solid #1f2937;">
<button id="ma" style="flex:1;padding:9px 0;border:none;background:#22c55e;color:#052e16;font-weight:700;cursor:pointer;font-size:12px;">Export All</button>
<button id="ms" style="flex:1;padding:9px 0;border:none;background:#1f2937;color:#94a3b8;font-weight:600;cursor:pointer;font-size:12px;">Select Mode</button>
</div>
<label style="display:flex;align-items:center;gap:8px;padding:8px 14px;border-bottom:1px solid #1f2937;font-size:12px;color:#d1d5db;cursor:pointer;">
<input id="ii" type="checkbox" style="accent-color:#22c55e;cursor:pointer;">
<span>Include input fields</span>
</label>
<div id="pa" style="padding:14px;display:flex;flex-direction:column;gap:10px;">
<div id="info" style="font-size:12px;color:#9ca3af;line-height:1.6;"></div>
<button id="ea" data-label="Export All Dropdowns" style="padding:10px;border-radius:8px;border:none;background:#22c55e;color:#052e16;font-weight:700;cursor:pointer;font-size:13px;">Export All Dropdowns</button>
</div>
<div id="ps" style="display:none;flex-direction:column;flex:1;overflow:hidden;">
<div style="padding:8px 14px;background:#0b3b62;font-size:12px;color:#bfdbfe;"><b>Hover</b> to highlight. <b>Click</b> a field on the page to toggle it. Press <b>Esc</b> to exit selection mode.</div>
<div style="padding:8px 14px;display:flex;gap:8px;border-bottom:1px solid #1f2937;">
<button id="sa" style="flex:1;padding:5px;border-radius:6px;border:1px solid #374151;background:#1f2937;color:#e5e7eb;cursor:pointer;font-size:11px;">All</button>
<button id="sc" style="flex:1;padding:5px;border-radius:6px;border:1px solid #374151;background:#1f2937;color:#e5e7eb;cursor:pointer;font-size:11px;">Clear</button>
</div>
<div id="list" style="overflow-y:auto;flex:1;padding:8px 6px;min-height:80px;max-height:calc(84vh - 290px);"></div>
<div style="padding:10px 14px;border-top:1px solid #1f2937;">
<button id="es" data-label="Export Selected" style="width:100%;padding:9px;border-radius:8px;border:none;background:#22c55e;color:#052e16;font-weight:700;cursor:pointer;font-size:13px;">Export Selected</button>
</div>
</div>
<div id="st" style="padding:4px 14px 10px;font-size:11px;color:#6b7280;text-align:center;min-height:20px;"></div>
`;
(document.body || document.documentElement).appendChild(box);
const st = box.querySelector('#st');
const list = box.querySelector('#list');
const pa = box.querySelector('#pa');
const ps = box.querySelector('#ps');
const ma = box.querySelector('#ma');
const ms = box.querySelector('#ms');
const ea = box.querySelector('#ea');
const es = box.querySelector('#es');
const ii = box.querySelector('#ii');
const info = box.querySelector('#info');
const chosen = new Set();
ii.checked = includeInputs;
function targetDisabled(el) {
return disabledish(el) || disabledish(activator(el)) || disabledish(nearbySelect(el));
}
function refreshInfo() {
const count = activeTargets(includeInputs).length;
info.innerHTML = `Exports <b style="color:#e5e7eb;">all ${count} ${includeInputs ? 'fields' : 'dropdowns'}</b> found on this page.<br>${includeInputs ? 'Supports dropdowns, text inputs, textareas, shadow DOM, iframes, hidden selects, generic field wrappers, React/portal menus, and virtualized option lists.' : 'Supports native selects, shadow DOM, iframes, hidden selects, generic field wrappers, React/portal menus, and virtualized option lists.'}`;
ea.dataset.label = includeInputs ? 'Export All Fields' : 'Export All Dropdowns';
if (!ea.disabled) ea.textContent = ea.dataset.label;
}
function activeSelected() {
const active = new Set(activeTargets(includeInputs));
return [...chosen].filter(el => active.has(el));
}
function pruneChosen() {
const active = new Set(everyTarget());
[...chosen].forEach(el => {
if (!active.has(el) || !el.isConnected) chosen.delete(el);
});
}
function status() {
const selectedCount = activeSelected().length;
if (ps.style.display !== 'none') st.textContent = `${selectedCount} / ${activeTargets(includeInputs).length} selected`;
es.style.opacity = selectedCount ? '1' : '0.45';
es.disabled = !selectedCount;
}
function build() {
pruneChosen();
const current = activeTargets(includeInputs);
list.innerHTML = '';
if (!current.length) {
list.innerHTML = `<div style="padding:16px;color:#6b7280;text-align:center;">${includeInputs ? 'No matching fields detected.' : 'No dropdowns detected.'}</div>`;
status();
return;
}
current.forEach(el => {
const row = document.createElement('div');
const cb = document.createElement('input');
const label = document.createElement('span');
const tag = document.createElement('span');
const fieldLabel = labelFor(el);
const kind = isTextField(el) ? 'input' : (el.tagName === 'SELECT' ? 'native' : 'js');
const disabled = targetDisabled(el);
row.style.cssText = 'display:flex;align-items:center;gap:8px;padding:7px 8px;border-radius:7px;cursor:pointer;transition:background .15s;margin-bottom:2px;';
cb.type = 'checkbox';
cb.style.cssText = 'width:15px;height:15px;accent-color:#22c55e;cursor:pointer;flex-shrink:0;';
label.textContent = fieldLabel.length > 46 ? `${fieldLabel.slice(0, 46)}...` : fieldLabel;
label.style.cssText = `overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;${disabled ? 'color:#9ca3af;' : ''}`;
tag.textContent = kind;
tag.style.cssText = `font-size:10px;padding:1px 5px;border-radius:4px;flex-shrink:0;background:${kind === 'native' ? '#1d4ed8' : kind === 'input' ? '#92400e' : '#7c3aed'};color:${kind === 'native' ? '#bfdbfe' : kind === 'input' ? '#fde68a' : '#ddd6fe'};`;
const state = document.createElement('span');
state.textContent = 'disabled';
state.style.cssText = 'font-size:10px;padding:1px 5px;border-radius:4px;flex-shrink:0;background:#3f3f46;color:#e4e4e7;';
const sync = () => {
cb.checked = chosen.has(el);
row.style.background = chosen.has(el) ? '#14532d55' : 'transparent';
};
const toggle = () => {
if (chosen.has(el)) chosen.delete(el);
else chosen.add(el);
sync();
refreshHighlights(chosen);
status();
};
row.append(cb, label, tag);
if (disabled) row.append(state);
row.addEventListener('click', toggle);
cb.addEventListener('click', event => {
event.preventDefault();
event.stopPropagation();
toggle();
});
row.addEventListener('mouseenter', () => {
row.style.background = chosen.has(el) ? '#14532d88' : '#1f2937';
if (!chosen.has(el)) hl(el, '#3b82f6');
});
row.addEventListener('mouseleave', () => {
row.style.background = chosen.has(el) ? '#14532d55' : 'transparent';
if (!chosen.has(el)) unhl(el);
});
sync();
list.appendChild(row);
});
status();
}
function eventTargetField(event) {
const path = typeof event.composedPath === 'function' ? event.composedPath() : [event.target];
return activeTargets(includeInputs).find(target => path.includes(target) || path.some(node => node instanceof Element && target.contains(node)));
}
function onOver(event) {
if (!selecting || exporting || box.contains(event.target)) return;
const el = eventTargetField(event);
if (!el) {
if (hover && !chosen.has(hover)) unhl(hover);
hover = null;
return;
}
if (hover && hover !== el && !chosen.has(hover)) unhl(hover);
hover = el;
if (!chosen.has(el)) hl(el, '#3b82f6');
}
function onClick(event) {
if (!selecting || exporting || box.contains(event.target)) return;
const el = eventTargetField(event);
if (!el) return;
event.preventDefault();
event.stopImmediatePropagation();
if (chosen.has(el)) chosen.delete(el);
else chosen.add(el);
refreshHighlights(chosen);
build();
status();
}
function onKeydown(event) {
if (!selecting) return;
if (event.key === 'Escape') {
event.preventDefault();
mode('all');
}
}
function listeners(on) {
if (on === selecting) return;
const docs = on ? allDocs() : selectionDocs;
docs.forEach(doc => {
doc[on ? 'addEventListener' : 'removeEventListener']('mouseover', onOver, true);
doc[on ? 'addEventListener' : 'removeEventListener']('click', onClick, true);
doc[on ? 'addEventListener' : 'removeEventListener']('keydown', onKeydown, true);
});
selectionDocs = on ? docs : [];
selecting = on;
if (!on && hover && !chosen.has(hover)) {
unhl(hover);
hover = null;
}
}
function rescanUI(message) {
refreshScan();
pruneChosen();
if (selecting) {
listeners(false);
listeners(true);
}
refreshHighlights(chosen);
refreshInfo();
if (ps.style.display !== 'none') build();
else status();
if (message) {
st.textContent = message;
st.style.color = '#60a5fa';
}
}
function mode(name) {
const allMode = name === 'all';
pa.style.display = allMode ? 'flex' : 'none';
ps.style.display = allMode ? 'none' : 'flex';
ma.style.background = allMode ? '#22c55e' : '#1f2937';
ma.style.color = allMode ? '#052e16' : '#94a3b8';
ms.style.background = allMode ? '#1f2937' : '#22c55e';
ms.style.color = allMode ? '#94a3b8' : '#052e16';
st.style.color = '#6b7280';
rescanUI();
if (allMode) {
listeners(false);
st.textContent = '';
} else {
listeners(true);
build();
status();
}
}
async function copyText(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return;
}
throw new Error('ClipboardUnavailable');
}
function openFallbackText(text) {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.cssText = 'position:fixed;top:5%;left:5%;width:90%;height:90%;z-index:2147483647;padding:12px;font:13px monospace;background:#111827;color:#e5e7eb;border:2px solid #374151;border-radius:8px;';
document.body.appendChild(ta);
ta.select();
const close = document.createElement('button');
close.textContent = 'Close';
close.style.cssText = 'position:fixed;top:6%;right:6%;z-index:2147483647;padding:6px 14px;background:#b91c1c;color:white;border:none;border-radius:6px;cursor:pointer;';
close.onclick = () => {
ta.remove();
close.remove();
};
document.body.appendChild(close);
}
async function run(which, btn) {
exporting = true;
listeners(false);
st.style.color = '#6b7280';
btn.textContent = 'Scanning...';
btn.disabled = true;
try {
rescanUI();
const selected = which === 'all' ? activeTargets(includeInputs) : activeSelected();
if (!selected.length) {
st.textContent = 'Nothing selected.';
st.style.color = '#f59e0b';
return;
}
const result = await exportTargets(selected, st);
window.__ddx_lastExport = {
version: VERSION,
exportedAt: new Date().toISOString(),
includeInputs,
count: selected.length,
results: result
};
const { text, total } = formatOutput(result, selected.length, includeInputs);
try {
await copyText(text);
st.textContent = `Copied ${total} ${includeInputs ? 'items' : 'options'} to the clipboard. Raw data is also in window.__ddx_lastExport.`;
st.style.color = '#22c55e';
} catch (err) {
if (!(err && err.name === 'NotAllowedError')) console.error('Field Exporter: clipboard export failed', err);
openFallbackText(text);
st.textContent = `Opened fallback text view with ${total} ${includeInputs ? 'items' : 'options'}. Raw data is also in window.__ddx_lastExport.`;
st.style.color = '#f59e0b';
}
} finally {
btn.textContent = btn.dataset.label;
btn.disabled = false;
exporting = false;
if (ps.style.display !== 'none') listeners(true);
}
}
box.querySelector('#x').addEventListener('click', cleanup);
box.querySelector('#rs').addEventListener('click', () => rescanUI('Rescanned page.'));
ma.addEventListener('click', () => mode('all'));
ms.addEventListener('click', () => mode('sel'));
ii.addEventListener('change', () => {
includeInputs = ii.checked;
refreshInfo();
refreshHighlights(chosen);
if (ps.style.display !== 'none') build();
else {
status();
st.textContent = '';
}
});
box.querySelector('#sa').addEventListener('click', () => {
activeTargets(includeInputs).forEach(el => chosen.add(el));
build();
refreshHighlights(chosen);
});
box.querySelector('#sc').addEventListener('click', () => {
chosen.clear();
everyTarget().forEach(unhl);
build();
});
ea.addEventListener('click', async () => {
await run('all', ea);
});
es.addEventListener('click', async () => {
if (!activeSelected().length) return;
await run('selected', es);
});
const hdr = box.querySelector('#h');
let drag = false;
let ox = 0;
let oy = 0;
hdr.addEventListener('mousedown', event => {
drag = true;
const rect = box.getBoundingClientRect();
ox = event.clientX - rect.left;
oy = event.clientY - rect.top;
event.preventDefault();
});
function onMove(event) {
if (!drag) return;
box.style.right = 'auto';
box.style.left = `${event.clientX - ox}px`;
box.style.top = `${event.clientY - oy}px`;
}
function onUp() {
drag = false;
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
function cleanup() {
listeners(false);
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
everyTarget().forEach(unhl);
box.remove();
}
box.__cleanup = cleanup;
refreshInfo();
mode('all');
})();
Author
Debug script that was used while making the react version.
javascript:(function(){
function isVisible(el) {
if (!el) return false;
const r = el.getBoundingClientRect();
if (r.width < 2 || r.height < 2) return false;
const s = getComputedStyle(el);
return s.display !== 'none' && s.visibility !== 'hidden' && s.opacity !== '0';
}
const report = [];
function log(section, data) {
report.push({ section, data });
}
// ── 1. All SELECT elements ─────────────────────────────────────────────────
const nativeSelects = [...document.querySelectorAll('select')];
log('NATIVE <select> elements', nativeSelects.map(el => ({
id: el.id || '(no id)',
name: el.name || '(no name)',
class: el.className || '(no class)',
visible: isVisible(el),
optionCount: el.options.length,
options: [...el.options].map(o => `"${o.text}" [value="${o.value}"]`),
rect: el.getBoundingClientRect(),
})));
// ── 2. ARIA-based candidates ───────────────────────────────────────────────
const ariaSelectors = [
'[role="combobox"]',
'[role="listbox"]',
'[aria-haspopup]',
'[aria-expanded]',
];
const ariaCandidates = [];
ariaSelectors.forEach(sel => {
document.querySelectorAll(sel).forEach(el => {
ariaCandidates.push({
selector: sel,
tag: el.tagName,
id: el.id || '(none)',
class: el.className?.toString().slice(0,80) || '(none)',
role: el.getAttribute('role'),
ariaHaspopup: el.getAttribute('aria-haspopup'),
ariaExpanded: el.getAttribute('aria-expanded'),
visible: isVisible(el),
text: el.textContent.trim().replace(/\s+/g,' ').slice(0,60),
rect: (() => { const r = el.getBoundingClientRect(); return {top:Math.round(r.top),left:Math.round(r.left),w:Math.round(r.width),h:Math.round(r.height)}; })(),
});
});
});
log('ARIA-based candidates', ariaCandidates);
// ── 3. All buttons on the page ────────────────────────────────────────────
const buttons = [...document.querySelectorAll('button,[role="button"]')].filter(isVisible);
log('Visible BUTTONS', buttons.map(el => ({
tag: el.tagName,
id: el.id || '(none)',
class: el.className?.toString().slice(0,80) || '(none)',
text: el.textContent.trim().replace(/\s+/g,' ').slice(0,60),
ariaHaspopup: el.getAttribute('aria-haspopup'),
ariaExpanded: el.getAttribute('aria-expanded'),
rect: (() => { const r = el.getBoundingClientRect(); return {top:Math.round(r.top),left:Math.round(r.left),w:Math.round(r.width),h:Math.round(r.height)}; })(),
})));
// ── 4. Field-sized elements with cursor:pointer ───────────────────────────
const pointerEls = [];
document.querySelectorAll('div[class],span[class],[tabindex="0"]').forEach(el => {
if (!isVisible(el)) return;
const r = el.getBoundingClientRect();
if (r.width < 80 || r.height < 24 || r.height > 100) return;
if (getComputedStyle(el).cursor !== 'pointer') return;
pointerEls.push({
tag: el.tagName,
id: el.id || '(none)',
class: el.className?.toString().slice(0,80) || '(none)',
text: el.textContent.trim().replace(/\s+/g,' ').slice(0,60),
rect: {top:Math.round(r.top),left:Math.round(r.left),w:Math.round(r.width),h:Math.round(r.height)},
});
});
log('Field-sized cursor:pointer elements', pointerEls);
// ── 5. Elements containing "Select…" placeholder text ────────────────────
const placeholderEls = [];
const placeholderRe = /^(select|choose|pick)(\.\.\.|\u2026|\s|$)/i;
document.querySelectorAll('*').forEach(el => {
if (!isVisible(el)) return;
const direct = [...el.childNodes]
.filter(n => n.nodeType === 3)
.map(n => n.textContent.trim())
.join(' ');
const inner = el.querySelector('[class*="placeholder"],[class*="Placeholder"],[data-placeholder]');
const text = (inner?.textContent || direct).trim();
if (placeholderRe.test(text)) {
placeholderEls.push({
tag: el.tagName,
id: el.id || '(none)',
class: el.className?.toString().slice(0,80) || '(none)',
text: el.textContent.trim().replace(/\s+/g,' ').slice(0,60),
rect: (() => { const r = el.getBoundingClientRect(); return {top:Math.round(r.top),left:Math.round(r.left),w:Math.round(r.width),h:Math.round(r.height)}; })(),
});
}
});
log('"Select…" placeholder text elements', placeholderEls);
// ── 6. What's currently in the DOM that looks like options ───────────────
const OPTION_SEL = '[role="option"],[role="menuitem"],[aria-selected],[data-value],li[class*="option"],div[class*="option"],li[class*="item"]';
const existingOptions = [...document.querySelectorAll(OPTION_SEL)].filter(isVisible);
log('Currently visible "option-like" elements (before any clicking)', existingOptions.map(el => ({
tag: el.tagName,
role: el.getAttribute('role'),
class: el.className?.toString().slice(0,60),
text: el.textContent.trim().replace(/\s+/g,' ').slice(0,60),
})));
// ── Render report ─────────────────────────────────────────────────────────
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:2147483647;background:rgba(0,0,0,0.85);display:flex;align-items:center;justify-content:center;font-family:monospace;';
const box = document.createElement('div');
box.style.cssText = 'background:#0d1117;color:#c9d1d9;width:90vw;height:90vh;border-radius:10px;display:flex;flex-direction:column;border:1px solid #30363d;overflow:hidden;';
const header = document.createElement('div');
header.style.cssText = 'padding:12px 16px;background:#161b22;border-bottom:1px solid #30363d;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;';
header.innerHTML = `<span style="font-weight:700;font-size:14px;color:#58a6ff;">🔍 Dropdown Debug Report</span><button id="__dbg_copy__" style="padding:5px 12px;background:#238636;color:white;border:none;border-radius:6px;cursor:pointer;font-size:12px;margin-right:8px;">📋 Copy JSON</button><button id="__dbg_close__" style="background:none;border:none;color:#8b949e;font-size:20px;cursor:pointer;line-height:1;">×</button>`;
const body = document.createElement('div');
body.style.cssText = 'overflow:auto;flex:1;padding:16px;font-size:12px;line-height:1.6;';
report.forEach(({ section, data }) => {
const sec = document.createElement('div');
sec.style.cssText = 'margin-bottom:24px;';
const title = document.createElement('div');
title.style.cssText = 'color:#f0883e;font-weight:700;font-size:13px;margin-bottom:8px;padding-bottom:4px;border-bottom:1px solid #21262d;';
title.textContent = `▶ ${section} (${Array.isArray(data) ? data.length : '—'})`;
const pre = document.createElement('pre');
pre.style.cssText = 'margin:0;background:#161b22;padding:12px;border-radius:6px;overflow-x:auto;color:#c9d1d9;white-space:pre-wrap;word-break:break-word;';
pre.textContent = JSON.stringify(data, null, 2);
sec.append(title, pre); body.appendChild(sec);
});
box.append(header, body);
overlay.appendChild(box);
document.body.appendChild(overlay);
overlay.querySelector('#__dbg_close__').onclick = () => overlay.remove();
overlay.querySelector('#__dbg_copy__').onclick = () => {
navigator.clipboard.writeText(JSON.stringify(report, null, 2)).then(() => {
overlay.querySelector('#__dbg_copy__').textContent = '✅ Copied!';
setTimeout(() => { overlay.querySelector('#__dbg_copy__')&&(overlay.querySelector('#__dbg_copy__').textContent='📋 Copy JSON'); }, 2000);
});
};
})();
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
🗂️ Dropdown Exporter — Bookmarklet
<select>support — reads options directly without clicking, zero side effects<label for="">,aria-label,name, or visible text, in that priority order▸ When parent = "..."labeling