Files
mnote/rust/crates/mnote-web/browser/sidebar-filetree-command-runtime.js
T

1572 lines
70 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
const {
applyRemoveDocumentDelta,
createFileTreeFolder,
createPage,
cssEscape,
currentDocumentId,
currentSourceKind,
deleteSingleFileTreeAsset,
dispatchSidebarEvent,
dispatchTreeCommand,
ensureFileTreeWritableTarget,
fileTreeRuntimeDeps,
fileTreeRuntimeFunction,
fileTreeSelectionRuntimeDeps,
fileTreeSelectionRuntimeFunction,
isFileTreePageRow,
localFilePathFromAssetId,
normalizeFileTreePageRenameTitle,
openConvexAssetFromFileTree,
openEditorAttachmentDetail,
openEditorAttachmentDownload,
openEditorAttachmentEditTab,
openEditorAttachmentNewWindow,
refreshLocalFolderSidebarSnapshot,
resolveWorkspaceId,
runtimeState,
selectedSidebarFileTreeSelection,
updateTitleEverywhere,
validateFileTreeRename,
} = dependencies;
const sidebarFileTreeSelection = selectedSidebarFileTreeSelection;
function rowTitle(row) {
var title = row ? row.querySelector(':scope > .tree-link > .tree-link-title') : null;
return title && title.textContent ? title.textContent.trim() : '无标题';
}
function rowCenter(row) {
var rect = row.getBoundingClientRect();
return { x: rect.left + Math.min(rect.width - 12, 180), y: rect.top + Math.min(rect.height, 22) };
}
async function renameFileTreeAsset(assetId, title) {
var response = await fetch('/api/media/batch', {
method: 'POST',
credentials: 'include',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ action: 'rename', assetIds: [assetId], newName: title })
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error((payload && payload.error) || '重命名附件失败');
return payload;
}
function beginFileTreeInlineRename(row) {
if (!(row instanceof HTMLElement)) return false;
if (row.querySelector('.tree-rename-input')) return true;
var rowKind = row.getAttribute('data-row-kind') || '';
var documentId = row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || '';
var assetId = row.getAttribute('data-asset-id') || '';
if (!documentId && !assetId) return false;
if (rowKind && ['document', 'doc', 'index', 'markdown', 'asset'].indexOf(rowKind) < 0 && !assetId) return false;
var link = row.querySelector(':scope > .tree-link');
var title = rowTitle(row);
if (!(link instanceof HTMLElement)) return false;
link.hidden = true;
var input = document.createElement('input');
input.type = 'text';
input.className = 'tree-rename-input';
input.setAttribute('data-rename-id', row.getAttribute('data-row-id') || '');
input.value = title;
input.style.minWidth = '0';
input.style.flex = '1 1 auto';
input.style.height = '22px';
input.style.border = '1px solid #93c5fd';
input.style.borderRadius = '3px';
input.style.padding = '0 4px';
input.style.font = 'inherit';
input.style.background = '#fff';
input.style.color = '#1f2937';
var validation = document.createElement('div');
validation.setAttribute('data-testid', 'tree-rename-validation');
validation.setAttribute('data-mnote-rename-validation', 'true');
validation.style.fontSize = '12px';
validation.style.color = '#dc2626';
validation.style.padding = '2px 4px';
validation.hidden = true;
var setValidation = function(message) {
var text = String(message || '').trim();
validation.textContent = text;
validation.hidden = !text;
input.setAttribute('aria-invalid', text ? 'true' : 'false');
};
var closed = false;
var committing = false;
var close = function() {
if (closed) return;
closed = true;
if (input.parentElement) input.parentElement.removeChild(input);
if (validation.parentElement) validation.parentElement.removeChild(validation);
link.hidden = false;
};
var commit = function() {
if (closed || committing) return;
var nextTitle = input.value.trim();
if (!nextTitle || nextTitle === title) {
close();
return;
}
var validationMessage = validateFileTreeRename(row, nextTitle);
if (validationMessage) {
setValidation(validationMessage);
return;
}
setValidation('');
var commandTitle = isFileTreePageRow(row) ? normalizeFileTreePageRenameTitle(nextTitle) : nextTitle;
committing = true;
input.disabled = true;
var work = assetId
? Promise.resolve().then(function(){
document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-asset-id="' + cssEscape(assetId) + '"] .tree-link-title').forEach(function(titleNode) {
titleNode.textContent = commandTitle;
});
close();
return renameFileTreeAsset(assetId, commandTitle);
}).then(function(){ return null; })
: dispatchTreeCommand(row, {
action: 'rename',
workspaceId: resolveWorkspaceId(row),
documentId: documentId,
title: commandTitle
}).then(function(){ updateTitleEverywhere(documentId, commandTitle); });
void work.then(close).catch(function(error) {
committing = false;
input.disabled = false;
window.alert(error && error.message ? error.message : '重命名失败');
});
};
input.addEventListener('click', function(event) { event.stopPropagation(); });
input.addEventListener('dblclick', function(event) { event.stopPropagation(); });
input.addEventListener('keydown', function(event) {
event.stopPropagation();
if (event.key === 'Enter') {
event.preventDefault();
commit();
} else if (event.key === 'Escape') {
event.preventDefault();
close();
}
});
input.addEventListener('blur', commit);
link.parentElement.insertBefore(input, link.nextSibling);
link.parentElement.insertBefore(validation, input.nextSibling);
window.requestAnimationFrame(function() {
input.focus();
input.select();
});
return true;
}
function closeTreeContextMenu() {
if (runtimeState.activeTreeContextMenu && runtimeState.activeTreeContextMenu.parentElement) {
runtimeState.activeTreeContextMenu.parentElement.removeChild(runtimeState.activeTreeContextMenu);
}
runtimeState.activeTreeContextMenu = null;
}
function copyTreeContextValue(value, actionName) {
var text = String(value || '');
var done = function() {
document.documentElement.setAttribute('data-mnote-tree-context-last-copy', actionName || 'copy');
};
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
return navigator.clipboard.writeText(text).then(done).catch(function(){});
}
var textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', 'readonly');
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
try { document.execCommand('copy'); } catch (_) {}
document.body.removeChild(textarea);
done();
return Promise.resolve();
}
function triggerBrowserDownload(url) {
if (!url) return false;
var link = document.createElement('a');
link.href = url;
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.download = '';
link.style.position = 'fixed';
link.style.left = '-9999px';
link.style.top = '0';
document.body.appendChild(link);
try {
link.click();
} catch (_) {
if (typeof window.open === 'function') window.open(url, '_blank', 'noopener,noreferrer');
}
window.setTimeout(function() {
if (link.parentElement) link.parentElement.removeChild(link);
}, 1000);
return true;
}
function recordFileTreeAction(action, detail) {
var normalized = String(action || '').trim() || 'unknown';
var rowId = String(detail && detail.rowId || '').trim();
var documentId = String(detail && detail.documentId || '').trim();
var assetId = String(detail && detail.assetId || '').trim();
document.documentElement.setAttribute('data-mnote-filetree-last-action', normalized);
if (rowId) document.documentElement.setAttribute('data-mnote-filetree-last-action-row-id', rowId);
if (documentId) document.documentElement.setAttribute('data-mnote-filetree-last-action-document-id', documentId);
if (assetId) document.documentElement.setAttribute('data-mnote-filetree-last-action-asset-id', assetId);
window.dispatchEvent(new CustomEvent('tree.filetree.action', {
detail: Object.assign({}, detail || {}, { action: normalized })
}));
}
function recordFileTreeActionStatus(status, detail) {
var normalized = String(status || '').trim() || 'unknown';
document.documentElement.setAttribute('data-mnote-filetree-last-action-status', normalized);
window.dispatchEvent(new CustomEvent('tree.filetree.action.status', {
detail: Object.assign({}, detail || {}, { status: normalized })
}));
}
function documentHref(documentId, workspaceId) {
var url = new URL('/documents/' + encodeURIComponent(documentId), window.location.origin);
if (workspaceId) url.searchParams.set('workspaceId', workspaceId);
copyWorkspaceSourceParams(url);
return url.toString();
}
function convertToPreviousSiblingChild(trigger, detail) {
var documentId = detail.documentId || '';
var row = document.querySelector('#sidebar-tree-root .tree-row[data-node-id="' + cssEscape(documentId) + '"]');
if (!(row instanceof HTMLElement)) return;
var parentId = row.getAttribute('data-parent-id') || '';
var siblings = Array.from(document.querySelectorAll('#sidebar-tree-root .tree-row[data-shell-mode="page"]')).filter(function(candidate) {
return (candidate.getAttribute('data-parent-id') || '') === parentId;
});
var index = siblings.indexOf(row);
if (index <= 0) {
window.alert('当前页面前面没有同级页面。');
return;
}
var previous = siblings[index - 1];
var previousId = previous.getAttribute('data-node-id') || '';
var children = previous.parentElement ? previous.parentElement.querySelectorAll(':scope > .tree-children > .tree-node > .tree-row[data-shell-mode="page"]') : [];
void dispatchTreeCommand(trigger || row, {
action: 'move',
workspaceId: detail.workspaceId || resolveWorkspaceId(row),
documentId: documentId,
parentId: previousId,
sortOrder: children.length
});
}
function fileTreeCopyPath(detail, trigger) {
// 对 local_folder 复制真实相对路径,不复制标题
if (currentSourceKind() === 'local_folder') {
if (detail.assetId) {
var path = localFilePathFromAssetId(detail.assetId);
if (path) {
try {
return decodeURIComponent(path.replace(/~2F/g, '/'));
} catch (_) {
return path.replace(/~2F/g, '/');
}
}
}
if (detail.documentId) {
var docPath = String(detail.documentId || '')
.replace(/^local-md:/, '')
.replace(/^local-dir:/, '')
.replace(/~2F/g, '/');
if (docPath) {
try {
return decodeURIComponent(docPath);
} catch (_) {
return docPath;
}
}
}
}
return detail.title || '';
}
function fileTreeMenuTargetParentId(detail, trigger) {
var rowKind = String(detail && detail.rowKind || '').trim();
var rowId = String(detail && detail.rowId || '').trim();
var documentId = String(detail && detail.documentId || '').trim();
if (rowKind === 'folder' && rowId) return rowId;
if (rowKind === 'directory' && rowId) return rowId;
if (documentId) return documentId;
if (trigger && typeof trigger.getAttribute === 'function') {
var triggerKind = String(trigger.getAttribute('data-row-kind') || '').trim();
var triggerRowId = String(trigger.getAttribute('data-row-id') || '').trim();
if ((triggerKind === 'folder' || triggerKind === 'directory') && triggerRowId) return triggerRowId;
}
return '';
}
function withOfficeEditModeGuard(callback) {
document.documentElement.setAttribute('data-mnote-last-office-edit-mode-requested', 'true');
document.documentElement.setAttribute('data-mnote-last-office-edit-mode-guard', 'silent');
callback();
}
function handleTreeContextMenuAction(action, detail, trigger) {
closeTreeContextMenu();
detail = detail || {};
if (detail.contextKind === 'attachment') {
if (action === 'copy-link') {
void copyTreeContextValue(detail.href || '', 'attachment-copy-link');
return;
}
if (action === 'download') {
openEditorAttachmentDownload(detail);
return;
}
if (action === 'popup-preview') {
openEditorAttachmentDetail(detail);
return;
}
if (action === 'new-window') {
openEditorAttachmentNewWindow(detail);
return;
}
if (action === 'new-window-edit') {
withOfficeEditModeGuard(function() {
openEditorAttachmentNewWindow(detail, 'edit');
});
return;
}
if (action === 'open-edit-mode') {
withOfficeEditModeGuard(function() {
void openEditorAttachmentEditTab(detail);
});
return;
}
if (action === 'right-preview') {
dispatchSidebarEvent('tree.attachment.open-right', detail);
return;
}
if (action === 'copy-id') {
void copyTreeContextValue(detail.assetId || '', 'attachment-copy-id');
return;
}
dispatchSidebarEvent('tree.attachment.action', { action: action, attachment: detail });
return;
}
var documentId = detail.documentId || '';
var workspaceId = detail.workspaceId || resolveWorkspaceId(trigger || document.body);
var title = detail.title || '无标题';
var isAsset = detail.contextKind === 'filetree' && detail.assetId && detail.rowKind !== 'document' && detail.rowKind !== 'index';
if (detail.contextKind === 'filetree' && action === 'download') {
downloadSelectedFileTreeAssetRows(detail, trigger);
return;
}
if (isAsset && action === 'new-window') {
recordFileTreeAction('new-window', detail);
void openConvexAssetFromFileTree({ ...detail, openTarget: 'new-window' });
return;
}
if (isAsset && action === 'open-edit-mode') {
withOfficeEditModeGuard(function() {
recordFileTreeAction('open-edit-mode', detail);
void openConvexAssetFromFileTree({ ...detail, openTarget: 'edit-mode' });
});
return;
}
if (isAsset && action === 'open-right') {
recordFileTreeAction('open-right', detail);
void openConvexAssetFromFileTree({ ...detail, openTarget: 'side' });
return;
}
if (action === 'open-right') {
recordFileTreeAction('open-right', detail);
dispatchSidebarEvent('tree.page.open-right', detail);
return;
}
if (action === 'share') {
dispatchSidebarEvent('tree.page.share', detail);
return;
}
if (action === 'copy-link') {
void copyTreeContextValue(documentHref(documentId, workspaceId), 'copy-link');
return;
}
if (action === 'copy-link-title') {
void copyTreeContextValue(title + ' ' + documentHref(documentId, workspaceId), 'copy-link-title');
return;
}
if (action === 'copy-reference-inline') {
void copyTreeContextValue('((' + title + ' ' + documentId + '))', 'copy-reference-inline');
return;
}
if (action === 'copy-reference-embed') {
void copyTreeContextValue('{{' + title + ' ' + documentId + '}}', 'copy-reference-embed');
return;
}
if (action === 'copy-id') {
void copyTreeContextValue(documentId || detail.assetId || detail.rowId || '', 'copy-id');
return;
}
if (action === 'delete-trash' && detail.contextKind === 'filetree') {
var selectedFileTreeRows = selectedSidebarFileTreeRows();
if (selectedFileTreeRows.length > 1) {
void deleteSelectedSidebarFileTreeRows(trigger || document.body);
return;
}
}
if (action === 'delete-trash' && isAsset) {
if (!window.confirm('确定要将“' + title + '”删除到垃圾桶吗?')) return;
recordFileTreeAction('delete-trash', detail);
recordFileTreeActionStatus('pending', detail);
void deleteSingleFileTreeAsset(detail, trigger).then(function() {
recordFileTreeActionStatus('archived', Object.assign({}, detail, { undo: 'trash-modal' }));
if (currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot();
}).catch(function(error) {
recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' }));
window.alert(error && error.message ? error.message : '资源删除失败');
});
return;
}
if (action === 'new-file') {
void createPage(trigger || document.body, fileTreeMenuTargetParentId(detail, trigger) || null);
return;
}
if (action === 'new-folder') {
recordFileTreeAction('new-folder', detail);
void createFileTreeFolder(trigger || document.body, fileTreeMenuTargetParentId(detail, trigger) || null).then(function(ok) {
recordFileTreeActionStatus(ok ? 'created' : 'skipped', detail);
}).catch(function(error) {
recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' }));
window.alert(error && error.message ? error.message : '新建文件夹失败');
});
return;
}
if (action === 'paste-into') {
var pasteRow = trigger && trigger.closest ? trigger.closest('.tree-row[data-shell-mode="filetree"]') : trigger;
recordFileTreeAction('paste-into', detail);
void pasteSidebarFileTreeClipboard(pasteRow).then(function(ok) {
recordFileTreeActionStatus(ok ? 'applied' : 'skipped', detail);
}).catch(function(error) {
recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' }));
window.alert(error && error.message ? error.message : '粘贴失败');
});
return;
}
if (action === 'copy-path') {
void copyTreeContextValue(fileTreeCopyPath(detail, trigger), 'copy-path');
return;
}
if (action === 'refresh') {
document.documentElement.setAttribute('data-mnote-filetree-refresh-requested', 'true');
dispatchSidebarEvent('tree.filetree.refresh', detail);
return;
}
if (action === 'collapse-all') {
document.querySelectorAll('#sidebar-file-tree-root .tree-row[aria-expanded="true"]').forEach(function(row) {
if (!(row instanceof HTMLElement)) return;
row.setAttribute('aria-expanded', 'false');
var node = row.closest('.tree-node');
var children = node ? node.querySelector(':scope > .tree-children') : null;
if (children) children.classList.add('tree-children--collapsed');
});
return;
}
if (action === 'reveal') {
if (trigger && typeof trigger.scrollIntoView === 'function') {
trigger.scrollIntoView({ block: 'nearest' });
trigger.focus && trigger.focus();
}
return;
}
if (action === 'duplicate') {
dispatchSidebarEvent('tree.page.duplicate', detail);
return;
}
if (action === 'rename') {
if (detail.contextKind === 'filetree' && trigger) {
var renameRow = trigger.closest ? trigger.closest('.tree-row[data-shell-mode="filetree"]') : trigger;
if (beginFileTreeInlineRename(renameRow)) return;
}
var nextTitle = window.prompt('重命名页面', title);
if (nextTitle && nextTitle.trim() && documentId) {
void dispatchTreeCommand(trigger || document.body, {
action: 'rename',
workspaceId: workspaceId,
documentId: documentId,
title: nextTitle.trim()
}).then(function(){ updateTitleEverywhere(documentId, nextTitle.trim()); });
}
return;
}
if (action === 'create-child') {
void createPage(trigger || document.body, documentId);
return;
}
if (action === 'convert-child') {
convertToPreviousSiblingChild(trigger, detail);
return;
}
var deleteTargetId = documentId || String(detail.rowId || '').trim();
if (action === 'delete-trash' && deleteTargetId) {
if (!window.confirm('确定要将“' + title + '”删除到垃圾桶吗?')) return;
recordFileTreeAction('delete-trash', detail);
recordFileTreeActionStatus('pending', detail);
void dispatchTreeCommand(trigger || document.body, {
action: 'archive',
workspaceId: workspaceId,
documentId: deleteTargetId
}).then(function() {
recordFileTreeActionStatus('archived', Object.assign({}, detail, { undo: 'trash-modal' }));
if (currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot();
}).catch(function(error) {
recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' }));
window.alert(error && error.message ? error.message : '删除失败');
});
}
}
function appendTreeContextMenuButton(menu, item, detail, trigger) {
if (item.separator) {
var sep = document.createElement('div');
sep.className = 'mnote-tree-context-menu__separator';
sep.setAttribute('role', 'separator');
menu.appendChild(sep);
return;
}
var button = document.createElement('button');
button.type = 'button';
button.className = item.danger ? 'mnote-tree-context-menu__item mnote-tree-context-menu__item--danger' : 'mnote-tree-context-menu__item';
button.setAttribute('role', 'menuitem');
button.setAttribute('data-action', item.action);
button.disabled = item.disabled === true;
if (item.title) button.title = item.title;
var icon = document.createElement('span');
icon.className = 'material-symbols-outlined mnote-tree-context-menu__icon';
icon.setAttribute('aria-hidden', 'true');
icon.setAttribute('data-icon', item.icon || 'radio_button_unchecked');
var label = document.createElement('span');
label.className = 'mnote-tree-context-menu__label';
label.textContent = item.label;
button.appendChild(icon);
button.appendChild(label);
if (item.shortcut) {
var shortcut = document.createElement('span');
shortcut.className = 'mnote-tree-context-menu__shortcut';
shortcut.textContent = item.shortcut;
button.appendChild(shortcut);
}
button.addEventListener('click', function(event) {
event.preventDefault();
event.stopPropagation();
handleTreeContextMenuAction(item.action, detail, trigger);
});
menu.appendChild(button);
}
// ── CommandContext 启用态辅助:与 Rust core-protocol CommandContext 保持同一口径 ──
function buildSidebarFileTreeContext(kind) {
var runtime = window.__mnoteFileTreeContextMenuRuntime;
if (runtime && typeof runtime.buildSidebarFileTreeContext === 'function') {
try {
return runtime.buildSidebarFileTreeContext(kind, {
selection: sidebarFileTreeSelection,
currentSourceKind: currentSourceKind,
workspaceReadonly: function() {
return document.documentElement.getAttribute('data-mnote-workspace-readonly') === 'true';
},
queryRowById: function(rowId) {
return document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(rowId) + '"]');
}
});
} catch (_) {}
}
var s = sidebarFileTreeSelection;
var rowIds = Array.from(s.selectedRowIds || []);
var rows = [];
for (var i = 0; i < rowIds.length; i += 1) {
var row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(rowIds[i]) + '"]');
if (row instanceof HTMLElement) rows.push(row);
}
var resourceKinds = {};
for (var i = 0; i < rows.length; i += 1) {
var rk = rows[i].getAttribute('data-row-kind') || 'unknown';
resourceKinds[rk] = true;
}
var rkKeys = Object.keys(resourceKinds);
return {
'workspace.sourceKind': currentSourceKind() || '',
'workspace.readonly': document.documentElement.getAttribute('data-mnote-workspace-readonly') === 'true',
'tree.focusKind': kind === 'filetree' ? 'file_tree' : kind === 'page' ? 'page_tree' : kind,
'tree.selectionCount': rows.length,
'tree.selectionResourceKind': rkKeys.length === 1 ? rkKeys[0] : 'mixed'
};
}
function evaluateSidebarFileTreeWhen(ctx, expr) {
var runtime = window.__mnoteFileTreeContextMenuRuntime;
if (runtime && typeof runtime.evaluateSidebarFileTreeWhen === 'function') {
return runtime.evaluateSidebarFileTreeWhen(ctx || {}, expr || '');
}
if (!expr) return true;
try {
expr = expr.trim();
// 词法切分:支持 key、!key、==、!=、&&、|| 和括号分组
var tokens = [];
var i = 0;
while (i < expr.length) {
var ch = expr[i];
if (ch === ' ' || ch === '\t') { i++; continue; }
if (ch === '(') { tokens.push({ t: '(' }); i++; continue; }
if (ch === ')') { tokens.push({ t: ')' }); i++; continue; }
if (ch === '&' && expr[i+1] === '&') { tokens.push({ t: '&&' }); i += 2; continue; }
if (ch === '|' && expr[i+1] === '|') { tokens.push({ t: '||' }); i += 2; continue; }
if (ch === '=' && expr[i+1] === '=') { tokens.push({ t: '==' }); i += 2; continue; }
if (ch === '!' && expr[i+1] === '=') { tokens.push({ t: '!=' }); i += 2; continue; }
if (ch === '!') { tokens.push({ t: '!' }); i++; continue; }
if (ch === '"' || ch === "'") {
i++;
var str = '';
while (i < expr.length && expr[i] !== ch) { str += expr[i]; i++; }
if (i < expr.length) i++;
tokens.push({ t: 'str', v: str });
continue;
}
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '.' || (ch >= '0' && ch <= '9')) {
var id = '';
while (i < expr.length && ((expr[i] >= 'a' && expr[i] <= 'z') || (expr[i] >= 'A' && expr[i] <= 'Z') || (expr[i] >= '0' && expr[i] <= '9') || expr[i] === '_' || expr[i] === '.')) { id += expr[i]; i++; }
tokens.push({ t: 'id', v: id });
continue;
}
throw new Error('Unexpected char: ' + ch);
}
var pos = 0;
function peek() { return tokens[pos]; }
function consume() { return tokens[pos++]; }
function expect(t) {
var tok = consume();
if (!tok || tok.t !== t) throw new Error('Expected ' + t + ' got ' + (tok ? tok.t : 'EOF'));
return tok;
}
function ctxVal(key) {
var v = ctx[key];
if (v === undefined || v === null) return '';
return v;
}
function parseOr() {
var left = parseAnd();
while (peek() && peek().t === '||') { consume(); var right = parseAnd(); left = left || right; }
return left;
}
function parseAnd() {
var left = parsePrimary();
while (peek() && peek().t === '&&') { consume(); var right = parsePrimary(); left = left && right; }
return left;
}
function parsePrimary() {
if (!peek()) throw new Error('Unexpected EOF');
if (peek().t === '(') {
consume();
var val = parseOr();
expect(')');
return val;
}
if (peek().t === '!') {
consume();
var keyTok = consume();
if (!keyTok || keyTok.t !== 'id') throw new Error('Expected key after !');
return !ctxVal(keyTok.v);
}
if (peek().t === 'id') {
var keyTok = consume();
if (peek() && peek().t === '==') {
consume();
var valTok = consume();
if (!valTok) throw new Error('Expected value after ==');
var val = valTok.t === 'str' ? valTok.v : (valTok.v || '');
// 与 Rust evaluate_when 保持一致:按 string / bool / number 比较。
var cv = ctxVal(keyTok.v);
if (val === 'true') return cv === true || cv === 'true';
if (val === 'false') return cv === false || cv === '' || cv === 'false';
var num = Number(val);
if (!isNaN(num) && String(num) === val) return Number(cv) === num;
return String(cv) === val;
}
if (peek() && peek().t === '!=') {
consume();
var valTok = consume();
if (!valTok) throw new Error('Expected value after !=');
var val = valTok.t === 'str' ? valTok.v : (valTok.v || '');
var cv = ctxVal(keyTok.v);
if (val === 'true') return !(cv === true || cv === 'true');
if (val === 'false') return !(cv === false || cv === '' || cv === 'false');
var num = Number(val);
if (!isNaN(num) && String(num) === val) return Number(cv) !== num;
return String(cv) !== val;
}
// 单独 key:按 truthy 规则判断。
var cv = ctxVal(keyTok.v);
if (typeof cv === 'boolean') return cv;
if (typeof cv === 'number') return cv !== 0;
return cv !== '' && cv !== undefined && cv !== null;
}
throw new Error('Unexpected token: ' + peek().t);
}
return parseOr();
} catch(e) {
return true;
}
}
function openTreeContextMenu(kind, detail, x, y, trigger) {
closeTreeContextMenu();
detail = Object.assign({}, detail || {}, { contextKind: kind });
var menu = document.createElement('div');
menu.className = 'mnote-tree-context-menu';
menu.setAttribute('role', 'menu');
menu.setAttribute('data-testid', 'mnote-tree-context-menu');
menu.setAttribute('data-kind', kind);
var isAttachment = kind === 'attachment';
var isAsset = kind === 'filetree' && detail.assetId && detail.rowKind !== 'document' && detail.rowKind !== 'index';
var isFileTreeDownload = kind === 'filetree' && detail.downloadable;
var ctx = buildSidebarFileTreeContext(kind);
var items = isAttachment ? [
{ action: 'duplicate', icon: 'file_copy', label: '拷贝副本', shortcut: 'Ctrl + D' },
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, when: '!workspace.readonly' },
{ separator: true },
{ action: 'copy-link', icon: 'link', label: '复制链接' },
{ action: 'move-embed', icon: 'subdirectory_arrow_right', label: '移动/嵌入到...', shortcut: 'Alt+Shift+M/G' },
{ action: 'history', icon: 'history', label: '块历史...' },
{ separator: true },
{ action: 'popup-preview', icon: 'preview', label: '弹窗预览' },
{ action: 'right-preview', icon: 'right_panel_open', label: '右侧预览' },
{ action: 'open-edit-mode', icon: 'edit_note', label: '弹窗编辑' },
{ action: 'new-window-edit', icon: 'open_in_new', label: '新窗口编辑' },
{ action: 'new-window', icon: 'open_in_new', label: '在新窗口打开' },
{ action: 'download', icon: 'download', label: '下载' },
{ action: 'replace-file', icon: 'sync', label: '更换文件' },
{ action: 'rename-attachment', icon: 'drive_file_rename_outline', label: '重命名' },
{ action: 'comment', icon: 'mode_comment', label: '评论', shortcut: 'Ctrl+Alt+M' },
{ action: 'caption', icon: 'notes', label: '添加说明文字' },
{ separator: true },
{ action: 'color', icon: 'format_paint', label: '颜色' }
] : isAsset ? [
{ action: 'open-edit-mode', icon: 'edit_note', label: '使用编辑模式打开' },
{ action: 'new-window', icon: 'open_in_new', label: '在新窗口打开' },
{ action: 'download', icon: 'download', label: Number(detail.selectedDownloadCount || detail.selectedAssetCount || 0) > 1 ? '下载 ' + Number(detail.selectedDownloadCount || detail.selectedAssetCount || 0) + ' 个项目' : '下载' },
{ action: 'rename', icon: 'edit', label: '重命名', shortcut: 'F2', when: '!workspace.readonly' },
{ action: 'copy-id', icon: 'tag', label: '复制资源 ID' },
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, when: '!workspace.readonly' },
{ separator: true },
{ action: 'copy-path', icon: 'content_copy', label: 'Copy Path' },
{ action: 'refresh', icon: 'refresh', label: 'Refresh' },
{ action: 'reveal', icon: 'my_location', label: 'Reveal' }
] : kind === 'filetree' ? [
{ action: 'open-right', icon: 'right_panel_open', label: '在右侧边栏打开', shortcut: 'Alt+' },
{ action: 'download', icon: 'download', label: Number(detail.selectedDownloadCount || 0) > 1 ? '下载 ' + Number(detail.selectedDownloadCount || 0) + ' 个项目' : '下载', disabled: !isFileTreeDownload, title: isFileTreeDownload ? '下载当前本地文件或文件夹' : '当前项目没有可下载的本地路径' },
{ separator: true },
{ action: 'copy-link', icon: 'link', label: '复制访问链接' },
{ action: 'copy-reference-inline', icon: 'content_copy', label: '复制页面引用链接' },
{ action: 'copy-id', icon: 'tag', label: '复制页面ID' },
{ action: 'copy-path', icon: 'content_copy', label: 'Copy Path' },
{ separator: true },
{ action: 'new-file', icon: 'note_add', label: 'New File', when: '!workspace.readonly' },
{ action: 'new-folder', icon: 'create_new_folder', label: 'New Folder', disabled: currentSourceKind() !== 'local_folder', title: currentSourceKind() === 'local_folder' ? '在当前目录下创建子文件夹' : '仅 local folder 支持创建文件夹', when: '!workspace.readonly' },
{ action: 'paste-into', icon: 'content_paste', label: '粘贴到此处', disabled: !runtimeState.sidebarFileTreeClipboard, title: runtimeState.sidebarFileTreeClipboard ? '粘贴到当前文件树目标' : '剪贴板为空', when: '!workspace.readonly' },
{ action: 'refresh', icon: 'refresh', label: 'Refresh' },
{ action: 'collapse-all', icon: 'unfold_less', label: 'Collapse All' },
{ action: 'reveal', icon: 'my_location', label: 'Reveal' },
{ separator: true },
{ action: 'rename', icon: 'edit', label: '重命名', shortcut: 'F2', when: '!workspace.readonly' },
{ separator: true },
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, when: '!workspace.readonly' }
] : [
{ action: 'open-right', icon: 'right_panel_open', label: '在右侧边栏打开', shortcut: 'Alt+' },
{ separator: true },
{ action: 'copy-link', icon: 'link', label: '复制访问链接' },
{ action: 'copy-reference-inline', icon: 'content_copy', label: '复制页面引用链接' },
{ action: 'copy-id', icon: 'tag', label: '复制页面ID' },
{ separator: true },
{ action: 'rename', icon: 'edit', label: '重命名', when: '!workspace.readonly' },
{ separator: true },
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, when: '!workspace.readonly' }
];
items.forEach(function(item) {
if (item.when !== undefined && !evaluateSidebarFileTreeWhen(ctx, item.when)) {
item = Object.assign({}, item, { disabled: true, title: item.title || '当前上下文不支持此操作' });
}
appendTreeContextMenuButton(menu, item, detail, trigger);
});
document.body.appendChild(menu);
var rect = menu.getBoundingClientRect();
var left = Math.min(Math.max(8, x || 8), Math.max(8, window.innerWidth - rect.width - 8));
var top = Math.min(Math.max(8, y || 8), Math.max(8, window.innerHeight - rect.height - 8));
menu.style.left = left + 'px';
menu.style.top = top + 'px';
runtimeState.activeTreeContextMenu = menu;
}
function openPageTreeContextMenu(row, x, y, trigger) {
if (!(row instanceof HTMLElement)) return;
var documentId = row.getAttribute('data-node-id') || '';
openTreeContextMenu('page', {
documentId: documentId,
rowId: documentId,
rowKind: 'document',
title: rowTitle(row),
workspaceId: resolveWorkspaceId(row)
}, x, y, trigger || row);
}
function openFileTreeContextMenu(row, x, y, trigger) {
if (!(row instanceof HTMLElement)) return;
var documentId = row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || '';
var selectedDownloadRows = selectedSidebarFileTreeRowsForDownload(row);
var selectedAssetRows = selectedDownloadRows.filter(isFileTreeDownloadableAssetRow);
openTreeContextMenu('filetree', {
documentId: documentId,
rowId: row.getAttribute('data-row-id') || '',
rowKind: row.getAttribute('data-row-kind') || '',
assetId: row.getAttribute('data-asset-id') || '',
localRelativePath: fileTreeRowLocalRelativePath(row),
title: rowTitle(row),
workspaceId: resolveWorkspaceId(row),
downloadable: isFileTreeDownloadableRow(row),
selectedDownloadCount: selectedDownloadRows.length,
selectedDownloadRowIds: selectedDownloadRows.map(function(downloadRow) { return downloadRow.getAttribute('data-row-id') || ''; }).filter(Boolean),
selectedAssetCount: selectedAssetRows.length,
selectedAssetRowIds: selectedAssetRows.map(function(assetRow) { return assetRow.getAttribute('data-row-id') || ''; }).filter(Boolean)
}, x, y, trigger || row);
}
function visibleFileTreeRows() {
var runtimeFn = fileTreeSelectionRuntimeFunction('visibleFileTreeRows');
if (runtimeFn) return runtimeFn(fileTreeSelectionRuntimeDeps());
return Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]'))
.filter(function(row) {
if (!(row instanceof HTMLElement)) return false;
if (row.closest('.tree-children--collapsed')) return false;
return row.offsetParent !== null || row.getClientRects().length > 0;
});
}
function syncSidebarFileTreeSelection() {
var runtimeFn = fileTreeSelectionRuntimeFunction('syncSidebarFileTreeSelection');
if (runtimeFn) {
runtimeFn(sidebarFileTreeSelection, fileTreeSelectionRuntimeDeps());
return;
}
var rows = visibleFileTreeRows();
rows.forEach(function(row) {
var rowId = row.getAttribute('data-row-id') || '';
row.setAttribute('data-selected', String(Boolean(rowId && sidebarFileTreeSelection.selectedRowIds.has(rowId))));
row.setAttribute('data-focused', String(rowId === sidebarFileTreeSelection.focusedRowId));
});
window.dispatchEvent(new CustomEvent('tree.filetree.selection.changed', {
detail: {
selectedRowIds: Array.from(sidebarFileTreeSelection.selectedRowIds),
anchorRowId: sidebarFileTreeSelection.anchorRowId,
focusedRowId: sidebarFileTreeSelection.focusedRowId
}
}));
}
function selectSidebarFileTreeRow(row, modifiers) {
var runtimeFn = fileTreeSelectionRuntimeFunction('selectSidebarFileTreeRow');
if (runtimeFn) return runtimeFn(row, sidebarFileTreeSelection, modifiers, fileTreeSelectionRuntimeDeps());
if (!(row instanceof HTMLElement)) return [];
var rowId = row.getAttribute('data-row-id') || '';
if (!rowId) return [];
var rows = visibleFileTreeRows();
var visibleRowIds = rows.map(function(item) { return item.getAttribute('data-row-id') || ''; }).filter(Boolean);
var selected = new Set(sidebarFileTreeSelection.selectedRowIds);
var shiftKey = Boolean(modifiers && modifiers.shiftKey);
var ctrlKey = Boolean(modifiers && (modifiers.ctrlKey || modifiers.metaKey));
if (shiftKey && sidebarFileTreeSelection.anchorRowId) {
var anchorIndex = visibleRowIds.indexOf(sidebarFileTreeSelection.anchorRowId);
var targetIndex = visibleRowIds.indexOf(rowId);
if (anchorIndex >= 0 && targetIndex >= 0) {
selected = new Set(visibleRowIds.slice(Math.min(anchorIndex, targetIndex), Math.max(anchorIndex, targetIndex) + 1));
} else {
selected = new Set([rowId]);
sidebarFileTreeSelection.anchorRowId = rowId;
}
} else if (ctrlKey) {
if (selected.has(rowId) && selected.size > 1) selected.delete(rowId);
else selected.add(rowId);
sidebarFileTreeSelection.anchorRowId = rowId;
} else {
selected = new Set([rowId]);
sidebarFileTreeSelection.anchorRowId = rowId;
}
sidebarFileTreeSelection.selectedRowIds = selected;
sidebarFileTreeSelection.focusedRowId = rowId;
syncSidebarFileTreeSelection();
return Array.from(selected);
}
function selectSidebarFileTreeDocument(documentId, options) {
var id = String(documentId || '').trim();
if (!id) return false;
var row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + id) + '"]')
|| document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-document-id="' + cssEscape(id) + '"]')
|| document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-doc-id="' + cssEscape(id) + '"]');
if (!(row instanceof HTMLElement)) return false;
var rowId = row.getAttribute('data-row-id') || '';
if (!rowId) return false;
selectSidebarFileTreeRow(row, { ctrlKey: false, metaKey: false, shiftKey: false });
document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-active="true"]').forEach(function(activeRow) {
if (activeRow instanceof HTMLElement) activeRow.setAttribute('data-active', 'false');
});
row.setAttribute('data-active', 'true');
if (options && options.scrollIntoView !== false) {
try {
row.scrollIntoView({ block: 'nearest' });
} catch (_) {
row.scrollIntoView();
}
}
return true;
}
function selectSidebarFileTreeRowById(rowId, options) {
var id = String(rowId || '').trim();
if (!id) return false;
var row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(id) + '"]');
if (!(row instanceof HTMLElement)) return false;
selectSidebarFileTreeRow(row, { ctrlKey: false, metaKey: false, shiftKey: false });
document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-active="true"]').forEach(function(activeRow) {
if (activeRow instanceof HTMLElement) activeRow.setAttribute('data-active', 'false');
});
row.setAttribute('data-active', 'true');
if (options && options.scrollIntoView !== false) {
try {
row.scrollIntoView({ block: 'nearest' });
} catch (_) {
row.scrollIntoView();
}
}
return true;
}
function pendingLocalFolderRestoreRowId() {
if (window.__mnotePendingLocalFolderRestoreFiletreeRowId) {
return String(window.__mnotePendingLocalFolderRestoreFiletreeRowId || '').trim();
}
if (!window.localStorage) return '';
var rowId = '';
try {
rowId = String(window.localStorage.getItem('mnote.pendingLocalFolderRestoreFiletreeRowId') || '').trim();
} catch (_) {
rowId = '';
}
if (!rowId && typeof window.name === 'string' && window.name.indexOf('mnote.pendingLocalFolderRestoreFiletreeRowId=') === 0) {
rowId = decodeURIComponent(window.name.slice('mnote.pendingLocalFolderRestoreFiletreeRowId='.length));
}
if (rowId) window.__mnotePendingLocalFolderRestoreFiletreeRowId = rowId;
return rowId;
}
function clearPendingLocalFolderRestoreRowId() {
window.__mnotePendingLocalFolderRestoreFiletreeRowId = '';
try {
window.localStorage.removeItem('mnote.pendingLocalFolderRestoreFiletreeRowId');
} catch (_) {}
if (typeof window.name === 'string' && window.name.indexOf('mnote.pendingLocalFolderRestoreFiletreeRowId=') === 0) {
window.name = '';
}
}
function applyPendingLocalFolderRestoreFocusOnce() {
var rowId = pendingLocalFolderRestoreRowId();
if (!rowId) return false;
document.documentElement.setAttribute('data-mnote-local-folder-restore-pending-row-id', rowId);
if (!selectSidebarFileTreeRowById(rowId, { scrollIntoView: true })) {
document.documentElement.setAttribute('data-mnote-local-folder-restore-focus-status', 'row-missing');
return false;
}
document.documentElement.setAttribute('data-mnote-local-folder-restore-focused-row-id', rowId);
document.documentElement.setAttribute('data-mnote-local-folder-restore-focus-status', 'focused');
return true;
}
function schedulePendingLocalFolderRestoreFocus() {
if (!pendingLocalFolderRestoreRowId()) return false;
if (window.__mnotePendingLocalFolderRestoreFocusTimer) return false;
var attempts = 0;
var focused = false;
focused = applyPendingLocalFolderRestoreFocusOnce() || focused;
window.__mnotePendingLocalFolderRestoreFocusTimer = window.setInterval(function() {
attempts += 1;
focused = applyPendingLocalFolderRestoreFocusOnce() || focused;
if (attempts >= 20) {
if (focused) clearPendingLocalFolderRestoreRowId();
else document.documentElement.setAttribute('data-mnote-local-folder-restore-focus-status', 'timeout');
window.clearInterval(window.__mnotePendingLocalFolderRestoreFocusTimer);
window.__mnotePendingLocalFolderRestoreFocusTimer = 0;
}
}, 250);
return focused;
}
function selectedSidebarFileTreeRowIdsForDrag(row) {
var runtimeFn = fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRowIdsForDrag');
if (runtimeFn) return runtimeFn(row, sidebarFileTreeSelection);
var rowId = row instanceof HTMLElement ? row.getAttribute('data-row-id') || '' : '';
if (rowId && sidebarFileTreeSelection.selectedRowIds.has(rowId)) {
return Array.from(sidebarFileTreeSelection.selectedRowIds);
}
return rowId ? [rowId] : [];
}
function selectedSidebarFileTreeRows() {
var runtimeFn = fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRows');
if (runtimeFn) return runtimeFn(sidebarFileTreeSelection, fileTreeSelectionRuntimeDeps());
var selectedIds = sidebarFileTreeSelection.selectedRowIds;
var rows = visibleFileTreeRows().filter(function(row) {
var rowId = row.getAttribute('data-row-id') || '';
return rowId && selectedIds.has(rowId);
});
if (rows.length > 0) return rows;
if (sidebarFileTreeSelection.focusedRowId) {
var focused = document.querySelector(
'#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(sidebarFileTreeSelection.focusedRowId) + '"]'
);
if (focused instanceof HTMLElement) return [focused];
}
return [];
}
function fileTreeRowDocumentId(row) {
var runtimeFn = fileTreeRuntimeFunction('fileTreeRowDocumentId');
if (runtimeFn) return runtimeFn(row);
if (!(row instanceof HTMLElement)) return '';
return String(row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || '').trim();
}
function fileTreeRowAssetId(row) {
var runtimeFn = fileTreeRuntimeFunction('fileTreeRowAssetId');
if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps());
if (!(row instanceof HTMLElement)) return '';
var assetId = String(row.getAttribute('data-asset-id') || '').trim();
if (assetId) return assetId;
if (currentSourceKind() === 'local_folder' && fileTreeRowKind(row) === 'asset') {
return String(row.getAttribute('data-row-id') || '').trim();
}
return '';
}
function decodeLocalEncodedPath(value) {
var runtimeFn = fileTreeRuntimeFunction('decodeLocalEncodedPath');
if (runtimeFn) return runtimeFn(value);
var path = String(value || '').trim().replace(/~2F/g, '/');
if (!path) return '';
try {
return decodeURIComponent(path);
} catch (_) {
return path;
}
}
function fileTreeRowLocalRelativePath(row) {
var runtimeFn = fileTreeRuntimeFunction('fileTreeRowLocalRelativePath');
if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps());
if (!(row instanceof HTMLElement)) return '';
var direct = String(row.getAttribute('data-local-relative-path') || '').trim();
if (direct) return direct;
var assetPath = localFilePathFromAssetId(fileTreeRowAssetId(row));
if (assetPath) return assetPath;
var rowId = String(row.getAttribute('data-row-id') || '').trim();
var nodeId = String(row.getAttribute('data-node-id') || '').trim();
var documentId = fileTreeRowDocumentId(row);
var kind = fileTreeRowKind(row);
if (rowId.indexOf('local:asset:') === 0) return rowId.slice('local:asset:'.length);
if (rowId.indexOf('local:markdown:') === 0) return rowId.slice('local:markdown:'.length);
if (rowId.indexOf('local:folder:') === 0) return rowId.slice('local:folder:'.length);
if (rowId.indexOf('local:node:') === 0) return rowId.slice('local:node:'.length);
if (nodeId.indexOf('local:node:') === 0) return nodeId.slice('local:node:'.length);
if ((kind === 'document' || kind === 'doc' || kind === 'markdown') && documentId.indexOf('local-md:') === 0) {
return decodeLocalEncodedPath(documentId.slice('local-md:'.length));
}
if ((kind === 'folder' || kind === 'directory' || kind === 'index') && documentId.indexOf('local-dir:') === 0) {
return decodeLocalEncodedPath(documentId.slice('local-dir:'.length));
}
return '';
}
function fileTreeRowLocalUploadTargetRelativePath(row) {
var runtimeFn = fileTreeRuntimeFunction('fileTreeRowLocalUploadTargetRelativePath');
if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps());
if (!(row instanceof HTMLElement)) return '';
var kind = fileTreeRowKind(row);
var relativePath = fileTreeRowLocalRelativePath(row);
if (!relativePath) return '';
if (kind === 'folder' || kind === 'directory') return relativePath;
var lastSlash = relativePath.lastIndexOf('/');
return lastSlash >= 0 ? relativePath.slice(0, lastSlash) : '';
}
function fileTreeRowKind(row) {
var runtimeFn = fileTreeRuntimeFunction('fileTreeRowKind');
if (runtimeFn) return runtimeFn(row);
if (!(row instanceof HTMLElement)) return '';
return String(row.getAttribute('data-row-kind') || '').trim();
}
function isFileTreeDownloadableAssetRow(row) {
var runtimeFn = fileTreeRuntimeFunction('isFileTreeDownloadableAssetRow');
if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps());
if (!(row instanceof HTMLElement)) return false;
var kind = fileTreeRowKind(row);
if (kind === 'document' || kind === 'doc' || kind === 'index' || kind === 'markdown' || kind === 'folder' || kind === 'directory') return false;
return Boolean(fileTreeRowAssetId(row));
}
function isFileTreeDownloadableRow(row) {
var runtimeFn = fileTreeRuntimeFunction('isFileTreeDownloadableRow');
if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps());
if (!(row instanceof HTMLElement)) return false;
if (currentSourceKind() !== 'local_folder') return isFileTreeDownloadableAssetRow(row);
return Boolean(fileTreeRowLocalRelativePath(row) || isFileTreeDownloadableAssetRow(row));
}
function fileTreeAssetDownloadDetail(row) {
var runtimeFn = fileTreeRuntimeFunction('fileTreeAssetDownloadDetail');
if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps());
if (!isFileTreeDownloadableRow(row)) return null;
var title = rowTitle(row);
var relativePath = fileTreeRowLocalRelativePath(row);
var assetId = fileTreeRowAssetId(row) || (relativePath ? 'local-file:' + relativePath : '');
return {
contextKind: 'filetree',
documentId: fileTreeRowDocumentId(row),
rowId: row.getAttribute('data-row-id') || '',
rowKind: fileTreeRowKind(row),
assetId: assetId,
localRelativePath: relativePath,
title: title,
fileName: title,
workspaceId: resolveWorkspaceId(row)
};
}
function selectedSidebarFileTreeRowsForDownload(contextRow) {
var contextRowId = contextRow instanceof HTMLElement ? contextRow.getAttribute('data-row-id') || '' : '';
var selectedRows = selectedSidebarFileTreeRows().filter(isFileTreeDownloadableRow);
if (contextRowId && sidebarFileTreeSelection.selectedRowIds.has(contextRowId) && selectedRows.length > 0) return selectedRows;
return isFileTreeDownloadableRow(contextRow) ? [contextRow] : selectedRows;
}
function selectedSidebarFileTreeAssetRowsForDownload(contextRow) {
return selectedSidebarFileTreeRowsForDownload(contextRow).filter(isFileTreeDownloadableAssetRow);
}
function downloadSelectedFileTreeAssetRows(detail, trigger) {
var contextRow = trigger && trigger.closest ? trigger.closest('.tree-row[data-shell-mode="filetree"]') : trigger;
if (!(contextRow instanceof HTMLElement) && detail && detail.rowId) {
contextRow = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(detail.rowId) + '"]');
}
var rows = selectedSidebarFileTreeRowsForDownload(contextRow);
var seen = new Set();
var downloads = [];
rows.forEach(function(row) {
var item = fileTreeAssetDownloadDetail(row);
var key = item && (item.localRelativePath || item.assetId || item.rowId);
if (!item || !key || seen.has(key)) return;
seen.add(key);
downloads.push(item);
});
if (downloads.length === 0 && detail && detail.assetId) {
downloads.push(Object.assign({}, detail, { fileName: detail.fileName || detail.title || '附件' }));
} else if (downloads.length === 0 && detail && detail.localRelativePath) {
downloads.push(Object.assign({}, detail, {
assetId: detail.assetId || 'local-file:' + detail.localRelativePath,
fileName: detail.fileName || detail.title || '附件'
}));
}
if (downloads.length === 0) {
recordFileTreeActionStatus('blocked', Object.assign({}, detail || {}, { reason: 'no-downloadable-assets' }));
return false;
}
var primary = Object.assign({}, downloads[0], {
count: downloads.length,
assetIds: downloads.map(function(item) { return item.assetId || ''; }).filter(Boolean),
localRelativePaths: downloads.map(function(item) { return item.localRelativePath || ''; }).filter(Boolean)
});
recordFileTreeAction(downloads.length > 1 ? 'bulk-download' : 'download', primary);
recordFileTreeActionStatus('requested', primary);
document.documentElement.setAttribute('data-mnote-filetree-download-count', String(downloads.length));
document.documentElement.setAttribute('data-mnote-filetree-download-asset-ids', downloads.map(function(item) { return item.assetId || ''; }).filter(Boolean).join(','));
document.documentElement.setAttribute('data-mnote-filetree-download-paths', downloads.map(function(item) { return item.localRelativePath || ''; }).filter(Boolean).join(','));
downloads.forEach(function(item) {
void openEditorAttachmentDownload(item);
});
return true;
}
function hasSelectedDocumentAncestor(row, selectedDocRowIds) {
var node = row instanceof HTMLElement ? row.closest('.tree-node') : null;
while (node && node.parentElement) {
var parentChildren = node.parentElement.closest('.tree-children');
var parentNode = parentChildren ? parentChildren.closest('.tree-node') : null;
var parentRow = parentNode ? parentNode.querySelector(':scope > .tree-row[data-shell-mode="filetree"]') : null;
if (parentRow instanceof HTMLElement) {
var parentRowId = parentRow.getAttribute('data-row-id') || '';
if (selectedDocRowIds.has(parentRowId)) return true;
}
node = parentNode;
}
return false;
}
function classifySidebarFileTreeAsset(row) {
var badge = row instanceof HTMLElement ? row.querySelector('.tree-kind-badge') : null;
var iconKind = badge instanceof HTMLElement ? String(badge.getAttribute('data-kind') || '').trim() : '';
var objectKind = row instanceof HTMLElement ? String(row.getAttribute('data-object-kind') || '').trim() : '';
var title = rowTitle(row).toLowerCase();
if (objectKind === 'mindmap' || iconKind === 'mindmap') return 'mindmap';
if (objectKind === 'table' || iconKind === 'table' || iconKind === 'luckysheet' || title.indexOf('.luckysheet') >= 0) return 'table';
return 'file';
}
function buildSidebarFileTreeDeletePlan(rows) {
var docRows = [];
var folderRows = [];
var assetRows = [];
rows.forEach(function(row) {
var kind = fileTreeRowKind(row);
if ((kind === 'document' || kind === 'doc' || kind === 'index' || kind === 'markdown') && fileTreeRowDocumentId(row)) {
docRows.push(row);
return;
}
if (kind === 'folder' && currentSourceKind() === 'local_folder') {
folderRows.push(row);
return;
}
if (fileTreeRowAssetId(row)) assetRows.push(row);
});
var selectedContainerRowIds = new Set(docRows.concat(folderRows).map(function(row) { return row.getAttribute('data-row-id') || ''; }).filter(Boolean));
var topDocRows = [];
var topFolderRows = [];
var seenDocs = new Set();
var seenFolders = new Set();
docRows.forEach(function(row) {
var documentId = fileTreeRowDocumentId(row);
if (!documentId || seenDocs.has(documentId)) return;
if (hasSelectedDocumentAncestor(row, selectedContainerRowIds)) return;
seenDocs.add(documentId);
topDocRows.push(row);
});
folderRows.forEach(function(row) {
var rowId = row.getAttribute('data-row-id') || '';
if (!rowId || seenFolders.has(rowId)) return;
if (hasSelectedDocumentAncestor(row, selectedContainerRowIds)) return;
seenFolders.add(rowId);
topFolderRows.push(row);
});
var selectedTopContainerRows = new Set(topDocRows.concat(topFolderRows).map(function(row) { return row.getAttribute('data-row-id') || ''; }).filter(Boolean));
var fileAssetRows = [];
var mindmapRows = [];
var tableRows = [];
var seenAssets = new Set();
assetRows.forEach(function(row) {
var assetId = fileTreeRowAssetId(row);
if (!assetId || seenAssets.has(assetId)) return;
if (hasSelectedDocumentAncestor(row, selectedTopContainerRows)) return;
seenAssets.add(assetId);
var assetKind = classifySidebarFileTreeAsset(row);
if (assetKind === 'mindmap') mindmapRows.push(row);
else if (assetKind === 'table') tableRows.push(row);
else fileAssetRows.push(row);
});
return {
docRows: topDocRows,
folderRows: topFolderRows,
fileAssetRows: fileAssetRows,
mindmapRows: mindmapRows,
tableRows: tableRows
};
}
function sidebarFileTreeDeleteConfirmText(plan) {
var docCount = plan.docRows.length;
var folderCount = plan.folderRows.length;
var fileCount = plan.fileAssetRows.length;
var mindmapCount = plan.mindmapRows.length;
var tableCount = plan.tableRows.length;
var parts = [];
if (docCount > 0) parts.push(docCount + ' 个页面(删除到垃圾桶)');
if (folderCount > 0) parts.push(folderCount + ' 个文件夹(删除到垃圾桶)');
if (fileCount > 0) parts.push(fileCount + ' 个附件(删除,10 分钟内可撤销)');
if (mindmapCount > 0) parts.push(mindmapCount + ' 个思维导图(移入垃圾桶,10 分钟内可恢复)');
if (tableCount > 0) parts.push(tableCount + (currentSourceKind() === 'local_folder' ? ' 个在线表格(移入垃圾桶,10 分钟内可恢复)' : ' 个在线表格(删除)'));
return '确认删除选中的 ' + parts.join(' + ') + ' 吗?';
}
async function postSidebarFileTreeJson(url, body) {
var response = await fetch(url, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body || {})
});
var payload = await response.json().catch(function() { return null; });
if (!response.ok) {
throw new Error((payload && (payload.error || payload.message)) || (url + ' failed: ' + response.status));
}
return payload;
}
function fileTreeRowsByRowIds(rowIds) {
var runtimeFn = fileTreeRuntimeFunction('fileTreeRowsByRowIds');
if (runtimeFn) return runtimeFn(rowIds, fileTreeRuntimeDeps());
return (rowIds || []).map(function(rowId) {
return document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(rowId) + '"]');
}).filter(function(row) { return row instanceof HTMLElement; });
}
function fileTreeChildCount(documentId) {
var runtimeFn = fileTreeRuntimeFunction('fileTreeChildCount');
if (runtimeFn) return runtimeFn(documentId, fileTreeRuntimeDeps());
if (!documentId) return 0;
var row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + documentId) + '"]');
var node = row ? row.closest('.tree-node') : null;
var children = node ? node.querySelectorAll(':scope > .tree-children > .tree-node > .tree-row[data-shell-mode="filetree"][data-row-kind="document"], :scope > .tree-children > .tree-node > .tree-row[data-shell-mode="filetree"][data-row-kind="doc"]') : [];
return children.length;
}
async function pasteSidebarFileTreeClipboard(trigger) {
if (!runtimeState.sidebarFileTreeClipboard || !Array.isArray(runtimeState.sidebarFileTreeClipboard.rowIds) || runtimeState.sidebarFileTreeClipboard.rowIds.length === 0) return false;
var targetRow = trigger instanceof HTMLElement ? trigger : null;
if (!targetRow && sidebarFileTreeSelection.focusedRowId) {
targetRow = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(sidebarFileTreeSelection.focusedRowId) + '"]');
}
var targetDocumentId = fileTreeRowDocumentId(targetRow) || currentDocumentId();
if (!targetDocumentId) return false;
var action = runtimeState.sidebarFileTreeClipboard.action === 'cut' ? 'move' : 'copy';
var rows = fileTreeRowsByRowIds(runtimeState.sidebarFileTreeClipboard.rowIds);
if (rows.length === 0) return false;
var writable = await ensureFileTreeWritableTarget('paste', targetRow, runtimeState.sidebarFileTreeClipboard.rowIds, action === 'copy');
if (!writable) return false;
recordFileTreeAction('paste', {
rowId: targetRow ? targetRow.getAttribute('data-row-id') || '' : '',
documentId: targetDocumentId,
sourceRowIds: runtimeState.sidebarFileTreeClipboard.rowIds,
clipboardAction: runtimeState.sidebarFileTreeClipboard.action
});
var plan = buildSidebarFileTreeDeletePlan(rows);
var workspaceId = resolveWorkspaceId(targetRow || document.body);
var failures = [];
if (action === 'copy') {
dispatchSidebarEvent('tree.filetree.copy-requested', { rowIds: runtimeState.sidebarFileTreeClipboard.rowIds, targetDocumentId: targetDocumentId });
recordFileTreeActionStatus('copy-requested', { documentId: targetDocumentId });
return true;
}
for (var i = 0; i < plan.docRows.length; i += 1) {
var docRow = plan.docRows[i];
var documentId = fileTreeRowDocumentId(docRow);
if (!documentId || documentId === targetDocumentId) continue;
try {
await dispatchTreeCommand(targetRow || docRow, {
action: 'move',
workspaceId: workspaceId,
documentId: documentId,
parentId: targetDocumentId,
sortOrder: fileTreeChildCount(targetDocumentId) + i
});
} catch (error) {
failures.push(documentId + ': ' + (error && error.message ? error.message : '移动页面失败'));
}
}
var assetIds = plan.fileAssetRows.map(fileTreeRowAssetId).filter(Boolean);
if (assetIds.length > 0) {
try {
await postSidebarFileTreeJson('/api/media/batch', {
action: 'move',
assetIds: assetIds,
targetDocumentId: targetDocumentId
});
} catch (error) {
failures.push(assetIds.join(',') + ': ' + (error && error.message ? error.message : '移动附件失败'));
}
}
if (failures.length > 0) {
recordFileTreeActionStatus('failed', { documentId: targetDocumentId, fallback: 'alert' });
window.alert('部分对象移动失败:' + failures.join(''));
return false;
}
runtimeState.sidebarFileTreeClipboard = null;
recordFileTreeActionStatus('applied', { documentId: targetDocumentId });
return true;
}
async function deleteSelectedSidebarFileTreeRows(trigger) {
var rows = selectedSidebarFileTreeRows();
var plan = buildSidebarFileTreeDeletePlan(rows);
var total = plan.docRows.length + plan.folderRows.length + plan.fileAssetRows.length + plan.mindmapRows.length + plan.tableRows.length;
if (total === 0) return false;
if (!window.confirm(sidebarFileTreeDeleteConfirmText(plan))) return false;
recordFileTreeAction('bulk-delete', { rowId: trigger instanceof HTMLElement ? trigger.getAttribute('data-row-id') || '' : '', count: total });
recordFileTreeActionStatus('pending', { count: total });
var failures = [];
for (var i = 0; i < plan.docRows.length; i += 1) {
var docRow = plan.docRows[i];
var documentId = fileTreeRowDocumentId(docRow);
try {
await dispatchTreeCommand(trigger || docRow, {
action: 'archive',
workspaceId: resolveWorkspaceId(docRow),
documentId: documentId
});
applyRemoveDocumentDelta({ documentId: documentId });
} catch (error) {
failures.push(documentId);
}
}
for (var fd = 0; fd < plan.folderRows.length; fd += 1) {
var folderRow = plan.folderRows[fd];
var folderId = folderRow.getAttribute('data-row-id') || folderRow.getAttribute('data-node-id') || '';
try {
await dispatchTreeCommand(trigger || folderRow, {
action: 'archive',
workspaceId: resolveWorkspaceId(folderRow),
documentId: folderId
});
} catch (error) {
failures.push(folderId);
}
}
if (plan.fileAssetRows.length > 0) {
var fileAssetIds = plan.fileAssetRows.map(fileTreeRowAssetId).filter(Boolean);
try {
if (currentSourceKind() === 'local_folder') {
for (var lf = 0; lf < fileAssetIds.length; lf += 1) {
await dispatchTreeCommand(trigger || plan.fileAssetRows[lf], {
action: 'archive',
workspaceId: resolveWorkspaceId(plan.fileAssetRows[lf]),
documentId: fileAssetIds[lf]
});
}
} else {
await postSidebarFileTreeJson('/api/media/batch', { action: 'delete', assetIds: fileAssetIds });
}
fileAssetIds.forEach(removeFileTreeAssetRow);
} catch (error) {
failures = failures.concat(fileAssetIds);
}
}
for (var m = 0; m < plan.mindmapRows.length; m += 1) {
var mindmapRow = plan.mindmapRows[m];
var mindmapId = fileTreeRowAssetId(mindmapRow);
try {
if (currentSourceKind() === 'local_folder') {
await dispatchTreeCommand(trigger || mindmapRow, {
action: 'archive',
workspaceId: resolveWorkspaceId(mindmapRow),
documentId: mindmapId
});
} else {
var mindmapDocId = fileTreeRowDocumentId(mindmapRow);
var mindmapResponse = await fetch('/api/mindmap/' + encodeURIComponent(mindmapDocId) + '/' + encodeURIComponent(mindmapId), { method: 'DELETE' });
if (!mindmapResponse.ok) throw new Error('mindmap_delete_failed_' + mindmapResponse.status);
}
removeFileTreeAssetRow(mindmapId);
} catch (error) {
failures.push(mindmapId);
}
}
for (var t = 0; t < plan.tableRows.length; t += 1) {
var tableRow = plan.tableRows[t];
var tableId = fileTreeRowAssetId(tableRow);
try {
if (currentSourceKind() === 'local_folder') {
await dispatchTreeCommand(trigger || tableRow, {
action: 'archive',
workspaceId: resolveWorkspaceId(tableRow),
documentId: tableId
});
} else {
var tableResponse = await fetch('/api/tables/' + encodeURIComponent(tableId), { method: 'DELETE' });
if (!tableResponse.ok) throw new Error('table_delete_failed_' + tableResponse.status);
window.dispatchEvent(new CustomEvent('online-table-deleted', { detail: { tableId: tableId } }));
}
removeFileTreeAssetRow(tableId);
} catch (error) {
failures.push(tableId);
}
}
sidebarFileTreeSelection.selectedRowIds = new Set();
sidebarFileTreeSelection.focusedRowId = null;
syncSidebarFileTreeSelection();
if (failures.length > 0) {
recordFileTreeActionStatus('failed', { count: total, failures: failures.slice(0, 20), fallback: 'alert' });
window.alert('部分对象删除失败:' + failures.slice(0, 5).join(', ') + (failures.length > 5 ? '…' : ''));
return false;
}
document.documentElement.setAttribute('data-mnote-filetree-bulk-delete-applied', 'true');
recordFileTreeActionStatus('archived', { count: total, undo: 'trash-modal' });
if (currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot();
return true;
}
return {
rowTitle,
rowCenter,
renameFileTreeAsset,
beginFileTreeInlineRename,
closeTreeContextMenu,
copyTreeContextValue,
triggerBrowserDownload,
recordFileTreeAction,
recordFileTreeActionStatus,
documentHref,
convertToPreviousSiblingChild,
fileTreeCopyPath,
fileTreeMenuTargetParentId,
withOfficeEditModeGuard,
handleTreeContextMenuAction,
appendTreeContextMenuButton,
buildSidebarFileTreeContext,
evaluateSidebarFileTreeWhen,
openTreeContextMenu,
openPageTreeContextMenu,
openFileTreeContextMenu,
visibleFileTreeRows,
syncSidebarFileTreeSelection,
selectSidebarFileTreeRow,
selectSidebarFileTreeDocument,
selectSidebarFileTreeRowById,
pendingLocalFolderRestoreRowId,
clearPendingLocalFolderRestoreRowId,
applyPendingLocalFolderRestoreFocusOnce,
schedulePendingLocalFolderRestoreFocus,
selectedSidebarFileTreeRowIdsForDrag,
selectedSidebarFileTreeRows,
fileTreeRowDocumentId,
fileTreeRowAssetId,
decodeLocalEncodedPath,
fileTreeRowLocalRelativePath,
fileTreeRowLocalUploadTargetRelativePath,
fileTreeRowKind,
isFileTreeDownloadableAssetRow,
isFileTreeDownloadableRow,
fileTreeAssetDownloadDetail,
selectedSidebarFileTreeRowsForDownload,
selectedSidebarFileTreeAssetRowsForDownload,
downloadSelectedFileTreeAssetRows,
hasSelectedDocumentAncestor,
classifySidebarFileTreeAsset,
buildSidebarFileTreeDeletePlan,
sidebarFileTreeDeleteConfirmText,
postSidebarFileTreeJson,
fileTreeRowsByRowIds,
fileTreeChildCount,
pasteSidebarFileTreeClipboard,
deleteSelectedSidebarFileTreeRows,
};
};