divan/web/src/pages/mod/rev/[id].astro

323 lines
27 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="ہٹائیں" aria-label="ہٹائیں"><Icon name="close" size={18} /></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="ہٹائیں" aria-label="ہٹائیں"><Icon name="close" size={18} /></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="سب سے اوپر" aria-label="سب سے اوپر"><Icon name="top" size={18} /></button>
<button type="button" class="tool" data-move="up" title="اوپر" aria-label="اوپر"><Icon name="up" size={18} /></button>
<button type="button" class="tool" data-move="down" title="نیچے" aria-label="نیچے"><Icon name="down" size={18} /></button>
<button type="button" class="tool" data-move="bottom" title="سب سے نیچے" aria-label="سب سے نیچے"><Icon name="bottom" size={18} /></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="نیا شعر (دو مصرعے)" aria-label="نیا شعر (دو مصرعے)"><Icon name="couplet" size={18} /></button>
<button type="button" class="tool" data-cmd="para" title="نثر کا نیا پیراگراف" aria-label="نثر کا نیا پیراگراف"><Icon name="para" size={18} /></button>
<button type="button" class="tool" data-cmd="h2" title="باب کا عنوان" aria-label="باب کا عنوان"><Icon name="h2" size={18} /></button>
<button type="button" class="tool" data-cmd="h3" title="ذیلی عنوان" aria-label="ذیلی عنوان"><Icon name="h3" size={18} /></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="منتخب لفظ کو لغت سے جوڑیں" aria-label="منتخب لفظ کو لغت سے جوڑیں"><Icon name="dict" size={18} /></button>
<button type="button" class="tool" data-cmd="note" title="حاشیہ لگائیں" aria-label="حاشیہ لگائیں"><Icon name="note" size={18} /></button>
<button type="button" class="tool" data-cmd="variant" title="نسخوں کا فرق" aria-label="نسخوں کا فرق"><Icon name="variant" size={18} /></button>
<span class="sep"></span>
<button type="button" class="tool" data-cmd="undo" title="پچھلی تبدیلی واپس" aria-label="پچھلی تبدیلی واپس"><Icon name="undo" size={18} /></button>
<button type="button" class="tool" data-cmd="redo" title="تبدیلی دوبارہ" aria-label="تبدیلی دوبارہ"><Icon name="redo" size={18} /></button>
<button type="button" class="tool" data-urdu-rom="dv-editor-host" title="رومن حروف سے اردو" aria-label="رومن حروف سے اردو">اب</button>
<button type="button" class="tool" data-urdu-kb="dv-editor-host" title="اردو کی بورڈ" aria-label="اردو کی بورڈ"><Icon name="keyboard" size={18} /></button>
<span class="sep"></span>
<button type="button" class="tool" data-cmd="code" aria-pressed="false" title="ساتھ ساتھ دیوان متن (کوڈ) دیکھیں" aria-label="ساتھ ساتھ دیوان متن (کوڈ) دیکھیں"><Icon name="code" size={18} /></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" title="ہٹائیں" aria-label="ہٹائیں"><svg class="icon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></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>