Text selection menu like Ganjoor's #26
@ -48,6 +48,13 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک
|
||||
<div id="kb" class="wrap kb" hidden></div>
|
||||
</header>
|
||||
<main class="wrap"><slot /></main>
|
||||
<div id="selmenu" class="selmenu" role="toolbar" aria-label="منتخب عبارت" hidden>
|
||||
<button type="button" data-act="meaning" title="معنی اور تلفظ">معنی</button>
|
||||
<button type="button" data-act="search" title="دیوان میں تلاش">تلاش</button>
|
||||
<button type="button" data-act="copy" title="حوالے کے ساتھ کاپی">کاپی</button>
|
||||
<button type="button" data-act="share" title="اس عبارت کا لنک">شیئر</button>
|
||||
<button type="button" data-act="google" title="گوگل میں تلاش">گوگل</button>
|
||||
</div>
|
||||
<aside id="dict" class="dict" hidden aria-live="polite">
|
||||
<button type="button" class="dict-close" aria-label="بند کریں">✕</button>
|
||||
<div class="dict-body"></div>
|
||||
@ -258,20 +265,57 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک
|
||||
dictBody.replaceChildren(...out, src);
|
||||
}
|
||||
const TRIM = /["'«»()\[\]،۔؟؛!:.,\-–—_]+|^\s+|\s+$/g; // punctuation and dashes anywhere, outer spaces
|
||||
let selTimer;
|
||||
// selection in the reading text: Ganjoor-style menu by the selection; a single word also opens the sidebar
|
||||
const menu = document.getElementById('selmenu');
|
||||
const oneWord = (t) => { const w = t.replace(TRIM, '').replace(/\u0614/g, ''); return /^[\p{Script=Arabic}\p{M}\u200C]{1,40}$/u.test(w) ? w : null; };
|
||||
let selText = '', selTimer;
|
||||
const hideMenu = () => { menu.hidden = true; };
|
||||
document.addEventListener('selectionchange', () => {
|
||||
clearTimeout(selTimer);
|
||||
selTimer = setTimeout(() => {
|
||||
const sel = getSelection();
|
||||
if (!sel || sel.isCollapsed || !main.contains(sel.anchorNode)) return;
|
||||
const w = sel.toString().replace(TRIM, '').replace(/\u0614/g, ''); // drop the takhallus sign
|
||||
if (/\s/.test(w)) return; // one word only
|
||||
if (/^[\p{Script=Arabic}\p{M}\u200C]{1,40}$/u.test(w)) showWord(w);
|
||||
const inText = sel && !sel.isCollapsed && main.contains(sel.anchorNode) && !sel.anchorNode.parentElement?.closest('form, input, textarea');
|
||||
selText = inText ? sel.toString().replace(/\u0614/g, '').trim() : '';
|
||||
if (!selText) return hideMenu();
|
||||
const w = oneWord(selText);
|
||||
menu.querySelector('[data-act=meaning]').hidden = !w;
|
||||
menu.hidden = false;
|
||||
// below the selection, centred, kept inside the viewport (absolute: moves with the page)
|
||||
const r = sel.getRangeAt(0).getBoundingClientRect(), m = menu.getBoundingClientRect();
|
||||
menu.style.top = `${r.bottom + scrollY + 8}px`;
|
||||
menu.style.left = `${Math.max(8, Math.min(innerWidth - m.width - 8, r.left + r.width / 2 - m.width / 2)) + scrollX}px`;
|
||||
if (w) showWord(w);
|
||||
}, 350);
|
||||
});
|
||||
menu.onmousedown = (e) => e.preventDefault(); // keep the selection
|
||||
const cite = () => { // "text\n— poet، title\nlink" for copying
|
||||
const art = document.querySelector('[data-cite]');
|
||||
return `${selText}\n— ${art?.dataset.cite ?? document.title}\n${location.origin}${location.pathname}`;
|
||||
};
|
||||
const flash = (b, msg) => { const t = b.textContent; b.textContent = msg; setTimeout(() => (b.textContent = t), 1200); };
|
||||
menu.onclick = async (e) => {
|
||||
const b = e.target.closest('button'); if (!b) return;
|
||||
const q = /\s/.test(selText) ? `"${selText}"` : selText, enc = encodeURIComponent(selText);
|
||||
switch (b.dataset.act) {
|
||||
case 'meaning': showWord(oneWord(selText)); break;
|
||||
case 'search': location.href = `/search?q=${encodeURIComponent(q)}`; break;
|
||||
case 'google': open(`https://www.google.com/search?q=${enc}`, '_blank', 'noopener'); break;
|
||||
case 'copy':
|
||||
try { await navigator.clipboard.writeText(cite()); flash(b, 'کاپی ہو گیا'); } catch (err) { flash(b, 'ناکام'); }
|
||||
break;
|
||||
case 'share': { // this page with the selection highlighted (?hl=), as search results link
|
||||
const url = `${location.origin}${location.pathname}?hl=${encodeURIComponent(q)}`;
|
||||
if (navigator.share) navigator.share({ title: document.title, text: selText, url }).catch(() => {});
|
||||
else try { await navigator.clipboard.writeText(url); flash(b, 'لنک کاپی ہو گیا'); } catch (err) { flash(b, 'ناکام'); }
|
||||
}
|
||||
}
|
||||
};
|
||||
const closeDict = () => { dict.hidden = true; current = ''; document.body.classList.remove('dict-open'); };
|
||||
dict.querySelector('.dict-close').onclick = closeDict;
|
||||
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !dict.hidden) closeDict(); });
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
if (!menu.hidden) hideMenu(); else if (!dict.hidden) closeDict();
|
||||
});
|
||||
|
||||
document.getElementById('theme-toggle').onclick = () => {
|
||||
const dark = root.dataset.theme ? root.dataset.theme === 'dark' : matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
@ -80,7 +80,7 @@ const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? pa
|
||||
</p>
|
||||
)}
|
||||
{page.poem.radif != null && <p class="muted radif">{page.poem.radif ? `ردیف: ${page.poem.radif}` : 'غیر مردف'}</p>}
|
||||
<article class="poem">
|
||||
<article class="poem" data-cite={`${page.poet.nickname}، ${page.poem.title}`}>
|
||||
{blocks.map((b) =>
|
||||
b.kind === 'couplet' ? <div class="couplet" data-mark={b.mark}>{b.lines.map((l) => <p><Seg s={show(l)} /></p>)}</div>
|
||||
: b.kind === 'para' ? <p class="para"><Seg s={show(b.lines[0])} /></p>
|
||||
|
||||
@ -166,3 +166,10 @@ h1 + .muted { text-align: center; margin-top: 0; }
|
||||
@media (max-width: 700px) {
|
||||
.dict { top: auto; right: 0; width: auto; max-height: 60vh; border-right: 0; border-top: 1.5px solid var(--border); border-radius: 14px 14px 0 0; }
|
||||
}
|
||||
|
||||
/* selection menu (Ganjoor's, by the selected text) */
|
||||
.selmenu { position: absolute; z-index: 30; display: flex; gap: 2px; padding: 4px; border-radius: 10px;
|
||||
background: rgb(14 17 17 / .92); box-shadow: 0 4px 14px rgb(0 0 0 / .25); }
|
||||
.selmenu[hidden] { display: none; }
|
||||
.selmenu button { font: inherit; font-size: .85rem; color: #f3ead8; background: none; border: 0; border-radius: 7px; padding: 2px 10px; cursor: pointer; }
|
||||
.selmenu button:hover, .selmenu button:focus-visible { background: rgb(201 160 80 / .25); outline: none; }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user