| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771 |
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>DocCenter — VL-Code</title>
- <style>
- :root {
- --bg: #0d1117; --bg2: #161b22; --bg3: #21262d; --border: #30363d;
- --text: #e6edf3; --text2: #8b949e; --accent: #58a6ff; --green: #3fb950;
- --yellow: #d29922; --red: #f85149; --purple: #a371f7;
- --font: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
- }
- * { margin:0; padding:0; box-sizing:border-box; }
- body { background:var(--bg); color:var(--text); font-family:var(--font); font-size:13px; height:100vh; display:flex; flex-direction:column; }
- body.embed header .embed-hidden,
- body.embed .status-bar { display:none; }
- body.embed header { padding:8px 12px; }
- body.embed .layout { border-top:1px solid var(--border); }
- body.embed .sidebar { width:300px; }
- body.landing-embed .sidebar-actions,
- body.landing-embed .mode-tabs,
- body.landing-embed #btnSave,
- body.landing-embed #btnPublish { display:none !important; }
- body.landing-embed .toolbar { padding:8px 12px; }
- /* Header */
- header {
- background:var(--bg2); border-bottom:1px solid var(--border);
- padding:8px 16px; display:flex; align-items:center; gap:12px; flex-shrink:0;
- }
- header h1 { font-size:15px; color:var(--accent); font-weight:600; white-space:nowrap; }
- header .spacer { flex:1; }
- .hdr-btn {
- background:var(--bg3); color:var(--text2); border:1px solid var(--border);
- padding:5px 12px; border-radius:5px; cursor:pointer; font-family:var(--font); font-size:11px; white-space:nowrap;
- }
- .hdr-btn:hover { background:var(--border); color:var(--text); }
- .hdr-btn-primary { background:var(--accent); color:#fff; border-color:var(--accent); }
- .hdr-btn-primary:hover { background:#79b8ff; }
- .hdr-btn-green { background:var(--green); color:#fff; border-color:var(--green); }
- .hdr-btn-green:hover { background:#56d364; }
- .hdr-btn:disabled { opacity:0.4; cursor:not-allowed; }
- /* Layout */
- .layout { flex:1; display:flex; overflow:hidden; }
- /* Sidebar */
- .sidebar {
- width:280px; min-width:200px; background:var(--bg2); border-right:1px solid var(--border);
- display:flex; flex-direction:column;
- }
- .sidebar-header { padding:10px 12px 6px; }
- .sidebar-header h3 { font-size:11px; color:var(--text2); text-transform:uppercase; letter-spacing:1px; margin-bottom:8px; }
- .sidebar-controls {
- display:flex; align-items:center; gap:6px; margin-top:8px;
- }
- .sidebar-controls select {
- flex:1; background:var(--bg); border:1px solid var(--border); color:var(--text);
- padding:5px 8px; border-radius:5px; font-family:var(--font); font-size:11px; outline:none;
- }
- .sidebar-controls select:focus { border-color:var(--accent); }
- .sidebar-note { font-size:10px; color:var(--text2); line-height:1.5; margin-top:8px; }
- .search-box {
- width:100%; background:var(--bg); border:1px solid var(--border); color:var(--text);
- padding:6px 10px; border-radius:5px; font-family:var(--font); font-size:12px; outline:none;
- }
- .search-box:focus { border-color:var(--accent); }
- .search-box::placeholder { color:var(--text2); }
- .sidebar-actions { display:flex; gap:4px; padding:6px 12px; border-bottom:1px solid var(--border); }
- .sa-btn {
- flex:1; display:flex; align-items:center; justify-content:center; gap:4px;
- background:var(--bg3); color:var(--text2); border:1px solid var(--border);
- padding:4px 8px; border-radius:4px; cursor:pointer; font-family:var(--font); font-size:10px;
- }
- .sa-btn:hover { background:var(--border); color:var(--text); }
- .doc-list { flex:1; overflow-y:auto; padding:4px 0; }
- .doc-item {
- padding:8px 12px; cursor:pointer; border-left:3px solid transparent;
- display:flex; flex-direction:column; gap:2px;
- }
- .doc-item:hover { background:var(--bg3); }
- .doc-item.active { background:var(--bg3); border-left-color:var(--accent); }
- .doc-item .doc-name { font-size:12px; color:var(--text); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
- .doc-item .doc-meta { display:flex; gap:6px; flex-wrap:wrap; }
- .doc-item .doc-tags { display:flex; gap:3px; margin-top:2px; flex-wrap:wrap; }
- .doc-badge {
- font-size:10px; padding:2px 6px; border-radius:999px; border:1px solid var(--border);
- color:var(--text2); background:rgba(255,255,255,0.02);
- }
- .doc-badge-id { color:var(--accent); border-color:rgba(88,166,255,0.25); background:rgba(88,166,255,0.08); }
- .doc-badge-path { color:var(--purple); border-color:rgba(163,113,247,0.25); background:rgba(163,113,247,0.08); }
- .doc-badge-version { color:var(--green); border-color:rgba(63,185,80,0.25); background:rgba(63,185,80,0.08); }
- .doc-badge-scope-official { color:#f2cc60; border-color:rgba(242,204,96,0.25); background:rgba(242,204,96,0.08); }
- .doc-badge-scope-user { color:#ffab70; border-color:rgba(255,171,112,0.25); background:rgba(255,171,112,0.08); }
- .doc-tag {
- font-size:9px; padding:1px 5px; border-radius:8px;
- background:rgba(88,166,255,0.15); color:var(--accent);
- }
- .list-status { padding:12px; text-align:center; color:var(--text2); font-size:11px; }
- /* Main content */
- .main-content { flex:1; display:flex; flex-direction:column; overflow:hidden; }
- /* Toolbar */
- .toolbar {
- background:var(--bg2); border-bottom:1px solid var(--border);
- padding:6px 16px; display:flex; align-items:center; gap:8px; flex-shrink:0;
- }
- .toolbar .doc-title { font-size:13px; font-weight:600; color:var(--text); flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
- .toolbar .doc-id { font-size:10px; color:var(--text2); margin-right:8px; }
- .mode-tabs { display:flex; gap:2px; }
- .mode-tab {
- padding:4px 12px; border-radius:4px; cursor:pointer; font-size:11px;
- color:var(--text2); background:transparent; border:1px solid transparent;
- font-family:var(--font);
- }
- .mode-tab:hover { color:var(--text); }
- .mode-tab.active { background:var(--bg3); color:var(--text); border-color:var(--border); }
- /* Editor / Viewer */
- .content-area { flex:1; overflow:auto; position:relative; }
- .content-area textarea {
- width:100%; height:100%; background:var(--bg); color:var(--text); border:none;
- padding:16px 20px; font-family:var(--font); font-size:13px; line-height:1.6;
- resize:none; outline:none; tab-size:2;
- }
- .content-area .md-view {
- padding:16px 24px; line-height:1.7; max-width:900px;
- }
- .md-view h1 { font-size:22px; margin:16px 0 8px; color:var(--accent); border-bottom:1px solid var(--border); padding-bottom:6px; }
- .md-view h2 { font-size:18px; margin:14px 0 6px; color:var(--text); }
- .md-view h3 { font-size:15px; margin:12px 0 4px; color:var(--text); }
- .md-view h4 { font-size:13px; margin:10px 0 4px; color:var(--text2); }
- .md-view p { margin:6px 0; }
- .md-view a { color:var(--accent); text-decoration:none; }
- .md-view a:hover { text-decoration:underline; }
- .md-view code {
- background:var(--bg3); padding:1px 5px; border-radius:3px; font-size:12px; color:var(--yellow);
- }
- .md-view pre {
- background:var(--bg3); border:1px solid var(--border); border-radius:6px;
- padding:12px 16px; margin:8px 0; overflow-x:auto; font-size:12px; line-height:1.5;
- }
- .md-view pre code { background:none; padding:0; color:var(--text); }
- .md-view blockquote {
- border-left:3px solid var(--accent); padding:4px 12px; margin:8px 0;
- color:var(--text2); background:rgba(88,166,255,0.05);
- }
- .md-view ul, .md-view ol { padding-left:20px; margin:6px 0; }
- .md-view li { margin:3px 0; }
- .md-view table { border-collapse:collapse; margin:8px 0; width:100%; }
- .md-view th, .md-view td { border:1px solid var(--border); padding:6px 10px; text-align:left; font-size:12px; }
- .md-view th { background:var(--bg3); color:var(--text2); font-weight:600; }
- .md-view hr { border:none; border-top:1px solid var(--border); margin:12px 0; }
- .md-view img { max-width:100%; border-radius:4px; }
- /* Empty state */
- .empty-state {
- display:flex; flex-direction:column; align-items:center; justify-content:center;
- height:100%; color:var(--text2); gap:8px;
- }
- .empty-state .icon { font-size:48px; opacity:0.3; }
- .empty-state p { font-size:13px; }
- /* Status bar */
- .status-bar {
- background:var(--bg2); border-top:1px solid var(--border);
- padding:4px 16px; font-size:10px; color:var(--text2);
- display:flex; align-items:center; gap:12px; flex-shrink:0;
- }
- .status-bar .modified { color:var(--yellow); }
- .status-bar .saved { color:var(--green); }
- /* Toast */
- .toast {
- position:fixed; bottom:20px; right:20px; padding:10px 20px;
- background:var(--bg3); border:1px solid var(--border); border-radius:6px;
- color:var(--text); font-size:12px; z-index:1000;
- opacity:0; transform:translateY(10px); transition:all 0.2s;
- }
- .toast.show { opacity:1; transform:translateY(0); }
- .toast.error { border-color:var(--red); color:var(--red); }
- .toast.success { border-color:var(--green); color:var(--green); }
- /* Scrollbar */
- ::-webkit-scrollbar { width:6px; height:6px; }
- ::-webkit-scrollbar-track { background:transparent; }
- ::-webkit-scrollbar-thumb { background:var(--bg3); border-radius:3px; }
- ::-webkit-scrollbar-thumb:hover { background:var(--border); }
- /* Dialog overlay */
- .dialog-overlay {
- position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.6);
- display:none; align-items:center; justify-content:center; z-index:500;
- }
- .dialog-overlay.open { display:flex; }
- .dialog {
- background:var(--bg2); border:1px solid var(--border); border-radius:8px;
- padding:20px; min-width:360px; max-width:500px;
- }
- .dialog h3 { font-size:14px; margin-bottom:12px; color:var(--text); }
- .dialog label { display:block; font-size:11px; color:var(--text2); margin-bottom:4px; margin-top:10px; }
- .dialog input, .dialog textarea {
- width:100%; background:var(--bg); border:1px solid var(--border); color:var(--text);
- padding:6px 10px; border-radius:4px; font-family:var(--font); font-size:12px; outline:none;
- }
- .dialog input:focus, .dialog textarea:focus { border-color:var(--accent); }
- .dialog .dialog-actions { display:flex; gap:8px; justify-content:flex-end; margin-top:16px; }
- </style>
- </head>
- <body>
- <header>
- <h1>DocCenter</h1>
- <span class="hdr-btn embed-hidden" onclick="location.href='/'">Back to IDE</span>
- <div class="spacer"></div>
- <button class="hdr-btn" id="btnRefresh" onclick="refreshList()">Refresh</button>
- <button class="hdr-btn hdr-btn-primary" id="btnSave" onclick="saveDoc()" disabled>Save</button>
- <button class="hdr-btn hdr-btn-green" id="btnPublish" onclick="publishDoc()" disabled>Publish</button>
- </header>
- <div class="layout">
- <!-- Sidebar: Document List -->
- <div class="sidebar">
- <div class="sidebar-header">
- <h3>Documents</h3>
- <input type="text" class="search-box" id="searchInput" placeholder="Search name / path / ID..." oninput="filterDocs()">
- <div class="sidebar-controls">
- <select id="sortSelect" onchange="renderVisibleDocs()">
- <option value="name">Sort: Name</option>
- <option value="path">Sort: Path</option>
- <option value="id">Sort: Doc ID</option>
- <option value="updated">Sort: Updated</option>
- <option value="version">Sort: Version</option>
- </select>
- </div>
- <div class="sidebar-note">Find the official doc, copy its <code>Doc ID</code>, then fill it into VLCode's <code>Official Doc IDs</code>.</div>
- </div>
- <div class="sidebar-actions">
- <div class="sa-btn" onclick="showCreateDialog()">+ New Doc</div>
- <div class="sa-btn" onclick="loadTags()">Tags</div>
- </div>
- <div class="doc-list" id="docList">
- <div class="list-status">Loading...</div>
- </div>
- </div>
- <!-- Main Content -->
- <div class="main-content">
- <div class="toolbar" id="toolbar" style="display:none;">
- <span class="doc-id" id="docIdLabel"></span>
- <span class="doc-title" id="docTitleLabel"></span>
- <div class="mode-tabs">
- <div class="mode-tab active" data-mode="view" onclick="setMode('view')">View</div>
- <div class="mode-tab" data-mode="edit" onclick="setMode('edit')">Edit</div>
- </div>
- </div>
- <div class="content-area" id="contentArea">
- <div class="empty-state">
- <div class="icon">📄</div>
- <p>Select a document to view or edit</p>
- </div>
- </div>
- </div>
- </div>
- <div class="status-bar">
- <span id="statusText">Ready</span>
- <div class="spacer" style="flex:1;"></div>
- <span id="statusModified"></span>
- <span id="statusChars"></span>
- </div>
- <!-- Create Document Dialog -->
- <div class="dialog-overlay" id="createDialog">
- <div class="dialog">
- <h3>Create New Document</h3>
- <label>Document Name</label>
- <input type="text" id="newDocName" placeholder="My Document">
- <label>Path (numeric ID)</label>
- <input type="number" id="newDocPath" placeholder="110" min="1">
- <label>Initial Content (optional)</label>
- <textarea id="newDocContent" rows="4" placeholder="# Title"></textarea>
- <div class="dialog-actions">
- <button class="hdr-btn" onclick="closeCreateDialog()">Cancel</button>
- <button class="hdr-btn hdr-btn-primary" onclick="createDoc()">Create</button>
- </div>
- </div>
- </div>
- <!-- Publish Dialog -->
- <div class="dialog-overlay" id="publishDialog">
- <div class="dialog">
- <h3>Publish Version</h3>
- <label>Version Message</label>
- <input type="text" id="publishMessage" placeholder="Describe this version...">
- <div class="dialog-actions">
- <button class="hdr-btn" onclick="closePublishDialog()">Cancel</button>
- <button class="hdr-btn hdr-btn-green" onclick="confirmPublish()">Publish</button>
- </div>
- </div>
- </div>
- <div class="toast" id="toast"></div>
- <script>
- // --- State ---
- let allDocs = [];
- let currentDoc = null; // { id, name, path, content, ... }
- let originalContent = ''; // track if modified
- let mode = 'view'; // 'view' | 'edit'
- const urlParams = new URLSearchParams(window.location.search);
- const embedMode = urlParams.get('embed') || '';
- function isOfficialDoc(doc = {}) {
- const id = parseInt(doc.id ?? doc._id, 10);
- return Number.isInteger(id) && id > 0 && id < 1000;
- }
- function docScope(doc = {}) {
- return isOfficialDoc(doc)
- ? { label: 'Official', className: 'doc-badge-scope-official' }
- : { label: 'User', className: 'doc-badge-scope-user' };
- }
- function parseDocTimestamp(doc = {}) {
- const raw = doc.updatedAt || doc.updated_at || doc.createdAt || doc.created_at || '';
- const ts = raw ? new Date(raw).getTime() : 0;
- return Number.isFinite(ts) ? ts : 0;
- }
- function parseDocVersion(doc = {}) {
- return parseInt(doc.latestVersion || doc.latest_version || doc.version || 0, 10) || 0;
- }
- function sortDocs(docs) {
- const sortKey = document.getElementById('sortSelect')?.value || 'name';
- const items = [...docs];
- if (sortKey === 'path') {
- return items.sort((a, b) =>
- (parseInt(a.path || '0', 10) || Number.MAX_SAFE_INTEGER) - (parseInt(b.path || '0', 10) || Number.MAX_SAFE_INTEGER)
- || String(a.name || '').localeCompare(String(b.name || ''), 'zh-CN', { numeric: true, sensitivity: 'base' })
- );
- }
- if (sortKey === 'id') {
- return items.sort((a, b) =>
- (parseInt(a.id || a._id || '0', 10) || Number.MAX_SAFE_INTEGER) - (parseInt(b.id || b._id || '0', 10) || Number.MAX_SAFE_INTEGER)
- || String(a.name || '').localeCompare(String(b.name || ''), 'zh-CN', { numeric: true, sensitivity: 'base' })
- );
- }
- if (sortKey === 'updated') {
- return items.sort((a, b) =>
- parseDocTimestamp(b) - parseDocTimestamp(a)
- || String(a.name || '').localeCompare(String(b.name || ''), 'zh-CN', { numeric: true, sensitivity: 'base' })
- );
- }
- if (sortKey === 'version') {
- return items.sort((a, b) =>
- parseDocVersion(b) - parseDocVersion(a)
- || String(a.name || '').localeCompare(String(b.name || ''), 'zh-CN', { numeric: true, sensitivity: 'base' })
- );
- }
- return items.sort((a, b) =>
- String(a.name || '').localeCompare(String(b.name || ''), 'zh-CN', { numeric: true, sensitivity: 'base' })
- || (parseInt(a.path || '0', 10) || Number.MAX_SAFE_INTEGER) - (parseInt(b.path || '0', 10) || Number.MAX_SAFE_INTEGER)
- || (parseInt(a.id || a._id || '0', 10) || 0) - (parseInt(b.id || b._id || '0', 10) || 0)
- );
- }
- function getFilteredDocs() {
- const q = document.getElementById('searchInput').value.toLowerCase().trim();
- const filtered = !q ? allDocs : allDocs.filter(d =>
- (d.name || '').toLowerCase().includes(q) ||
- String(d.path || '').includes(q) ||
- String(d.id || d._id || '').includes(q) ||
- (d.tags || []).some(t => (t.name || String(t)).toLowerCase().includes(q))
- );
- return sortDocs(filtered);
- }
- function renderVisibleDocs() {
- renderDocList(getFilteredDocs());
- }
- function applyEmbedMode() {
- if (!embedMode) return;
- document.body.classList.add('embed');
- if (embedMode === 'landing') {
- document.body.classList.add('landing-embed');
- mode = 'view';
- }
- }
- // --- API helper ---
- async function dcApi(action, body = {}) {
- const res = await fetch(`/api/doccenter/${action}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(body),
- });
- if (!res.ok) {
- const err = await res.json().catch(() => ({ error: res.statusText }));
- throw new Error(err.error || `HTTP ${res.status}`);
- }
- return res.json();
- }
- // --- Toast ---
- function toast(msg, type = '') {
- const el = document.getElementById('toast');
- el.textContent = msg;
- el.className = 'toast show ' + type;
- clearTimeout(el._timer);
- el._timer = setTimeout(() => el.className = 'toast', 3000);
- }
- // --- Document List ---
- async function refreshList() {
- const listEl = document.getElementById('docList');
- listEl.innerHTML = '<div class="list-status">Loading...</div>';
- setStatus('Fetching document list...');
- try {
- const res = await dcApi('list', { keyword: '', tagId: 0, page: 1, pageSize: 200 });
- allDocs = res?.data?.list || res?.list || [];
- renderVisibleDocs();
- setStatus(`${allDocs.length} documents loaded`);
- } catch (e) {
- listEl.innerHTML = `<div class="list-status" style="color:var(--red)">Error: ${esc(e.message)}</div>`;
- setStatus('Failed to load documents');
- toast(e.message, 'error');
- }
- }
- function renderDocList(docs) {
- const listEl = document.getElementById('docList');
- if (!docs.length) {
- listEl.innerHTML = '<div class="list-status">No documents found</div>';
- return;
- }
- listEl.innerHTML = docs.map(d => {
- const docId = parseInt(d.id ?? d._id, 10);
- const isActive = currentDoc && currentDoc.id === docId;
- const tags = (d.tags || []).map(t => `<span class="doc-tag">${esc(t.name || t)}</span>`).join('');
- const scope = docScope(d);
- const latestVersion = d.latestVersion || d.latest_version || d.version || 0;
- return `<div class="doc-item${isActive ? ' active' : ''}" data-id="${docId}" onclick="selectDoc(${docId})">
- <span class="doc-name">${esc(d.name || 'Untitled')}</span>
- <div class="doc-meta">
- <span class="doc-badge ${scope.className}">${scope.label}</span>
- <span class="doc-badge doc-badge-id">ID ${docId}</span>
- <span class="doc-badge doc-badge-path">Path ${d.path ?? docId}</span>
- <span class="doc-badge doc-badge-version">v${latestVersion}</span>
- </div>
- <div class="doc-tags">${tags}${formatDate(d.updatedAt || d.createdAt) ? `<span class="doc-tag">${esc(formatDate(d.updatedAt || d.createdAt))}</span>` : ''}</div>
- </div>`;
- }).join('');
- }
- function filterDocs() {
- renderVisibleDocs();
- }
- // --- Select & Load Document ---
- async function selectDoc(docId) {
- if (currentDoc && currentDoc.id === docId) return;
- if (currentDoc && isModified()) {
- if (!confirm('You have unsaved changes. Discard them?')) return;
- }
- setStatus(`Loading document #${docId}...`);
- try {
- const res = await dcApi('get', { docId });
- const data = res?.data || res;
- currentDoc = {
- id: data.id || docId,
- name: data.name || 'Untitled',
- path: data.path,
- content: data.currentContent || data.content || '',
- updatedAt: data.updatedAt,
- };
- originalContent = currentDoc.content;
- showDocument();
- updateButtons();
- renderDocList(allDocs); // refresh active highlight
- setStatus(`Loaded: ${currentDoc.name}`);
- } catch (e) {
- toast('Failed to load document: ' + e.message, 'error');
- setStatus('Error loading document');
- }
- }
- function showDocument() {
- const toolbar = document.getElementById('toolbar');
- toolbar.style.display = 'flex';
- document.getElementById('docIdLabel').textContent = `Doc ID ${currentDoc.id}`;
- document.getElementById('docTitleLabel').textContent = currentDoc.name;
- renderContent();
- }
- function renderContent() {
- const area = document.getElementById('contentArea');
- if (mode === 'edit') {
- area.innerHTML = `<textarea id="editor" spellcheck="false" oninput="onEditorInput()">${esc(currentDoc.content)}</textarea>`;
- document.getElementById('editor').focus();
- } else {
- area.innerHTML = `<div class="md-view">${renderMarkdown(currentDoc.content)}</div>`;
- }
- updateStatusChars();
- }
- function onEditorInput() {
- const editor = document.getElementById('editor');
- if (editor) currentDoc.content = editor.value;
- updateButtons();
- updateStatusChars();
- }
- function isModified() {
- return currentDoc && currentDoc.content !== originalContent;
- }
- function updateButtons() {
- const mod = isModified();
- document.getElementById('btnSave').disabled = !currentDoc || !mod;
- document.getElementById('btnPublish').disabled = !currentDoc;
- const modEl = document.getElementById('statusModified');
- modEl.textContent = mod ? 'Modified' : '';
- modEl.className = mod ? 'modified' : '';
- }
- function updateStatusChars() {
- const el = document.getElementById('statusChars');
- if (currentDoc) {
- const len = currentDoc.content.length;
- const lines = currentDoc.content.split('\n').length;
- el.textContent = `${lines} lines, ${len} chars`;
- } else {
- el.textContent = '';
- }
- }
- // --- Mode switching ---
- function setMode(m) {
- if (embedMode === 'landing' && m !== 'view') return;
- mode = m;
- document.querySelectorAll('.mode-tab').forEach(t => t.classList.toggle('active', t.dataset.mode === m));
- if (currentDoc) renderContent();
- }
- // --- Save ---
- async function saveDoc() {
- if (!currentDoc || !isModified()) return;
- setStatus('Saving...');
- document.getElementById('btnSave').disabled = true;
- try {
- await dcApi('save', { docId: currentDoc.id, content: currentDoc.content });
- originalContent = currentDoc.content;
- updateButtons();
- toast('Document saved', 'success');
- setStatus('Saved');
- } catch (e) {
- toast('Save failed: ' + e.message, 'error');
- setStatus('Save failed');
- document.getElementById('btnSave').disabled = false;
- }
- }
- // --- Publish ---
- function publishDoc() {
- if (!currentDoc) return;
- document.getElementById('publishMessage').value = '';
- document.getElementById('publishDialog').classList.add('open');
- }
- function closePublishDialog() {
- document.getElementById('publishDialog').classList.remove('open');
- }
- async function confirmPublish() {
- const msg = document.getElementById('publishMessage').value.trim() || 'Published from VL-Code';
- closePublishDialog();
- setStatus('Publishing...');
- try {
- // Save first if modified
- if (isModified()) {
- await dcApi('save', { docId: currentDoc.id, content: currentDoc.content });
- originalContent = currentDoc.content;
- }
- await dcApi('publish', {
- docId: currentDoc.id,
- path: currentDoc.path,
- currentContent: currentDoc.content,
- changeNote: msg,
- message: msg,
- });
- toast('Version published', 'success');
- setStatus('Published');
- updateButtons();
- } catch (e) {
- toast('Publish failed: ' + e.message, 'error');
- setStatus('Publish failed');
- }
- }
- // --- Create ---
- function showCreateDialog() {
- document.getElementById('newDocName').value = '';
- document.getElementById('newDocPath').value = '';
- document.getElementById('newDocContent').value = '';
- document.getElementById('createDialog').classList.add('open');
- }
- function closeCreateDialog() {
- document.getElementById('createDialog').classList.remove('open');
- }
- async function createDoc() {
- const name = document.getElementById('newDocName').value.trim();
- const pathVal = document.getElementById('newDocPath').value.trim();
- const content = document.getElementById('newDocContent').value;
- if (!name) { toast('Name is required', 'error'); return; }
- closeCreateDialog();
- setStatus('Creating document...');
- try {
- const body = { name, content: content || '' };
- if (pathVal) body.path = parseInt(pathVal, 10);
- const res = await dcApi('create', body);
- toast('Document created', 'success');
- await refreshList();
- // Auto-select the new doc
- const newId = res?.data?.id || res?.id;
- if (newId) selectDoc(newId);
- } catch (e) {
- toast('Create failed: ' + e.message, 'error');
- setStatus('Create failed');
- }
- }
- // --- Tags ---
- async function loadTags() {
- try {
- const res = await dcApi('tags', {});
- const tags = res?.data || res?.list || [];
- toast(`${tags.length} tags loaded. (Tag management coming soon)`, '');
- } catch (e) {
- toast('Failed to load tags: ' + e.message, 'error');
- }
- }
- // --- Status ---
- function setStatus(text) {
- document.getElementById('statusText').textContent = text;
- }
- // --- Markdown renderer (lightweight, no deps) ---
- function renderMarkdown(md) {
- if (!md) return '<p style="color:var(--text2)">Empty document</p>';
- let html = esc(md);
- // Code blocks (``` ... ```)
- html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) =>
- `<pre><code>${code}</code></pre>`
- );
- // Inline code
- html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
- // Headers
- html = html.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
- html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
- html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
- html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
- // Horizontal rules
- html = html.replace(/^---+$/gm, '<hr>');
- // Bold and italic
- html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
- html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
- // Links
- html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
- // Images
- html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
- // Blockquotes
- html = html.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>');
- // Tables: detect lines with |
- html = html.replace(/((?:^\|.+\|$\n?)+)/gm, (block) => {
- const rows = block.trim().split('\n').filter(r => r.trim());
- if (rows.length < 2) return block;
- // Check if second row is separator
- const isSep = /^\|[\s\-:]+\|$/.test(rows[1]);
- let table = '<table>';
- rows.forEach((row, i) => {
- if (isSep && i === 1) return; // skip separator
- const cells = row.split('|').filter((_, ci, arr) => ci > 0 && ci < arr.length - 1);
- const tag = (isSep && i === 0) ? 'th' : 'td';
- table += '<tr>' + cells.map(c => `<${tag}>${c.trim()}</${tag}>`).join('') + '</tr>';
- });
- table += '</table>';
- return table;
- });
- // Unordered lists
- html = html.replace(/^[\-\*] (.+)$/gm, '<li>$1</li>');
- html = html.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>');
- // Ordered lists
- html = html.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
- // Paragraphs: wrap remaining text blocks
- html = html.replace(/^(?!<[a-z])([\s\S]+?)(?=\n\n|$)/gm, (match) => {
- const trimmed = match.trim();
- if (!trimmed || /^</.test(trimmed)) return match;
- return `<p>${trimmed}</p>`;
- });
- // Clean up double newlines
- html = html.replace(/\n{2,}/g, '\n');
- return html;
- }
- // --- Helpers ---
- function esc(s) {
- if (!s) return '';
- return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
- }
- function formatDate(ts) {
- if (!ts) return '';
- try {
- const d = new Date(ts);
- if (isNaN(d)) return '';
- return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
- } catch { return ''; }
- }
- // --- Keyboard shortcuts ---
- document.addEventListener('keydown', (e) => {
- // Ctrl/Cmd+S = Save
- if ((e.ctrlKey || e.metaKey) && e.key === 's') {
- e.preventDefault();
- saveDoc();
- }
- // Escape closes dialogs
- if (e.key === 'Escape') {
- closeCreateDialog();
- closePublishDialog();
- }
- });
- // Close dialogs on overlay click
- document.querySelectorAll('.dialog-overlay').forEach(el => {
- el.addEventListener('click', (e) => {
- if (e.target === el) el.classList.remove('open');
- });
- });
- // --- Init ---
- applyEmbedMode();
- refreshList();
- </script>
- </body>
- </html>
|