divan/web/src/pages/mod/rev/[id].astro
Anas Rashid c4bf52dd97 Icons and colour roles: header icons with tooltips and a red log out; buttons coloured by what they do; محفوظ شدہ and پسندیدہ لکھاری
- Icon component (Lucide shapes, ISC), currentColor, decorative (the control keeps its words in aria-label/title).
- Colour roles: --action (primary, filled), --ok (approve, publish), --danger (log out, delete, reject); .btn variants
  and .icon-btn; light and dark.
- Header: admin, moderation, saved, word book, account/sign in, font, theme as icons; log out in red.
- Review: save/send filled, approve/publish green, return plain, reject red, with icons. Account: save, log out, password,
  delete. Reading pages: edit, history, arrange, tag. E-books: add, download, covers; the reader's controls as icons.
- نشانات is now محفوظ شدہ; favourite writers have their own page, پسندیدہ لکھاری (/writers).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 15:23:06 +02:00

323 lines
25 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
// One revision: edit (author, while open), preview, diff against the text it started from, actions, timeline
import Base from '../../../layouts/Base.astro';
import ModNav from '../../../components/ModNav.astro';
import { asUser, COOKIE } from '../../../lib/auth';
import { STATUS, EVENT, when } from '../../../lib/mod';
import { ud } from '../../../lib/urdu';
import DivanPreview from '../../../components/DivanPreview.astro';
import Tags from '../../../components/Tags.astro';
import EbookFields from '../../../components/EbookFields.astro';
import { api } from '../../../lib/api';
const me = Astro.locals.user;
if (!me) return Astro.redirect(`/signin?next=${Astro.url.pathname}`);
const token = Astro.cookies.get(COOKIE)!.value, id = Number(Astro.params.id) || 0;
let error = '', done = '';
if (Astro.request.method === 'POST') {
const f = await Astro.request.formData(), act = String(f.get('act'));
let r: any = { ok: true, data: {} };
if (act === 'save' || act === 'submit') r = await asUser(token, `/api/mod/revisions/${id}/save`, { content: f.get('content'), summary: f.get('summary') });
if (r.ok && r.data.discarded) return Astro.redirect('/mod?unchanged=1'); // nothing changed: no draft kept
if (r.ok && act !== 'save') r = await asUser(token, `/api/mod/revisions/${id}/${act}`, { comment: f.get('comment') ?? '' });
if (!r.ok) error = r.data.error ?? 'کچھ غلط ہو گیا';
else done = act === 'save' ? 'محفوظ ہو گیا' : `${STATUS[r.data.status] ?? r.data.status}${r.data.version ? ` · ورژن ${ud(r.data.version)}` : ''}`;
}
const res = await asUser(token, `/api/mod/revisions/${id}`);
if (!res.ok) return new Response(res.data.error ?? 'نہیں ملا', { status: res.status });
const { revision: rev, diff, changes, events, may } = res.data;
// an arrangement (#41) lists a book/section's items, one per line: "<url part> <title>"
const order = rev.entity === 'order';
const items = order ? rev.content.split('\n').filter(Boolean).map((l: string) => ({ line: l, title: l.slice(l.indexOf(' ') + 1) })) : [];
// tagging (#52): one row per tag; a work's tags may be on one couplet (counted from 1)
const tagging = rev.entity.startsWith('tags-'), tagKind = rev.entity.slice(5);
const TAG_TYPES = ['موضوع', 'صنف', 'بحر', 'شخصیت', 'مقام', 'دور', 'ٹیگ'];
const tagRows = tagging ? rev.content.split('\n').filter(Boolean).map((l: string) => {
const m = l.match(/^(?:شعر (\d+) › )?([^:]+):\s*(.+)$/); return { couplet: Number(m?.[1] ?? 0), type: m?.[2] ?? 'ٹیگ', name: m?.[3] ?? l };
}) : [];
const couplets = tagging && tagKind === 'work' ? (await asUser(token, `/api/mod/tags/work/${rev.entity_id}`)).data.couplets ?? 0 : 0;
const knownTags = tagging ? ((await api<{ name: string }[]>('/api/tags')) ?? []).map((t) => t.name) : [];
// an e-book's details: title, source, copyright, note
const ebook = rev.entity === 'ebook';
const EB: [string, string][] = [['title', 'عنوان'], ['writer', 'مصنف'], ['coauthors', 'شریک مصنفین'], ['language', 'زبان'], ['direction', 'سمت'],
['tags', 'ٹیگ'], ['source', 'ماخذ'], ['licence', 'اجازت'], ['note', 'تفصیل']];
const ebMeta = ebook ? Object.fromEntries(EB.map(([k, label]) => [k, (rev.content.split('\n').find((l: string) => l.startsWith(label + ':')) ?? '').slice(label.length + 1).trim()])) : {};
// each action in its colour and with its icon: approve and publish confirm, return goes back, reject is destructive
const acts = ([['approve', 'منظور کریں', 'ok', 'check'], ['return', 'واپس بھیجیں', '', 'undo'], ['reject', 'مسترد کریں', 'danger', 'close'],
['publish', 'شائع کریں', 'ok', 'upload']] as const).filter(([a]) => may[a]);
import Icon from '../../../components/Icon.astro';
---
<Base title={`مسودہ: ${rev.work_title}`}>
<h1>{order && 'ترتیب: '}{tagging && 'ٹیگ: '}{ebook && 'ای بک: '}{rev.work_title}</h1>
<ModNav />
<p class="center">
<span class={`status s-${rev.status}`}>{STATUS[rev.status]}</span>
· <bdi dir="ltr">{rev.author_email}</bdi>
· {rev.base_version ? `ورژن ${ud(rev.base_version)} سے` : 'ویکی ماخذ کے متن سے'}
· <a href={rev.work_url}>{order ? 'حصہ' : tagging ? 'صفحہ' : ebook ? 'کتاب دیکھیں' : 'کلام'}</a>{!ebook && <> · <a href={tagging ? `/mod/tags/${tagKind}/${rev.entity_id}` : `/mod/${order ? 'order' : 'work'}/${rev.entity_id}`}>تاریخچہ</a></>}
</p>
{error && <p class="form-error" role="alert">{error}</p>}
{done && <p class="form-done" role="status">{done}</p>}
{may.save && ebook ? (
<form method="post" class="mod-edit account-form">
<label>ترمیم کا خلاصہ<input name="summary" value={rev.summary ?? ''} maxlength="500" data-urdu /></label>
<EbookFields v={ebMeta} />
<textarea name="content" id="eb-content" hidden>{rev.content}</textarea>
<div class="filter-actions">
<button name="act" value="save" class="btn primary"><Icon name="save" size={18} /> محفوظ کریں</button>
<button name="act" value="submit" class="btn primary"><Icon name={me.role === 'admin' ? 'upload' : 'send'} size={18} /> {me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
</div>
</form>
) : may.save && tagging ? (
<form method="post" class="mod-edit">
<label>ترمیم کا خلاصہ<input name="summary" value={rev.summary ?? ''} maxlength="500" data-urdu placeholder="مثلاً: موضوعات اور شخصیات کے ٹیگ" /></label>
<p class="muted note">ہر ٹیگ کی قسم چنیں اور نام لکھیں۔{couplets > 0 && ' ٹیگ پورے کلام پر یا کسی ایک شعر پر لگ سکتا ہے۔'} پہلے سے موجود نام تجویز ہوتے ہیں۔</p>
<div id="tag-rows">
{tagRows.map((t: any, i: number) => <div class="tag-row">
{couplets > 0 && <select class="tool tag-couplet" aria-label="کہاں"><option value="0">پورا کلام</option>{Array.from({ length: couplets }, (_, i) => <option value={i + 1} selected={t.couplet === i + 1}>شعر {ud(i + 1)}</option>)}</select>}
<select class="tool tag-type" aria-label="قسم">{TAG_TYPES.map((x) => <option selected={x === t.type}>{x}</option>)}</select>
<span class="in-box-field tag-name-box"><input class="tag-name" id={`tag-name-${i}`} list="tag-names" value={t.name} maxlength="60" data-urdu aria-label="ٹیگ کا نام" placeholder="ٹیگ کا نام" />
<button type="button" class="in-box rom" data-urdu-rom={`tag-name-${i}`} aria-pressed="false" title="رومن حروف سے اردو">اب</button>
<button type="button" class="in-box" data-urdu-kb={`tag-name-${i}`} aria-pressed="false" title="اردو کی بورڈ" aria-label="اردو کی بورڈ"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M6 10h.01M10 10h.01M14 10h.01M18 10h.01M7 14h10" stroke-linecap="round" stroke-width="2"/></svg></button></span>
<button type="button" class="tool" data-remove title="ہٹائیں">✕</button>
</div>)}
</div>
<template id="tag-row">{[{ couplet: 0, type: 'موضوع', name: '' }].map((t, i) => <div class="tag-row">
{couplets > 0 && <select class="tool tag-couplet" aria-label="کہاں"><option value="0">پورا کلام</option>{Array.from({ length: couplets }, (_, i) => <option value={i + 1} selected={t.couplet === i + 1}>شعر {ud(i + 1)}</option>)}</select>}
<select class="tool tag-type" aria-label="قسم">{TAG_TYPES.map((x) => <option selected={x === t.type}>{x}</option>)}</select>
<span class="in-box-field tag-name-box"><input class="tag-name" id={`tag-name-${i}`} list="tag-names" value={t.name} maxlength="60" data-urdu aria-label="ٹیگ کا نام" placeholder="ٹیگ کا نام" />
<button type="button" class="in-box rom" data-urdu-rom={`tag-name-${i}`} aria-pressed="false" title="رومن حروف سے اردو">اب</button>
<button type="button" class="in-box" data-urdu-kb={`tag-name-${i}`} aria-pressed="false" title="اردو کی بورڈ" aria-label="اردو کی بورڈ"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M6 10h.01M10 10h.01M14 10h.01M18 10h.01M7 14h10" stroke-linecap="round" stroke-width="2"/></svg></button></span>
<button type="button" class="tool" data-remove title="ہٹائیں">✕</button>
</div>)}</template>
<datalist id="tag-names">{knownTags.map((n) => <option value={n} />)}</datalist>
<p><button type="button" class="tool" id="tag-add">+ ٹیگ</button></p>
<textarea name="content" id="tag-content" hidden>{rev.content}</textarea>
<div class="filter-actions">
<button name="act" value="save" class="btn primary"><Icon name="save" size={18} /> محفوظ کریں</button>
<button name="act" value="submit" class="btn primary"><Icon name={me.role === 'admin' ? 'upload' : 'send'} size={18} /> {me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
</div>
</form>
) : may.save && order ? (
<form method="post" class="mod-edit">
<label>ترمیم کا خلاصہ<input name="summary" value={rev.summary ?? ''} maxlength="500" data-urdu placeholder="مثلاً: غزلیں مطبوعہ دیوان کی ترتیب میں" /></label>
<p class="muted note">کسی چیز کو کھینچ کر اس کی جگہ رکھیں، یا ↑ ↓ سے ایک جگہ اوپر نیچے کریں؛ «اول» سب سے اوپر، «آخر» سب سے نیچے۔</p>
<ol class="order-list" id="order-list">
{items.map((it: any) => (
<li draggable="true" data-line={it.line}>
<span class="order-title">{it.title}</span>
<span class="order-tools">
<button type="button" class="tool" data-move="top" title="سب سے اوپر">اول</button>
<button type="button" class="tool" data-move="up" title="اوپر">↑</button>
<button type="button" class="tool" data-move="down" title="نیچے">↓</button>
<button type="button" class="tool" data-move="bottom" title="سب سے نیچے">آخر</button>
</span>
</li>
))}
</ol>
<textarea name="content" id="order-content" hidden>{rev.content}</textarea>
<div class="filter-actions">
<button name="act" value="save" class="btn primary"><Icon name="save" size={18} /> محفوظ کریں</button>
<button name="act" value="submit" class="btn primary"><Icon name={me.role === 'admin' ? 'upload' : 'send'} size={18} /> {me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
</div>
</form>
) : may.save ? (
<form method="post" class="mod-edit">
<label>ترمیم کا خلاصہ<input name="summary" value={rev.summary ?? ''} maxlength="500" data-urdu placeholder="مثلاً: دوسرے شعر کا مصرع درست کیا" /></label>
<nav class="admin-nav editor-tabs" role="tablist">
<button type="button" role="tab" data-tab="editor" aria-selected="true">ایڈیٹر</button>
<button type="button" role="tab" data-tab="source" aria-selected="false">سورس (دیوان متن)</button>
</nav>
<div id="editor-pane">
<fieldset class="meta-fields"><legend>کلام کا خاکہ</legend><div id="meta-rows"></div>
<button type="button" id="meta-add" class="tool">+ معلومات</button></fieldset>
<div class="toolbar" role="toolbar" aria-label="ایڈیٹر">
<button type="button" class="tool" data-cmd="couplet" title="نیا شعر (دو مصرعے)">شعر</button>
<button type="button" class="tool" data-cmd="para" title="نثر کا نیا پیراگراف">پیراگراف</button>
<button type="button" class="tool" data-cmd="h2" title="باب کا عنوان">باب کا عنوان</button>
<button type="button" class="tool" data-cmd="h3" title="ذیلی عنوان">ذیلی عنوان</button>
<span class="sep"></span>
<button type="button" class="tool" data-cmd="label" data-label="مطلع">مطلع</button>
<button type="button" class="tool" data-cmd="label" data-label="حسن مطلع">حسن مطلع</button>
<button type="button" class="tool" data-cmd="label" data-label="مقطع">مقطع</button>
<span class="sep"></span>
<button type="button" class="tool" data-cmd="dict" title="منتخب لفظ کو لغت سے جوڑیں">لغت کا ربط</button>
<button type="button" class="tool" data-cmd="note" title="حاشیہ لگائیں">حاشیہ</button>
<button type="button" class="tool" data-cmd="variant" title="نسخوں کا فرق">نسخہ</button>
<span class="sep"></span>
<button type="button" class="tool" data-cmd="undo" title="پچھلی تبدیلی واپس">واپس</button>
<button type="button" class="tool" data-cmd="redo" title="تبدیلی دوبارہ">دوبارہ</button>
<button type="button" class="tool" data-urdu-rom="dv-editor-host" title="رومن حروف سے اردو">اب</button>
<button type="button" class="tool" data-urdu-kb="dv-editor-host" title="اردو کی بورڈ">کی بورڈ</button>
<span class="sep"></span>
<button type="button" class="tool" data-cmd="code" aria-pressed="false" title="ساتھ ساتھ دیوان متن (کوڈ) دیکھیں">کوڈ</button>
</div>
<div class="editor-split">
<div id="dv-editor-host"></div>
<pre id="live-code" class="live-code" dir="rtl" hidden aria-label="دیوان متن"></pre>
</div>
<p class="muted note">Enter: اسی شعر یا بند میں نیا مصرع · خالی مصرع پر Enter: نیا شعر · <a href="/mod/guide" target="_blank">لکھنے کا مکمل طریقہ</a></p>
</div>
<label class="mod-text" id="source-pane" hidden>متن (دیوان متن)
<textarea name="content" id="mod-content" data-urdu rows="22" dir="rtl" spellcheck="false">{rev.content}</textarea>
</label>
<div class="filter-actions">
<button name="act" value="save" class="btn primary"><Icon name="save" size={18} /> محفوظ کریں</button>
<button name="act" value="submit" class="btn primary"><Icon name={me.role === 'admin' ? 'upload' : 'send'} size={18} /> {me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
</div>
</form>
) : rev.summary && <p class="center">خلاصہ: {rev.summary}</p>}
<section class="lib-section">
<h2>تبدیلیاں <small class="muted">{ud(changes)} سطریں</small></h2>
{changes === 0 ? <p class="muted center">ابھی کوئی تبدیلی نہیں۔</p> : (
<div class="diff" dir="rtl">
{diff.map((d: any) => d.op === '=' ? null : <div class={d.op === '+' ? 'add' : 'del'}><span>{d.op === '+' ? '+' : '−'}</span>{d.text || ' '}</div>)}
</div>
)}
</section>
<section class="lib-section">
<h2>پیش نظارہ</h2>
{order ? <ol class="toc">{items.map((it: any) => <li>{it.title}</li>)}</ol>
: ebook ? <dl class="eb-meta">{EB.map(([k, label]) => ebMeta[k] && <><dt>{label}</dt><dd>{ebMeta[k]}</dd></>)}</dl>
: tagging ? (tagRows.length ? tagRows.map((t: any) => <div class="tag-preview">{t.couplet > 0 && <small class="muted">شعر {ud(t.couplet)}:</small>}<Tags tags={[t]} /></div>) : <p class="muted center">کوئی ٹیگ نہیں</p>)
: <DivanPreview text={rev.content} />}
</section>
{acts.length > 0 && (
<form method="post" class="account-form mod-actions">
<label>تبصرہ (واپس بھیجنے یا مسترد کرنے کے لیے ضروری)<textarea name="comment" rows="2" data-urdu maxlength="2000"></textarea></label>
<div class="filter-actions">{acts.map(([a, label, role, icon]) => <button name="act" value={a} class={`btn ${role} act-${a}`}><Icon name={icon} size={18} /> {label}</button>)}</div>
</form>
)}
<section class="lib-section">
<h2>سرگرمی</h2>
<ol class="timeline">
{events.map((e: any) => (
<li><bdi dir="ltr">{when(e.at)}</bdi> · <bdi dir="ltr">{e.actor_email}</bdi> · <strong>{EVENT[e.action] ?? e.action}</strong>{e.comment && <>: {e.comment}</>}</li>
))}
</ol>
</section>
<script>
// e-book details -> the hidden text, one "label: value" per line
const ebText = document.getElementById('eb-content') as HTMLTextAreaElement | null;
if (ebText) {
const LABEL: Record<string, string> = { title: 'عنوان', writer: 'مصنف', coauthors: 'شریک مصنفین', language: 'زبان', direction: 'سمت',
tags: 'ٹیگ', source: 'ماخذ', licence: 'اجازت', note: 'تفصیل' };
const sync = () => {
const val = (k: string) => [...ebText.form!.querySelectorAll<HTMLInputElement>(`[data-eb="${k}"]`)].find((i) => i.type !== 'radio' || i.checked)?.value ?? '';
ebText.value = Object.keys(LABEL).map((k) => `${LABEL[k]}: ${val(k).replace(/\s+/g, ' ').trim()}`).join('\n');
};
ebText.form!.addEventListener('input', sync); ebText.form!.addEventListener('change', sync); ebText.form!.addEventListener('submit', sync);
}
// tagging: rows -> the hidden text ("شعر 3 › موضوع: عشق"), on every change and before saving
const rowsBox = document.getElementById('tag-rows');
const tagText = document.getElementById('tag-content') as HTMLTextAreaElement | null;
if (rowsBox && tagText) {
const sync = () => {
tagText.value = [...rowsBox.querySelectorAll('.tag-row')].map((r) => {
const c = Number((r.querySelector('.tag-couplet') as HTMLSelectElement | null)?.value ?? 0);
const type = (r.querySelector('.tag-type') as HTMLSelectElement).value, name = (r.querySelector('.tag-name') as HTMLInputElement).value.trim();
return name ? `${c ? `شعر ${c} › ` : ''}${type}: ${name}` : '';
}).filter(Boolean).join('\n');
};
rowsBox.addEventListener('input', sync); rowsBox.addEventListener('change', sync);
rowsBox.addEventListener('click', (e) => { if ((e.target as HTMLElement).closest('[data-remove]')) { (e.target as HTMLElement).closest('.tag-row')!.remove(); sync(); } });
document.getElementById('tag-add')!.onclick = () => {
const row = (document.getElementById('tag-row') as HTMLTemplateElement).content.firstElementChild!.cloneNode(true) as HTMLElement;
// a fresh id for the new name box, so its keyboard buttons point at it
const nid = `tag-name-new-${Date.now()}`;
row.querySelector('.tag-name')!.id = nid; row.querySelectorAll('[data-urdu-rom], [data-urdu-kb]').forEach((b) => b.setAttribute(b.hasAttribute('data-urdu-rom') ? 'data-urdu-rom' : 'data-urdu-kb', nid));
rowsBox.append(row); (row.querySelector('.tag-name') as HTMLInputElement).focus();
};
tagText.form!.addEventListener('submit', sync);
}
// arranging: drag an item, or move it with the buttons; the hidden text follows the list
const list = document.getElementById('order-list');
const text = document.getElementById('order-content') as HTMLTextAreaElement | null;
if (list && text) {
const sync = () => { text.value = [...list.children].map((li) => (li as HTMLElement).dataset.line).join('\n'); };
const flash = (li: Element) => { li.classList.add('moved'); li.scrollIntoView({ block: 'nearest' }); setTimeout(() => li.classList.remove('moved'), 600); };
list.addEventListener('click', (e) => {
const b = (e.target as HTMLElement).closest<HTMLButtonElement>('[data-move]'); if (!b) return;
const li = b.closest('li')!, m = b.dataset.move;
if (m === 'up' && li.previousElementSibling) li.previousElementSibling.before(li);
else if (m === 'down' && li.nextElementSibling) li.nextElementSibling.after(li);
else if (m === 'top') list.prepend(li);
else if (m === 'bottom') list.append(li);
sync(); flash(li); b.focus();
});
let dragged: HTMLElement | null = null;
list.addEventListener('dragstart', (e) => { dragged = (e.target as HTMLElement).closest('li'); dragged?.classList.add('dragging'); });
list.addEventListener('dragend', () => { dragged?.classList.remove('dragging'); dragged = null; sync(); });
list.addEventListener('dragover', (e) => {
e.preventDefault();
const over = (e.target as HTMLElement).closest('li');
if (!dragged || !over || over === dragged) return;
const r = over.getBoundingClientRect();
e.clientY > r.top + r.height / 2 ? over.after(dragged) : over.before(dragged);
});
}
// the structured editor (Tiptap) and the source view edit the same Divan text; saving always sends the text
import { createEditor, fromEditor, toEditor, actions } from '../../../lib/editor';
import { parse } from '../../../../../api/src/divantext.ts';
const host = document.getElementById('dv-editor-host');
const source = document.getElementById('mod-content') as HTMLTextAreaElement | null;
if (host && source) {
const { editor, meta } = createEditor(host, source.value);
const rows = document.getElementById('meta-rows')!;
const KNOWN = ['عنوان', 'شاعر', 'مصنف', 'صنف', 'ردیف', 'قافیہ', 'بحر', 'ماخذ', 'ماخذ_ربط'];
const row = (k = '', v = '') => {
const r = document.createElement('div'); r.className = 'meta-row';
r.innerHTML = '<input class="meta-k" list="meta-keys" aria-label="معلومات کا نام"><input class="meta-v" data-urdu aria-label="قدر"><button type="button" class="tool" aria-label="ہٹائیں">✕</button>';
(r.children[0] as HTMLInputElement).value = k; (r.children[1] as HTMLInputElement).value = v;
(r.children[2] as HTMLButtonElement).onclick = () => r.remove();
rows.append(r);
};
const fillMeta = (m: Record<string, string>) => { rows.replaceChildren(); Object.entries(m).forEach(([k, v]) => row(k, v)); if (!rows.children.length) row('عنوان', ''); };
const readMeta = () => Object.fromEntries([...rows.querySelectorAll('.meta-row')].map((r) => [(r.children[0] as HTMLInputElement).value.trim(), (r.children[1] as HTMLInputElement).value.trim()]).filter(([k, v]) => k && v));
const keys = document.createElement('datalist'); keys.id = 'meta-keys'; keys.innerHTML = KNOWN.map((k) => `<option value="${k}">`).join(''); document.body.append(keys);
fillMeta(meta);
document.getElementById('meta-add')!.onclick = () => row();
const sync = () => { source.value = fromEditor(editor.getJSON(), readMeta()); };
// the code beside the editor (read-only; the سورس tab edits it), updated as you type
const code = document.getElementById('live-code')!;
const showCode = () => { if (!code.hidden) code.textContent = fromEditor(editor.getJSON(), readMeta()); };
editor.on('update', showCode);
rows.addEventListener('input', showCode);
let tab = 'editor';
document.querySelectorAll<HTMLButtonElement>('[data-tab]').forEach((b) => (b.onclick = () => {
if (b.dataset.tab === tab) return;
if (tab === 'editor') sync();
else { const doc = parse(source.value); editor.commands.setContent(toEditor(doc)); fillMeta(doc.meta); }
tab = b.dataset.tab!;
document.getElementById('editor-pane')!.hidden = tab !== 'editor';
document.getElementById('source-pane')!.hidden = tab !== 'source';
document.querySelectorAll('[data-tab]').forEach((t) => t.setAttribute('aria-selected', String(t === b)));
}));
source.form!.addEventListener('submit', () => { if (tab === 'editor') sync(); });
// toolbar buttons must not take the focus (and the selected word) away from the editor
document.querySelector('.toolbar')!.addEventListener('mousedown', (e) => e.preventDefault());
document.querySelector('.toolbar')!.addEventListener('click', (e) => {
const b = (e.target as HTMLElement).closest<HTMLButtonElement>('[data-cmd]'); if (!b) return;
const c = b.dataset.cmd!;
if (c === 'code') { code.hidden = !code.hidden; b.setAttribute('aria-pressed', String(!code.hidden)); code.parentElement!.classList.toggle('with-code', !code.hidden); showCode(); }
else if (c === 'undo') editor.chain().focus().undo().run();
else if (c === 'redo') editor.chain().focus().redo().run();
else if (c === 'h2' || c === 'h3') actions.heading(editor, c === 'h2' ? 2 : 3);
else if (c === 'label') actions.label(editor, b.dataset.label!);
else (actions as any)[c](editor);
});
// a note or variant shows its text when clicked
host.addEventListener('click', (e) => {
const n = (e.target as HTMLElement).closest('sup.note, span.variant');
if (n) alert(n.getAttribute('title'));
});
}
</script>
</Base>