Skip to content

Instantly share code, notes, and snippets.

@pohy
Created January 30, 2026 13:52
Show Gist options
  • Select an option

  • Save pohy/0a726eb96c0c060d9200dcd846497005 to your computer and use it in GitHub Desktop.

Select an option

Save pohy/0a726eb96c0c060d9200dcd846497005 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Adminer table search
// @namespace https://davidpohan.cz/
// @version 2026-01-30
// @description Dynamic table search. Ctrl/Cmd+K or / to focus the search input. Enter to "select" the first table. Ctrl/Cmd+Enter to "show structure" of the first table. Up/Down or Ctrl/Cmd+N/P to "navigate" through table list.
// @author pohy
// @match http://localhost:1666/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=165.73
// @grant none
// ==/UserScript==
(async function() {
'use strict';
const CONTAINER_ID = 'adminer-table-search-container'
const FOCUSED_ID = 'adminer-table-search-focused'
const tablesEl = document.getElementById('tables')
if (!tablesEl) return
applyCustomStyles()
await attachFuzzyScript()
const sqlAreaEl = document.querySelector('.sqlarea[contenteditable]')
const tablesSearchableEl = tablesEl.cloneNode(true)
tablesEl.style.setProperty('visibility', 'hidden')
tablesEl.id = 'tables_orig'
tablesEl.parentElement.insertBefore(tablesSearchableEl, tablesEl)
const initialQ = localStorage.getItem(getStorageKey('q')) ?? ''
const inputEl = document.createElement('input')
inputEl.addEventListener('input', onSearchInput)
inputEl.addEventListener('keydown', onSearchKeydown)
inputEl.value = initialQ
if (sqlAreaEl) {
sqlAreaEl.focus()
} else {
inputEl.setAttribute('autofocus', 'autofocus')
const onFirstFocus = () => {
inputEl.select()
inputEl.removeEventListener('focus', onFirstFocus)
}
inputEl.addEventListener('focus', onFirstFocus)
}
const searchStatusEl = document.createElement('span')
const searchContainerEl = document.createElement('div')
searchContainerEl.id = CONTAINER_ID
searchContainerEl.appendChild(inputEl)
searchContainerEl.appendChild(searchStatusEl)
tablesSearchableEl.parentElement.insertBefore(searchContainerEl, tablesSearchableEl)
updateTableList(inputEl.value)
window.addEventListener('keydown', (e) => {
const isCmdK = e.key.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey)
const isSlash = e.key === '/'
if (!isCmdK && !isSlash) {
return;
}
e.preventDefault()
e.stopPropagation()
inputEl.focus()
inputEl.select()
}, true)
function onSearchInput(e) {
const q = e.currentTarget.value.toLowerCase()
localStorage.setItem(getStorageKey('q'), q)
updateTableList(q)
}
function onSearchKeydown(e) {
const key = e.key.toLowerCase()
const isEnter = key === 'enter'
const isCmdEnter = isEnter && (e.metaKey || e.ctrlKey)
const isUp = key === 'arrowup' || (key === 'p' && e.ctrlKey)
const isDown = key === 'arrowdown' || (key === 'n' && e.ctrlKey)
if (isCmdEnter) {
e.preventDefault()
e.stopPropagation()
getFocusedTableEl()?.querySelector('a.structure')?.click()
return
}
if (isEnter) {
e.preventDefault()
getFocusedTableEl()?.querySelector('a.select')?.click()
return
}
if (isUp) {
e.preventDefault()
const focusedTableEl = getFocusedTableEl()
const prevTableEl = focusedTableEl.previousElementSibling
? focusedTableEl.previousElementSibling
: focusedTableEl.parentElement.lastElementChild
focusTableEl(prevTableEl)
}
if (isDown) {
e.preventDefault()
const focusedTableEl = getFocusedTableEl()
const nextTableEl = focusedTableEl.nextElementSibling
? focusedTableEl.nextElementSibling
: focusedTableEl.parentElement.firstElementChild
focusTableEl(nextTableEl)
}
}
function updateTableList(q) {
const allNodes = [...tablesEl.childNodes]
const tableCount = allNodes.filter(node => node.tagName == 'LI').length // TODO: The count is likely wrong `#tables li a.structure
const fuzzyOptions = {
extract: (node) => {
if (node.tagName != 'LI') return ''
return node.children[1].textContent.toLowerCase()
},
}
const fuzzyFilteredChildren = typeof fuzzy === 'undefined'
? allNodes.map(n => ({ original: n }) )
: fuzzy.filter(q, allNodes, fuzzyOptions)
const filteredChildren = fuzzyFilteredChildren
.map(r => r.original)
.map(node => {
const clonedNode = node.cloneNode(true)
const selectAnchorNode = clonedNode.childNodes[0]
if (selectAnchorNode !== undefined) {
selectAnchorNode.tabIndex = 0
}
return clonedNode
})
tablesSearchableEl.replaceChildren(...filteredChildren)
searchStatusEl.innerText = `${Math.min(tableCount, filteredChildren.length)}/${tableCount}`
// Only focus the "active" table on initial load. On search change, focus the first table
const activeTableEl = q === initialQ ? tablesSearchableEl.querySelector('#tables li:has(a.active)') : null
focusTableEl(
activeTableEl ?? tablesSearchableEl.firstChild
)
}
function applyCustomStyles() {
const styleEl = document.createElement('style')
styleEl.textContent = `
#tables:hover {
overflow: visible;
}
#${CONTAINER_ID} {
padding: 0.8em 0 0 1em;
display: flex;
justify-content: space-between;
align-items: center;
}
#${FOCUSED_ID} {
text-decoration: underline;
}
`;
document.head.appendChild(styleEl)
}
function attachFuzzyScript() {
return new Promise(resolve => {
const scriptEl = document.createElement('script')
scriptEl.type = 'text/javascript'
scriptEl.src = 'https://cdn.githubraw.com/mattyork/fuzzy/39e3f256/fuzzy-min.js'
scriptEl.addEventListener('load', () => resolve())
document.body.appendChild(scriptEl)
})
}
function getStorageKey(key) {
const prefix = '@adminerSearch'
const host = window.location.host
const db = document.querySelector('#dbs select[name="db"]').value
return [prefix, host, db, key].join(':')
}
function focusTableEl(tableEl) {
const alreadyFocusedEl = document.getElementById(FOCUSED_ID)
if (alreadyFocusedEl) {
alreadyFocusedEl.id = ""
}
tableEl.id = FOCUSED_ID
}
function getFocusedTableEl() {
return document.getElementById(FOCUSED_ID)
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment