From 7567e38c4f4386f4097066e345c42246c09c890f Mon Sep 17 00:00:00 2001
From: mirivlad
';
- });
-
- // Inline code
- html = html.replace(/`([^`\n]+)`/g, '' + code.trimEnd() + '$1');
-
- // Headings
- html = html.replace(/^######\s+(.+)$/gm, '$1
');
- html = html.replace(/^#####\s+(.+)$/gm, '$1
');
- html = html.replace(/^####\s+(.+)$/gm, '$1
');
- html = html.replace(/^###\s+(.+)$/gm, '$1
');
- html = html.replace(/^##\s+(.+)$/gm, '$1
');
- html = html.replace(/^#\s+(.+)$/gm, '$1
');
-
- // Horizontal rule
- html = html.replace(/^---+$/gm, '
');
- html = html.replace(/^\*\*\*+$/gm, '
');
-
- // Bold and italic
- html = html.replace(/\*\*\*(.+?)\*\*\*/g, '$1');
- html = html.replace(/\*\*(.+?)\*\*/g, '$1');
- html = html.replace(/\*(.+?)\*/g, '$1');
-
- // Strikethrough
- html = html.replace(/~~(.+?)~~/g, '$1');
-
- // Blockquote
- html = html.replace(/^>\s+(.+)$/gm, '$1
');
-
- // Unordered list items
- html = html.replace(/^[\-\*]\s+(.+)$/gm, '
- html = html.replace(/((?:
$1
');
-
- // Links [text](url) — render as text since we can't navigate internally
- html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1');
-
- // Images  — render as placeholder
- html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '[image: $1]');
-
- // Paragraphs: double newlines
- html = html.replace(/\n\n+/g, '
'); - html = '
' + html + '
'; - - // Clean up empty paragraphs - html = html.replace(/\s*<\/p>/g, ''); - html = html.replace(/
\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) {
var elem = document.createElement(tag);
if (attrs) {
@@ -132,6 +58,7 @@
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 === 'innerHTML') elem.innerHTML = attrs[k];
+ else if (k === 'textContent') elem.textContent = attrs[k];
else elem.setAttribute(k, attrs[k]);
});
}
@@ -144,6 +71,135 @@
return elem;
}
+ function escapeHtml(s) {
+ return String(s == null ? '' : s)
+ .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, '$1');
+ html = html.replace(/!\[([^\]]*)\]\((https?:\/\/[^)]+)\)/g, '');
+ html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^)]+|mailto:[^)]+)\)/g, '$1');
+ html = html.replace(/\*\*\*(.+?)\*\*\*/g, '$1');
+ html = html.replace(/\*\*(.+?)\*\*/g, '$1');
+ html = html.replace(/\*(.+?)\*/g, '$1');
+ 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('
| ' + renderInline(cell.trim()) + ' | '; }).join('') + '
' + renderInline(line) + '
'); + } + + lines.forEach(function (line) { + var fence = line.match(/^```(\w*)\s*$/); + if (fence) { + if (inCode) { + out.push('' + escapeHtml(code.join('\n')) + '');
+ 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('' + renderInline(quote[1]) + ''); + 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('
' + escapeHtml(code.join('\n')) + '');
+ closeList();
+ closeTable();
+ return out.join('\n');
+ }
+
function detectMode(props) {
var ctx = props.request && props.request.context;
if (ctx && (ctx.notesMode || ctx.isInsideNotesFolder)) return 'notes-markdown';
@@ -152,19 +208,36 @@
return 'text';
}
- function detectContextLabel(mode) {
- if (mode === 'notes-markdown') return 'notes';
- if (mode === 'generic-markdown') return 'markdown';
- return 'text';
- }
-
function fileName(path) {
- if (!path) return '';
- var parts = path.split('/');
- return parts[parts.length - 1];
+ var parts = String(path || '').split('/');
+ 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 = {
mount: function (containerEl, props, api) {
injectStyles();
@@ -173,145 +246,178 @@
var request = props.request || {};
var resourcePath = request.path || '';
- var mode = request.mode || 'view';
+ var requestedMode = request.mode || 'view';
var editorMode = detectMode(props);
var isMarkdown = editorMode === 'generic-markdown' || editorMode === 'notes-markdown';
- var previewVisible = isMarkdown && mode === 'view';
-
+ var viewMode = isMarkdown ? (requestedMode === 'edit' ? 'edit' : 'preview') : 'edit';
var currentContent = '';
var savedContent = '';
var dirty = false;
- var saveState = ''; // '' | 'saved' | 'error'
+ var saveState = '';
+ var lastSavedAt = '';
var saveTimer = null;
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 contextLabel = el('span', { className: 'de-toolbar-context' }, [fileName(resourcePath)]);
-
- var notesBadge = null;
- if (editorMode === 'notes-markdown') {
- notesBadge = el('span', { className: 'de-notes-badge' }, ['notes context']);
- }
-
+ 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 spacer = el('span', { className: 'de-toolbar-spacer' });
-
- var editBtn = null;
- var previewBtn = null;
- var saveBtn = el('button', { className: 'de-toolbar-btn' }, ['Save']);
- saveBtn.disabled = true;
-
- 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 editBtn = isMarkdown ? el('button', { className: 'de-toolbar-btn', 'data-editor-mode-button': 'edit' }, ['Edit']) : null;
+ var previewBtn = isMarkdown ? el('button', { className: 'de-toolbar-btn', 'data-editor-mode-button': 'preview' }, ['Preview']) : null;
+ var splitBtn = isMarkdown ? el('button', { className: 'de-toolbar-btn', 'data-editor-mode-button': 'split' }, ['Split']) : null;
+ var reloadBtn = el('button', { className: 'de-toolbar-btn', 'data-editor-action': 'reload' }, ['Reload']);
+ var saveBtn = el('button', { className: 'de-toolbar-btn', 'data-editor-action': 'save' }, ['Save']);
+ var statusEl = el('span', { className: 'de-status', 'data-save-state': '' });
var toolbarChildren = [modeLabel, contextLabel];
if (notesBadge) toolbarChildren.push(notesBadge);
toolbarChildren.push(spacer);
- if (editBtn) toolbarChildren.push(editBtn);
- if (previewBtn) toolbarChildren.push(previewBtn);
- toolbarChildren.push(saveBtn);
- toolbarChildren.push(statusEl);
+ [editBtn, previewBtn, splitBtn, reloadBtn, saveBtn, statusEl].forEach(function (node) { if (node) toolbarChildren.push(node); });
+ containerEl.appendChild(el('div', { className: 'de-toolbar' }, toolbarChildren));
- var toolbar = el('div', { className: 'de-toolbar' }, toolbarChildren);
- containerEl.appendChild(toolbar);
+ var mdToolbar = null;
+ 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' });
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') {
- var notesInfo = el('div', { className: 'de-notes-info' }, [
- 'Notes context active — internal links, backlinks, and widgets deferred.'
- ]);
- containerEl.appendChild(notesInfo);
+ containerEl.appendChild(el('div', { className: 'de-notes-info' }, ['Notes context active. Note actions, backlinks, and graph tools are reserved for the future Notes plugin.']));
+ }
+
+ 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() {
- if (saveState === 'error') {
+ if (saveState === 'saving') {
+ statusEl.textContent = 'Saving...';
+ statusEl.className = 'de-status saving';
+ } else if (saveState === 'error') {
statusEl.textContent = 'Error saving';
statusEl.className = 'de-status error';
- } else if (saveState === 'saved') {
- statusEl.textContent = 'Saved';
- statusEl.className = 'de-status saved';
} else if (dirty) {
statusEl.textContent = 'Modified';
statusEl.className = 'de-status dirty';
+ } else if (lastSavedAt) {
+ statusEl.textContent = saveState === 'saved' ? 'Saved ' + lastSavedAt : 'Saved';
+ statusEl.className = 'de-status saved';
} else {
statusEl.textContent = '';
statusEl.className = 'de-status';
}
- saveBtn.disabled = !dirty;
+ saveBtn.disabled = !dirty || saveState === 'saving';
}
function updatePreview() {
- if (previewEl) {
- previewEl.innerHTML = renderMarkdown(currentContent);
- }
+ if (previewEl) previewEl.innerHTML = isMarkdown ? renderMarkdown(currentContent) : '' + escapeHtml(currentContent) + ''; } - // ── Save ───────────────────────────────────────────── - function save() { - if (!dirty || disposed) return; + function syncFromTextarea() { + if (!textarea) return; + currentContent = textarea.value; + dirty = currentContent !== savedContent; saveState = ''; + updateLineNumbers(); updateStatus(); - api.files.writeText(resourcePath, currentContent, { - createIfMissing: false, - overwrite: true - }).then(function () { + updatePreview(); + } + + 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; savedContent = currentContent; dirty = false; saveState = 'saved'; + lastSavedAt = new Date().toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); updateStatus(); if (saveTimer) clearTimeout(saveTimer); saveTimer = setTimeout(function () { - if (!disposed) { saveState = ''; updateStatus(); } - }, 2000); + if (!disposed) { + saveState = ''; + updateStatus(); + } + }, 2500); }).catch(function (err) { if (disposed) return; saveState = 'error'; @@ -320,105 +426,69 @@ }); } - // ── Toolbar events ─────────────────────────────────── - saveBtn.addEventListener('click', save); - - if (editBtn) { - editBtn.addEventListener('click', function () { - if (mode === 'edit') return; - mode = 'edit'; - previewVisible = false; + function reloadFromDisk() { + if (dirty && !window.confirm('Discard unsaved changes and reload from disk?')) return; + editorWrap.innerHTML = ''; + editorWrap.appendChild(el('div', { className: 'de-loading' }, ['Loading...'])); + api.files.readText(resourcePath).then(function (content) { + if (disposed) return; + currentContent = String(content == null ? '' : content); + savedContent = currentContent; + dirty = false; + saveState = ''; 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) { - previewBtn.addEventListener('click', function () { - if (previewVisible) return; - 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; + function setMode(nextMode) { + if (!isMarkdown || viewMode === nextMode) return; + viewMode = nextMode; rebuildEditorArea(); - }).catch(function (err) { - if (disposed) return; - editorWrap.innerHTML = ''; - var msg = (err && err.message) ? err.message : String(err); - editorWrap.appendChild(el('div', { className: 'de-error' }, [ - el('div', {}, ['Failed to load file']), - el('div', { className: 'de-error-msg' }, [msg]) - ])); - }); + } + + function applyMarkdownAction(action) { + if (!textarea && viewMode === 'preview') { + setMode('edit'); + } + 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 () { disposed = true; 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) { @@ -430,11 +500,7 @@ } }; - /* ── Register ────────────────────────────────────────────── */ window.VerstakPluginRegister('verstak.default-editor', { - components: { - DefaultEditor: DefaultEditor - } + components: { DefaultEditor: DefaultEditor } }); - })(); diff --git a/plugins/files/frontend/src/index.js b/plugins/files/frontend/src/index.js index 7e30d9c..e83c9ac 100644 --- a/plugins/files/frontend/src/index.js +++ b/plugins/files/frontend/src/index.js @@ -6,7 +6,6 @@ (function () { 'use strict'; - /* ── Style injection ─────────────────────────────────────── */ function injectStyles() { if (document.getElementById('files-style-injected')) return; var style = document.createElement('style'); @@ -16,33 +15,40 @@ } 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-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-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:hover{background:#2a2a4e;border-color:#4ecca3}', - '.files-toolbar-btn:disabled{opacity:0.4;cursor:default}', - '.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-item{color:#4ecca3;cursor:pointer;padding:0.1rem 0.3rem;border-radius:3px}', + '.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:.45rem;padding:.5rem .75rem;border-bottom:1px solid #16213e;flex-shrink:0;background:#12122a;flex-wrap:wrap}', + '.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,.files-row-btn:hover{background:#2a2a4e;border-color:#4ecca3}', + '.files-toolbar-btn:disabled,.files-row-btn:disabled{opacity:.45;cursor:default;border-color:#333}', + '.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:.1rem .3rem;border-radius:3px;white-space:nowrap}', '.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-list{flex:1;overflow-y:auto;padding:0.5rem 0}', - '.files-item{display:flex;align-items:center;gap:0.6rem;padding:0.4rem 0.75rem;cursor:pointer;font-size:0.85rem}', - '.files-item:hover{background:#1a1a2e}', + '.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-filter{width:11rem}', + '.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-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-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}', - '.files-item-meta{font-size:0.7rem;color:#666;flex-shrink:0}', - '.files-empty{flex:1;display:flex;align-items:center;justify-content:center;color:#666;font-size:0.9rem}', - '.files-loading{flex:1;display:flex;align-items:center;justify-content:center;color:#666}', - '.files-error{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#e74c3c;gap:0.5rem;padding:1rem}', - '.files-error-msg{font-size:0.85rem;color:#aaa;max-width:400px;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-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:focus{border-color:#4ecca3}', + '.files-item-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}', + '.files-item-meta{font-size:.74rem;color:#777;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}', + '.files-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:.35rem;white-space:nowrap}', + '.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:.5rem;padding:1rem}', + '.files-error-msg{font-size:.85rem;color:#aaa;max-width:420px;text-align:center}', + '.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,.files-rename-input{flex:1;min-width:160px}', + '@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'); - /* ── Utilities ──────────────────────────────────────────── */ function el(tag, attrs, children) { var elem = document.createElement(tag); if (attrs) { @@ -68,148 +74,247 @@ return ''; } + 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) { - 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'); - } - var ext = (entry.extension || '').toLowerCase(); - 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'); - } + 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'); + 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'); + if (ext === 'json' || ext === 'yaml' || ext === 'yml' || ext === 'toml') return '{ }'; return svgIcon('M6 2h9l5 5v15H6V2Zm8 1.5V8h4.5L14 3.5Z'); } function formatSize(bytes) { - if (bytes == null || bytes === 0) return ''; + if (!bytes) return ''; if (bytes < 1024) return bytes + ' B'; if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / 1048576).toFixed(1) + ' MB'; } - function sortEntries(entries) { - var folders = entries.filter(function (e) { return e.type === 'folder'; }); - var files = entries.filter(function (e) { return e.type !== 'folder'; }); - folders.sort(function (a, b) { return a.name.localeCompare(b.name); }); - files.sort(function (a, b) { return a.name.localeCompare(b.name); }); - return folders.concat(files); + function formatDate(value) { + if (!value) return ''; + var date = new Date(value); + if (isNaN(date.getTime())) return ''; + return date.toLocaleString(undefined, { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }); + } + + function typeLabel(entry) { + if (entry.type === 'folder') return 'folder'; + return (entry.extension || extension(entry.name) || 'file').toLowerCase(); } - /* ── FilesView component ─────────────────────────────────── */ var FilesView = { mount: function (containerEl, props, api) { injectStyles(); containerEl.innerHTML = ''; 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 entries = []; + var selectedPath = ''; + var filterText = ''; + var sortMode = 'folder-name'; + var createMode = ''; + var renameTarget = null; 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 breadcrumb = el('div', { className: 'files-breadcrumb' }); - var refreshBtn = el('button', { className: 'files-toolbar-btn' }, ['Refresh']); - var createFolderBtn = el('button', { className: 'files-toolbar-btn' }, ['+ Folder']); - var createFileBtn = el('button', { className: 'files-toolbar-btn' }, ['+ File']); + var upBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'up', title: 'Up' }, ['Up']); + var refreshBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'refresh', title: 'Refresh' }, ['Refresh']); + 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(refreshBtn); - toolbar.appendChild(createFolderBtn); - toolbar.appendChild(createFileBtn); + [upBtn, refreshBtn, newFolderBtn, newMdBtn, newTextBtn, openBtn, renameBtn, trashBtn, filterInput, sortSelect].forEach(function (node) { toolbar.appendChild(node); }); containerEl.appendChild(toolbar); - var listContainer = el('div', { className: 'files-list' }); + var listContainer = el('div', { className: 'files-list', 'data-files-list': '' }); containerEl.appendChild(listContainer); - var createBar = el('div', { className: 'files-create-bar', style: { display: 'none' } }); - var createInput = el('input', { className: 'files-create-input', placeholder: 'Name...' }); - var createConfirmBtn = el('button', { className: 'files-toolbar-btn' }, ['Create']); - var createCancelBtn = el('button', { className: 'files-toolbar-btn' }, ['Cancel']); - createBar.appendChild(createInput); - createBar.appendChild(createConfirmBtn); - createBar.appendChild(createCancelBtn); - containerEl.appendChild(createBar); + var createPanel = el('div', { className: 'files-panel', style: { display: 'none' } }); + var createInput = el('input', { className: 'files-create-input', 'data-files-create-input': '' }); + var createConfirm = el('button', { className: 'files-toolbar-btn', 'data-files-create-confirm': '' }, ['Create']); + var createCancel = el('button', { className: 'files-toolbar-btn' }, ['Cancel']); + createPanel.appendChild(createInput); + createPanel.appendChild(createConfirm); + createPanel.appendChild(createCancel); + 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) { - return String(path || '').split('/').filter(Boolean).join('/'); + function selectedEntry() { + return entries.find(function (entry) { return entry.relativePath === selectedPath; }) || null; } - function scopedPath(localPath) { - localPath = cleanPath(localPath); - if (!workspaceRoot) return localPath; - return localPath ? workspaceRoot + '/' + localPath : workspaceRoot; - } - - 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 updateButtons() { + var sel = selectedEntry(); + upBtn.disabled = !currentPath; + openBtn.disabled = !sel; + renameBtn.disabled = !sel; + trashBtn.disabled = !sel; } function updateBreadcrumb() { breadcrumb.innerHTML = ''; - var rootLabel = props && props.workspaceNode && props.workspaceNode.title ? props.workspaceNode.title : 'Root'; - var rootItem = el('span', { className: 'files-breadcrumb-item', onClick: function () { navigateTo(''); } }, [rootLabel]); - breadcrumb.appendChild(rootItem); - if (currentPath) { - var parts = currentPath.split('/'); - 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 root = el('span', { className: currentPath ? 'files-breadcrumb-item' : 'files-breadcrumb-current', onClick: function () { navigateTo(''); } }, [workspaceName]); + breadcrumb.appendChild(root); + if (!currentPath) { + updateButtons(); + return; } + 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() { listContainer.innerHTML = ''; - if (entries.length === 0) { - listContainer.appendChild(el('div', { className: 'files-empty' }, ['Empty folder'])); + var header = el('div', { className: 'files-header' }, [ + 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; } - 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) { - currentPath = path; - updateBreadcrumb(); - loadEntries(); + shown.forEach(function (entry) { + var row = el('div', { + className: 'files-item' + (entry.relativePath === selectedPath ? ' selected' : ''), + '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() { + selectedPath = ''; listContainer.innerHTML = ''; listContainer.appendChild(el('div', { className: 'files-loading' }, ['Loading...'])); + updateBreadcrumb(); api.files.list(scopedPath(currentPath)).then(function (result) { if (disposed) return; entries = result || []; @@ -217,18 +322,33 @@ }).catch(function (err) { if (disposed) return; listContainer.innerHTML = ''; - var msg = (err && err.message) ? err.message : String(err); listContainer.appendChild(el('div', { className: 'files-error' }, [ 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) { - var ext = entry.extension ? '.' + entry.extension : ''; - var isMd = ext === '.md' || ext === '.markdown'; + function navigateTo(path) { + currentPath = cleanPath(path); + 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 isMd = ext === '.md' || ext === '.markdown'; var isNotes = entryLocalPath.split('/')[0] === 'Notes'; var context = { sourcePluginId: 'verstak.files', sourceView: 'files' }; if (isMd && isNotes) { @@ -240,61 +360,115 @@ path: entry.relativePath, mode: 'view', extension: ext, - context: context, - }).catch(function (err) { - console.error('[files] openResource error:', err); - }); + context: context + }).catch(function (err) { console.error('[files] openResource error:', err); }); } function startCreate(mode) { createMode = mode; createInput.value = ''; - createInput.placeholder = mode === 'folder' ? 'Folder name...' : 'File name (e.g. note.md)...'; - createBar.style.display = 'flex'; + createInput.placeholder = mode === 'folder' ? 'Folder name' : (mode === 'markdown' ? 'Markdown file name' : 'Text file name'); + createPanel.style.display = 'flex'; createInput.focus(); } function cancelCreate() { createMode = ''; - createBar.style.display = 'none'; + createPanel.style.display = 'none'; } function confirmCreate() { var name = createInput.value.trim(); if (!name) return; - var localCreatePath = currentPath ? currentPath + '/' + name : name; - var path = scopedPath(localCreatePath); - var promise; - if (createMode === 'folder') { - promise = api.files.createFolder(path); - } else { - promise = api.files.writeText(path, '', { createIfMissing: true, overwrite: false }); - } + var mode = createMode; + if (createMode === 'markdown' && !/\.(md|markdown)$/i.test(name)) name += '.md'; + if (createMode === 'text' && !/\.[^/.]+$/.test(name)) name += '.txt'; + var full = scopedPath(currentPath ? currentPath + '/' + name : name); + var promise = mode === 'folder' + ? api.files.createFolder(full) + : api.files.writeText(full, '', { createIfMissing: true, overwrite: false }); promise.then(function () { cancelCreate(); 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) { - var msg = (err && err.message) ? err.message : String(err); 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); - createFolderBtn.addEventListener('click', function () { startCreate('folder'); }); - createFileBtn.addEventListener('click', function () { startCreate('file'); }); - createConfirmBtn.addEventListener('click', confirmCreate); - createCancelBtn.addEventListener('click', cancelCreate); - createInput.addEventListener('keydown', function (e) { - if (e.key === 'Enter') confirmCreate(); - if (e.key === 'Escape') cancelCreate(); + upBtn.addEventListener('click', goUp); + newFolderBtn.addEventListener('click', function () { startCreate('folder'); }); + newMdBtn.addEventListener('click', function () { startCreate('markdown'); }); + newTextBtn.addEventListener('click', function () { startCreate('text'); }); + openBtn.addEventListener('click', function () { openEntry(selectedEntry()); }); + renameBtn.addEventListener('click', function () { beginRename(); }); + 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(); - containerEl.__filesCleanup = function () { - disposed = true; - }; + containerEl.__filesCleanup = function () { disposed = true; }; }, unmount: function (containerEl) { @@ -306,11 +480,7 @@ } }; - /* ── Register ────────────────────────────────────────────── */ window.VerstakPluginRegister('verstak.files', { - components: { - FilesView: FilesView - } + components: { FilesView: FilesView } }); - })(); diff --git a/plugins/files/plugin.json b/plugins/files/plugin.json index 6e48e41..494e03c 100644 --- a/plugins/files/plugin.json +++ b/plugins/files/plugin.json @@ -4,7 +4,7 @@ "name": "Files", "version": "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", "icon": "folder", "provides": [ @@ -17,6 +17,7 @@ "permissions": [ "files.read", "files.write", + "files.delete", "workbench.open", "ui.register" ],