Last active
June 20, 2025 06:39
-
-
Save stanley2058/472b3b9bd21d187e65e26b2fd71aa340 to your computer and use it in GitHub Desktop.
Highlight all `ui-` class on a web 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
// ==UserScript== | |
// @name HackMD `ui-` classes highlighter | |
// @namespace http://hackmd.io/ | |
// @version 0.1.2 | |
// @description Show all `ui-` classes | |
// @author stanley2058, Yukaii | |
// @match https://hackmd.io/* | |
// @match https://local.localhost/* | |
// @icon https://www.google.com/s2/favicons?sz=64&domain=hackmd.io | |
// @grant GM.getValue | |
// @grant GM.setValue | |
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js | |
// ==/UserScript== | |
const setVal = (k, v) => typeof GM !== 'undefined' ? GM.setValue(k, v) : Promise.resolve(localStorage.setItem(k, v)); | |
const getVal = (k, v) => typeof GM !== 'undefined' ? GM.getValue(k, v) : Promise.resolve(localStorage.getItem(k) ?? v); | |
(async function() { | |
let highlighting = false; | |
let overlays = new Map(); | |
// Add tooltip styles | |
const style = document.createElement('style'); | |
style.textContent = ` | |
#highlight-toggle { | |
position: relative; | |
} | |
#highlight-toggle::after { | |
content: attr(title); | |
position: absolute; | |
background: #333; | |
color: white; | |
padding: 4px 8px; | |
border-radius: 4px; | |
font-size: 12px; | |
white-space: pre; | |
opacity: 0; | |
visibility: hidden; | |
transition: opacity 0.2s, visibility 0.2s; | |
pointer-events: none; | |
bottom: 120%; | |
left: 50%; | |
transform: translateX(-50%); | |
} | |
#highlight-toggle::before { | |
content: ''; | |
position: absolute; | |
border: 5px solid transparent; | |
border-top-color: #333; | |
opacity: 0; | |
visibility: hidden; | |
transition: opacity 0.2s, visibility 0.2s; | |
pointer-events: none; | |
bottom: calc(120% - 10px); | |
left: 50%; | |
transform: translateX(-50%); | |
} | |
#highlight-toggle:hover::after, | |
#highlight-toggle:hover::before { | |
opacity: 1; | |
visibility: visible; | |
} | |
`; | |
document.head.appendChild(style); | |
function validatePosition(position) { | |
const defaultPosition = { left: '10px', bottom: '10px' }; | |
if (!position) return defaultPosition; | |
const left = parseInt(position.left); | |
const bottom = parseInt(position.bottom); | |
if (isNaN(left) || isNaN(bottom) || | |
left < 0 || left > window.innerWidth - 40 || | |
bottom < 0 || bottom > window.innerHeight - 40) { | |
return defaultPosition; | |
} | |
return { | |
left: `${left}px`, | |
bottom: `${bottom}px` | |
}; | |
} | |
const savedPosition = validatePosition(JSON.parse(await getVal('highlightBtnPosition', null))); | |
const isVisible = await getVal('highlightBtnVisible') !== 'false'; | |
const toggleBtn = document.createElement('button'); | |
toggleBtn.id = 'highlight-toggle'; | |
toggleBtn.setAttribute('aria-label', 'Toggle UI Class Highlighter'); | |
toggleBtn.setAttribute('title', 'Toggle UI Class Highlighter\nLeft click: Toggle highlight\nRight click: Hide button\nCtrl+click: Remove overlay'); | |
toggleBtn.setAttribute('draggable', 'true'); | |
Object.assign(toggleBtn.style, { | |
position: 'fixed', | |
left: savedPosition.left, | |
bottom: savedPosition.bottom, | |
zIndex: '10002', | |
padding: '8px', | |
background: '#FFA500', | |
border: 'none', | |
borderRadius: '50%', | |
cursor: 'pointer', | |
width: '40px', | |
height: '40px', | |
display: isVisible ? 'flex' : 'none', | |
alignItems: 'center', | |
justifyContent: 'center', | |
opacity: '0.95', | |
}); | |
toggleBtn.innerHTML = '🔍'; | |
toggleBtn.addEventListener('mouseenter', () => { | |
toggleBtn.style.background = '#FF8C00'; | |
}); | |
toggleBtn.addEventListener('mouseleave', () => { | |
toggleBtn.style.background = '#FFA500'; | |
}); | |
const dropOverlay = document.createElement('div'); | |
Object.assign(dropOverlay.style, { | |
position: 'fixed', | |
left: 0, | |
top: 0, | |
zIndex: '10001', | |
width: '100dvw', | |
height: '100dvh', | |
display: 'none', | |
}); | |
let isDragging = false; | |
let currentX; | |
let currentY; | |
let initialX; | |
let initialY; | |
let xOffset = 0; | |
let yOffset = 0; | |
toggleBtn.addEventListener('dragstart', (e) => { | |
dropOverlay.style.display = 'block'; | |
initialX = e.clientX - xOffset; | |
initialY = e.clientY - yOffset; | |
}); | |
dropOverlay.addEventListener('dragover', (e) => { | |
e.preventDefault(); | |
}); | |
dropOverlay.addEventListener('drop', (e) => { | |
dropOverlay.style.display = 'none'; | |
currentX = e.clientX - initialX; | |
currentY = e.clientY - initialY; | |
xOffset = currentX; | |
yOffset = currentY; | |
const { width, height } = toggleBtn.getBoundingClientRect(); | |
const left = Math.max(0, Math.min(window.innerWidth - 40, e.clientX - width / 2)); | |
const bottom = Math.max(0, Math.min(window.innerHeight - 40, window.innerHeight - e.clientY - height / 2)); | |
toggleBtn.style.left = `${left}px`; | |
toggleBtn.style.bottom = `${bottom}px`; | |
setVal('highlightBtnPosition', JSON.stringify({ | |
left: toggleBtn.style.left, | |
bottom: toggleBtn.style.bottom | |
})); | |
}); | |
toggleBtn.addEventListener('contextmenu', (e) => { | |
e.preventDefault(); | |
toggleBtn.style.display = 'none'; | |
setVal('highlightBtnVisible', 'false'); | |
}); | |
async function enableUIClassDebug () { | |
const currentPosition = validatePosition(JSON.parse(await getVal('highlightBtnPosition', null))); | |
toggleBtn.style.left = currentPosition.left; | |
toggleBtn.style.bottom = currentPosition.bottom; | |
toggleBtn.style.display = 'flex'; | |
setVal('highlightBtnVisible', 'true'); | |
setVal('highlightBtnPosition', JSON.stringify(currentPosition)); | |
console.log('UI Class Debug enabled. The button has been restored to a valid position.'); | |
}; | |
window.enableUIClassDebug = enableUIClassDebug; | |
function getTrackingClasses(element) { | |
return Array.from(element.classList) | |
.filter(className => className.startsWith('ui-')); | |
} | |
function createOverlay(element) { | |
const rect = element.getBoundingClientRect(); | |
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; | |
const scrollTop = window.pageYOffset || document.documentElement.scrollTop; | |
const overlay = document.createElement('div'); | |
Object.assign(overlay.style, { | |
position: 'absolute', | |
background: 'rgba(255, 255, 0, 0.3)', | |
border: '2px solid #FFA500', | |
zIndex: '10000', | |
left: (rect.left + scrollLeft) + 'px', | |
top: (rect.top + scrollTop) + 'px', | |
width: rect.width + 'px', | |
height: rect.height + 'px' | |
}); | |
const label = document.createElement('div'); | |
Object.assign(label.style, { | |
position: 'absolute', | |
background: '#FFA500', | |
color: 'black', | |
padding: '2px 6px', | |
borderRadius: '3px', | |
fontSize: '12px', | |
fontFamily: 'monospace', | |
zIndex: '10001', | |
display: 'none', | |
left: (rect.left + scrollLeft) + 'px', | |
whiteSpace: 'nowrap', | |
transform: '' | |
}); | |
const trackingClasses = getTrackingClasses(element).join(', '); | |
label.textContent = trackingClasses; | |
overlay.addEventListener('mouseenter', () => { | |
const viewportHeight = window.innerHeight; | |
const viewportWidth = window.innerWidth; | |
const elementCenterY = rect.top + (rect.height / 2); | |
const elementCenterX = rect.left + (rect.width / 2); | |
if (elementCenterY < viewportHeight / 2) { | |
label.style.top = (rect.bottom + scrollTop + 4) + 'px'; | |
} else { | |
label.style.top = (rect.top + scrollTop - 24) + 'px'; | |
} | |
if (elementCenterX < viewportWidth / 2) { | |
label.style.transform = ''; | |
} else { | |
label.style.transform = `translateX(calc(-100% + ${rect.width}px))`; | |
} | |
label.style.display = 'block'; | |
}); | |
overlay.addEventListener('mouseleave', () => { | |
label.style.display = 'none'; | |
}); | |
overlay.addEventListener('click', (e) => { | |
window.navigator.clipboard | |
.writeText(label.innerText) | |
.catch(console.error); | |
if (!e.ctrlKey && !e.metaKey) return; | |
document.body.removeChild(overlay); | |
document.body.removeChild(label); | |
e.preventDefault(); | |
e.stopPropagation(); | |
}); | |
document.body.appendChild(overlay); | |
document.body.appendChild(label); | |
return [overlay, label]; | |
} | |
function toggleHighlights() { | |
highlighting = !highlighting; | |
if (highlighting) { | |
$("[class^='ui-'],[class*=' ui-']").each((_, element) => { | |
const [overlay, label] = createOverlay(element); | |
overlays.set(element, [overlay, label]); | |
}); | |
} else { | |
for (const [overlay, label] of overlays.values()) { | |
overlay.remove(); | |
label.remove(); | |
} | |
overlays.clear(); | |
} | |
} | |
toggleBtn.addEventListener('click', toggleHighlights); | |
document.body.appendChild(dropOverlay); | |
document.body.appendChild(toggleBtn); | |
window.addEventListener('keydown', async (e) => { | |
const shouldTriggerToggle = | |
(e.ctrlKey && e.shiftKey && e.key === 'U') || | |
(e.metaKey && e.shiftKey && e.key === 'u'); | |
const shouldToggleButtonDisplay = | |
(e.ctrlKey && e.shiftKey && e.key === 'H') || | |
(e.metaKey && e.shiftKey && e.key === 'h'); | |
if (shouldTriggerToggle) { | |
e.preventDefault(); | |
toggleHighlights(); | |
} | |
if (shouldToggleButtonDisplay) { | |
e.preventDefault(); | |
const disabled = await getVal('highlightBtnVisible') === 'false'; | |
if (disabled) { | |
enableUIClassDebug(); | |
} else { | |
toggleBtn.style.display = 'none'; | |
setVal('highlightBtnVisible', 'false'); | |
} | |
} | |
}); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Minified bookmarklet of v0.1.2 and instructions generated by Gemini 2.5 Pro:
How to use:
javascript:...
code block into the URL/Location field of the bookmark.