feat: Notes plugin + Files improvements + Default editor wiki links
plugins/files: - SVG icons for toolbar actions (iconButton helper) - Multi-select cut/copy/paste (Ctrl+X/C/V) - Mouse back/forward button navigation (history stack) - Navigation history persisted per workspace (window.__filesHistoryByWorkspace) - Context menu: icons, notes integration (Create Note, Open Overview) - Drag-and-drop folder target fix plugins/default-editor: - Wiki links [[Title]] in notes context (renderInline with isNotesContext) - Save/Read via Notes API when editorMode === 'notes-markdown' - Internal link click handler (open note via api.request.open) plugins/platform-test: - Mouse Event Inspector card for debugging back/forward buttons plugins/notes (new): - Basic notes plugin scaffold (plugin.json + frontend entry) - Contributes workspace item 'Notes'
This commit is contained in:
parent
d3079be007
commit
321c8e58d1
|
|
@ -84,8 +84,12 @@
|
|||
return escapeHtml(s).replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function renderInline(text) {
|
||||
function renderInline(text, isNotesContext) {
|
||||
var html = escapeHtml(text);
|
||||
// Internal wiki links [[Title]] — only render in notes context
|
||||
if (isNotesContext) {
|
||||
html = html.replace(/\[\[([^\]]+)\]\]/g, '<a href="#" class="internal-link" data-note-link="$1">$1</a>');
|
||||
}
|
||||
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>');
|
||||
|
|
@ -95,7 +99,7 @@
|
|||
return html;
|
||||
}
|
||||
|
||||
function renderMarkdown(text) {
|
||||
function renderMarkdown(text, isNotesContext) {
|
||||
var lines = String(text || '').split(/\r?\n/);
|
||||
var out = [];
|
||||
var inCode = false;
|
||||
|
|
@ -113,14 +117,14 @@
|
|||
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>';
|
||||
return '<tr>' + row.map(function (cell) { return '<td>' + renderInline(cell.trim(), isNotesContext) + '</td>'; }).join('') + '</tr>';
|
||||
}).join('') + '</tbody></table>');
|
||||
table = [];
|
||||
}
|
||||
function pushParagraph(line) {
|
||||
closeList();
|
||||
closeTable();
|
||||
if (line.trim()) out.push('<p>' + renderInline(line) + '</p>');
|
||||
if (line.trim()) out.push('<p>' + renderInline(line, isNotesContext) + '</p>');
|
||||
}
|
||||
|
||||
lines.forEach(function (line) {
|
||||
|
|
@ -154,7 +158,7 @@
|
|||
if (heading) {
|
||||
closeList();
|
||||
closeTable();
|
||||
out.push('<h' + heading[1].length + '>' + renderInline(heading[2]) + '</h' + heading[1].length + '>');
|
||||
out.push('<h' + heading[1].length + '>' + renderInline(heading[2], isNotesContext) + '</h' + heading[1].length + '>');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -169,7 +173,7 @@
|
|||
if (quote) {
|
||||
closeList();
|
||||
closeTable();
|
||||
out.push('<blockquote>' + renderInline(quote[1]) + '</blockquote>');
|
||||
out.push('<blockquote>' + renderInline(quote[1], isNotesContext) + '</blockquote>');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -185,9 +189,9 @@
|
|||
listType = desired;
|
||||
}
|
||||
if (task) {
|
||||
out.push('<li><input class="task" type="checkbox" disabled ' + (task[1].toLowerCase() === 'x' ? 'checked' : '') + '> ' + renderInline(task[2]) + '</li>');
|
||||
out.push('<li><input class="task" type="checkbox" disabled ' + (task[1].toLowerCase() === 'x' ? 'checked' : '') + '> ' + renderInline(task[2], isNotesContext) + '</li>');
|
||||
} else {
|
||||
out.push('<li>' + renderInline((ordered || unordered)[1]) + '</li>');
|
||||
out.push('<li>' + renderInline((ordered || unordered)[1], isNotesContext) + '</li>');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -337,7 +341,7 @@
|
|||
}
|
||||
|
||||
function updatePreview() {
|
||||
if (previewEl) previewEl.innerHTML = isMarkdown ? renderMarkdown(currentContent) : '<pre>' + escapeHtml(currentContent) + '</pre>';
|
||||
if (previewEl) previewEl.innerHTML = isMarkdown ? renderMarkdown(currentContent, editorMode === 'notes-markdown') : '<pre>' + escapeHtml(currentContent) + '</pre>';
|
||||
}
|
||||
|
||||
function syncFromTextarea() {
|
||||
|
|
@ -404,7 +408,15 @@
|
|||
if (!dirty || disposed) return Promise.resolve();
|
||||
saveState = 'saving';
|
||||
updateStatus();
|
||||
return api.files.writeText(resourcePath, currentContent, { createIfMissing: false, overwrite: true }).then(function () {
|
||||
var savePromise;
|
||||
if (editorMode === 'notes-markdown') {
|
||||
savePromise = api.backend.call('SaveNote', resourcePath, currentContent).then(function (errStr) {
|
||||
if (errStr) throw new Error(errStr);
|
||||
});
|
||||
} else {
|
||||
savePromise = api.files.writeText(resourcePath, currentContent, { createIfMissing: false, overwrite: true });
|
||||
}
|
||||
return savePromise.then(function () {
|
||||
if (disposed) return;
|
||||
savedContent = currentContent;
|
||||
dirty = false;
|
||||
|
|
@ -430,7 +442,16 @@
|
|||
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) {
|
||||
var readPromise;
|
||||
if (editorMode === 'notes-markdown') {
|
||||
readPromise = api.backend.call('ReadNote', resourcePath).then(function (result) {
|
||||
var content = Array.isArray(result) ? result[0] : result;
|
||||
return content == null ? '' : content;
|
||||
});
|
||||
} else {
|
||||
readPromise = api.files.readText(resourcePath);
|
||||
}
|
||||
readPromise.then(function (content) {
|
||||
if (disposed) return;
|
||||
currentContent = String(content == null ? '' : content);
|
||||
savedContent = currentContent;
|
||||
|
|
@ -485,6 +506,24 @@
|
|||
|
||||
reloadFromDisk();
|
||||
|
||||
containerEl.addEventListener('click', function (event) {
|
||||
var link = event.target.closest('.internal-link');
|
||||
if (!link) return;
|
||||
event.preventDefault();
|
||||
var noteTitle = link.getAttribute('data-note-link');
|
||||
if (!noteTitle) return;
|
||||
var parentPath = resourcePath;
|
||||
var idx = parentPath.indexOf('/Notes/');
|
||||
if (idx !== -1) {
|
||||
parentPath = parentPath.substring(0, idx);
|
||||
}
|
||||
api.request.open({
|
||||
path: parentPath + '/Notes/' + noteTitle + '.md',
|
||||
mode: 'view',
|
||||
context: { notesMode: true }
|
||||
});
|
||||
});
|
||||
|
||||
containerEl.__deCleanup = function () {
|
||||
disposed = true;
|
||||
if (saveTimer) clearTimeout(saveTimer);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,10 @@
|
|||
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;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,.files-row-btn{display:inline-flex;align-items:center;justify-content:center;border:1px solid #333;border-radius:4px;background:#1a1a2e;color:#ccc;cursor:pointer;line-height:1}',
|
||||
'.files-toolbar-btn{width:2rem;height:2rem;padding:0}',
|
||||
'.files-row-btn{width:1.75rem;height:1.75rem;padding:0}',
|
||||
'.files-toolbar-btn svg,.files-row-btn svg{width:16px;height:16px;display:block;fill:currentColor}',
|
||||
'.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}',
|
||||
|
|
@ -27,7 +30,7 @@
|
|||
'.files-breadcrumb-sep{color:#555}',
|
||||
'.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-sort{width:9.5rem;appearance:none;background-color:#0d0d1a;background-image:linear-gradient(45deg,transparent 50%,#8b8ba8 50%),linear-gradient(135deg,#8b8ba8 50%,transparent 50%);background-position:calc(100% - 14px) 50%,calc(100% - 9px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:1.6rem}',
|
||||
'.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)}',
|
||||
|
|
@ -49,6 +52,7 @@
|
|||
'@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%}}',
|
||||
'.files-ctx-menu{position:fixed;z-index:9999;min-width:180px;background:#1a1a2e;border:1px solid #333;border-radius:6px;padding:6px 0;box-shadow:0 8px 24px rgba(0,0,0,.5);font-size:.84rem;color:#e0e0e0;user-select:none}',
|
||||
'.files-ctx-menu-item{padding:6px 16px;cursor:pointer;white-space:nowrap;display:flex;align-items:center;gap:.5rem}',
|
||||
'.files-ctx-menu-item svg{width:14px;height:14px;fill:currentColor;opacity:.9}',
|
||||
'.files-ctx-menu-item:hover{background:#2a2a4e}',
|
||||
'.files-ctx-menu-item.danger{color:#e74c3c}',
|
||||
'.files-ctx-menu-item.danger:hover{background:#2a2a4e;color:#ff6b6b}',
|
||||
|
|
@ -93,6 +97,33 @@
|
|||
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="' + path + '" fill="currentColor"/></svg>';
|
||||
}
|
||||
|
||||
var ACTION_ICONS = {
|
||||
back: 'M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.42-1.41L7.83 13H20v-2z',
|
||||
forward: 'M4 13h12.17l-5.59 5.59L12 20l8-8-8-8-1.42 1.41L16.17 11H4v2z',
|
||||
up: 'M4 12l1.41 1.41L11 7.83V20h2V7.83l5.59 5.58L20 12 12 4l-8 8z',
|
||||
refresh: 'M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z',
|
||||
folderAdd: 'M20 6h-8.17l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-1 8h-3v3h-2v-3h-3v-2h3V9h2v3h3v2z',
|
||||
markdownAdd: 'M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 8V4l5 5h-5zm-6 6h2v-3l2 3h1l2-3v3h2v-6h-2l-2.5 3.5L9 10H7v6z',
|
||||
textAdd: 'M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 8V4l5 5h-5zM8 13h8v2H8v-2zm0 4h8v2H8v-2z',
|
||||
open: 'M14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7zM5 5h6V3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-6h-2v6H5V5z',
|
||||
rename: 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z',
|
||||
trash: 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM8 9h8v10H8V9zm7.5-5-1-1h-5l-1 1H5v2h14V4z',
|
||||
cut: 'M9.64 7.64c.23-.5.36-1.05.36-1.64 0-2.21-1.79-4-4-4S2 3.79 2 6s1.79 4 4 4c.59 0 1.14-.13 1.64-.36L10 12l-2.36 2.36C7.14 14.13 6.59 14 6 14c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4c0-.59-.13-1.14-.36-1.64L12 14l7 7h3L9.64 7.64zM6 8c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm0 12c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm6-8.5c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zM19 3l-6 6 2 2 7-8h-3z',
|
||||
copy: 'M16 1H4c-1.1 0-2 .9-2 2v12h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z',
|
||||
paste: 'M19 2h-4.18C14.4.84 13.3 0 12 0S9.6.84 9.18 2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm7 18H5V4h2v3h10V4h2v16z'
|
||||
};
|
||||
|
||||
function iconButton(action, title, iconKey, onClick, extraClass) {
|
||||
return el('button', {
|
||||
className: (extraClass || 'files-toolbar-btn'),
|
||||
'data-files-action': action,
|
||||
title: title,
|
||||
'aria-label': title,
|
||||
innerHTML: svgIcon(ACTION_ICONS[iconKey || action] || ACTION_ICONS.open),
|
||||
onClick: onClick
|
||||
});
|
||||
}
|
||||
|
||||
function cleanPath(path) {
|
||||
return String(path || '').split('/').filter(Boolean).join('/');
|
||||
}
|
||||
|
|
@ -222,7 +253,10 @@
|
|||
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 = '';
|
||||
window.__filesHistoryByWorkspace = window.__filesHistoryByWorkspace || {};
|
||||
var historyKey = workspaceRoot || workspaceName;
|
||||
var savedHistory = window.__filesHistoryByWorkspace[historyKey] || { stack: [''], index: 0, currentPath: '' };
|
||||
var currentPath = cleanPath(savedHistory.currentPath || '');
|
||||
var entries = [];
|
||||
var selectedPaths = {};
|
||||
var lastClickedPath = '';
|
||||
|
|
@ -231,8 +265,12 @@
|
|||
var createMode = '';
|
||||
var renameTarget = null;
|
||||
var disposed = false;
|
||||
var historyStack = [''];
|
||||
var historyIndex = 0;
|
||||
var historyStack = Array.isArray(savedHistory.stack) && savedHistory.stack.length ? savedHistory.stack.map(cleanPath) : [currentPath];
|
||||
var historyIndex = Math.max(0, Math.min(Number(savedHistory.index) || 0, historyStack.length - 1));
|
||||
if (historyStack[historyIndex] !== currentPath) {
|
||||
historyStack = [currentPath];
|
||||
historyIndex = 0;
|
||||
}
|
||||
var navigatingHistory = false;
|
||||
|
||||
function scopedPath(local) {
|
||||
|
|
@ -249,17 +287,19 @@
|
|||
|
||||
var toolbar = el('div', { className: 'files-toolbar' });
|
||||
var breadcrumb = el('div', { className: 'files-breadcrumb' });
|
||||
var backBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'back', title: 'Back (Alt+Left)' }, ['\u2190']);
|
||||
var forwardBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'forward', title: 'Forward (Alt+Right)' }, ['\u2192']);
|
||||
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 pasteBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'paste' }, ['Paste']);
|
||||
var backBtn = iconButton('back', 'Back', 'back', goBack);
|
||||
var forwardBtn = iconButton('forward', 'Forward', 'forward', goForward);
|
||||
var upBtn = iconButton('up', 'Up', 'up', goUp);
|
||||
var refreshBtn = iconButton('refresh', 'Refresh', 'refresh', loadEntries);
|
||||
var newFolderBtn = iconButton('new-folder', 'New folder', 'folderAdd', function () { startCreate('folder'); });
|
||||
var newMdBtn = iconButton('new-markdown', 'New markdown file', 'markdownAdd', function () { startCreate('markdown'); });
|
||||
var newTextBtn = iconButton('new-text', 'New text file', 'textAdd', function () { startCreate('text'); });
|
||||
var openBtn = iconButton('open', 'Open', 'open', function () { openEntry(selectedEntry()); });
|
||||
var renameBtn = iconButton('rename', 'Rename', 'rename', function () { beginRename(); });
|
||||
var trashBtn = iconButton('trash', 'Move to trash', 'trash', function () { trashEntry(); });
|
||||
var cutBtn = iconButton('cut', 'Cut', 'cut', function () { cutSelection(); });
|
||||
var copyBtn = iconButton('copy', 'Copy', 'copy', function () { copySelection(); });
|
||||
var pasteBtn = iconButton('paste', 'Paste', 'paste', function () { pasteEntry(); });
|
||||
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']),
|
||||
|
|
@ -269,7 +309,7 @@
|
|||
el('option', { value: 'size-desc' }, ['Size'])
|
||||
]);
|
||||
toolbar.appendChild(breadcrumb);
|
||||
[backBtn, forwardBtn, upBtn, refreshBtn, newFolderBtn, newMdBtn, newTextBtn, openBtn, renameBtn, trashBtn, pasteBtn, filterInput, sortSelect].forEach(function (node) { toolbar.appendChild(node); });
|
||||
[backBtn, forwardBtn, upBtn, refreshBtn, newFolderBtn, newMdBtn, newTextBtn, openBtn, renameBtn, trashBtn, cutBtn, copyBtn, pasteBtn, filterInput, sortSelect].forEach(function (node) { toolbar.appendChild(node); });
|
||||
containerEl.appendChild(toolbar);
|
||||
|
||||
var listContainer = el('div', { className: 'files-list', 'data-files-list': '' });
|
||||
|
|
@ -299,6 +339,12 @@
|
|||
return entries.find(function (entry) { return entry.relativePath === keys[0]; }) || null;
|
||||
}
|
||||
|
||||
function selectedEntries() {
|
||||
return Object.keys(selectedPaths).map(function (path) {
|
||||
return entries.find(function (entry) { return entry.relativePath === path; }) || null;
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
function selectedCount() {
|
||||
return Object.keys(selectedPaths).length;
|
||||
}
|
||||
|
|
@ -309,7 +355,9 @@
|
|||
openBtn.disabled = count !== 1;
|
||||
renameBtn.disabled = count !== 1;
|
||||
trashBtn.disabled = count === 0;
|
||||
pasteBtn.disabled = !window.__filesClipboard;
|
||||
cutBtn.disabled = count === 0;
|
||||
copyBtn.disabled = count === 0;
|
||||
pasteBtn.disabled = !(window.__filesClipboard && window.__filesClipboard.items && window.__filesClipboard.items.length);
|
||||
}
|
||||
|
||||
function updateHistoryButtons() {
|
||||
|
|
@ -317,6 +365,14 @@
|
|||
forwardBtn.disabled = historyIndex >= historyStack.length - 1;
|
||||
}
|
||||
|
||||
function saveHistoryState() {
|
||||
window.__filesHistoryByWorkspace[historyKey] = {
|
||||
stack: historyStack.slice(),
|
||||
index: historyIndex,
|
||||
currentPath: currentPath
|
||||
};
|
||||
}
|
||||
|
||||
function updateBreadcrumb() {
|
||||
breadcrumb.innerHTML = '';
|
||||
var root = el('span', { className: currentPath ? 'files-breadcrumb-item' : 'files-breadcrumb-current', onClick: function () { navigateTo(''); } }, [workspaceName]);
|
||||
|
|
@ -458,10 +514,9 @@
|
|||
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
|
||||
iconButton('row-open', 'Open', 'open', function (event) { event.stopPropagation(); openEntry(entry); }, 'files-row-btn'),
|
||||
iconButton('row-rename', 'Rename', 'rename', function (event) { event.stopPropagation(); beginRename(entry); }, 'files-row-btn'),
|
||||
iconButton('row-trash', 'Move to trash', 'trash', function (event) { event.stopPropagation(); trashEntry(entry); }, 'files-row-btn')
|
||||
])
|
||||
]);
|
||||
listContainer.appendChild(row);
|
||||
|
|
@ -504,6 +559,7 @@
|
|||
cancelCreate();
|
||||
cancelRename();
|
||||
updateHistoryButtons();
|
||||
saveHistoryState();
|
||||
loadEntries();
|
||||
}
|
||||
|
||||
|
|
@ -661,17 +717,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
backBtn.addEventListener('click', goBack);
|
||||
forwardBtn.addEventListener('click', goForward);
|
||||
refreshBtn.addEventListener('click', loadEntries);
|
||||
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(); });
|
||||
pasteBtn.addEventListener('click', function () { pasteEntry(); });
|
||||
filterInput.addEventListener('input', function () { filterText = filterInput.value; renderList(); });
|
||||
sortSelect.addEventListener('change', function () { sortMode = sortSelect.value; renderList(); });
|
||||
createConfirm.addEventListener('click', confirmCreate);
|
||||
|
|
@ -691,11 +736,15 @@
|
|||
ctxTarget = null;
|
||||
}
|
||||
|
||||
function ctxItem(label, cls, onClick) {
|
||||
function ctxItem(label, cls, onClick, action, iconKey) {
|
||||
return el('div', {
|
||||
className: 'files-ctx-menu-item' + (cls ? ' ' + cls : ''),
|
||||
'data-files-menu-action': action || '',
|
||||
onClick: function (e) { e.stopPropagation(); hideCtxMenu(); onClick(); }
|
||||
}, [label]);
|
||||
}, [
|
||||
iconKey ? el('span', { innerHTML: svgIcon(ACTION_ICONS[iconKey] || ACTION_ICONS.open) }) : null,
|
||||
label
|
||||
]);
|
||||
}
|
||||
|
||||
function ctxSep() {
|
||||
|
|
@ -706,25 +755,77 @@
|
|||
ctxTarget = entry;
|
||||
ctxMenu.innerHTML = '';
|
||||
if (entry) {
|
||||
if (!selectedPaths[entry.relativePath]) {
|
||||
selectedPaths = {};
|
||||
selectedPaths[entry.relativePath] = true;
|
||||
lastClickedPath = entry.relativePath;
|
||||
renderList();
|
||||
}
|
||||
var isFolder = entry.type === 'folder';
|
||||
ctxMenu.appendChild(ctxItem(isFolder ? 'Open Folder' : 'Open', '', function () { openEntry(entry); }));
|
||||
ctxMenu.appendChild(ctxSep());
|
||||
ctxMenu.appendChild(ctxItem('Rename', '', function () { beginRename(entry); }));
|
||||
ctxMenu.appendChild(ctxItem('Duplicate', '', function () { duplicateEntry(entry); }));
|
||||
ctxMenu.appendChild(ctxSep());
|
||||
ctxMenu.appendChild(ctxItem('Cut', '', function () { cutEntry(entry); }));
|
||||
if (entry.type !== 'folder') {
|
||||
ctxMenu.appendChild(ctxItem('Copy', '', function () { copyEntry(entry); }));
|
||||
ctxMenu.appendChild(ctxItem(isFolder ? 'Open Folder' : 'Open', '', function () { openEntry(entry); }, 'open', 'open'));
|
||||
var entryLocalPath = localPath(entry.relativePath);
|
||||
var isNotes = entryLocalPath === 'Notes' || entryLocalPath.split('/')[0] === 'Notes';
|
||||
if (isNotes) {
|
||||
ctxMenu.appendChild(ctxSep());
|
||||
if (isFolder) {
|
||||
ctxMenu.appendChild(ctxItem('Create Note', '', function () {
|
||||
var title = prompt('Note title:');
|
||||
if (!title) return;
|
||||
api.backend.call('NormalizeNoteTitle', title).then(function (result) {
|
||||
var safeTitle = Array.isArray(result) ? result[0] : (typeof result === 'string' ? result : title);
|
||||
if (!safeTitle) safeTitle = title;
|
||||
return api.backend.call('CreateNote', scopedPath(entryLocalPath), safeTitle).then(function (cr) {
|
||||
var crErr = Array.isArray(cr) ? cr[1] : '';
|
||||
if (crErr) { window.alert(crErr); return; }
|
||||
loadEntries();
|
||||
var crValue = Array.isArray(cr) ? cr[0] : cr;
|
||||
var notePath = (crValue && crValue.path) ? crValue.path : '';
|
||||
if (notePath) {
|
||||
api.workbench.openResource({
|
||||
kind: 'vault-file',
|
||||
path: localPath(notePath),
|
||||
mode: 'edit',
|
||||
extension: '.md',
|
||||
context: { notesMode: true, sourcePluginId: 'verstak.files' }
|
||||
}).catch(function () {});
|
||||
}
|
||||
});
|
||||
}).catch(function (err) { window.alert('Failed to create note: ' + (err.message || String(err))); });
|
||||
}, 'create-note', 'markdownAdd'));
|
||||
ctxMenu.appendChild(ctxItem('Open Overview', '', function () {
|
||||
api.backend.call('EnsureOverview', scopedPath(entryLocalPath)).then(function (result) {
|
||||
var ovValue = Array.isArray(result) ? result[0] : result;
|
||||
var overviewPath = (ovValue && ovValue.path) ? ovValue.path : '';
|
||||
if (overviewPath) {
|
||||
api.workbench.openResource({
|
||||
kind: 'vault-file',
|
||||
path: localPath(overviewPath),
|
||||
mode: 'view',
|
||||
extension: '.md',
|
||||
context: { notesMode: true, sourcePluginId: 'verstak.files' }
|
||||
}).catch(function () {});
|
||||
}
|
||||
}).catch(function (err) { window.alert('Failed to open overview: ' + (err.message || String(err))); });
|
||||
}, 'open-overview', 'open'));
|
||||
}
|
||||
}
|
||||
ctxMenu.appendChild(ctxSep());
|
||||
ctxMenu.appendChild(ctxItem('Move to Trash', 'danger', function () { trashEntry(entry); }));
|
||||
ctxMenu.appendChild(ctxItem('Rename', '', function () { beginRename(entry); }, 'rename', 'rename'));
|
||||
if (entry.type !== 'folder') {
|
||||
ctxMenu.appendChild(ctxItem('Duplicate', '', function () { duplicateEntry(entry); }, 'duplicate', 'copy'));
|
||||
}
|
||||
ctxMenu.appendChild(ctxSep());
|
||||
ctxMenu.appendChild(ctxItem('Cut', '', function () { cutSelection(); }, 'cut', 'cut'));
|
||||
ctxMenu.appendChild(ctxItem('Copy', '', function () { copySelection(); }, 'copy', 'copy'));
|
||||
ctxMenu.appendChild(ctxSep());
|
||||
ctxMenu.appendChild(ctxItem('Move to Trash', 'danger', function () { trashEntry(); }, 'trash', 'trash'));
|
||||
} else {
|
||||
ctxMenu.appendChild(ctxItem('New Folder', '', function () { startCreate('folder'); }));
|
||||
ctxMenu.appendChild(ctxItem('New Markdown', '', function () { startCreate('markdown'); }));
|
||||
ctxMenu.appendChild(ctxItem('New Text', '', function () { startCreate('text'); }));
|
||||
if (window.__filesClipboard) {
|
||||
ctxMenu.appendChild(ctxItem('New Folder', '', function () { startCreate('folder'); }, 'new-folder', 'folderAdd'));
|
||||
ctxMenu.appendChild(ctxItem('New Markdown', '', function () { startCreate('markdown'); }, 'new-markdown', 'markdownAdd'));
|
||||
ctxMenu.appendChild(ctxItem('New Text', '', function () { startCreate('text'); }, 'new-text', 'textAdd'));
|
||||
if (window.__filesClipboard && window.__filesClipboard.items && window.__filesClipboard.items.length) {
|
||||
ctxMenu.appendChild(ctxSep());
|
||||
ctxMenu.appendChild(ctxItem('Paste', '', function () { pasteEntry(); }));
|
||||
ctxMenu.appendChild(ctxItem('Paste', '', function () { pasteEntry(); }, 'paste', 'paste'));
|
||||
}
|
||||
}
|
||||
ctxMenu.style.display = 'block';
|
||||
|
|
@ -773,64 +874,73 @@
|
|||
});
|
||||
}
|
||||
|
||||
function cutEntry(entry) {
|
||||
if (!entry) return;
|
||||
console.log('[files] Cut:', entry.relativePath);
|
||||
window.__filesClipboard = { action: 'cut', path: entry.relativePath, name: entry.name, isFolder: entry.type === 'folder' };
|
||||
function clipboardItemsFromSelection() {
|
||||
var selected = selectedEntries();
|
||||
if (selected.length === 0) return [];
|
||||
return selected.map(function (entry) {
|
||||
return { path: entry.relativePath, name: entry.name, type: entry.type };
|
||||
});
|
||||
}
|
||||
|
||||
function setClipboard(action, items) {
|
||||
if (!items || !items.length) return;
|
||||
window.__filesClipboard = {
|
||||
action: action,
|
||||
workspaceRoot: workspaceRoot,
|
||||
items: items
|
||||
};
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
function copyEntry(entry) {
|
||||
if (!entry) return;
|
||||
if (entry.type === 'folder') { console.log('[files] Copy for folders not yet supported'); return; }
|
||||
console.log('[files] Copy:', entry.relativePath);
|
||||
window.__filesClipboard = { action: 'copy', path: entry.relativePath, name: entry.name, isFolder: false };
|
||||
updateButtons();
|
||||
function cutSelection() {
|
||||
setClipboard('cut', clipboardItemsFromSelection());
|
||||
}
|
||||
|
||||
function copySelection() {
|
||||
var items = clipboardItemsFromSelection().filter(function (item) { return item.type !== 'folder'; });
|
||||
if (items.length === 0) return;
|
||||
setClipboard('copy', items);
|
||||
}
|
||||
|
||||
function uniqueDestinationName(name, occupied) {
|
||||
if (!occupied[name]) return name;
|
||||
var dot = name.lastIndexOf('.');
|
||||
var base = dot > 0 ? name.slice(0, dot) : name;
|
||||
var ext = dot > 0 ? name.slice(dot) : '';
|
||||
for (var i = 2; i < 100; i += 1) {
|
||||
var candidate = base + ' (' + i + ')' + ext;
|
||||
if (!occupied[candidate]) return candidate;
|
||||
}
|
||||
return base + ' (' + Date.now() + ')' + ext;
|
||||
}
|
||||
|
||||
function pasteEntry() {
|
||||
var clip = window.__filesClipboard;
|
||||
if (!clip || !clip.path) return;
|
||||
var fromRelative = clip.path;
|
||||
var fromScoped = scopedPath(fromRelative);
|
||||
var isFolder = clip.isFolder;
|
||||
var clipName = clip.name;
|
||||
var dot = clipName.lastIndexOf('.');
|
||||
var base = dot > 0 ? clipName.slice(0, dot) : clipName;
|
||||
var ext = dot > 0 ? clipName.slice(dot) : '';
|
||||
var maxAttempts = 100;
|
||||
|
||||
function tryName(n) {
|
||||
var newName = n === 1 ? clipName : base + ' (' + n + ')' + ext;
|
||||
var to = scopedPath(currentPath ? currentPath + '/' + newName : newName);
|
||||
return api.files.metadata(to).then(function () {
|
||||
if (n >= maxAttempts) {
|
||||
console.error('[files] Paste failed: all ' + maxAttempts + ' name variations are taken');
|
||||
return null;
|
||||
}
|
||||
return tryName(n + 1);
|
||||
}, function () {
|
||||
if (isFolder && clip.action === 'cut') {
|
||||
return api.files.move(fromRelative, to, { overwrite: false }).then(function () {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
return api.files.readText(fromScoped).then(function (content) {
|
||||
return api.files.writeText(to, content, { createIfMissing: true, overwrite: false });
|
||||
}).then(function () {
|
||||
if (clip.action === 'cut') {
|
||||
return api.files.trash(fromRelative);
|
||||
}
|
||||
});
|
||||
});
|
||||
if (!clip || !clip.items || clip.items.length === 0) return;
|
||||
if (clip.workspaceRoot && clip.workspaceRoot !== workspaceRoot) {
|
||||
window.alert('Clipboard items belong to another workspace.');
|
||||
return;
|
||||
}
|
||||
var destinationDir = scopedPath(currentPath);
|
||||
var occupied = {};
|
||||
entries.forEach(function (entry) { occupied[entry.name] = true; });
|
||||
|
||||
tryName(1).then(function (result) {
|
||||
if (result !== null) {
|
||||
if (clip.action === 'cut') window.__filesClipboard = null;
|
||||
loadEntries();
|
||||
console.log('[files] Pasted:', clip.action, from);
|
||||
var tasks = clip.items.map(function (item) {
|
||||
var newName = uniqueDestinationName(item.name, occupied);
|
||||
occupied[newName] = true;
|
||||
var to = destinationDir ? destinationDir + '/' + newName : newName;
|
||||
if (clip.action === 'cut') {
|
||||
if (item.path === to || to.indexOf(item.path + '/') === 0) return Promise.resolve();
|
||||
return api.files.move(item.path, to, { overwrite: false });
|
||||
}
|
||||
return api.files.readText(item.path).then(function (content) {
|
||||
return api.files.writeText(to, content, { createIfMissing: true, overwrite: false });
|
||||
});
|
||||
});
|
||||
|
||||
Promise.allSettled(tasks).then(function () {
|
||||
if (clip.action === 'cut') window.__filesClipboard = null;
|
||||
loadEntries();
|
||||
}).catch(function (err) {
|
||||
console.error('[files] Paste failed:', err);
|
||||
});
|
||||
|
|
@ -901,11 +1011,81 @@
|
|||
if (!sourcePaths || !sourcePaths.length) return;
|
||||
var row = e.target.closest('.files-item');
|
||||
if (row && row.getAttribute('data-file-type') === 'folder') {
|
||||
var targetRel = row.getAttribute('data-file-path');
|
||||
moveFiles(sourcePaths, targetRel);
|
||||
moveFiles(sourcePaths, row.getAttribute('data-file-path'));
|
||||
} else {
|
||||
moveFiles(sourcePaths, scopedPath(currentPath));
|
||||
}
|
||||
});
|
||||
|
||||
var lastMouseHistoryAt = 0;
|
||||
var lastMouseHistoryButton = 0;
|
||||
function mouseHistoryButton(event) {
|
||||
if (event.button === 3 || event.button === 8 || event.buttons === 8 || event.buttons === 128 || event.which === 8) return 'back';
|
||||
if (event.button === 4 || event.button === 9 || event.buttons === 16 || event.buttons === 256 || event.which === 9) return 'forward';
|
||||
return '';
|
||||
}
|
||||
|
||||
function handleMouseHistory(event) {
|
||||
var button = mouseHistoryButton(event);
|
||||
if (!button) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
var now = Date.now();
|
||||
if (button === lastMouseHistoryButton && now - lastMouseHistoryAt < 120) return;
|
||||
lastMouseHistoryButton = button;
|
||||
lastMouseHistoryAt = now;
|
||||
|
||||
try {
|
||||
if (window.localStorage && window.localStorage.getItem('verstak-debug') === 'true') {
|
||||
console.log('[debug] [Files] mouse history event', {
|
||||
type: event.type,
|
||||
direction: button,
|
||||
button: event.button,
|
||||
buttons: event.buttons,
|
||||
which: event.which,
|
||||
pointerType: event.pointerType || '',
|
||||
currentPath: currentPath
|
||||
});
|
||||
}
|
||||
} catch (err) {}
|
||||
|
||||
if (button === 'back') goBack();
|
||||
else goForward();
|
||||
}
|
||||
|
||||
function handleWindowHistoryKey(event) {
|
||||
if (event.defaultPrevented) return;
|
||||
if (event.target && ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].indexOf(event.target.tagName) !== -1) return;
|
||||
|
||||
var key = event.key || '';
|
||||
var ctrl = event.ctrlKey || event.metaKey;
|
||||
var direction = '';
|
||||
if (key === 'ArrowLeft' && event.altKey) direction = 'back';
|
||||
else if (key === 'ArrowRight' && event.altKey) direction = 'forward';
|
||||
else if (key === '[' && ctrl) direction = 'back';
|
||||
else if (key === ']' && ctrl) direction = 'forward';
|
||||
else if (key === 'BrowserBack' || key === 'XF86Back' || event.keyCode === 166) direction = 'back';
|
||||
else if (key === 'BrowserForward' || key === 'XF86Forward' || event.keyCode === 167) direction = 'forward';
|
||||
if (!direction) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (direction === 'back') goBack();
|
||||
else goForward();
|
||||
}
|
||||
|
||||
containerEl.addEventListener('mousedown', handleMouseHistory, true);
|
||||
containerEl.addEventListener('pointerdown', handleMouseHistory, true);
|
||||
window.addEventListener('pointerdown', handleMouseHistory, true);
|
||||
document.addEventListener('pointerdown', handleMouseHistory, true);
|
||||
window.addEventListener('mousedown', handleMouseHistory, true);
|
||||
document.addEventListener('mousedown', handleMouseHistory, true);
|
||||
window.addEventListener('mouseup', handleMouseHistory, true);
|
||||
window.addEventListener('auxclick', handleMouseHistory, true);
|
||||
window.addEventListener('keydown', handleWindowHistoryKey);
|
||||
|
||||
containerEl.addEventListener('keydown', function (event) {
|
||||
if (event.target && ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].indexOf(event.target.tagName) !== -1) return;
|
||||
|
||||
|
|
@ -1006,6 +1186,21 @@
|
|||
renderList();
|
||||
return;
|
||||
}
|
||||
if (ctrl && key.toLowerCase() === 'x') {
|
||||
event.preventDefault();
|
||||
cutSelection();
|
||||
return;
|
||||
}
|
||||
if (ctrl && key.toLowerCase() === 'c') {
|
||||
event.preventDefault();
|
||||
copySelection();
|
||||
return;
|
||||
}
|
||||
if (ctrl && key.toLowerCase() === 'v') {
|
||||
event.preventDefault();
|
||||
pasteEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === 'ArrowDown' || key === 'ArrowUp' || key === 'Home' || key === 'End' || key === 'PageDown' || key === 'PageUp') {
|
||||
event.preventDefault();
|
||||
|
|
@ -1045,6 +1240,14 @@
|
|||
disposed = true;
|
||||
document.removeEventListener('click', onDocClick);
|
||||
document.removeEventListener('keydown', onDocKeydown);
|
||||
window.removeEventListener('mousedown', handleMouseHistory, true);
|
||||
window.removeEventListener('pointerdown', handleMouseHistory, true);
|
||||
document.removeEventListener('pointerdown', handleMouseHistory, true);
|
||||
containerEl.removeEventListener('pointerdown', handleMouseHistory, true);
|
||||
document.removeEventListener('mousedown', handleMouseHistory, true);
|
||||
window.removeEventListener('mouseup', handleMouseHistory, true);
|
||||
window.removeEventListener('auxclick', handleMouseHistory, true);
|
||||
window.removeEventListener('keydown', handleWindowHistoryKey);
|
||||
if (ctxMenu && ctxMenu.parentNode) ctxMenu.parentNode.removeChild(ctxMenu);
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,464 @@
|
|||
/* ===========================================================
|
||||
Notes Plugin — Verstak v2 Frontend Bundle
|
||||
Contract: window.VerstakPluginRegister(id, { components })
|
||||
=========================================================== */
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function injectStyles() {
|
||||
if (document.getElementById('notes-style-injected')) return;
|
||||
var style = document.createElement('style');
|
||||
style.id = 'notes-style-injected';
|
||||
style.textContent = STYLES;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
var STYLES = [
|
||||
'.notes-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}',
|
||||
'.notes-toolbar{display:flex;align-items:center;gap:.45rem;padding:.5rem .75rem;border-bottom:1px solid #16213e;flex-shrink:0;background:#12122a;flex-wrap:wrap}',
|
||||
'.notes-btn{font-size:.78rem;padding:.32rem .65rem;border:1px solid #333;border-radius:4px;background:#1a1a2e;color:#ccc;cursor:pointer;display:inline-flex;align-items:center;gap:.35rem}',
|
||||
'.notes-btn:hover{background:#2a2a4e;border-color:#4ecca3}',
|
||||
'.notes-btn:disabled{opacity:.45;cursor:default}',
|
||||
'.notes-btn.primary{background:#1a3a2a;border-color:#4ecca3;color:#4ecca3}',
|
||||
'.notes-btn.primary:hover{background:#2a4a3a}',
|
||||
'.notes-list{flex:1;overflow:auto;min-height:0}',
|
||||
'.notes-item{display:flex;align-items:center;gap:.5rem;padding:.45rem .75rem;border-bottom:1px solid rgba(22,33,62,.55);cursor:pointer;font-size:.85rem}',
|
||||
'.notes-item:hover{background:#17172d}',
|
||||
'.notes-item.overview{background:#111126;border-left:2px solid #4ecca3}',
|
||||
'.notes-item.selected{background:#1a2a3a}',
|
||||
'.notes-item-icon{width:1.25rem;height:1.25rem;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;color:#8b8ba8}',
|
||||
'.notes-item-icon svg{width:16px;height:16px;display:block;fill:currentColor}',
|
||||
'.notes-item-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
||||
'.notes-item-actions{display:flex;gap:.25rem;opacity:0;transition:opacity .15s}',
|
||||
'.notes-item:hover .notes-item-actions{opacity:1}',
|
||||
'.notes-item-btn{width:1.5rem;height:1.5rem;display:inline-flex;align-items:center;justify-content:center;border:1px solid #333;border-radius:3px;background:transparent;color:#888;cursor:pointer;padding:0;font-size:.7rem}',
|
||||
'.notes-item-btn:hover{background:#2a2a4e;border-color:#4ecca3;color:#4ecca3}',
|
||||
'.notes-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#666;font-size:.85rem;padding:2rem;gap:.5rem}',
|
||||
'.notes-empty-hint{font-size:.75rem;color:#555}',
|
||||
'.notes-error{flex:1;display:flex;align-items:center;justify-content:center;color:#e74c3c;padding:1rem;font-size:.85rem}',
|
||||
'.notes-panel{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border-top:1px solid #16213e;flex-shrink:0;background:#12122a}',
|
||||
'.notes-input{flex:1;font-size:.78rem;padding:.32rem .5rem;border:1px solid #333;border-radius:4px;background:#0d0d1a;color:#e0e0e0;outline:none;min-width:120px}',
|
||||
'.notes-input:focus{border-color:#4ecca3}',
|
||||
'.notes-title-bar{padding:.4rem .75rem;font-size:.72rem;color:#8b8ba8;background:#101028;border-bottom:1px solid #16213e;text-transform:uppercase;letter-spacing:.04em}',
|
||||
'.notes-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:10000;display:flex;align-items:center;justify-content:center}',
|
||||
'.notes-modal{width:380px;max-width:90vw;padding:20px;background:#1a1a2e;border:1px solid #333;border-radius:10px;color:#e0e0e0;font-family:inherit;box-shadow:0 12px 40px rgba(0,0,0,.5)}',
|
||||
'.notes-modal-title{font-size:.9rem;font-weight:600;margin-bottom:12px}',
|
||||
'.notes-modal-msg{font-size:.82rem;color:#aaa;margin-bottom:16px;word-wrap:break-word}',
|
||||
'.notes-modal-actions{display:flex;justify-content:flex-end;gap:8px}',
|
||||
'.notes-modal-btn{font-size:.8rem;padding:.35rem .9rem;border:1px solid #333;border-radius:5px;cursor:pointer;font-family:inherit}',
|
||||
'.notes-modal-btn.cancel{background:#2a2a4e;color:#ccc}',
|
||||
'.notes-modal-btn.cancel:hover{background:#3a3a5e}',
|
||||
'.notes-modal-btn.confirm{background:#4ecca3;color:#111;border-color:#4ecca3}',
|
||||
'.notes-modal-btn.confirm:hover{background:#3dbb92}',
|
||||
'.notes-status{font-size:.72rem;color:#8b8ba8;padding:.15rem .5rem;white-space:nowrap}',
|
||||
'.notes-status.success{color:#4ecca3}',
|
||||
'.notes-status.error{color:#e74c3c}',
|
||||
'.notes-status.loading{color:#79c0ff}',
|
||||
'@media(max-width:600px){.notes-toolbar{flex-direction:column;align-items:stretch}}'
|
||||
].join('\n');
|
||||
|
||||
function el(tag, attrs, children) {
|
||||
var elem = document.createElement(tag);
|
||||
if (attrs) {
|
||||
Object.keys(attrs).forEach(function (k) {
|
||||
if (k === 'className') elem.className = 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 === 'innerHTML') elem.innerHTML = attrs[k];
|
||||
else if (k === 'textContent') elem.textContent = attrs[k];
|
||||
else elem.setAttribute(k, attrs[k]);
|
||||
});
|
||||
}
|
||||
if (children) {
|
||||
(Array.isArray(children) ? children : [children]).forEach(function (c) {
|
||||
if (c == null) return;
|
||||
elem.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
|
||||
});
|
||||
}
|
||||
return elem;
|
||||
}
|
||||
|
||||
function svgIcon(path) {
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="' + path + '" fill="currentColor"/></svg>';
|
||||
}
|
||||
|
||||
var ICONS = {
|
||||
note: 'M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 9h-1v1c0 .83-.67 1.5-1.5 1.5S9 12.83 9 12V9.5c0-.83.67-1.5 1.5-1.5S12 8.67 12 9.5v1h1v-1c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5v2.5c0 .83-.67 1.5-1.5 1.5s-1.5-.67-1.5-1.5v-1z',
|
||||
overview: 'M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-1 9h-4v4h-2v-4H9V9h4V5h2v4h4v2z',
|
||||
add: 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z',
|
||||
rename: 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z',
|
||||
open: 'M14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7zM5 5h6V3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-6h-2v6H5V5z',
|
||||
search: 'M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'
|
||||
};
|
||||
|
||||
function iconSvg(name) {
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="' + (ICONS[name] || ICONS.note) + '" fill="currentColor"/></svg>';
|
||||
}
|
||||
|
||||
function fileName(path) {
|
||||
var parts = String(path || '').split('/');
|
||||
return parts[parts.length - 1] || '';
|
||||
}
|
||||
|
||||
var NotesView = {
|
||||
mount: function (containerEl, props, api) {
|
||||
injectStyles();
|
||||
containerEl.innerHTML = '';
|
||||
containerEl.className = 'notes-root';
|
||||
containerEl.setAttribute('data-plugin-id', 'verstak.notes');
|
||||
|
||||
var workspaceNode = props && props.workspaceNode;
|
||||
var workspaceRoot = (workspaceNode && (workspaceNode.rootPath || workspaceNode.name || workspaceNode.id)) || '';
|
||||
var workspaceName = workspaceRoot || (workspaceNode && (workspaceNode.name || workspaceNode.title)) || 'Workspace';
|
||||
|
||||
var notes = [];
|
||||
var selectedPath = '';
|
||||
var statusText = '';
|
||||
var statusClass = '';
|
||||
var disposed = false;
|
||||
|
||||
function notesParent() {
|
||||
return workspaceRoot || '';
|
||||
}
|
||||
|
||||
// ─── Backend bridge ──────────────────────────────────────
|
||||
// Uses api.backend.call() from VerstakPluginAPI — the official bridge.
|
||||
// Direct window.go.api.App access is NOT allowed per platform rules.
|
||||
|
||||
// ─── UI Elements ────────────────────────────────────────
|
||||
|
||||
var toolbar = el('div', { className: 'notes-toolbar' });
|
||||
var overviewBtn = el('button', { className: 'notes-btn primary', 'data-action': 'overview', innerHTML: iconSvg('overview') + ' Overview' });
|
||||
var createBtn = el('button', { className: 'notes-btn', 'data-action': 'create', innerHTML: iconSvg('add') + ' New Note' });
|
||||
var statusEl = el('span', { className: 'notes-status' });
|
||||
toolbar.appendChild(overviewBtn);
|
||||
toolbar.appendChild(createBtn);
|
||||
toolbar.appendChild(el('span', { style: { flex: '1' } }));
|
||||
toolbar.appendChild(statusEl);
|
||||
containerEl.appendChild(toolbar);
|
||||
|
||||
var titleBar = el('div', { className: 'notes-title-bar' }, ['Notes in ' + workspaceName]);
|
||||
containerEl.appendChild(titleBar);
|
||||
|
||||
var listContainer = el('div', { className: 'notes-list', 'data-notes-list': '' });
|
||||
containerEl.appendChild(listContainer);
|
||||
|
||||
var createPanel = el('div', { className: 'notes-panel', style: { display: 'none' } });
|
||||
var createInput = el('input', { className: 'notes-input', 'data-notes-create-input': '', placeholder: 'Note title' });
|
||||
var createConfirm = el('button', { className: 'notes-btn', textContent: 'Create' });
|
||||
var createCancel = el('button', { className: 'notes-btn', textContent: 'Cancel' });
|
||||
createPanel.appendChild(createInput);
|
||||
createPanel.appendChild(createConfirm);
|
||||
createPanel.appendChild(createCancel);
|
||||
containerEl.appendChild(createPanel);
|
||||
|
||||
var renamePanel = el('div', { className: 'notes-panel', style: { display: 'none' } });
|
||||
var renameInput = el('input', { className: 'notes-input', 'data-notes-rename-input': '', placeholder: 'New title' });
|
||||
var renameConfirm = el('button', { className: 'notes-btn', textContent: 'Rename' });
|
||||
var renameCancel = el('button', { className: 'notes-btn', textContent: 'Cancel' });
|
||||
renamePanel.appendChild(renameInput);
|
||||
renamePanel.appendChild(renameConfirm);
|
||||
renamePanel.appendChild(renameCancel);
|
||||
containerEl.appendChild(renamePanel);
|
||||
|
||||
// ─── Core Functions ─────────────────────────────────────
|
||||
|
||||
function setStatus(text, cls) {
|
||||
statusText = text;
|
||||
statusClass = cls || '';
|
||||
if (statusEl) {
|
||||
statusEl.textContent = text;
|
||||
statusEl.className = 'notes-status' + (cls ? ' ' + cls : '');
|
||||
}
|
||||
if (text) {
|
||||
setTimeout(function () {
|
||||
if (!disposed && statusEl && statusEl.textContent === text) {
|
||||
statusEl.textContent = '';
|
||||
statusEl.className = 'notes-status';
|
||||
}
|
||||
}, 4000);
|
||||
}
|
||||
}
|
||||
|
||||
function loadNotes() {
|
||||
listContainer.innerHTML = '';
|
||||
listContainer.appendChild(el('div', { className: 'notes-empty' }, ['Loading...']));
|
||||
|
||||
var parent = notesParent();
|
||||
api.backend.call('ListNotes', parent).then(function (result) {
|
||||
if (disposed) return;
|
||||
var unpack = unpackResult(result);
|
||||
if (unpack.error) {
|
||||
renderEmpty('Could not load notes: ' + unpack.error);
|
||||
return;
|
||||
}
|
||||
notes = unpack.value || [];
|
||||
renderList();
|
||||
}).catch(function (err) {
|
||||
if (disposed) return;
|
||||
renderEmpty('Error: ' + (err.message || err));
|
||||
});
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
listContainer.innerHTML = '';
|
||||
if (!notes || notes.length === 0) {
|
||||
renderEmpty('No notes yet');
|
||||
return;
|
||||
}
|
||||
notes.forEach(function (note) {
|
||||
var row = el('div', {
|
||||
className: 'notes-item' + (note.isOverview ? ' overview' : '') + (note.path === selectedPath ? ' selected' : ''),
|
||||
'data-note-path': note.path,
|
||||
'data-note-title': note.title,
|
||||
onClick: function () { selectNote(note); },
|
||||
onDblclick: function () { openNote(note); }
|
||||
}, [
|
||||
el('span', { className: 'notes-item-icon', innerHTML: note.isOverview ? iconSvg('overview') : iconSvg('note') }),
|
||||
el('span', { className: 'notes-item-name', textContent: note.title || fileName(note.path), title: note.title || note.path }),
|
||||
el('span', { className: 'notes-item-actions' }, [
|
||||
el('button', {
|
||||
className: 'notes-item-btn',
|
||||
title: 'Open',
|
||||
innerHTML: iconSvg('open'),
|
||||
onClick: function (e) { e.stopPropagation(); openNote(note); }
|
||||
}),
|
||||
!note.isOverview ? el('button', {
|
||||
className: 'notes-item-btn',
|
||||
title: 'Rename',
|
||||
innerHTML: iconSvg('rename'),
|
||||
onClick: function (e) { e.stopPropagation(); beginRename(note); }
|
||||
}) : null
|
||||
])
|
||||
]);
|
||||
listContainer.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function renderEmpty(msg) {
|
||||
listContainer.innerHTML = '';
|
||||
listContainer.appendChild(el('div', { className: 'notes-empty' }, [
|
||||
el('div', {}, [iconSvg('note')]),
|
||||
el('div', {}, [msg]),
|
||||
el('div', { className: 'notes-empty-hint' }, ['Click "New Note" to create one'])
|
||||
]));
|
||||
}
|
||||
|
||||
function selectNote(note) {
|
||||
selectedPath = note.path;
|
||||
renderList();
|
||||
}
|
||||
|
||||
function openNote(note) {
|
||||
if (!note) return;
|
||||
var ext = '.md';
|
||||
api.workbench.openResource({
|
||||
kind: 'vault-file',
|
||||
path: note.path,
|
||||
mode: 'view',
|
||||
extension: ext,
|
||||
context: {
|
||||
sourcePluginId: 'verstak.notes',
|
||||
sourceView: 'notes',
|
||||
isInsideNotesFolder: true,
|
||||
notesMode: true,
|
||||
notesScopePath: notesParent()
|
||||
}
|
||||
}).catch(function (err) { console.error('[notes] openResource:', err); });
|
||||
}
|
||||
|
||||
function openOverview() {
|
||||
setStatus('Opening overview...', 'loading');
|
||||
var parent = notesParent();
|
||||
api.backend.call('EnsureOverview', parent).then(function (result) {
|
||||
if (disposed) return;
|
||||
var unpack = unpackResult(result);
|
||||
if (unpack.error) {
|
||||
setStatus('Error: ' + unpack.error, 'error');
|
||||
return;
|
||||
}
|
||||
var overviewPath = (unpack.value && unpack.value.path) || '';
|
||||
if (!overviewPath) {
|
||||
setStatus('Failed to get overview path', 'error');
|
||||
return;
|
||||
}
|
||||
loadNotes();
|
||||
api.workbench.openResource({
|
||||
kind: 'vault-file',
|
||||
path: overviewPath,
|
||||
mode: 'view',
|
||||
extension: '.md',
|
||||
context: {
|
||||
sourcePluginId: 'verstak.notes',
|
||||
sourceView: 'notes',
|
||||
isInsideNotesFolder: true,
|
||||
notesMode: true,
|
||||
notesScopePath: notesParent()
|
||||
}
|
||||
}).catch(function (err) { setStatus('Error opening overview: ' + (err.message || err), 'error'); });
|
||||
}).catch(function (err) {
|
||||
setStatus('Error: ' + (err.message || err), 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Create ────────────────────────────────────────────
|
||||
|
||||
function showCreate() {
|
||||
createInput.value = '';
|
||||
createPanel.style.display = 'flex';
|
||||
createInput.focus();
|
||||
}
|
||||
|
||||
function hideCreate() {
|
||||
createPanel.style.display = 'none';
|
||||
}
|
||||
|
||||
function confirmCreate() {
|
||||
var title = createInput.value.trim();
|
||||
if (!title) return;
|
||||
setStatus('Creating note...', 'loading');
|
||||
var parent = notesParent();
|
||||
api.backend.call('CreateNote', parent, title).then(function (result) {
|
||||
if (disposed) return;
|
||||
var unpack = unpackResult(result);
|
||||
if (unpack.error) {
|
||||
setStatus('Error: ' + unpack.error, 'error');
|
||||
return;
|
||||
}
|
||||
var data = unpack.value || {};
|
||||
if (data.conflict) {
|
||||
showConflictModal(title, data.path);
|
||||
return;
|
||||
}
|
||||
hideCreate();
|
||||
setStatus('Note created', 'success');
|
||||
loadNotes();
|
||||
// Open the newly created note
|
||||
if (data.path) {
|
||||
api.workbench.openResource({
|
||||
kind: 'vault-file',
|
||||
path: data.path,
|
||||
mode: 'edit',
|
||||
extension: '.md',
|
||||
context: {
|
||||
sourcePluginId: 'verstak.notes',
|
||||
sourceView: 'notes',
|
||||
isInsideNotesFolder: true,
|
||||
notesMode: true,
|
||||
notesScopePath: parent
|
||||
}
|
||||
}).catch(function () {});
|
||||
}
|
||||
}).catch(function (err) {
|
||||
setStatus('Error: ' + (err.message || err), 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Rename ─────────────────────────────────────────────
|
||||
|
||||
function beginRename(note) {
|
||||
renameTarget = note;
|
||||
renameInput.value = note.title || fileName(note.path);
|
||||
renamePanel.style.display = 'flex';
|
||||
renameInput.focus();
|
||||
renameInput.select();
|
||||
}
|
||||
|
||||
function hideRename() {
|
||||
renameTarget = null;
|
||||
renamePanel.style.display = 'none';
|
||||
}
|
||||
|
||||
function confirmRename() {
|
||||
if (!renameTarget) return;
|
||||
var newTitle = renameInput.value.trim();
|
||||
if (!newTitle) return;
|
||||
setStatus('Renaming...', 'loading');
|
||||
api.backend.call('RenameNote', renameTarget.path, newTitle).then(function (result) {
|
||||
if (disposed) return;
|
||||
var unpack = unpackResult(result);
|
||||
if (unpack.error) {
|
||||
setStatus('Error: ' + unpack.error, 'error');
|
||||
return;
|
||||
}
|
||||
var data = unpack.value || {};
|
||||
if (data.conflict) {
|
||||
showConflictModal(newTitle, data.path);
|
||||
return;
|
||||
}
|
||||
hideRename();
|
||||
setStatus('Note renamed', 'success');
|
||||
loadNotes();
|
||||
}).catch(function (err) {
|
||||
setStatus('Error: ' + (err.message || err), 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Conflict Modal ─────────────────────────────────────
|
||||
|
||||
function showConflictModal(title, existingPath) {
|
||||
var overlay = el('div', { className: 'notes-modal-overlay' });
|
||||
var modal = el('div', { className: 'notes-modal' }, [
|
||||
el('div', { className: 'notes-modal-title' }, ['Name Conflict']),
|
||||
el('div', { className: 'notes-modal-msg' }, [
|
||||
'A note with the title "' + title + '" already exists.',
|
||||
' Please choose a different title.'
|
||||
].join('')),
|
||||
el('div', { className: 'notes-modal-actions' }, [
|
||||
el('button', { className: 'notes-modal-btn confirm', textContent: 'OK', onClick: function () { overlay.remove(); createInput.focus(); } })
|
||||
])
|
||||
]);
|
||||
overlay.appendChild(modal);
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
// ─── Event Wiring ───────────────────────────────────────
|
||||
|
||||
overviewBtn.addEventListener('click', openOverview);
|
||||
createBtn.addEventListener('click', showCreate);
|
||||
createConfirm.addEventListener('click', confirmCreate);
|
||||
createCancel.addEventListener('click', hideCreate);
|
||||
renameConfirm.addEventListener('click', confirmRename);
|
||||
renameCancel.addEventListener('click', hideRename);
|
||||
createInput.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') confirmCreate();
|
||||
if (e.key === 'Escape') hideCreate();
|
||||
});
|
||||
renameInput.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') confirmRename();
|
||||
if (e.key === 'Escape') hideRename();
|
||||
});
|
||||
|
||||
// ─── Init ───────────────────────────────────────────────
|
||||
|
||||
loadNotes();
|
||||
|
||||
containerEl.__notesCleanup = function () {
|
||||
disposed = true;
|
||||
};
|
||||
},
|
||||
|
||||
unmount: function (containerEl) {
|
||||
if (containerEl.__notesCleanup) {
|
||||
containerEl.__notesCleanup();
|
||||
containerEl.__notesCleanup = null;
|
||||
}
|
||||
containerEl.innerHTML = '';
|
||||
}
|
||||
};
|
||||
|
||||
function unpackResult(result) {
|
||||
if (Array.isArray(result) && result.length === 2) {
|
||||
return { value: result[0], error: result[1] || '' };
|
||||
}
|
||||
if (result && result.value !== undefined && result.error !== undefined) {
|
||||
return result;
|
||||
}
|
||||
return { value: result, error: '' };
|
||||
}
|
||||
|
||||
window.VerstakPluginRegister('verstak.notes', {
|
||||
components: { NotesView: NotesView }
|
||||
});
|
||||
})();
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "verstak.notes",
|
||||
"name": "Notes",
|
||||
"version": "0.1.0",
|
||||
"apiVersion": "0.1.0",
|
||||
"description": "Workspace-scoped notes manager with create, rename, overview, and workbench integration.",
|
||||
"source": "official",
|
||||
"icon": "file-text",
|
||||
"provides": [
|
||||
"verstak/notes/v1"
|
||||
],
|
||||
"requires": [
|
||||
"verstak/core/files/v1",
|
||||
"verstak/core/workbench/v1"
|
||||
],
|
||||
"optionalRequires": [
|
||||
"verstak/core/notes/v1"
|
||||
],
|
||||
"permissions": [
|
||||
"files.read",
|
||||
"files.write",
|
||||
"workbench.open",
|
||||
"ui.register"
|
||||
],
|
||||
"frontend": {
|
||||
"entry": "frontend/src/index.js"
|
||||
},
|
||||
"contributes": {
|
||||
"workspaceItems": [
|
||||
{
|
||||
"id": "verstak.notes.workspace",
|
||||
"title": "Notes",
|
||||
"icon": "file-text",
|
||||
"component": "NotesView"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -458,12 +458,151 @@
|
|||
apiStatusList,
|
||||
]);
|
||||
|
||||
/* ── Mouse Event Inspector ──────────────────────────────────── */
|
||||
var mouseLog = [];
|
||||
var mouseCapturing = false;
|
||||
var mouseHandlers = [];
|
||||
var mouseEventTypes = ['pointerdown', 'pointerup', 'mousedown', 'mouseup', 'auxclick'];
|
||||
var mouseLogContainer = el('pre', {
|
||||
className: 'pt-mouse-log',
|
||||
style: {
|
||||
background: '#0d0d1a',
|
||||
color: '#4ecca3',
|
||||
padding: '0.75rem',
|
||||
borderRadius: '6px',
|
||||
fontSize: '0.75rem',
|
||||
fontFamily: 'monospace',
|
||||
maxHeight: '300px',
|
||||
overflow: 'auto',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all',
|
||||
margin: '0.5rem 0',
|
||||
lineHeight: '1.5',
|
||||
},
|
||||
}, ['']);
|
||||
|
||||
var mouseCountSpan = span('pt-list-value', '0');
|
||||
|
||||
function renderMouseLog() {
|
||||
if (mouseLog.length === 0) {
|
||||
mouseLogContainer.textContent = '(no events captured)';
|
||||
mouseCountSpan.textContent = '0';
|
||||
return;
|
||||
}
|
||||
var lines = mouseLog.map(function (e, i) {
|
||||
return '[' + (i + 1) + '] ' + e.type + ' button=' + e.button + ' buttons=' + e.buttons +
|
||||
' which=' + e.which + ' pointerType=' + e.pointerType +
|
||||
' defaultPrevented=' + e.defaultPrevented +
|
||||
' target=' + e.targetTag + (e.targetClass ? '.' + e.targetClass : '');
|
||||
});
|
||||
mouseLogContainer.textContent = lines.join('\n');
|
||||
mouseCountSpan.textContent = String(mouseLog.length);
|
||||
mouseLogContainer.scrollTop = mouseLogContainer.scrollHeight;
|
||||
}
|
||||
|
||||
function onMouseEvent(e) {
|
||||
mouseLog.push({
|
||||
type: e.type,
|
||||
button: e.button,
|
||||
buttons: e.buttons,
|
||||
which: e.which,
|
||||
pointerType: e.pointerType || '',
|
||||
defaultPrevented: e.defaultPrevented,
|
||||
targetTag: e.target ? e.target.tagName : '',
|
||||
targetClass: e.target ? (e.target.className || '') : '',
|
||||
time: Date.now(),
|
||||
});
|
||||
if (mouseLog.length > 200) mouseLog.shift();
|
||||
renderMouseLog();
|
||||
}
|
||||
|
||||
function startMouseCapture() {
|
||||
if (mouseCapturing) return;
|
||||
mouseCapturing = true;
|
||||
mouseEventTypes.forEach(function (type) {
|
||||
var handler = function (e) { onMouseEvent(e); };
|
||||
window.addEventListener(type, handler, true);
|
||||
mouseHandlers.push({ type: type, handler: handler });
|
||||
});
|
||||
startStopBtn.textContent = '■ Stop Capture';
|
||||
startStopBtn.setAttribute('data-mouse-capturing', 'true');
|
||||
renderMouseLog();
|
||||
}
|
||||
|
||||
function stopMouseCapture() {
|
||||
if (!mouseCapturing) return;
|
||||
mouseCapturing = false;
|
||||
mouseHandlers.forEach(function (h) {
|
||||
window.removeEventListener(h.type, h.handler, true);
|
||||
});
|
||||
mouseHandlers = [];
|
||||
startStopBtn.textContent = '▶ Start Capture';
|
||||
startStopBtn.setAttribute('data-mouse-capturing', 'false');
|
||||
}
|
||||
|
||||
var startStopBtn = el('button', {
|
||||
className: 'btn btn-primary',
|
||||
style: { marginRight: '0.5rem' },
|
||||
onClick: function () {
|
||||
if (mouseCapturing) { stopMouseCapture(); } else { startMouseCapture(); }
|
||||
},
|
||||
}, ['▶ Start Capture']);
|
||||
|
||||
var clearBtn = el('button', {
|
||||
className: 'btn btn-secondary',
|
||||
style: { marginRight: '0.5rem' },
|
||||
onClick: function () {
|
||||
mouseLog = [];
|
||||
renderMouseLog();
|
||||
},
|
||||
}, ['Clear']);
|
||||
|
||||
var copyBtn = el('button', {
|
||||
className: 'btn btn-secondary',
|
||||
onClick: function () {
|
||||
var json = JSON.stringify(mouseLog, null, 2);
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(json).then(function () {
|
||||
copyBtn.textContent = '✓ Copied!';
|
||||
setTimeout(function () { copyBtn.textContent = 'Copy JSON'; }, 1500);
|
||||
});
|
||||
} else {
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = json;
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
copyBtn.textContent = '✓ Copied!';
|
||||
setTimeout(function () { copyBtn.textContent = 'Copy JSON'; }, 1500);
|
||||
}
|
||||
},
|
||||
}, ['Copy JSON']);
|
||||
|
||||
trackCleanup(function () { stopMouseCapture(); });
|
||||
|
||||
var mouseCard = div('pt-card', [
|
||||
el('h3', { className: 'pt-card-title' }, ['Mouse Event Inspector']),
|
||||
el('p', { style: { margin: '0 0 0.5rem', color: '#a0a0b8', fontSize: '0.8rem' } }, [
|
||||
'Captures ALL mouse/pointer events on window. Press back/forward buttons to see what WebKitGTK reports.',
|
||||
]),
|
||||
el('div', { style: { display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.5rem' } }, [
|
||||
startStopBtn,
|
||||
clearBtn,
|
||||
copyBtn,
|
||||
span('pt-list-label', ' Events:'),
|
||||
mouseCountSpan,
|
||||
]),
|
||||
mouseLogContainer,
|
||||
]);
|
||||
|
||||
/* ── Assemble ──────────────────────────────────────────────── */
|
||||
containerEl.appendChild(header);
|
||||
containerEl.appendChild(badgeRow);
|
||||
containerEl.appendChild(bridgeCard);
|
||||
containerEl.appendChild(testsCard);
|
||||
containerEl.appendChild(capsCard);
|
||||
containerEl.appendChild(mouseCard);
|
||||
containerEl.appendChild(infoCard);
|
||||
containerEl.appendChild(apiCard);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -458,12 +458,151 @@
|
|||
apiStatusList,
|
||||
]);
|
||||
|
||||
/* ── Mouse Event Inspector ──────────────────────────────────── */
|
||||
var mouseLog = [];
|
||||
var mouseCapturing = false;
|
||||
var mouseHandlers = [];
|
||||
var mouseEventTypes = ['pointerdown', 'pointerup', 'mousedown', 'mouseup', 'auxclick'];
|
||||
var mouseLogContainer = el('pre', {
|
||||
className: 'pt-mouse-log',
|
||||
style: {
|
||||
background: '#0d0d1a',
|
||||
color: '#4ecca3',
|
||||
padding: '0.75rem',
|
||||
borderRadius: '6px',
|
||||
fontSize: '0.75rem',
|
||||
fontFamily: 'monospace',
|
||||
maxHeight: '300px',
|
||||
overflow: 'auto',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all',
|
||||
margin: '0.5rem 0',
|
||||
lineHeight: '1.5',
|
||||
},
|
||||
}, ['']);
|
||||
|
||||
var mouseCountSpan = span('pt-list-value', '0');
|
||||
|
||||
function renderMouseLog() {
|
||||
if (mouseLog.length === 0) {
|
||||
mouseLogContainer.textContent = '(no events captured)';
|
||||
mouseCountSpan.textContent = '0';
|
||||
return;
|
||||
}
|
||||
var lines = mouseLog.map(function (e, i) {
|
||||
return '[' + (i + 1) + '] ' + e.type + ' button=' + e.button + ' buttons=' + e.buttons +
|
||||
' which=' + e.which + ' pointerType=' + e.pointerType +
|
||||
' defaultPrevented=' + e.defaultPrevented +
|
||||
' target=' + e.targetTag + (e.targetClass ? '.' + e.targetClass : '');
|
||||
});
|
||||
mouseLogContainer.textContent = lines.join('\n');
|
||||
mouseCountSpan.textContent = String(mouseLog.length);
|
||||
mouseLogContainer.scrollTop = mouseLogContainer.scrollHeight;
|
||||
}
|
||||
|
||||
function onMouseEvent(e) {
|
||||
mouseLog.push({
|
||||
type: e.type,
|
||||
button: e.button,
|
||||
buttons: e.buttons,
|
||||
which: e.which,
|
||||
pointerType: e.pointerType || '',
|
||||
defaultPrevented: e.defaultPrevented,
|
||||
targetTag: e.target ? e.target.tagName : '',
|
||||
targetClass: e.target ? (e.target.className || '') : '',
|
||||
time: Date.now(),
|
||||
});
|
||||
if (mouseLog.length > 200) mouseLog.shift();
|
||||
renderMouseLog();
|
||||
}
|
||||
|
||||
function startMouseCapture() {
|
||||
if (mouseCapturing) return;
|
||||
mouseCapturing = true;
|
||||
mouseEventTypes.forEach(function (type) {
|
||||
var handler = function (e) { onMouseEvent(e); };
|
||||
window.addEventListener(type, handler, true);
|
||||
mouseHandlers.push({ type: type, handler: handler });
|
||||
});
|
||||
startStopBtn.textContent = '■ Stop Capture';
|
||||
startStopBtn.setAttribute('data-mouse-capturing', 'true');
|
||||
renderMouseLog();
|
||||
}
|
||||
|
||||
function stopMouseCapture() {
|
||||
if (!mouseCapturing) return;
|
||||
mouseCapturing = false;
|
||||
mouseHandlers.forEach(function (h) {
|
||||
window.removeEventListener(h.type, h.handler, true);
|
||||
});
|
||||
mouseHandlers = [];
|
||||
startStopBtn.textContent = '▶ Start Capture';
|
||||
startStopBtn.setAttribute('data-mouse-capturing', 'false');
|
||||
}
|
||||
|
||||
var startStopBtn = el('button', {
|
||||
className: 'btn btn-primary',
|
||||
style: { marginRight: '0.5rem' },
|
||||
onClick: function () {
|
||||
if (mouseCapturing) { stopMouseCapture(); } else { startMouseCapture(); }
|
||||
},
|
||||
}, ['▶ Start Capture']);
|
||||
|
||||
var clearBtn = el('button', {
|
||||
className: 'btn btn-secondary',
|
||||
style: { marginRight: '0.5rem' },
|
||||
onClick: function () {
|
||||
mouseLog = [];
|
||||
renderMouseLog();
|
||||
},
|
||||
}, ['Clear']);
|
||||
|
||||
var copyBtn = el('button', {
|
||||
className: 'btn btn-secondary',
|
||||
onClick: function () {
|
||||
var json = JSON.stringify(mouseLog, null, 2);
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(json).then(function () {
|
||||
copyBtn.textContent = '✓ Copied!';
|
||||
setTimeout(function () { copyBtn.textContent = 'Copy JSON'; }, 1500);
|
||||
});
|
||||
} else {
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = json;
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
copyBtn.textContent = '✓ Copied!';
|
||||
setTimeout(function () { copyBtn.textContent = 'Copy JSON'; }, 1500);
|
||||
}
|
||||
},
|
||||
}, ['Copy JSON']);
|
||||
|
||||
trackCleanup(function () { stopMouseCapture(); });
|
||||
|
||||
var mouseCard = div('pt-card', [
|
||||
el('h3', { className: 'pt-card-title' }, ['Mouse Event Inspector']),
|
||||
el('p', { style: { margin: '0 0 0.5rem', color: '#a0a0b8', fontSize: '0.8rem' } }, [
|
||||
'Captures ALL mouse/pointer events on window. Press back/forward buttons to see what WebKitGTK reports.',
|
||||
]),
|
||||
el('div', { style: { display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.5rem' } }, [
|
||||
startStopBtn,
|
||||
clearBtn,
|
||||
copyBtn,
|
||||
span('pt-list-label', ' Events:'),
|
||||
mouseCountSpan,
|
||||
]),
|
||||
mouseLogContainer,
|
||||
]);
|
||||
|
||||
/* ── Assemble ──────────────────────────────────────────────── */
|
||||
containerEl.appendChild(header);
|
||||
containerEl.appendChild(badgeRow);
|
||||
containerEl.appendChild(bridgeCard);
|
||||
containerEl.appendChild(testsCard);
|
||||
containerEl.appendChild(capsCard);
|
||||
containerEl.appendChild(mouseCard);
|
||||
containerEl.appendChild(infoCard);
|
||||
containerEl.appendChild(apiCard);
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue