Created
January 30, 2026 13:52
-
-
Save pohy/0a726eb96c0c060d9200dcd846497005 to your computer and use it in GitHub Desktop.
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
| // ==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