From c558960ddcd0c663d4eafe41447a162d246e02bd Mon Sep 17 00:00:00 2001
From: Anas Rashid
Date: Thu, 8 Oct 2026 22:36:52 +0200
Subject: [PATCH 1/2] Text selection menu like Ganjoor's (#23)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Selecting reading text shows a menu by the selection: معنی (word sidebar, single words),
تلاش (search Divan, phrases quoted), کاپی (text with poet, title and link), شیئر (link to the
page with the selection highlighted via ?hl=; native share sheet where available), قرآن
(tanzil.net), گوگل. Escape or clearing the selection closes it; no layout shift.
Co-Authored-By: Claude Opus 5.5
---
web/src/layouts/Base.astro | 58 +++++++++++++++++++++++++++++++----
web/src/pages/[...path].astro | 2 +-
web/src/styles/global.css | 7 +++++
3 files changed, 60 insertions(+), 7 deletions(-)
diff --git a/web/src/layouts/Base.astro b/web/src/layouts/Base.astro
index 6c4bb5f3..75407d5c 100644
--- a/web/src/layouts/Base.astro
+++ b/web/src/layouts/Base.astro
@@ -48,6 +48,14 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک
+
✕
@@ -258,20 +266,58 @@ 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 'quran': open(`https://tanzil.net/#search/quran/${enc}`, '_blank', 'noopener'); 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;
diff --git a/web/src/pages/[...path].astro b/web/src/pages/[...path].astro
index 25b7949d..71790c5a 100644
--- a/web/src/pages/[...path].astro
+++ b/web/src/pages/[...path].astro
@@ -80,7 +80,7 @@ const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? pa
)}
{page.poem.radif != null && {page.poem.radif ? `ردیف: ${page.poem.radif}` : 'غیر مردف'}
}
-
+
{blocks.map((b) =>
b.kind === 'couplet' ?
: b.kind === 'para' ?
diff --git a/web/src/styles/global.css b/web/src/styles/global.css
index 98a07d74..c8dff88e 100644
--- a/web/src/styles/global.css
+++ b/web/src/styles/global.css
@@ -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; }
--
2.47.3
From e12f6eeb2a77e8d377bac16c96d83ac3adfdfbbb Mon Sep 17 00:00:00 2001
From: Anas Rashid
Date: Thu, 8 Oct 2026 22:56:46 +0200
Subject: [PATCH 2/2] =?UTF-8?q?Selection=20menu:=20remove=20the=20=D9=82?=
=?UTF-8?q?=D8=B1=D8=A2=D9=86=20option=20(owner=20request)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-Authored-By: Claude Opus 5.5
---
web/src/layouts/Base.astro | 2 --
1 file changed, 2 deletions(-)
diff --git a/web/src/layouts/Base.astro b/web/src/layouts/Base.astro
index 75407d5c..3abe1f5f 100644
--- a/web/src/layouts/Base.astro
+++ b/web/src/layouts/Base.astro
@@ -53,7 +53,6 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک
تلاش
کاپی
شیئر
- قرآن
گوگل
@@ -300,7 +299,6 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک
switch (b.dataset.act) {
case 'meaning': showWord(oneWord(selText)); break;
case 'search': location.href = `/search?q=${encodeURIComponent(q)}`; break;
- case 'quran': open(`https://tanzil.net/#search/quran/${enc}`, '_blank', 'noopener'); 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, 'ناکام'); }
--
2.47.3