--- // 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: " " 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>