323 lines
27 KiB
Plaintext
323 lines
27 KiB
Plaintext
---
|
||
// 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>
|