Skip to content

Instantly share code, notes, and snippets.

@minanagehsalalma
Last active March 25, 2026 17:24
Show Gist options
  • Select an option

  • Save minanagehsalalma/22178e2b2dc914235b848b357f3b5b1e to your computer and use it in GitHub Desktop.

Select an option

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.
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;">&times;</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);
})();
@minanagehsalalma
Copy link
Copy Markdown
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