Created
September 18, 2025 20:12
-
-
Save johnmellor/ef5ecf555f28b324b045e0c93548f0da to your computer and use it in GitHub Desktop.
Creates a link that will scroll to the selected text, and lets you copy it to your clipboard
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: | |
| /* Link to text fragment bookmarklet. Works on Chrome for Android (https://paul.kinlan.me/use-bookmarklets-on-chrome-on-android/) too, but you'll have to select text after activating the bookmarklet not before. Hackily thrown together with AI. */ | |
| (function () { | |
| const STABLE_MS = 1500; | |
| /* Helpers */ | |
| const getSelectedText = () => (window.getSelection()?.toString() || "").trim(); | |
| const buildTextFragmentURL = (text) => location.href.split('#')[0] + (location.hash.replace(/:~:.*/, '') || '#') + ':~:text=' + encodeURIComponent(text); | |
| async function copyToClipboard(text) { | |
| /* Prefer Async Clipboard, then execCommand; last resort prompt */ | |
| if (navigator.clipboard && window.isSecureContext) { | |
| try { await navigator.clipboard.writeText(text); return true; } catch (e) { } | |
| } | |
| try { | |
| const ta = document.createElement('textarea'); | |
| ta.value = text; | |
| ta.setAttribute('readonly', ''); | |
| ta.style.position = 'fixed'; | |
| ta.style.top = '-9999px'; | |
| document.body.appendChild(ta); | |
| ta.select(); | |
| const ok = document.execCommand('copy'); | |
| document.body.removeChild(ta); | |
| if (ok) return true; | |
| } catch (e) {} | |
| prompt('Copy this URL:', text); | |
| return false; | |
| } | |
| /* Top banner UI */ | |
| let banner, msgEl, copyBtn, cancelBtn, debounce = null, last = ''; | |
| function ensureBanner() { | |
| if (banner) return; | |
| banner = document.createElement('div'); | |
| banner.style.cssText = 'position:fixed;left:50%;transform:translateX(-50%);top:12px;max-width:92vw;z-index:2147483647;background:rgba(0,0,0,.85);color:#fff;padding:10px 12px;border-radius:10px;box-shadow:0 4px 14px rgba(0,0,0,.35);display:flex;gap:8px;align-items:center;font:14px/1.35 -apple-system,system-ui,Segoe UI,Roboto,Arial,sans-serif'; | |
| msgEl = document.createElement('span'); msgEl.style.flex = '1 1 auto'; | |
| copyBtn = document.createElement('button'); copyBtn.textContent = 'Copy link'; copyBtn.style.cssText = 'flex:0 0 auto;padding:6px 10px;border:none;border-radius:8px;background:#4caf50;color:#fff;font-weight:600;cursor:pointer'; | |
| cancelBtn = document.createElement('button'); cancelBtn.textContent = '×'; cancelBtn.style.cssText = 'flex:0 0 auto;padding:4px 8px;border:none;border-radius:8px;background:rgba(255,255,255,.15);color:#fff;cursor:pointer'; | |
| cancelBtn.addEventListener('click', () => cleanup('Canceled')); | |
| banner.append(msgEl, copyBtn, cancelBtn); | |
| (document.documentElement || document.body).appendChild(banner); | |
| } | |
| function showWaiting() { ensureBanner(); copyBtn.style.display = 'none'; msgEl.textContent = "Select text to link to"; } | |
| function showReady(url) { | |
| ensureBanner(); | |
| msgEl.textContent = ''; | |
| copyBtn.style.display = 'inline-block'; | |
| copyBtn.onclick = async () => { const ok = await copyToClipboard(url); cleanup(ok ? 'Copied!' : 'Shown prompt.'); }; | |
| } | |
| function cleanup(message) { | |
| document.removeEventListener('selectionchange', onSelectionChange, true); | |
| if (debounce) clearTimeout(debounce); | |
| debounce = null; | |
| if (banner) { | |
| msgEl.textContent = message || 'Done'; | |
| copyBtn.style.display = 'none'; | |
| setTimeout(() => { if (banner) { banner.remove(); banner = null; } }, 700); | |
| } | |
| } | |
| /* Waits for selection */ | |
| function onSelectionChange() { | |
| const cur = getSelectedText(); | |
| if (!cur) { | |
| last = ''; | |
| if (debounce) clearTimeout(debounce); | |
| showWaiting(); | |
| return; | |
| } | |
| if (cur !== last) { | |
| last = cur; | |
| if (debounce) clearTimeout(debounce); | |
| debounce = setTimeout(() => showReady(buildTextFragmentURL(cur)), STABLE_MS); | |
| } | |
| } | |
| /* Initialization */ | |
| const initial = getSelectedText(); | |
| if (initial) { | |
| showReady(buildTextFragmentURL(initial)); | |
| } else { | |
| showWaiting(); | |
| document.addEventListener('selectionchange', onSelectionChange, true); | |
| } | |
| document.addEventListener('visibilitychange', () => { if (document.hidden) cleanup(); }, { once: true }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment