Something went wrong. Try again.
hover google sheets notes with an image url to preview the image
Something went wrong. Try again.
3.4 kB · 95 lines · Created 2mo ago
JavaScript
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495// ==UserScript==// @name sheets note image preview// @match https://docs.google.com/spreadsheets/d/1KXHoBQPtwZbR135Gg185oguYL19rjz5QnS-qWCjYOFg/*// @grant none// @run-at document-idle// ==/UserScript==
(function () { 'use strict'; const URL_RE = /^https?:\/\/\S+\.(?:webp|png|jpe?g|gif|avif)(?:\?\S*)?$/i; const BOX = 320; const dims = new Map(); // url -> {w,h}
const style = document.createElement('style'); style.textContent = ` .annotation-bubble[data-preview] { overflow: visible !important; } .annotation-bubble[data-preview] textarea.annotation-editable { visibility: hidden !important; } .annotation-bubble[data-preview="pending"] { opacity: 0 !important; } .annotation-bubble img.__preview { position: absolute; top: 0; left: 0; background: #fff; border-radius: 6px; pointer-events: none; }`; document.documentElement.appendChild(style);
const urlOf = b => { const ta = b.querySelector('textarea.annotation-editable'); const t = ((ta ? ta.value : b.textContent) || '').trim(); return URL_RE.test(t) ? t : null; };
const size = (b, w, h) => { b.style.width = w + 'px'; b.style.height = h + 'px'; const inner = b.firstElementChild; if (inner) { inner.style.width = w + 'px'; inner.style.height = h + 'px'; } };
function apply(b) { const url = urlOf(b); if (url === b.__url) return; // idempotent b.__url = url; b.querySelector('img.__preview')?.remove();
if (!url) { // plain note: undo everything b.removeAttribute('data-preview'); b.style.width = b.style.height = ''; const inner = b.firstElementChild; if (inner) inner.style.width = inner.style.height = ''; return; }
const img = new Image(); img.className = '__preview'; img.src = url;
const cached = dims.get(url); if (cached) { // seen before: size now, no hidden phase b.setAttribute('data-preview', 'ready'); img.style.width = cached.w + 'px'; img.style.height = cached.h + 'px'; size(b, cached.w, cached.h); } else { b.setAttribute('data-preview', 'pending'); img.decode().then(() => { const k = BOX / Math.max(img.naturalWidth, img.naturalHeight); const w = Math.round(img.naturalWidth * k); const h = Math.round(img.naturalHeight * k); dims.set(url, { w, h }); if (b.__url !== url) return; // moved on already img.style.width = w + 'px'; img.style.height = h + 'px'; size(b, w, h); b.setAttribute('data-preview', 'ready'); }).catch(() => { if (b.__url === url) { b.__url = null; b.removeAttribute('data-preview'); } }); } b.appendChild(img); }
const scan = () => document.querySelectorAll('.annotation-bubble').forEach(apply);
new MutationObserver(muts => { for (const m of muts) { if (m.target.closest?.('.annotation-bubble')) { scan(); return; } for (const n of m.addedNodes) if (n.nodeType === 1 && n.querySelector?.('.annotation-bubble')) { scan(); return; } } }).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'], });
setInterval(scan, 250); // backstop for silent value swaps})();