plugins: files explorer and default editor improvements
- Files plugin: richer file list, breadcrumbs, toolbar, sorting, filter, rename/trash, row actions - Default Editor: line numbers, markdown toolbar, Edit/Preview/Split, reload/revert - Files plugin.json: update manifest
This commit is contained in:
parent
695c1c83d0
commit
7567e38c4f
|
|
@ -6,7 +6,6 @@
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/* ── Style injection ─────────────────────────────────────── */
|
|
||||||
function injectStyles() {
|
function injectStyles() {
|
||||||
if (document.getElementById('de-style-injected')) return;
|
if (document.getElementById('de-style-injected')) return;
|
||||||
var style = document.createElement('style');
|
var style = document.createElement('style');
|
||||||
|
|
@ -16,114 +15,41 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
var STYLES = [
|
var STYLES = [
|
||||||
'.de-root { display:flex; flex-direction:column; height:100%; min-height:0; overflow:hidden; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif; color:#e0e0e0; }',
|
'.de-root{display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;color:#e0e0e0;background:#0d0d1a}',
|
||||||
'.de-toolbar { display:flex; align-items:center; gap:0.5rem; padding:0.5rem 0.75rem; border-bottom:1px solid #16213e; flex-shrink:0; background:#12122a; }',
|
'.de-toolbar,.de-md-toolbar{display:flex;align-items:center;gap:.45rem;padding:.45rem .75rem;border-bottom:1px solid #16213e;flex-shrink:0;background:#12122a;flex-wrap:wrap}',
|
||||||
'.de-toolbar-mode { font-size:0.75rem; color:#4ecca3; padding:0.15rem 0.5rem; border-radius:3px; background:#1a2a3a; }',
|
'.de-md-toolbar{background:#101028;padding:.38rem .75rem}',
|
||||||
'.de-toolbar-context { font-size:0.7rem; color:#8b8ba8; margin-left:0.25rem; }',
|
'.de-toolbar-mode{font-size:.75rem;color:#4ecca3;padding:.15rem .5rem;border-radius:3px;background:#1a2a3a}',
|
||||||
'.de-toolbar-spacer { flex:1; }',
|
'.de-toolbar-context{font-size:.75rem;color:#a0a0bb;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
||||||
'.de-toolbar-btn { font-size:0.75rem; padding:0.25rem 0.6rem; border:1px solid #333; border-radius:4px; background:#1a1a2e; color:#ccc; cursor:pointer; }',
|
'.de-toolbar-spacer{flex:1}',
|
||||||
'.de-toolbar-btn:hover { background:#2a2a4e; border-color:#4ecca3; }',
|
'.de-toolbar-btn,.de-md-btn{font-size:.75rem;padding:.28rem .58rem;border:1px solid #333;border-radius:4px;background:#1a1a2e;color:#ccc;cursor:pointer}',
|
||||||
'.de-toolbar-btn.active { background:#1a3a2a; border-color:#4ecca3; color:#4ecca3; }',
|
'.de-md-btn{min-width:2rem;font-family:inherit}',
|
||||||
'.de-toolbar-btn:disabled { opacity:0.4; cursor:default; }',
|
'.de-toolbar-btn:hover,.de-md-btn:hover{background:#2a2a4e;border-color:#4ecca3}',
|
||||||
'.de-status { font-size:0.7rem; color:#8b8ba8; padding:0.15rem 0.5rem; }',
|
'.de-toolbar-btn.active{background:#1a3a2a;border-color:#4ecca3;color:#4ecca3}',
|
||||||
'.de-status.saved { color:#4ecca3; }',
|
'.de-toolbar-btn:disabled,.de-md-btn:disabled{opacity:.45;cursor:default}',
|
||||||
'.de-status.error { color:#e74c3c; }',
|
'.de-status{font-size:.72rem;color:#8b8ba8;padding:.15rem .5rem;white-space:nowrap}',
|
||||||
'.de-status.dirty { color:#f39c12; }',
|
'.de-status.saved{color:#4ecca3}.de-status.error{color:#e74c3c}.de-status.dirty{color:#f39c12}.de-status.saving{color:#79c0ff}',
|
||||||
'.de-editor-wrap { flex:1; display:flex; min-height:0; overflow:hidden; }',
|
'.de-editor-wrap{flex:1;display:flex;min-height:0;overflow:hidden;background:#0d0d1a}',
|
||||||
'.de-textarea { flex:1; width:100%; height:100%; resize:none; border:none; outline:none; padding:0.75rem; font-family:"SF Mono","Fira Code","Cascadia Code",monospace; font-size:0.85rem; line-height:1.6; background:#0d0d1a; color:#e0e0e0; tab-size:2; }',
|
'.de-pane{flex:1;min-width:0;min-height:0;display:flex;overflow:hidden}',
|
||||||
'.de-textarea:focus { outline:none; }',
|
'.de-pane+.de-pane{border-left:1px solid #16213e}',
|
||||||
'.de-preview { flex:1; height:100%; padding:0.75rem 1rem; overflow-y:auto; background:#0d0d1a; line-height:1.7; font-size:0.9rem; }',
|
'.de-editor-shell{flex:1;display:flex;min-width:0;min-height:0;overflow:hidden;background:#0d0d1a}',
|
||||||
'.de-preview h1,.de-preview h2,.de-preview h3,.de-preview h4,.de-preview h5,.de-preview h6 { color:#e0e0f0; margin:1rem 0 0.5rem; }',
|
'.de-lines{flex:0 0 auto;min-width:3rem;padding:.75rem .45rem;text-align:right;background:#0a0a15;color:#555;font-family:"SF Mono","Fira Code","Cascadia Code",Consolas,monospace;font-size:.82rem;line-height:1.6;user-select:none;overflow:hidden;white-space:pre}',
|
||||||
'.de-preview h1 { font-size:1.5rem; border-bottom:1px solid #16213e; padding-bottom:0.3rem; }',
|
'.de-textarea{flex:1;width:100%;height:100%;resize:none;border:0;outline:0;padding:.75rem;font-family:"SF Mono","Fira Code","Cascadia Code",Consolas,monospace;font-size:.86rem;line-height:1.6;background:#0d0d1a;color:#e0e0e0;tab-size:2;white-space:pre;overflow:auto}',
|
||||||
'.de-preview h2 { font-size:1.25rem; border-bottom:1px solid #16213e; padding-bottom:0.25rem; }',
|
'.de-preview{flex:1;height:100%;padding:1rem 1.15rem;overflow:auto;background:#0d0d1a;line-height:1.7;font-size:.92rem;color:#d8d8e8}',
|
||||||
'.de-preview h3 { font-size:1.1rem; }',
|
'.de-preview h1,.de-preview h2,.de-preview h3,.de-preview h4,.de-preview h5,.de-preview h6{color:#f0f0ff;margin:1rem 0 .5rem}',
|
||||||
'.de-preview p { margin:0.5rem 0; }',
|
'.de-preview h1{font-size:1.55rem;border-bottom:1px solid #16213e;padding-bottom:.35rem}.de-preview h2{font-size:1.3rem;border-bottom:1px solid #16213e;padding-bottom:.25rem}.de-preview h3{font-size:1.12rem}',
|
||||||
'.de-preview code { background:#1a1a2e; padding:0.15rem 0.35rem; border-radius:3px; font-size:0.85em; color:#4ecca3; }',
|
'.de-preview p{margin:.55rem 0}.de-preview code{background:#1a1a2e;padding:.15rem .35rem;border-radius:3px;font-size:.87em;color:#4ecca3}',
|
||||||
'.de-preview pre { background:#1a1a2e; padding:0.75rem; border-radius:4px; overflow-x:auto; margin:0.75rem 0; }',
|
'.de-preview pre{background:#1a1a2e;padding:.85rem;border-radius:4px;overflow:auto;margin:.8rem 0}.de-preview pre code{background:none;padding:0;color:#d8d8e8}',
|
||||||
'.de-preview pre code { background:none; padding:0; }',
|
'.de-preview ul,.de-preview ol{padding-left:1.5rem;margin:.55rem 0}.de-preview li{margin:.25rem 0}',
|
||||||
'.de-preview ul,.de-preview ol { padding-left:1.5rem; margin:0.5rem 0; }',
|
'.de-preview blockquote{border-left:3px solid #4ecca3;margin:.6rem 0;padding:.25rem .85rem;color:#aaa;background:#101028}',
|
||||||
'.de-preview li { margin:0.25rem 0; }',
|
'.de-preview a{color:#4ecca3;text-decoration:none}.de-preview a:hover{text-decoration:underline}',
|
||||||
'.de-preview blockquote { border-left:3px solid #4ecca3; margin:0.5rem 0; padding:0.25rem 0.75rem; color:#aaa; }',
|
'.de-preview table{border-collapse:collapse;margin:.8rem 0;max-width:100%;display:block;overflow:auto}.de-preview th,.de-preview td{border:1px solid #333;padding:.35rem .6rem;text-align:left}.de-preview th{background:#1a1a2e}',
|
||||||
'.de-preview a { color:#4ecca3; text-decoration:none; }',
|
'.de-preview img{max-width:100%;height:auto;border-radius:4px}.de-preview .task{margin-right:.4rem}',
|
||||||
'.de-preview a:hover { text-decoration:underline; }',
|
'.de-notes-badge{font-size:.65rem;padding:.1rem .4rem;border-radius:3px;background:#2a1a3a;color:#b388ff}',
|
||||||
'.de-preview hr { border:none; border-top:1px solid #16213e; margin:1rem 0; }',
|
'.de-notes-info{padding:.45rem .75rem;background:#111126;border-top:1px solid #16213e;font-size:.75rem;color:#8b8ba8;flex-shrink:0}',
|
||||||
'.de-preview table { border-collapse:collapse; margin:0.75rem 0; }',
|
'.de-loading,.de-error{flex:1;display:flex;align-items:center;justify-content:center;color:#777;padding:2rem}.de-error{color:#e74c3c;flex-direction:column;gap:.5rem}.de-error-msg{font-size:.85rem;color:#aaa;max-width:420px;text-align:center}',
|
||||||
'.de-preview th,.de-preview td { border:1px solid #333; padding:0.35rem 0.6rem; text-align:left; }',
|
'@media(max-width:780px){.de-editor-wrap{flex-direction:column}.de-pane+.de-pane{border-left:0;border-top:1px solid #16213e}.de-toolbar-context{max-width:100%}}'
|
||||||
'.de-preview th { background:#1a1a2e; }',
|
|
||||||
'.de-notes-badge { font-size:0.65rem; padding:0.1rem 0.4rem; border-radius:3px; background:#2a1a3a; color:#b388ff; margin-left:0.5rem; }',
|
|
||||||
'.de-notes-info { padding:0.5rem 0.75rem; background:#1a1a2a; border-top:1px solid #16213e; font-size:0.75rem; color:#8b8ba8; flex-shrink:0; }',
|
|
||||||
'.de-loading { flex:1; display:flex; align-items:center; justify-content:center; color:#666; }',
|
|
||||||
'.de-error { flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; color:#e74c3c; gap:0.5rem; }',
|
|
||||||
'.de-error-msg { font-size:0.85rem; color:#aaa; max-width:400px; text-align:center; }',
|
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
/* ── Simple markdown renderer (no raw HTML) ──────────────── */
|
|
||||||
function renderMarkdown(text) {
|
|
||||||
if (!text) return '';
|
|
||||||
var html = text
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>');
|
|
||||||
|
|
||||||
// Code blocks (``` ... ```)
|
|
||||||
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, function (_, lang, code) {
|
|
||||||
return '<pre><code class="lang-' + (lang || 'text') + '">' + code.trimEnd() + '</code></pre>';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Inline code
|
|
||||||
html = html.replace(/`([^`\n]+)`/g, '<code>$1</code>');
|
|
||||||
|
|
||||||
// Headings
|
|
||||||
html = html.replace(/^######\s+(.+)$/gm, '<h6>$1</h6>');
|
|
||||||
html = html.replace(/^#####\s+(.+)$/gm, '<h5>$1</h5>');
|
|
||||||
html = html.replace(/^####\s+(.+)$/gm, '<h4>$1</h4>');
|
|
||||||
html = html.replace(/^###\s+(.+)$/gm, '<h3>$1</h3>');
|
|
||||||
html = html.replace(/^##\s+(.+)$/gm, '<h2>$1</h2>');
|
|
||||||
html = html.replace(/^#\s+(.+)$/gm, '<h1>$1</h1>');
|
|
||||||
|
|
||||||
// Horizontal rule
|
|
||||||
html = html.replace(/^---+$/gm, '<hr>');
|
|
||||||
html = html.replace(/^\*\*\*+$/gm, '<hr>');
|
|
||||||
|
|
||||||
// Bold and italic
|
|
||||||
html = html.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
|
|
||||||
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
||||||
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
||||||
|
|
||||||
// Strikethrough
|
|
||||||
html = html.replace(/~~(.+?)~~/g, '<del>$1</del>');
|
|
||||||
|
|
||||||
// Blockquote
|
|
||||||
html = html.replace(/^>\s+(.+)$/gm, '<blockquote>$1</blockquote>');
|
|
||||||
|
|
||||||
// Unordered list items
|
|
||||||
html = html.replace(/^[\-\*]\s+(.+)$/gm, '<li>$1</li>');
|
|
||||||
|
|
||||||
// Ordered list items
|
|
||||||
html = html.replace(/^\d+\.\s+(.+)$/gm, '<li>$1</li>');
|
|
||||||
|
|
||||||
// Wrap consecutive <li> in <ul>
|
|
||||||
html = html.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>');
|
|
||||||
|
|
||||||
// Links [text](url) — render as text since we can't navigate internally
|
|
||||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a title="Link: $2">$1</a>');
|
|
||||||
|
|
||||||
// Images  — render as placeholder
|
|
||||||
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<span title="Image: $2" style="color:#666">[image: $1]</span>');
|
|
||||||
|
|
||||||
// Paragraphs: double newlines
|
|
||||||
html = html.replace(/\n\n+/g, '</p><p>');
|
|
||||||
html = '<p>' + html + '</p>';
|
|
||||||
|
|
||||||
// Clean up empty paragraphs
|
|
||||||
html = html.replace(/<p>\s*<\/p>/g, '');
|
|
||||||
html = html.replace(/<p>\s*(<(?:h[1-6]|ul|ol|pre|blockquote|hr))/g, '$1');
|
|
||||||
html = html.replace(/(<\/(?:h[1-6]|ul|ol|pre|blockquote|hr)>)\s*<\/p>/g, '$1');
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Utilities ──────────────────────────────────────────── */
|
|
||||||
function el(tag, attrs, children) {
|
function el(tag, attrs, children) {
|
||||||
var elem = document.createElement(tag);
|
var elem = document.createElement(tag);
|
||||||
if (attrs) {
|
if (attrs) {
|
||||||
|
|
@ -132,6 +58,7 @@
|
||||||
else if (k === 'style' && typeof attrs[k] === 'object') Object.assign(elem.style, attrs[k]);
|
else if (k === 'style' && typeof attrs[k] === 'object') Object.assign(elem.style, attrs[k]);
|
||||||
else if (k.slice(0, 2) === 'on') elem.addEventListener(k.slice(2).toLowerCase(), attrs[k]);
|
else if (k.slice(0, 2) === 'on') elem.addEventListener(k.slice(2).toLowerCase(), attrs[k]);
|
||||||
else if (k === 'innerHTML') elem.innerHTML = attrs[k];
|
else if (k === 'innerHTML') elem.innerHTML = attrs[k];
|
||||||
|
else if (k === 'textContent') elem.textContent = attrs[k];
|
||||||
else elem.setAttribute(k, attrs[k]);
|
else elem.setAttribute(k, attrs[k]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -144,6 +71,135 @@
|
||||||
return elem;
|
return elem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeHtml(s) {
|
||||||
|
return String(s == null ? '' : s)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeAttr(s) {
|
||||||
|
return escapeHtml(s).replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderInline(text) {
|
||||||
|
var html = escapeHtml(text);
|
||||||
|
html = html.replace(/`([^`\n]+)`/g, '<code>$1</code>');
|
||||||
|
html = html.replace(/!\[([^\]]*)\]\((https?:\/\/[^)]+)\)/g, '<img alt="$1" src="$2">');
|
||||||
|
html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^)]+|mailto:[^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
||||||
|
html = html.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
|
||||||
|
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
||||||
|
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMarkdown(text) {
|
||||||
|
var lines = String(text || '').split(/\r?\n/);
|
||||||
|
var out = [];
|
||||||
|
var inCode = false;
|
||||||
|
var codeLang = '';
|
||||||
|
var code = [];
|
||||||
|
var listType = '';
|
||||||
|
var table = [];
|
||||||
|
|
||||||
|
function closeList() {
|
||||||
|
if (listType) {
|
||||||
|
out.push('</' + listType + '>');
|
||||||
|
listType = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function closeTable() {
|
||||||
|
if (!table.length) return;
|
||||||
|
out.push('<table><tbody>' + table.map(function (row) {
|
||||||
|
return '<tr>' + row.map(function (cell) { return '<td>' + renderInline(cell.trim()) + '</td>'; }).join('') + '</tr>';
|
||||||
|
}).join('') + '</tbody></table>');
|
||||||
|
table = [];
|
||||||
|
}
|
||||||
|
function pushParagraph(line) {
|
||||||
|
closeList();
|
||||||
|
closeTable();
|
||||||
|
if (line.trim()) out.push('<p>' + renderInline(line) + '</p>');
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.forEach(function (line) {
|
||||||
|
var fence = line.match(/^```(\w*)\s*$/);
|
||||||
|
if (fence) {
|
||||||
|
if (inCode) {
|
||||||
|
out.push('<pre><code class="language-' + escapeAttr(codeLang || 'text') + '">' + escapeHtml(code.join('\n')) + '</code></pre>');
|
||||||
|
inCode = false;
|
||||||
|
code = [];
|
||||||
|
codeLang = '';
|
||||||
|
} else {
|
||||||
|
closeList();
|
||||||
|
closeTable();
|
||||||
|
inCode = true;
|
||||||
|
codeLang = fence[1] || 'text';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (inCode) {
|
||||||
|
code.push(line);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!line.trim()) {
|
||||||
|
closeList();
|
||||||
|
closeTable();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var heading = line.match(/^(#{1,6})\s+(.+)$/);
|
||||||
|
if (heading) {
|
||||||
|
closeList();
|
||||||
|
closeTable();
|
||||||
|
out.push('<h' + heading[1].length + '>' + renderInline(heading[2]) + '</h' + heading[1].length + '>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^\|.+\|$/.test(line) && !/^\|\s*-+/.test(line)) {
|
||||||
|
closeList();
|
||||||
|
table.push(line.replace(/^\||\|$/g, '').split('|'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (/^\|\s*-+/.test(line)) return;
|
||||||
|
|
||||||
|
var quote = line.match(/^>\s+(.+)$/);
|
||||||
|
if (quote) {
|
||||||
|
closeList();
|
||||||
|
closeTable();
|
||||||
|
out.push('<blockquote>' + renderInline(quote[1]) + '</blockquote>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var task = line.match(/^[-*]\s+\[([ xX])\]\s+(.+)$/);
|
||||||
|
var unordered = line.match(/^[-*]\s+(.+)$/);
|
||||||
|
var ordered = line.match(/^\d+\.\s+(.+)$/);
|
||||||
|
if (task || unordered || ordered) {
|
||||||
|
closeTable();
|
||||||
|
var desired = ordered ? 'ol' : 'ul';
|
||||||
|
if (listType !== desired) {
|
||||||
|
closeList();
|
||||||
|
out.push('<' + desired + '>');
|
||||||
|
listType = desired;
|
||||||
|
}
|
||||||
|
if (task) {
|
||||||
|
out.push('<li><input class="task" type="checkbox" disabled ' + (task[1].toLowerCase() === 'x' ? 'checked' : '') + '> ' + renderInline(task[2]) + '</li>');
|
||||||
|
} else {
|
||||||
|
out.push('<li>' + renderInline((ordered || unordered)[1]) + '</li>');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushParagraph(line);
|
||||||
|
});
|
||||||
|
if (inCode) out.push('<pre><code>' + escapeHtml(code.join('\n')) + '</code></pre>');
|
||||||
|
closeList();
|
||||||
|
closeTable();
|
||||||
|
return out.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
function detectMode(props) {
|
function detectMode(props) {
|
||||||
var ctx = props.request && props.request.context;
|
var ctx = props.request && props.request.context;
|
||||||
if (ctx && (ctx.notesMode || ctx.isInsideNotesFolder)) return 'notes-markdown';
|
if (ctx && (ctx.notesMode || ctx.isInsideNotesFolder)) return 'notes-markdown';
|
||||||
|
|
@ -152,19 +208,36 @@
|
||||||
return 'text';
|
return 'text';
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectContextLabel(mode) {
|
|
||||||
if (mode === 'notes-markdown') return 'notes';
|
|
||||||
if (mode === 'generic-markdown') return 'markdown';
|
|
||||||
return 'text';
|
|
||||||
}
|
|
||||||
|
|
||||||
function fileName(path) {
|
function fileName(path) {
|
||||||
if (!path) return '';
|
var parts = String(path || '').split('/');
|
||||||
var parts = path.split('/');
|
return parts[parts.length - 1] || '';
|
||||||
return parts[parts.length - 1];
|
}
|
||||||
|
|
||||||
|
function insertAround(textarea, before, after, placeholder) {
|
||||||
|
var start = textarea.selectionStart;
|
||||||
|
var end = textarea.selectionEnd;
|
||||||
|
var value = textarea.value;
|
||||||
|
var selected = value.slice(start, end) || placeholder || '';
|
||||||
|
textarea.value = value.slice(0, start) + before + selected + after + value.slice(end);
|
||||||
|
textarea.selectionStart = start + before.length;
|
||||||
|
textarea.selectionEnd = start + before.length + selected.length;
|
||||||
|
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
textarea.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function prefixLines(textarea, prefix, placeholder) {
|
||||||
|
var start = textarea.selectionStart;
|
||||||
|
var end = textarea.selectionEnd;
|
||||||
|
var value = textarea.value;
|
||||||
|
var selected = value.slice(start, end) || placeholder || '';
|
||||||
|
var replacement = selected.split('\n').map(function (line) { return prefix + line; }).join('\n');
|
||||||
|
textarea.value = value.slice(0, start) + replacement + value.slice(end);
|
||||||
|
textarea.selectionStart = start;
|
||||||
|
textarea.selectionEnd = start + replacement.length;
|
||||||
|
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
textarea.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── DefaultEditor component ─────────────────────────────── */
|
|
||||||
var DefaultEditor = {
|
var DefaultEditor = {
|
||||||
mount: function (containerEl, props, api) {
|
mount: function (containerEl, props, api) {
|
||||||
injectStyles();
|
injectStyles();
|
||||||
|
|
@ -173,145 +246,178 @@
|
||||||
|
|
||||||
var request = props.request || {};
|
var request = props.request || {};
|
||||||
var resourcePath = request.path || '';
|
var resourcePath = request.path || '';
|
||||||
var mode = request.mode || 'view';
|
var requestedMode = request.mode || 'view';
|
||||||
var editorMode = detectMode(props);
|
var editorMode = detectMode(props);
|
||||||
var isMarkdown = editorMode === 'generic-markdown' || editorMode === 'notes-markdown';
|
var isMarkdown = editorMode === 'generic-markdown' || editorMode === 'notes-markdown';
|
||||||
var previewVisible = isMarkdown && mode === 'view';
|
var viewMode = isMarkdown ? (requestedMode === 'edit' ? 'edit' : 'preview') : 'edit';
|
||||||
|
|
||||||
var currentContent = '';
|
var currentContent = '';
|
||||||
var savedContent = '';
|
var savedContent = '';
|
||||||
var dirty = false;
|
var dirty = false;
|
||||||
var saveState = ''; // '' | 'saved' | 'error'
|
var saveState = '';
|
||||||
|
var lastSavedAt = '';
|
||||||
var saveTimer = null;
|
var saveTimer = null;
|
||||||
var disposed = false;
|
var disposed = false;
|
||||||
|
var textarea = null;
|
||||||
|
var linesEl = null;
|
||||||
|
var previewEl = null;
|
||||||
|
|
||||||
|
containerEl.setAttribute('data-editor-mode', editorMode);
|
||||||
|
containerEl.setAttribute('data-resource-path', resourcePath);
|
||||||
|
containerEl.setAttribute('data-request-mode', requestedMode);
|
||||||
|
|
||||||
// ── Toolbar ──────────────────────────────────────────
|
|
||||||
var modeLabel = el('span', { className: 'de-toolbar-mode' }, [editorMode]);
|
var modeLabel = el('span', { className: 'de-toolbar-mode' }, [editorMode]);
|
||||||
var contextLabel = el('span', { className: 'de-toolbar-context' }, [fileName(resourcePath)]);
|
var contextLabel = el('span', { className: 'de-toolbar-context', title: resourcePath }, [resourcePath || fileName(resourcePath)]);
|
||||||
|
var notesBadge = editorMode === 'notes-markdown' ? el('span', { className: 'de-notes-badge', 'data-notes-badge': '' }, ['notes context']) : null;
|
||||||
var notesBadge = null;
|
|
||||||
if (editorMode === 'notes-markdown') {
|
|
||||||
notesBadge = el('span', { className: 'de-notes-badge' }, ['notes context']);
|
|
||||||
}
|
|
||||||
|
|
||||||
var spacer = el('span', { className: 'de-toolbar-spacer' });
|
var spacer = el('span', { className: 'de-toolbar-spacer' });
|
||||||
|
var editBtn = isMarkdown ? el('button', { className: 'de-toolbar-btn', 'data-editor-mode-button': 'edit' }, ['Edit']) : null;
|
||||||
var editBtn = null;
|
var previewBtn = isMarkdown ? el('button', { className: 'de-toolbar-btn', 'data-editor-mode-button': 'preview' }, ['Preview']) : null;
|
||||||
var previewBtn = null;
|
var splitBtn = isMarkdown ? el('button', { className: 'de-toolbar-btn', 'data-editor-mode-button': 'split' }, ['Split']) : null;
|
||||||
var saveBtn = el('button', { className: 'de-toolbar-btn' }, ['Save']);
|
var reloadBtn = el('button', { className: 'de-toolbar-btn', 'data-editor-action': 'reload' }, ['Reload']);
|
||||||
saveBtn.disabled = true;
|
var saveBtn = el('button', { className: 'de-toolbar-btn', 'data-editor-action': 'save' }, ['Save']);
|
||||||
|
var statusEl = el('span', { className: 'de-status', 'data-save-state': '' });
|
||||||
if (isMarkdown) {
|
|
||||||
editBtn = el('button', { className: 'de-toolbar-btn' + (mode === 'edit' ? ' active' : '') }, ['Edit']);
|
|
||||||
previewBtn = el('button', { className: 'de-toolbar-btn' + (previewVisible ? ' active' : '') }, ['Preview']);
|
|
||||||
if (mode === 'edit') {
|
|
||||||
previewBtn.classList.remove('active');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var statusEl = el('span', { className: 'de-status' });
|
|
||||||
|
|
||||||
var toolbarChildren = [modeLabel, contextLabel];
|
var toolbarChildren = [modeLabel, contextLabel];
|
||||||
if (notesBadge) toolbarChildren.push(notesBadge);
|
if (notesBadge) toolbarChildren.push(notesBadge);
|
||||||
toolbarChildren.push(spacer);
|
toolbarChildren.push(spacer);
|
||||||
if (editBtn) toolbarChildren.push(editBtn);
|
[editBtn, previewBtn, splitBtn, reloadBtn, saveBtn, statusEl].forEach(function (node) { if (node) toolbarChildren.push(node); });
|
||||||
if (previewBtn) toolbarChildren.push(previewBtn);
|
containerEl.appendChild(el('div', { className: 'de-toolbar' }, toolbarChildren));
|
||||||
toolbarChildren.push(saveBtn);
|
|
||||||
toolbarChildren.push(statusEl);
|
|
||||||
|
|
||||||
var toolbar = el('div', { className: 'de-toolbar' }, toolbarChildren);
|
var mdToolbar = null;
|
||||||
containerEl.appendChild(toolbar);
|
if (isMarkdown) {
|
||||||
|
mdToolbar = el('div', { className: 'de-md-toolbar' });
|
||||||
|
[
|
||||||
|
['heading', 'H', 'Heading'],
|
||||||
|
['bold', 'B', 'Bold'],
|
||||||
|
['italic', 'I', 'Italic'],
|
||||||
|
['link', 'Link', 'Link'],
|
||||||
|
['code', 'Code', 'Inline code'],
|
||||||
|
['code-block', '```', 'Code block'],
|
||||||
|
['bullet', '• List', 'Bullet list'],
|
||||||
|
['numbered', '1. List', 'Numbered list'],
|
||||||
|
['quote', 'Quote', 'Quote'],
|
||||||
|
['task', 'Task', 'Task item']
|
||||||
|
].forEach(function (item) {
|
||||||
|
mdToolbar.appendChild(el('button', { className: 'de-md-btn', 'data-md-action': item[0], title: item[2] }, [item[1]]));
|
||||||
|
});
|
||||||
|
containerEl.appendChild(mdToolbar);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Editor area ──────────────────────────────────────
|
|
||||||
var editorWrap = el('div', { className: 'de-editor-wrap' });
|
var editorWrap = el('div', { className: 'de-editor-wrap' });
|
||||||
containerEl.appendChild(editorWrap);
|
containerEl.appendChild(editorWrap);
|
||||||
|
|
||||||
var textarea = null;
|
|
||||||
var previewEl = null;
|
|
||||||
|
|
||||||
if (mode === 'edit' || !isMarkdown) {
|
|
||||||
textarea = el('textarea', { className: 'de-textarea', spellcheck: 'false' });
|
|
||||||
textarea.value = '';
|
|
||||||
editorWrap.appendChild(textarea);
|
|
||||||
|
|
||||||
textarea.addEventListener('input', function () {
|
|
||||||
currentContent = textarea.value;
|
|
||||||
dirty = currentContent !== savedContent;
|
|
||||||
updateStatus();
|
|
||||||
});
|
|
||||||
|
|
||||||
textarea.addEventListener('keydown', function (e) {
|
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
|
||||||
e.preventDefault();
|
|
||||||
save();
|
|
||||||
}
|
|
||||||
// Tab support
|
|
||||||
if (e.key === 'Tab') {
|
|
||||||
e.preventDefault();
|
|
||||||
var start = textarea.selectionStart;
|
|
||||||
var end = textarea.selectionEnd;
|
|
||||||
textarea.value = textarea.value.substring(0, start) + ' ' + textarea.value.substring(end);
|
|
||||||
textarea.selectionStart = textarea.selectionEnd = start + 2;
|
|
||||||
textarea.dispatchEvent(new Event('input'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMarkdown && previewVisible) {
|
|
||||||
previewEl = el('div', { className: 'de-preview' });
|
|
||||||
editorWrap.appendChild(previewEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Notes info bar ───────────────────────────────────
|
|
||||||
if (editorMode === 'notes-markdown') {
|
if (editorMode === 'notes-markdown') {
|
||||||
var notesInfo = el('div', { className: 'de-notes-info' }, [
|
containerEl.appendChild(el('div', { className: 'de-notes-info' }, ['Notes context active. Note actions, backlinks, and graph tools are reserved for the future Notes plugin.']));
|
||||||
'Notes context active — internal links, backlinks, and widgets deferred.'
|
}
|
||||||
]);
|
|
||||||
containerEl.appendChild(notesInfo);
|
function updateLineNumbers() {
|
||||||
|
if (!linesEl || !textarea) return;
|
||||||
|
var count = textarea.value.split('\n').length;
|
||||||
|
var numbers = [];
|
||||||
|
for (var i = 1; i <= count; i += 1) numbers.push(String(i));
|
||||||
|
linesEl.textContent = numbers.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Status helpers ───────────────────────────────────
|
|
||||||
function updateStatus() {
|
function updateStatus() {
|
||||||
if (saveState === 'error') {
|
if (saveState === 'saving') {
|
||||||
|
statusEl.textContent = 'Saving...';
|
||||||
|
statusEl.className = 'de-status saving';
|
||||||
|
} else if (saveState === 'error') {
|
||||||
statusEl.textContent = 'Error saving';
|
statusEl.textContent = 'Error saving';
|
||||||
statusEl.className = 'de-status error';
|
statusEl.className = 'de-status error';
|
||||||
} else if (saveState === 'saved') {
|
|
||||||
statusEl.textContent = 'Saved';
|
|
||||||
statusEl.className = 'de-status saved';
|
|
||||||
} else if (dirty) {
|
} else if (dirty) {
|
||||||
statusEl.textContent = 'Modified';
|
statusEl.textContent = 'Modified';
|
||||||
statusEl.className = 'de-status dirty';
|
statusEl.className = 'de-status dirty';
|
||||||
|
} else if (lastSavedAt) {
|
||||||
|
statusEl.textContent = saveState === 'saved' ? 'Saved ' + lastSavedAt : 'Saved';
|
||||||
|
statusEl.className = 'de-status saved';
|
||||||
} else {
|
} else {
|
||||||
statusEl.textContent = '';
|
statusEl.textContent = '';
|
||||||
statusEl.className = 'de-status';
|
statusEl.className = 'de-status';
|
||||||
}
|
}
|
||||||
saveBtn.disabled = !dirty;
|
saveBtn.disabled = !dirty || saveState === 'saving';
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePreview() {
|
function updatePreview() {
|
||||||
if (previewEl) {
|
if (previewEl) previewEl.innerHTML = isMarkdown ? renderMarkdown(currentContent) : '<pre>' + escapeHtml(currentContent) + '</pre>';
|
||||||
previewEl.innerHTML = renderMarkdown(currentContent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Save ─────────────────────────────────────────────
|
function syncFromTextarea() {
|
||||||
function save() {
|
if (!textarea) return;
|
||||||
if (!dirty || disposed) return;
|
currentContent = textarea.value;
|
||||||
|
dirty = currentContent !== savedContent;
|
||||||
saveState = '';
|
saveState = '';
|
||||||
|
updateLineNumbers();
|
||||||
updateStatus();
|
updateStatus();
|
||||||
api.files.writeText(resourcePath, currentContent, {
|
updatePreview();
|
||||||
createIfMissing: false,
|
}
|
||||||
overwrite: true
|
|
||||||
}).then(function () {
|
function makeEditorPane() {
|
||||||
|
var pane = el('div', { className: 'de-pane' });
|
||||||
|
var shell = el('div', { className: 'de-editor-shell' });
|
||||||
|
linesEl = el('div', { className: 'de-lines' });
|
||||||
|
textarea = el('textarea', { className: 'de-textarea', spellcheck: 'false', 'data-editor-textarea': '' });
|
||||||
|
textarea.value = currentContent;
|
||||||
|
textarea.addEventListener('input', syncFromTextarea);
|
||||||
|
textarea.addEventListener('scroll', function () { if (linesEl) linesEl.scrollTop = textarea.scrollTop; });
|
||||||
|
textarea.addEventListener('keydown', function (event) {
|
||||||
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {
|
||||||
|
event.preventDefault();
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
if (event.key === 'Tab') {
|
||||||
|
event.preventDefault();
|
||||||
|
insertAround(textarea, ' ', '', '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
shell.appendChild(linesEl);
|
||||||
|
shell.appendChild(textarea);
|
||||||
|
pane.appendChild(shell);
|
||||||
|
updateLineNumbers();
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makePreviewPane() {
|
||||||
|
var pane = el('div', { className: 'de-pane' });
|
||||||
|
previewEl = el('div', { className: 'de-preview', 'data-preview': '' });
|
||||||
|
pane.appendChild(previewEl);
|
||||||
|
updatePreview();
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rebuildEditorArea() {
|
||||||
|
editorWrap.innerHTML = '';
|
||||||
|
textarea = null;
|
||||||
|
linesEl = null;
|
||||||
|
previewEl = null;
|
||||||
|
if (!isMarkdown || viewMode === 'edit') editorWrap.appendChild(makeEditorPane());
|
||||||
|
if (isMarkdown && viewMode === 'preview') editorWrap.appendChild(makePreviewPane());
|
||||||
|
if (isMarkdown && viewMode === 'split') {
|
||||||
|
editorWrap.appendChild(makeEditorPane());
|
||||||
|
editorWrap.appendChild(makePreviewPane());
|
||||||
|
}
|
||||||
|
if (editBtn) editBtn.className = 'de-toolbar-btn' + (viewMode === 'edit' ? ' active' : '');
|
||||||
|
if (previewBtn) previewBtn.className = 'de-toolbar-btn' + (viewMode === 'preview' ? ' active' : '');
|
||||||
|
if (splitBtn) splitBtn.className = 'de-toolbar-btn' + (viewMode === 'split' ? ' active' : '');
|
||||||
|
updateStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
if (!dirty || disposed) return Promise.resolve();
|
||||||
|
saveState = 'saving';
|
||||||
|
updateStatus();
|
||||||
|
return api.files.writeText(resourcePath, currentContent, { createIfMissing: false, overwrite: true }).then(function () {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
savedContent = currentContent;
|
savedContent = currentContent;
|
||||||
dirty = false;
|
dirty = false;
|
||||||
saveState = 'saved';
|
saveState = 'saved';
|
||||||
|
lastSavedAt = new Date().toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
|
||||||
updateStatus();
|
updateStatus();
|
||||||
if (saveTimer) clearTimeout(saveTimer);
|
if (saveTimer) clearTimeout(saveTimer);
|
||||||
saveTimer = setTimeout(function () {
|
saveTimer = setTimeout(function () {
|
||||||
if (!disposed) { saveState = ''; updateStatus(); }
|
if (!disposed) {
|
||||||
}, 2000);
|
saveState = '';
|
||||||
|
updateStatus();
|
||||||
|
}
|
||||||
|
}, 2500);
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
saveState = 'error';
|
saveState = 'error';
|
||||||
|
|
@ -320,105 +426,69 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Toolbar events ───────────────────────────────────
|
function reloadFromDisk() {
|
||||||
saveBtn.addEventListener('click', save);
|
if (dirty && !window.confirm('Discard unsaved changes and reload from disk?')) return;
|
||||||
|
editorWrap.innerHTML = '';
|
||||||
if (editBtn) {
|
editorWrap.appendChild(el('div', { className: 'de-loading' }, ['Loading...']));
|
||||||
editBtn.addEventListener('click', function () {
|
api.files.readText(resourcePath).then(function (content) {
|
||||||
if (mode === 'edit') return;
|
if (disposed) return;
|
||||||
mode = 'edit';
|
currentContent = String(content == null ? '' : content);
|
||||||
previewVisible = false;
|
savedContent = currentContent;
|
||||||
|
dirty = false;
|
||||||
|
saveState = '';
|
||||||
rebuildEditorArea();
|
rebuildEditorArea();
|
||||||
|
}).catch(function (err) {
|
||||||
|
if (disposed) return;
|
||||||
|
editorWrap.innerHTML = '';
|
||||||
|
editorWrap.appendChild(el('div', { className: 'de-error' }, [
|
||||||
|
el('div', {}, ['Failed to load file']),
|
||||||
|
el('div', { className: 'de-error-msg' }, [(err && err.message) ? err.message : String(err)])
|
||||||
|
]));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (previewBtn) {
|
function setMode(nextMode) {
|
||||||
previewBtn.addEventListener('click', function () {
|
if (!isMarkdown || viewMode === nextMode) return;
|
||||||
if (previewVisible) return;
|
viewMode = nextMode;
|
||||||
previewVisible = true;
|
|
||||||
mode = isMarkdown ? 'view' : mode;
|
|
||||||
rebuildEditorArea();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function rebuildEditorArea() {
|
|
||||||
editorWrap.innerHTML = '';
|
|
||||||
textarea = null;
|
|
||||||
previewEl = null;
|
|
||||||
|
|
||||||
if (mode === 'edit' || !isMarkdown) {
|
|
||||||
textarea = el('textarea', { className: 'de-textarea', spellcheck: 'false' });
|
|
||||||
textarea.value = currentContent;
|
|
||||||
editorWrap.appendChild(textarea);
|
|
||||||
|
|
||||||
textarea.addEventListener('input', function () {
|
|
||||||
currentContent = textarea.value;
|
|
||||||
dirty = currentContent !== savedContent;
|
|
||||||
updateStatus();
|
|
||||||
});
|
|
||||||
|
|
||||||
textarea.addEventListener('keydown', function (e) {
|
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
|
||||||
e.preventDefault();
|
|
||||||
save();
|
|
||||||
}
|
|
||||||
if (e.key === 'Tab') {
|
|
||||||
e.preventDefault();
|
|
||||||
var start = textarea.selectionStart;
|
|
||||||
var end = textarea.selectionEnd;
|
|
||||||
textarea.value = textarea.value.substring(0, start) + ' ' + textarea.value.substring(end);
|
|
||||||
textarea.selectionStart = textarea.selectionEnd = start + 2;
|
|
||||||
textarea.dispatchEvent(new Event('input'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMarkdown && previewVisible) {
|
|
||||||
previewEl = el('div', { className: 'de-preview' });
|
|
||||||
editorWrap.appendChild(previewEl);
|
|
||||||
updatePreview();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update toolbar button states
|
|
||||||
if (editBtn) {
|
|
||||||
editBtn.className = 'de-toolbar-btn' + (mode === 'edit' ? ' active' : '');
|
|
||||||
}
|
|
||||||
if (previewBtn) {
|
|
||||||
previewBtn.className = 'de-toolbar-btn' + (previewVisible ? ' active' : '');
|
|
||||||
}
|
|
||||||
updateStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Load file ────────────────────────────────────────
|
|
||||||
editorWrap.appendChild(el('div', { className: 'de-loading' }, ['Loading...']));
|
|
||||||
|
|
||||||
api.files.readText(resourcePath).then(function (content) {
|
|
||||||
if (disposed) return;
|
|
||||||
editorWrap.innerHTML = '';
|
|
||||||
currentContent = content;
|
|
||||||
savedContent = content;
|
|
||||||
dirty = false;
|
|
||||||
rebuildEditorArea();
|
rebuildEditorArea();
|
||||||
}).catch(function (err) {
|
}
|
||||||
if (disposed) return;
|
|
||||||
editorWrap.innerHTML = '';
|
function applyMarkdownAction(action) {
|
||||||
var msg = (err && err.message) ? err.message : String(err);
|
if (!textarea && viewMode === 'preview') {
|
||||||
editorWrap.appendChild(el('div', { className: 'de-error' }, [
|
setMode('edit');
|
||||||
el('div', {}, ['Failed to load file']),
|
}
|
||||||
el('div', { className: 'de-error-msg' }, [msg])
|
if (!textarea) return;
|
||||||
]));
|
if (action === 'heading') prefixLines(textarea, '# ', '');
|
||||||
});
|
else if (action === 'bold') insertAround(textarea, '**', '**', 'bold text');
|
||||||
|
else if (action === 'italic') insertAround(textarea, '*', '*', 'italic text');
|
||||||
|
else if (action === 'link') insertAround(textarea, '[', '](https://)', 'link text');
|
||||||
|
else if (action === 'code') insertAround(textarea, '`', '`', 'code');
|
||||||
|
else if (action === 'code-block') insertAround(textarea, '```\n', '\n```', 'code');
|
||||||
|
else if (action === 'bullet') prefixLines(textarea, '- ', 'item');
|
||||||
|
else if (action === 'numbered') prefixLines(textarea, '1. ', 'item');
|
||||||
|
else if (action === 'quote') prefixLines(textarea, '> ', 'quote');
|
||||||
|
else if (action === 'task') prefixLines(textarea, '- [ ] ', 'task');
|
||||||
|
}
|
||||||
|
|
||||||
|
saveBtn.addEventListener('click', save);
|
||||||
|
reloadBtn.addEventListener('click', reloadFromDisk);
|
||||||
|
if (editBtn) editBtn.addEventListener('click', function () { setMode('edit'); });
|
||||||
|
if (previewBtn) previewBtn.addEventListener('click', function () { setMode('preview'); });
|
||||||
|
if (splitBtn) splitBtn.addEventListener('click', function () { setMode('split'); });
|
||||||
|
if (mdToolbar) {
|
||||||
|
mdToolbar.addEventListener('click', function (event) {
|
||||||
|
var button = event.target.closest('[data-md-action]');
|
||||||
|
if (!button) return;
|
||||||
|
applyMarkdownAction(button.getAttribute('data-md-action'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadFromDisk();
|
||||||
|
|
||||||
// ── Cleanup ──────────────────────────────────────────
|
|
||||||
containerEl.__deCleanup = function () {
|
containerEl.__deCleanup = function () {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
if (saveTimer) clearTimeout(saveTimer);
|
if (saveTimer) clearTimeout(saveTimer);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set data attributes for testability
|
|
||||||
containerEl.setAttribute('data-editor-mode', editorMode);
|
|
||||||
containerEl.setAttribute('data-resource-path', resourcePath);
|
|
||||||
containerEl.setAttribute('data-request-mode', mode);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
unmount: function (containerEl) {
|
unmount: function (containerEl) {
|
||||||
|
|
@ -430,11 +500,7 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ── Register ────────────────────────────────────────────── */
|
|
||||||
window.VerstakPluginRegister('verstak.default-editor', {
|
window.VerstakPluginRegister('verstak.default-editor', {
|
||||||
components: {
|
components: { DefaultEditor: DefaultEditor }
|
||||||
DefaultEditor: DefaultEditor
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/* ── Style injection ─────────────────────────────────────── */
|
|
||||||
function injectStyles() {
|
function injectStyles() {
|
||||||
if (document.getElementById('files-style-injected')) return;
|
if (document.getElementById('files-style-injected')) return;
|
||||||
var style = document.createElement('style');
|
var style = document.createElement('style');
|
||||||
|
|
@ -16,33 +15,40 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
var STYLES = [
|
var STYLES = [
|
||||||
'.files-root{display:flex;flex-direction:column;height:100%;min-height:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;color:#e0e0e0;background:#0d0d1a}',
|
'.files-root{display:flex;flex-direction:column;height:100%;min-height:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;color:#e0e0e0;background:#0d0d1a;outline:none}',
|
||||||
'.files-toolbar{display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0.75rem;border-bottom:1px solid #16213e;flex-shrink:0;background:#12122a}',
|
'.files-toolbar{display:flex;align-items:center;gap:.45rem;padding:.5rem .75rem;border-bottom:1px solid #16213e;flex-shrink:0;background:#12122a;flex-wrap:wrap}',
|
||||||
'.files-toolbar-btn{font-size:0.75rem;padding:0.25rem 0.6rem;border:1px solid #333;border-radius:4px;background:#1a1a2e;color:#ccc;cursor:pointer}',
|
'.files-toolbar-btn,.files-row-btn{font-size:.75rem;padding:.28rem .58rem;border:1px solid #333;border-radius:4px;background:#1a1a2e;color:#ccc;cursor:pointer}',
|
||||||
'.files-toolbar-btn:hover{background:#2a2a4e;border-color:#4ecca3}',
|
'.files-toolbar-btn:hover,.files-row-btn:hover{background:#2a2a4e;border-color:#4ecca3}',
|
||||||
'.files-toolbar-btn:disabled{opacity:0.4;cursor:default}',
|
'.files-toolbar-btn:disabled,.files-row-btn:disabled{opacity:.45;cursor:default;border-color:#333}',
|
||||||
'.files-breadcrumb{display:flex;align-items:center;gap:0.25rem;font-size:0.8rem;color:#8b8ba8;flex:1;min-width:0;overflow:hidden}',
|
'.files-breadcrumb{display:flex;align-items:center;gap:.25rem;font-size:.8rem;color:#8b8ba8;min-width:160px;flex:1;overflow:hidden}',
|
||||||
'.files-breadcrumb-item{color:#4ecca3;cursor:pointer;padding:0.1rem 0.3rem;border-radius:3px}',
|
'.files-breadcrumb-item{color:#4ecca3;cursor:pointer;padding:.1rem .3rem;border-radius:3px;white-space:nowrap}',
|
||||||
'.files-breadcrumb-item:hover{background:#1a2a3a}',
|
'.files-breadcrumb-item:hover{background:#1a2a3a}',
|
||||||
|
'.files-breadcrumb-current{color:#ddd;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
|
||||||
'.files-breadcrumb-sep{color:#555}',
|
'.files-breadcrumb-sep{color:#555}',
|
||||||
'.files-list{flex:1;overflow-y:auto;padding:0.5rem 0}',
|
'.files-filter,.files-sort,.files-create-input,.files-rename-input{font-size:.78rem;padding:.32rem .5rem;border:1px solid #333;border-radius:4px;background:#0d0d1a;color:#e0e0e0;outline:none}',
|
||||||
'.files-item{display:flex;align-items:center;gap:0.6rem;padding:0.4rem 0.75rem;cursor:pointer;font-size:0.85rem}',
|
'.files-filter{width:11rem}',
|
||||||
'.files-item:hover{background:#1a1a2e}',
|
'.files-sort{width:9.5rem}',
|
||||||
|
'.files-filter:focus,.files-sort:focus,.files-create-input:focus,.files-rename-input:focus{border-color:#4ecca3}',
|
||||||
|
'.files-list{flex:1;overflow:auto;min-height:0}',
|
||||||
|
'.files-header,.files-item{display:grid;grid-template-columns:minmax(160px,1fr) 90px 90px 150px 220px;align-items:center;gap:.5rem;padding:.38rem .75rem;border-bottom:1px solid rgba(22,33,62,.55)}',
|
||||||
|
'.files-header{position:sticky;top:0;background:#101028;color:#8b8ba8;font-size:.7rem;text-transform:uppercase;letter-spacing:.04em;z-index:1}',
|
||||||
|
'.files-item{font-size:.84rem;cursor:pointer}',
|
||||||
|
'.files-item:hover{background:#17172d}',
|
||||||
'.files-item.selected{background:#1a2a3a}',
|
'.files-item.selected{background:#1a2a3a}',
|
||||||
'.files-item-icon{width:1.2rem;height:1.2rem;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;color:#8b8ba8}',
|
'.files-namecell{display:flex;align-items:center;gap:.55rem;min-width:0}',
|
||||||
|
'.files-item-icon{width:1.25rem;height:1.25rem;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;color:#8b8ba8}',
|
||||||
'.files-item-icon svg{display:block;width:16px;height:16px}',
|
'.files-item-icon svg{display:block;width:16px;height:16px}',
|
||||||
'.files-item-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
'.files-item-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
||||||
'.files-item-meta{font-size:0.7rem;color:#666;flex-shrink:0}',
|
'.files-item-meta{font-size:.74rem;color:#777;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
|
||||||
'.files-empty{flex:1;display:flex;align-items:center;justify-content:center;color:#666;font-size:0.9rem}',
|
'.files-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:.35rem;white-space:nowrap}',
|
||||||
'.files-loading{flex:1;display:flex;align-items:center;justify-content:center;color:#666}',
|
'.files-empty,.files-loading{flex:1;display:flex;align-items:center;justify-content:center;color:#666;font-size:.9rem;padding:2rem}',
|
||||||
'.files-error{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#e74c3c;gap:0.5rem;padding:1rem}',
|
'.files-error{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#e74c3c;gap:.5rem;padding:1rem}',
|
||||||
'.files-error-msg{font-size:0.85rem;color:#aaa;max-width:400px;text-align:center}',
|
'.files-error-msg{font-size:.85rem;color:#aaa;max-width:420px;text-align:center}',
|
||||||
'.files-create-bar{display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0.75rem;border-top:1px solid #16213e;flex-shrink:0;background:#12122a}',
|
'.files-panel{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border-top:1px solid #16213e;flex-shrink:0;background:#12122a}',
|
||||||
'.files-create-input{flex:1;font-size:0.8rem;padding:0.3rem 0.5rem;border:1px solid #333;border-radius:4px;background:#0d0d1a;color:#e0e0e0;outline:none}',
|
'.files-create-input,.files-rename-input{flex:1;min-width:160px}',
|
||||||
'.files-create-input:focus{border-color:#4ecca3}',
|
'@media(max-width:760px){.files-header,.files-item{grid-template-columns:minmax(130px,1fr) 70px 0 0 150px}.files-header span:nth-child(3),.files-header span:nth-child(4),.files-item-meta.hide-narrow{display:none}.files-toolbar{align-items:stretch}.files-filter,.files-sort{width:100%}}'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
/* ── Utilities ──────────────────────────────────────────── */
|
|
||||||
function el(tag, attrs, children) {
|
function el(tag, attrs, children) {
|
||||||
var elem = document.createElement(tag);
|
var elem = document.createElement(tag);
|
||||||
if (attrs) {
|
if (attrs) {
|
||||||
|
|
@ -68,148 +74,247 @@
|
||||||
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="' + path + '" fill="currentColor"/></svg>';
|
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="' + path + '" fill="currentColor"/></svg>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanPath(path) {
|
||||||
|
return String(path || '').split('/').filter(Boolean).join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
function parentPath(path) {
|
||||||
|
path = cleanPath(path);
|
||||||
|
var idx = path.lastIndexOf('/');
|
||||||
|
return idx === -1 ? '' : path.slice(0, idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
function baseName(path) {
|
||||||
|
path = cleanPath(path);
|
||||||
|
var idx = path.lastIndexOf('/');
|
||||||
|
return idx === -1 ? path : path.slice(idx + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extension(name) {
|
||||||
|
var dot = String(name || '').lastIndexOf('.');
|
||||||
|
return dot > 0 ? name.slice(dot + 1).toLowerCase() : '';
|
||||||
|
}
|
||||||
|
|
||||||
function fileIcon(entry) {
|
function fileIcon(entry) {
|
||||||
if (entry.type === 'folder') {
|
if (entry.type === 'folder') return svgIcon('M3 5a2 2 0 0 1 2-2h5l2 3h7a2 2 0 0 1 2 2v1H3V5Zm0 6h18v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7Z');
|
||||||
return svgIcon('M3 5a2 2 0 0 1 2-2h5l2 3h7a2 2 0 0 1 2 2v1H3V5Zm0 6h18v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7Z');
|
var ext = (entry.extension || extension(entry.name)).toLowerCase();
|
||||||
}
|
if (ext === 'md' || ext === 'markdown') return svgIcon('M5 3h10l4 4v14H5V3Zm9 1.5V8h3.5L14 4.5ZM8 11h8v2H8v-2Zm0 4h8v2H8v-2Z');
|
||||||
var ext = (entry.extension || '').toLowerCase();
|
if (ext === 'json' || ext === 'yaml' || ext === 'yml' || ext === 'toml') return '{ }';
|
||||||
if (ext === 'md' || ext === 'markdown') {
|
|
||||||
return svgIcon('M5 3h10l4 4v14H5V3Zm9 1.5V8h3.5L14 4.5ZM8 11h8v2H8v-2Zm0 4h8v2H8v-2Z');
|
|
||||||
}
|
|
||||||
if (ext === 'txt' || ext === 'log') {
|
|
||||||
return svgIcon('M6 2h9l5 5v15H6V2Zm8 1.5V8h4.5L14 3.5ZM8 12h8v2H8v-2Zm0 4h8v2H8v-2Z');
|
|
||||||
}
|
|
||||||
if (ext === 'json') return '{ }';
|
|
||||||
if (ext === 'yaml' || ext === 'yml' || ext === 'toml') {
|
|
||||||
return svgIcon('M19.14 13l.57-1.43 1.79-.5-1-2.29-1.64.73-.29-.28-.73-1.64 2.29-1-1-2.29-1.79.5-.57 1.43-2.17.17-.57-1.43-1.79-.5-1 2.29 1.64.73-.29.28-.73 1.64-2.29-1-1 2.29 1.79.5.57 1.43-.57 1.43-1.79.5 1 2.29 1.64-.73.29.28.73 1.64-2.29 1 1 2.29 1.79-.5.57-1.43 2.17-.17.57 1.43 1.79.5 1-2.29-1.64-.73.29-.28.73-1.64 2.29 1 1-2.29-1.79-.5-.57-1.43ZM12 9a3 3 0 1 1 0 6 3 3 0 0 1 0-6Z');
|
|
||||||
}
|
|
||||||
return svgIcon('M6 2h9l5 5v15H6V2Zm8 1.5V8h4.5L14 3.5Z');
|
return svgIcon('M6 2h9l5 5v15H6V2Zm8 1.5V8h4.5L14 3.5Z');
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSize(bytes) {
|
function formatSize(bytes) {
|
||||||
if (bytes == null || bytes === 0) return '';
|
if (!bytes) return '';
|
||||||
if (bytes < 1024) return bytes + ' B';
|
if (bytes < 1024) return bytes + ' B';
|
||||||
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
||||||
return (bytes / 1048576).toFixed(1) + ' MB';
|
return (bytes / 1048576).toFixed(1) + ' MB';
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortEntries(entries) {
|
function formatDate(value) {
|
||||||
var folders = entries.filter(function (e) { return e.type === 'folder'; });
|
if (!value) return '';
|
||||||
var files = entries.filter(function (e) { return e.type !== 'folder'; });
|
var date = new Date(value);
|
||||||
folders.sort(function (a, b) { return a.name.localeCompare(b.name); });
|
if (isNaN(date.getTime())) return '';
|
||||||
files.sort(function (a, b) { return a.name.localeCompare(b.name); });
|
return date.toLocaleString(undefined, { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||||||
return folders.concat(files);
|
}
|
||||||
|
|
||||||
|
function typeLabel(entry) {
|
||||||
|
if (entry.type === 'folder') return 'folder';
|
||||||
|
return (entry.extension || extension(entry.name) || 'file').toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── FilesView component ─────────────────────────────────── */
|
|
||||||
var FilesView = {
|
var FilesView = {
|
||||||
mount: function (containerEl, props, api) {
|
mount: function (containerEl, props, api) {
|
||||||
injectStyles();
|
injectStyles();
|
||||||
containerEl.innerHTML = '';
|
containerEl.innerHTML = '';
|
||||||
containerEl.className = 'files-root';
|
containerEl.className = 'files-root';
|
||||||
|
containerEl.setAttribute('tabindex', '0');
|
||||||
|
containerEl.setAttribute('data-plugin-id', 'verstak.files');
|
||||||
|
|
||||||
var workspaceRoot = cleanPath(props && (props.workspaceRootPath || (props.workspaceNode && props.workspaceNode.path)) || '');
|
var workspaceNode = props && props.workspaceNode;
|
||||||
|
var workspaceRoot = cleanPath(props && (props.workspaceRootPath || (workspaceNode && (workspaceNode.rootPath || workspaceNode.name || workspaceNode.id))) || '');
|
||||||
|
var workspaceName = workspaceRoot || (workspaceNode && (workspaceNode.name || workspaceNode.title || workspaceNode.id)) || 'Workspace';
|
||||||
var currentPath = '';
|
var currentPath = '';
|
||||||
var entries = [];
|
var entries = [];
|
||||||
|
var selectedPath = '';
|
||||||
|
var filterText = '';
|
||||||
|
var sortMode = 'folder-name';
|
||||||
|
var createMode = '';
|
||||||
|
var renameTarget = null;
|
||||||
var disposed = false;
|
var disposed = false;
|
||||||
|
|
||||||
|
function scopedPath(local) {
|
||||||
|
local = cleanPath(local);
|
||||||
|
return workspaceRoot ? (local ? workspaceRoot + '/' + local : workspaceRoot) : local;
|
||||||
|
}
|
||||||
|
|
||||||
|
function localPath(full) {
|
||||||
|
full = cleanPath(full);
|
||||||
|
if (!workspaceRoot) return full;
|
||||||
|
if (full === workspaceRoot) return '';
|
||||||
|
return full.indexOf(workspaceRoot + '/') === 0 ? full.slice(workspaceRoot.length + 1) : full;
|
||||||
|
}
|
||||||
|
|
||||||
var toolbar = el('div', { className: 'files-toolbar' });
|
var toolbar = el('div', { className: 'files-toolbar' });
|
||||||
var breadcrumb = el('div', { className: 'files-breadcrumb' });
|
var breadcrumb = el('div', { className: 'files-breadcrumb' });
|
||||||
var refreshBtn = el('button', { className: 'files-toolbar-btn' }, ['Refresh']);
|
var upBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'up', title: 'Up' }, ['Up']);
|
||||||
var createFolderBtn = el('button', { className: 'files-toolbar-btn' }, ['+ Folder']);
|
var refreshBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'refresh', title: 'Refresh' }, ['Refresh']);
|
||||||
var createFileBtn = el('button', { className: 'files-toolbar-btn' }, ['+ File']);
|
var newFolderBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'new-folder' }, ['+ Folder']);
|
||||||
|
var newMdBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'new-markdown' }, ['+ Markdown']);
|
||||||
|
var newTextBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'new-text' }, ['+ Text']);
|
||||||
|
var openBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'open' }, ['Open']);
|
||||||
|
var renameBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'rename' }, ['Rename']);
|
||||||
|
var trashBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'trash' }, ['Trash']);
|
||||||
|
var filterInput = el('input', { className: 'files-filter', 'data-files-filter': '', placeholder: 'Filter current folder' });
|
||||||
|
var sortSelect = el('select', { className: 'files-sort', 'data-files-sort': '' }, [
|
||||||
|
el('option', { value: 'folder-name' }, ['Folders + name']),
|
||||||
|
el('option', { value: 'name-asc' }, ['Name']),
|
||||||
|
el('option', { value: 'type' }, ['Type']),
|
||||||
|
el('option', { value: 'modified-desc' }, ['Modified']),
|
||||||
|
el('option', { value: 'size-desc' }, ['Size'])
|
||||||
|
]);
|
||||||
toolbar.appendChild(breadcrumb);
|
toolbar.appendChild(breadcrumb);
|
||||||
toolbar.appendChild(refreshBtn);
|
[upBtn, refreshBtn, newFolderBtn, newMdBtn, newTextBtn, openBtn, renameBtn, trashBtn, filterInput, sortSelect].forEach(function (node) { toolbar.appendChild(node); });
|
||||||
toolbar.appendChild(createFolderBtn);
|
|
||||||
toolbar.appendChild(createFileBtn);
|
|
||||||
containerEl.appendChild(toolbar);
|
containerEl.appendChild(toolbar);
|
||||||
|
|
||||||
var listContainer = el('div', { className: 'files-list' });
|
var listContainer = el('div', { className: 'files-list', 'data-files-list': '' });
|
||||||
containerEl.appendChild(listContainer);
|
containerEl.appendChild(listContainer);
|
||||||
|
|
||||||
var createBar = el('div', { className: 'files-create-bar', style: { display: 'none' } });
|
var createPanel = el('div', { className: 'files-panel', style: { display: 'none' } });
|
||||||
var createInput = el('input', { className: 'files-create-input', placeholder: 'Name...' });
|
var createInput = el('input', { className: 'files-create-input', 'data-files-create-input': '' });
|
||||||
var createConfirmBtn = el('button', { className: 'files-toolbar-btn' }, ['Create']);
|
var createConfirm = el('button', { className: 'files-toolbar-btn', 'data-files-create-confirm': '' }, ['Create']);
|
||||||
var createCancelBtn = el('button', { className: 'files-toolbar-btn' }, ['Cancel']);
|
var createCancel = el('button', { className: 'files-toolbar-btn' }, ['Cancel']);
|
||||||
createBar.appendChild(createInput);
|
createPanel.appendChild(createInput);
|
||||||
createBar.appendChild(createConfirmBtn);
|
createPanel.appendChild(createConfirm);
|
||||||
createBar.appendChild(createCancelBtn);
|
createPanel.appendChild(createCancel);
|
||||||
containerEl.appendChild(createBar);
|
containerEl.appendChild(createPanel);
|
||||||
|
|
||||||
var createMode = ''; // 'folder' | 'file' | ''
|
var renamePanel = el('div', { className: 'files-panel', style: { display: 'none' } });
|
||||||
|
var renameInput = el('input', { className: 'files-rename-input', 'data-files-rename-input': '' });
|
||||||
|
var renameConfirm = el('button', { className: 'files-toolbar-btn', 'data-files-rename-confirm': '' }, ['Rename']);
|
||||||
|
var renameCancel = el('button', { className: 'files-toolbar-btn' }, ['Cancel']);
|
||||||
|
renamePanel.appendChild(renameInput);
|
||||||
|
renamePanel.appendChild(renameConfirm);
|
||||||
|
renamePanel.appendChild(renameCancel);
|
||||||
|
containerEl.appendChild(renamePanel);
|
||||||
|
|
||||||
function cleanPath(path) {
|
function selectedEntry() {
|
||||||
return String(path || '').split('/').filter(Boolean).join('/');
|
return entries.find(function (entry) { return entry.relativePath === selectedPath; }) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function scopedPath(localPath) {
|
function updateButtons() {
|
||||||
localPath = cleanPath(localPath);
|
var sel = selectedEntry();
|
||||||
if (!workspaceRoot) return localPath;
|
upBtn.disabled = !currentPath;
|
||||||
return localPath ? workspaceRoot + '/' + localPath : workspaceRoot;
|
openBtn.disabled = !sel;
|
||||||
}
|
renameBtn.disabled = !sel;
|
||||||
|
trashBtn.disabled = !sel;
|
||||||
function localPath(fullPath) {
|
|
||||||
fullPath = cleanPath(fullPath);
|
|
||||||
if (!workspaceRoot) return fullPath;
|
|
||||||
if (fullPath === workspaceRoot) return '';
|
|
||||||
if (fullPath.indexOf(workspaceRoot + '/') === 0) return fullPath.slice(workspaceRoot.length + 1);
|
|
||||||
return fullPath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateBreadcrumb() {
|
function updateBreadcrumb() {
|
||||||
breadcrumb.innerHTML = '';
|
breadcrumb.innerHTML = '';
|
||||||
var rootLabel = props && props.workspaceNode && props.workspaceNode.title ? props.workspaceNode.title : 'Root';
|
var root = el('span', { className: currentPath ? 'files-breadcrumb-item' : 'files-breadcrumb-current', onClick: function () { navigateTo(''); } }, [workspaceName]);
|
||||||
var rootItem = el('span', { className: 'files-breadcrumb-item', onClick: function () { navigateTo(''); } }, [rootLabel]);
|
breadcrumb.appendChild(root);
|
||||||
breadcrumb.appendChild(rootItem);
|
if (!currentPath) {
|
||||||
if (currentPath) {
|
updateButtons();
|
||||||
var parts = currentPath.split('/');
|
return;
|
||||||
var accumulated = '';
|
|
||||||
parts.forEach(function (part, i) {
|
|
||||||
breadcrumb.appendChild(el('span', { className: 'files-breadcrumb-sep' }, [' / ']));
|
|
||||||
accumulated += (accumulated ? '/' : '') + part;
|
|
||||||
(function (path) {
|
|
||||||
breadcrumb.appendChild(el('span', { className: 'files-breadcrumb-item', onClick: function () { navigateTo(path); } }, [part]));
|
|
||||||
})(accumulated);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
var parts = currentPath.split('/');
|
||||||
|
var acc = '';
|
||||||
|
parts.forEach(function (part, index) {
|
||||||
|
breadcrumb.appendChild(el('span', { className: 'files-breadcrumb-sep' }, ['/']));
|
||||||
|
acc += (acc ? '/' : '') + part;
|
||||||
|
var cls = index === parts.length - 1 ? 'files-breadcrumb-current' : 'files-breadcrumb-item';
|
||||||
|
(function (path) {
|
||||||
|
breadcrumb.appendChild(el('span', { className: cls, onClick: function () { if (cls !== 'files-breadcrumb-current') navigateTo(path); } }, [part]));
|
||||||
|
})(acc);
|
||||||
|
});
|
||||||
|
updateButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleEntries() {
|
||||||
|
var q = filterText.trim().toLowerCase();
|
||||||
|
var out = entries.filter(function (entry) {
|
||||||
|
if (entry.isHidden || entry.isReserved || entry.name === '.verstak') return false;
|
||||||
|
return !q || entry.name.toLowerCase().indexOf(q) !== -1;
|
||||||
|
});
|
||||||
|
out.sort(function (a, b) {
|
||||||
|
if (sortMode === 'folder-name') {
|
||||||
|
if (a.type === 'folder' && b.type !== 'folder') return -1;
|
||||||
|
if (a.type !== 'folder' && b.type === 'folder') return 1;
|
||||||
|
}
|
||||||
|
if (sortMode === 'type') {
|
||||||
|
var typeCmp = typeLabel(a).localeCompare(typeLabel(b));
|
||||||
|
if (typeCmp) return typeCmp;
|
||||||
|
}
|
||||||
|
if (sortMode === 'modified-desc') {
|
||||||
|
var timeCmp = new Date(b.modifiedAt || 0).getTime() - new Date(a.modifiedAt || 0).getTime();
|
||||||
|
if (timeCmp) return timeCmp;
|
||||||
|
}
|
||||||
|
if (sortMode === 'size-desc') {
|
||||||
|
var sizeCmp = (b.size || 0) - (a.size || 0);
|
||||||
|
if (sizeCmp) return sizeCmp;
|
||||||
|
}
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectEntry(entry) {
|
||||||
|
selectedPath = entry ? entry.relativePath : '';
|
||||||
|
renderList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderList() {
|
function renderList() {
|
||||||
listContainer.innerHTML = '';
|
listContainer.innerHTML = '';
|
||||||
if (entries.length === 0) {
|
var header = el('div', { className: 'files-header' }, [
|
||||||
listContainer.appendChild(el('div', { className: 'files-empty' }, ['Empty folder']));
|
el('span', {}, ['Name']),
|
||||||
|
el('span', {}, ['Type']),
|
||||||
|
el('span', {}, ['Size']),
|
||||||
|
el('span', {}, ['Modified']),
|
||||||
|
el('span', {}, ['Actions'])
|
||||||
|
]);
|
||||||
|
listContainer.appendChild(header);
|
||||||
|
|
||||||
|
var shown = visibleEntries();
|
||||||
|
if (shown.length === 0) {
|
||||||
|
listContainer.appendChild(el('div', { className: 'files-empty' }, [filterText ? 'No matches' : 'Empty folder']));
|
||||||
|
updateButtons();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var sorted = sortEntries(entries);
|
|
||||||
sorted.forEach(function (entry) {
|
|
||||||
if (entry.isHidden || entry.isReserved) return;
|
|
||||||
var item = el('div', { className: 'files-item' }, [
|
|
||||||
el('span', { className: 'files-item-icon', innerHTML: fileIcon(entry) }),
|
|
||||||
el('span', { className: 'files-item-name', textContent: entry.name }),
|
|
||||||
el('span', { className: 'files-item-meta', textContent: entry.type === 'folder' ? '' : formatSize(entry.size) }),
|
|
||||||
]);
|
|
||||||
if (entry.type === 'folder') {
|
|
||||||
item.addEventListener('dblclick', function () {
|
|
||||||
navigateTo(localPath(entry.relativePath));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
item.addEventListener('dblclick', function () {
|
|
||||||
openFile(entry);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
listContainer.appendChild(item);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigateTo(path) {
|
shown.forEach(function (entry) {
|
||||||
currentPath = path;
|
var row = el('div', {
|
||||||
updateBreadcrumb();
|
className: 'files-item' + (entry.relativePath === selectedPath ? ' selected' : ''),
|
||||||
loadEntries();
|
'data-file-name': entry.name,
|
||||||
|
'data-file-type': entry.type,
|
||||||
|
'data-file-path': entry.relativePath,
|
||||||
|
tabindex: '0',
|
||||||
|
onClick: function () { selectEntry(entry); },
|
||||||
|
onDblclick: function () { openEntry(entry); }
|
||||||
|
}, [
|
||||||
|
el('div', { className: 'files-namecell' }, [
|
||||||
|
el('span', { className: 'files-item-icon', innerHTML: fileIcon(entry) }),
|
||||||
|
el('span', { className: 'files-item-name', textContent: entry.name, title: entry.name })
|
||||||
|
]),
|
||||||
|
el('span', { className: 'files-item-meta' }, [typeLabel(entry)]),
|
||||||
|
el('span', { className: 'files-item-meta hide-narrow' }, [entry.type === 'folder' ? '' : formatSize(entry.size)]),
|
||||||
|
el('span', { className: 'files-item-meta hide-narrow' }, [formatDate(entry.modifiedAt)]),
|
||||||
|
el('div', { className: 'files-row-actions' }, [
|
||||||
|
el('button', { className: 'files-row-btn', onClick: function (event) { event.stopPropagation(); openEntry(entry); } }, ['Open']),
|
||||||
|
el('button', { className: 'files-row-btn', onClick: function (event) { event.stopPropagation(); beginRename(entry); } }, ['Rename']),
|
||||||
|
el('button', { className: 'files-row-btn', onClick: function (event) { event.stopPropagation(); trashEntry(entry); } }, ['Trash']),
|
||||||
|
entry.type === 'folder' ? el('button', { className: 'files-row-btn', onClick: function (event) { event.stopPropagation(); navigateTo(localPath(entry.relativePath)); startCreate('markdown'); } }, ['New here']) : null
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
listContainer.appendChild(row);
|
||||||
|
});
|
||||||
|
updateButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadEntries() {
|
function loadEntries() {
|
||||||
|
selectedPath = '';
|
||||||
listContainer.innerHTML = '';
|
listContainer.innerHTML = '';
|
||||||
listContainer.appendChild(el('div', { className: 'files-loading' }, ['Loading...']));
|
listContainer.appendChild(el('div', { className: 'files-loading' }, ['Loading...']));
|
||||||
|
updateBreadcrumb();
|
||||||
api.files.list(scopedPath(currentPath)).then(function (result) {
|
api.files.list(scopedPath(currentPath)).then(function (result) {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
entries = result || [];
|
entries = result || [];
|
||||||
|
|
@ -217,18 +322,33 @@
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
listContainer.innerHTML = '';
|
listContainer.innerHTML = '';
|
||||||
var msg = (err && err.message) ? err.message : String(err);
|
|
||||||
listContainer.appendChild(el('div', { className: 'files-error' }, [
|
listContainer.appendChild(el('div', { className: 'files-error' }, [
|
||||||
el('div', {}, ['Failed to load files']),
|
el('div', {}, ['Failed to load files']),
|
||||||
el('div', { className: 'files-error-msg' }, [msg]),
|
el('div', { className: 'files-error-msg' }, [(err && err.message) ? err.message : String(err)])
|
||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function openFile(entry) {
|
function navigateTo(path) {
|
||||||
var ext = entry.extension ? '.' + entry.extension : '';
|
currentPath = cleanPath(path);
|
||||||
var isMd = ext === '.md' || ext === '.markdown';
|
cancelCreate();
|
||||||
|
cancelRename();
|
||||||
|
loadEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
function goUp() {
|
||||||
|
if (currentPath) navigateTo(parentPath(currentPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEntry(entry) {
|
||||||
|
if (!entry) return;
|
||||||
|
if (entry.type === 'folder') {
|
||||||
|
navigateTo(localPath(entry.relativePath));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var ext = entry.extension ? '.' + entry.extension : (extension(entry.name) ? '.' + extension(entry.name) : '');
|
||||||
var entryLocalPath = localPath(entry.relativePath);
|
var entryLocalPath = localPath(entry.relativePath);
|
||||||
|
var isMd = ext === '.md' || ext === '.markdown';
|
||||||
var isNotes = entryLocalPath.split('/')[0] === 'Notes';
|
var isNotes = entryLocalPath.split('/')[0] === 'Notes';
|
||||||
var context = { sourcePluginId: 'verstak.files', sourceView: 'files' };
|
var context = { sourcePluginId: 'verstak.files', sourceView: 'files' };
|
||||||
if (isMd && isNotes) {
|
if (isMd && isNotes) {
|
||||||
|
|
@ -240,61 +360,115 @@
|
||||||
path: entry.relativePath,
|
path: entry.relativePath,
|
||||||
mode: 'view',
|
mode: 'view',
|
||||||
extension: ext,
|
extension: ext,
|
||||||
context: context,
|
context: context
|
||||||
}).catch(function (err) {
|
}).catch(function (err) { console.error('[files] openResource error:', err); });
|
||||||
console.error('[files] openResource error:', err);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function startCreate(mode) {
|
function startCreate(mode) {
|
||||||
createMode = mode;
|
createMode = mode;
|
||||||
createInput.value = '';
|
createInput.value = '';
|
||||||
createInput.placeholder = mode === 'folder' ? 'Folder name...' : 'File name (e.g. note.md)...';
|
createInput.placeholder = mode === 'folder' ? 'Folder name' : (mode === 'markdown' ? 'Markdown file name' : 'Text file name');
|
||||||
createBar.style.display = 'flex';
|
createPanel.style.display = 'flex';
|
||||||
createInput.focus();
|
createInput.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelCreate() {
|
function cancelCreate() {
|
||||||
createMode = '';
|
createMode = '';
|
||||||
createBar.style.display = 'none';
|
createPanel.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmCreate() {
|
function confirmCreate() {
|
||||||
var name = createInput.value.trim();
|
var name = createInput.value.trim();
|
||||||
if (!name) return;
|
if (!name) return;
|
||||||
var localCreatePath = currentPath ? currentPath + '/' + name : name;
|
var mode = createMode;
|
||||||
var path = scopedPath(localCreatePath);
|
if (createMode === 'markdown' && !/\.(md|markdown)$/i.test(name)) name += '.md';
|
||||||
var promise;
|
if (createMode === 'text' && !/\.[^/.]+$/.test(name)) name += '.txt';
|
||||||
if (createMode === 'folder') {
|
var full = scopedPath(currentPath ? currentPath + '/' + name : name);
|
||||||
promise = api.files.createFolder(path);
|
var promise = mode === 'folder'
|
||||||
} else {
|
? api.files.createFolder(full)
|
||||||
promise = api.files.writeText(path, '', { createIfMissing: true, overwrite: false });
|
: api.files.writeText(full, '', { createIfMissing: true, overwrite: false });
|
||||||
}
|
|
||||||
promise.then(function () {
|
promise.then(function () {
|
||||||
cancelCreate();
|
cancelCreate();
|
||||||
loadEntries();
|
loadEntries();
|
||||||
|
if (mode !== 'folder') {
|
||||||
|
var ext = extension(name);
|
||||||
|
api.workbench.openResource({ kind: 'vault-file', path: full, mode: 'edit', extension: ext ? '.' + ext : '', context: { sourcePluginId: 'verstak.files', sourceView: 'files' } }).catch(function () {});
|
||||||
|
}
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
var msg = (err && err.message) ? err.message : String(err);
|
|
||||||
createInput.value = '';
|
createInput.value = '';
|
||||||
createInput.placeholder = 'Error: ' + msg;
|
createInput.placeholder = 'Error: ' + ((err && err.message) ? err.message : String(err));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function beginRename(entry) {
|
||||||
|
entry = entry || selectedEntry();
|
||||||
|
if (!entry) return;
|
||||||
|
renameTarget = entry;
|
||||||
|
renameInput.value = entry.name;
|
||||||
|
renamePanel.style.display = 'flex';
|
||||||
|
renameInput.focus();
|
||||||
|
renameInput.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelRename() {
|
||||||
|
renameTarget = null;
|
||||||
|
renamePanel.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmRename() {
|
||||||
|
if (!renameTarget) return;
|
||||||
|
var newName = renameInput.value.trim();
|
||||||
|
if (!newName || newName === renameTarget.name) {
|
||||||
|
cancelRename();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var from = renameTarget.relativePath;
|
||||||
|
var targetParent = parentPath(from);
|
||||||
|
var to = targetParent ? targetParent + '/' + newName : newName;
|
||||||
|
api.files.move(from, to, { overwrite: false }).then(function () {
|
||||||
|
cancelRename();
|
||||||
|
loadEntries();
|
||||||
|
}).catch(function (err) {
|
||||||
|
renameInput.value = renameTarget.name;
|
||||||
|
renameInput.placeholder = 'Error: ' + ((err && err.message) ? err.message : String(err));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function trashEntry(entry) {
|
||||||
|
entry = entry || selectedEntry();
|
||||||
|
if (!entry) return;
|
||||||
|
if (!window.confirm('Move "' + entry.name + '" to trash?')) return;
|
||||||
|
api.files.trash(entry.relativePath).then(function () {
|
||||||
|
loadEntries();
|
||||||
|
}).catch(function (err) { window.alert((err && err.message) ? err.message : String(err)); });
|
||||||
|
}
|
||||||
|
|
||||||
refreshBtn.addEventListener('click', loadEntries);
|
refreshBtn.addEventListener('click', loadEntries);
|
||||||
createFolderBtn.addEventListener('click', function () { startCreate('folder'); });
|
upBtn.addEventListener('click', goUp);
|
||||||
createFileBtn.addEventListener('click', function () { startCreate('file'); });
|
newFolderBtn.addEventListener('click', function () { startCreate('folder'); });
|
||||||
createConfirmBtn.addEventListener('click', confirmCreate);
|
newMdBtn.addEventListener('click', function () { startCreate('markdown'); });
|
||||||
createCancelBtn.addEventListener('click', cancelCreate);
|
newTextBtn.addEventListener('click', function () { startCreate('text'); });
|
||||||
createInput.addEventListener('keydown', function (e) {
|
openBtn.addEventListener('click', function () { openEntry(selectedEntry()); });
|
||||||
if (e.key === 'Enter') confirmCreate();
|
renameBtn.addEventListener('click', function () { beginRename(); });
|
||||||
if (e.key === 'Escape') cancelCreate();
|
trashBtn.addEventListener('click', function () { trashEntry(); });
|
||||||
|
filterInput.addEventListener('input', function () { filterText = filterInput.value; renderList(); });
|
||||||
|
sortSelect.addEventListener('change', function () { sortMode = sortSelect.value; renderList(); });
|
||||||
|
createConfirm.addEventListener('click', confirmCreate);
|
||||||
|
createCancel.addEventListener('click', cancelCreate);
|
||||||
|
renameConfirm.addEventListener('click', confirmRename);
|
||||||
|
renameCancel.addEventListener('click', cancelRename);
|
||||||
|
createInput.addEventListener('keydown', function (event) { if (event.key === 'Enter') confirmCreate(); if (event.key === 'Escape') cancelCreate(); });
|
||||||
|
renameInput.addEventListener('keydown', function (event) { if (event.key === 'Enter') confirmRename(); if (event.key === 'Escape') cancelRename(); });
|
||||||
|
containerEl.addEventListener('keydown', function (event) {
|
||||||
|
if (event.target && ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].indexOf(event.target.tagName) !== -1) return;
|
||||||
|
if (event.key === 'Enter') openEntry(selectedEntry());
|
||||||
|
if (event.key === 'Delete' || event.key === 'Backspace') trashEntry();
|
||||||
|
if (event.key === 'F2') beginRename();
|
||||||
});
|
});
|
||||||
|
|
||||||
loadEntries();
|
loadEntries();
|
||||||
|
|
||||||
containerEl.__filesCleanup = function () {
|
containerEl.__filesCleanup = function () { disposed = true; };
|
||||||
disposed = true;
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
unmount: function (containerEl) {
|
unmount: function (containerEl) {
|
||||||
|
|
@ -306,11 +480,7 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ── Register ────────────────────────────────────────────── */
|
|
||||||
window.VerstakPluginRegister('verstak.files', {
|
window.VerstakPluginRegister('verstak.files', {
|
||||||
components: {
|
components: { FilesView: FilesView }
|
||||||
FilesView: FilesView
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "Files",
|
"name": "Files",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"apiVersion": "0.1.0",
|
"apiVersion": "0.1.0",
|
||||||
"description": "Minimal vault file navigator. Shows folders and files, opens files through Workbench openResource.",
|
"description": "Workspace-scoped vault file explorer with create, rename, trash, filtering, sorting, and Workbench openResource integration.",
|
||||||
"source": "official",
|
"source": "official",
|
||||||
"icon": "folder",
|
"icon": "folder",
|
||||||
"provides": [
|
"provides": [
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"files.read",
|
"files.read",
|
||||||
"files.write",
|
"files.write",
|
||||||
|
"files.delete",
|
||||||
"workbench.open",
|
"workbench.open",
|
||||||
"ui.register"
|
"ui.register"
|
||||||
],
|
],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue