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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Debug script that was used while making the react version.