收敛树命令与投影切换
This commit is contained in:
@@ -9,12 +9,14 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
|
||||
var PAGE_DRAG_MIME = 'application/x-mnote-page-tree-node';
|
||||
var FILETREE_DRAG_MIME = 'application/x-mnote-filetree-row-ids';
|
||||
var MNOTE_SIDEBAR_TREE_MODE_KEY = 'mnote.sidebar.tree.mode';
|
||||
var mnoteNavigationInFlight = '';
|
||||
var draggingPageNodeId = '';
|
||||
var activePageDropRow = null;
|
||||
var draggingFileTreeRowIds = [];
|
||||
var activeFileTreeDropRow = null;
|
||||
var projectionRefreshTimer = 0;
|
||||
var activeTreeContextMenu = null;
|
||||
|
||||
function closestAction(target, selector) {
|
||||
return target && typeof target.closest === 'function' ? target.closest(selector) : null;
|
||||
@@ -39,6 +41,40 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
return match ? decodeURIComponent(match[1]) : '';
|
||||
}
|
||||
|
||||
function normalizeSidebarTreeMode(value) {
|
||||
var mode = String(value || '').trim();
|
||||
return mode === 'filetree' ? 'filetree' : 'page';
|
||||
}
|
||||
|
||||
function readStoredSidebarTreeMode() {
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var fromUrl = normalizeSidebarTreeMode(params.get('treeView') || params.get('sidebarTree'));
|
||||
if (params.has('treeView') || params.has('sidebarTree')) return fromUrl;
|
||||
try {
|
||||
var stored = window.sessionStorage ? window.sessionStorage.getItem(MNOTE_SIDEBAR_TREE_MODE_KEY) : '';
|
||||
return normalizeSidebarTreeMode(stored);
|
||||
} catch (_) {
|
||||
return 'page';
|
||||
}
|
||||
}
|
||||
|
||||
function persistSidebarTreeMode(mode) {
|
||||
var normalized = normalizeSidebarTreeMode(mode);
|
||||
document.documentElement.setAttribute('data-mnote-sidebar-tree-mode', normalized);
|
||||
try {
|
||||
if (window.sessionStorage) window.sessionStorage.setItem(MNOTE_SIDEBAR_TREE_MODE_KEY, normalized);
|
||||
} catch (_) {}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function activeSidebarTreeMode() {
|
||||
var active = document.querySelector('[data-mnote-sidebar-tree-tab][aria-selected="true"]');
|
||||
if (active instanceof HTMLElement) {
|
||||
return normalizeSidebarTreeMode(active.getAttribute('data-mnote-sidebar-tree-tab'));
|
||||
}
|
||||
return readStoredSidebarTreeMode();
|
||||
}
|
||||
|
||||
function resolveWorkspaceId(trigger) {
|
||||
var direct = (trigger.getAttribute('data-workspace-id') || '').trim();
|
||||
if (direct) return direct;
|
||||
@@ -82,9 +118,29 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
}
|
||||
|
||||
function navigateToDocument(nodeId, workspaceId) {
|
||||
function navigateToDocument(nodeId, workspaceId, options) {
|
||||
if (!nodeId) return;
|
||||
var url = '/documents/' + encodeURIComponent(nodeId) + (workspaceId ? '?workspaceId=' + encodeURIComponent(workspaceId) : '');
|
||||
var treeView = normalizeSidebarTreeMode(options && options.treeView ? options.treeView : activeSidebarTreeMode());
|
||||
persistSidebarTreeMode(treeView);
|
||||
if (currentDocumentId() === nodeId && window.location.pathname.indexOf('/documents/') === 0) {
|
||||
document.querySelectorAll('.tree-row[data-active="true"], .tree-row[data-selected="true"]').forEach(function(row) {
|
||||
if (row instanceof HTMLElement) {
|
||||
row.setAttribute('data-active', 'false');
|
||||
row.setAttribute('data-selected', 'false');
|
||||
}
|
||||
});
|
||||
document.querySelectorAll('.tree-row[data-node-id="' + cssEscape(nodeId) + '"], .tree-row[data-document-id="' + cssEscape(nodeId) + '"], .tree-row[data-doc-id="' + cssEscape(nodeId) + '"]').forEach(function(row) {
|
||||
if (row instanceof HTMLElement) {
|
||||
if (row.getAttribute('data-shell-mode') === 'filetree') row.setAttribute('data-selected', 'true');
|
||||
else row.setAttribute('data-active', 'true');
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
var targetUrl = new URL('/documents/' + encodeURIComponent(nodeId), window.location.origin);
|
||||
if (workspaceId) targetUrl.searchParams.set('workspaceId', workspaceId);
|
||||
if (treeView === 'filetree') targetUrl.searchParams.set('treeView', 'filetree');
|
||||
var url = targetUrl.pathname + targetUrl.search;
|
||||
if (mnoteNavigationInFlight === url) return;
|
||||
mnoteNavigationInFlight = url;
|
||||
document.documentElement.setAttribute('data-mnote-navigation-pending', 'true');
|
||||
@@ -106,12 +162,12 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
title: '新页面'
|
||||
});
|
||||
var nextWorkspaceId = result.workspaceId || workspaceId;
|
||||
navigateToDocument(result.documentId, nextWorkspaceId);
|
||||
navigateToDocument(result.documentId, nextWorkspaceId, { treeView: activeSidebarTreeMode() });
|
||||
}
|
||||
|
||||
function switchSidebarTreeTab(trigger) {
|
||||
var mode = (trigger.getAttribute('data-mnote-sidebar-tree-tab') || 'page').trim();
|
||||
var shell = trigger.closest('[data-testid="wolai-sidebar-page-tree-shell"]');
|
||||
function applySidebarTreeTab(mode, shell) {
|
||||
mode = persistSidebarTreeMode(mode);
|
||||
shell = shell || document.querySelector('[data-testid="wolai-sidebar-page-tree-shell"]');
|
||||
if (!shell) return;
|
||||
var tabs = shell.querySelectorAll('[data-mnote-sidebar-tree-tab]');
|
||||
for (var i = 0; i < tabs.length; i++) {
|
||||
@@ -127,13 +183,25 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
}
|
||||
|
||||
function switchSidebarTreeTab(trigger) {
|
||||
var mode = trigger ? trigger.getAttribute('data-mnote-sidebar-tree-tab') : 'page';
|
||||
applySidebarTreeTab(mode, trigger ? trigger.closest('[data-testid="wolai-sidebar-page-tree-shell"]') : null);
|
||||
}
|
||||
|
||||
function restoreSidebarTreeTab() {
|
||||
applySidebarTreeTab(readStoredSidebarTreeMode(), null);
|
||||
}
|
||||
|
||||
function updateTitleEverywhere(documentId, title) {
|
||||
if (!documentId) return;
|
||||
var escaped = cssEscape(documentId);
|
||||
var selectors = [
|
||||
'[data-node-id="' + cssEscape(documentId) + '"] .tree-link-title',
|
||||
'[data-document-id="' + cssEscape(documentId) + '"] .tree-link-title',
|
||||
'[data-doc-id="' + cssEscape(documentId) + '"] .tree-link-title',
|
||||
'[data-node-id="' + cssEscape(documentId) + '"] .wolai-row-title'
|
||||
'.tree-row[data-node-id="' + escaped + '"] > .tree-link > .tree-link-title',
|
||||
'.tree-row[data-document-id="' + escaped + '"] > .tree-link > .tree-link-title',
|
||||
'.tree-row[data-doc-id="' + escaped + '"] > .tree-link > .tree-link-title',
|
||||
'.wolai-page-row[data-node-id="' + escaped + '"] > .wolai-row-title',
|
||||
'a[href="/documents/' + escaped + '"] > .wolai-row-title',
|
||||
'a[href^="/documents/' + escaped + '?"] > .wolai-row-title'
|
||||
];
|
||||
selectors.forEach(function(selector) {
|
||||
document.querySelectorAll(selector).forEach(function(node) {
|
||||
@@ -313,7 +381,267 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
window.dispatchEvent(new CustomEvent(name, { detail: detail }));
|
||||
}
|
||||
|
||||
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) };
|
||||
}
|
||||
|
||||
function closeTreeContextMenu() {
|
||||
if (activeTreeContextMenu && activeTreeContextMenu.parentElement) {
|
||||
activeTreeContextMenu.parentElement.removeChild(activeTreeContextMenu);
|
||||
}
|
||||
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 documentHref(documentId, workspaceId) {
|
||||
var url = new URL('/documents/' + encodeURIComponent(documentId), window.location.origin);
|
||||
if (workspaceId) url.searchParams.set('workspaceId', workspaceId);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function openTreePicker(mode, detail) {
|
||||
var url = new URL('/tree', window.location.origin);
|
||||
url.searchParams.set('mode', 'picker');
|
||||
url.searchParams.set('allowRootPick', '1');
|
||||
url.searchParams.set('intent', mode);
|
||||
if (detail.workspaceId) url.searchParams.set('workspaceId', detail.workspaceId);
|
||||
if (detail.documentId) {
|
||||
url.searchParams.set('sourceDocumentId', detail.documentId);
|
||||
url.searchParams.set('excludeIds', detail.documentId);
|
||||
}
|
||||
window.location.assign(url.pathname + url.search);
|
||||
}
|
||||
|
||||
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
|
||||
}).then(function(){ scheduleProjectionRefresh(detail.workspaceId || resolveWorkspaceId(row)); });
|
||||
}
|
||||
|
||||
function handleTreeContextMenuAction(action, detail, trigger) {
|
||||
closeTreeContextMenu();
|
||||
var documentId = detail.documentId || '';
|
||||
var workspaceId = detail.workspaceId || resolveWorkspaceId(trigger || document.body);
|
||||
var title = detail.title || '无标题';
|
||||
if (action === 'open-right') {
|
||||
dispatchSidebarEvent('tree.page.open-right', detail);
|
||||
return;
|
||||
}
|
||||
if (action === 'share') {
|
||||
dispatchSidebarEvent('tree.page.share', detail);
|
||||
return;
|
||||
}
|
||||
if (action === 'move') {
|
||||
openTreePicker('move', detail);
|
||||
return;
|
||||
}
|
||||
if (action === 'embed') {
|
||||
openTreePicker('embed', 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 === 'duplicate') {
|
||||
dispatchSidebarEvent('tree.page.duplicate', detail);
|
||||
return;
|
||||
}
|
||||
if (action === 'rename') {
|
||||
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;
|
||||
}
|
||||
if (action === 'delete-trash' && documentId) {
|
||||
if (!window.confirm('确定要将“' + title + '”删除到垃圾桶吗?')) return;
|
||||
void dispatchTreeCommand(trigger || document.body, {
|
||||
action: 'purge',
|
||||
workspaceId: workspaceId,
|
||||
documentId: documentId
|
||||
}).then(function(){ scheduleProjectionRefresh(workspaceId); });
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
function openTreeContextMenu(kind, detail, x, y, trigger) {
|
||||
closeTreeContextMenu();
|
||||
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 isAsset = kind === 'filetree' && detail.assetId && detail.rowKind !== 'document' && detail.rowKind !== 'index';
|
||||
var items = isAsset ? [
|
||||
{ action: 'open-right', icon: 'open_in_new', label: '在右侧边栏打开', shortcut: 'Alt + O' },
|
||||
{ action: 'move', icon: 'drive_file_move', label: '移动到...' },
|
||||
{ action: 'copy-id', icon: 'tag', label: '复制资源 ID' }
|
||||
] : [
|
||||
{ action: 'open-right', icon: 'right_panel_open', label: '在右侧边栏打开', shortcut: 'Alt + O' },
|
||||
{ action: 'share', icon: 'share', label: '共享...' },
|
||||
{ action: 'move', icon: 'drive_file_move', label: '移动到...' },
|
||||
{ action: 'embed', icon: 'account_tree', label: '嵌入到...' },
|
||||
{ action: 'copy-link', icon: 'link', label: '复制访问链接' },
|
||||
{ action: 'copy-link-title', icon: 'link', label: '复制访问链接(带标题)' },
|
||||
{ action: 'copy-reference-inline', icon: 'content_copy', label: '复制页面引用链接' },
|
||||
{ action: 'copy-id', icon: 'tag', label: '复制页面 ID' },
|
||||
{ action: 'duplicate', icon: 'file_copy', label: '拷贝副本' },
|
||||
{ separator: true },
|
||||
{ action: 'rename', icon: 'edit', label: '重命名' },
|
||||
{ action: 'create-child', icon: 'add', label: '新建子页面' },
|
||||
{ action: 'convert-child', icon: 'subdirectory_arrow_right', label: '转为上一个子页面' },
|
||||
{ action: 'delete-trash', icon: 'delete', label: '删除到垃圾桶', danger: true }
|
||||
];
|
||||
items.forEach(function(item) { 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';
|
||||
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') || '';
|
||||
openTreeContextMenu('filetree', {
|
||||
documentId: documentId,
|
||||
rowId: row.getAttribute('data-row-id') || '',
|
||||
rowKind: row.getAttribute('data-row-kind') || '',
|
||||
assetId: row.getAttribute('data-asset-id') || '',
|
||||
title: rowTitle(row),
|
||||
workspaceId: resolveWorkspaceId(row)
|
||||
}, x, y, trigger || row);
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if (activeTreeContextMenu && activeTreeContextMenu.contains(e.target)) return;
|
||||
if (activeTreeContextMenu) closeTreeContextMenu();
|
||||
|
||||
var tabTrigger = closestAction(e.target, '[data-mnote-sidebar-tree-tab]');
|
||||
if (tabTrigger) {
|
||||
e.preventDefault();
|
||||
@@ -350,7 +678,9 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
if (fileAction === 'menu') {
|
||||
e.preventDefault();
|
||||
var point = rowCenter(fileBtn || fileRow);
|
||||
dispatchSidebarEvent('tree.filetree.context-menu', { rowId: rowId, rowKind: rowKind, documentId: documentId || null, assetId: assetId || null });
|
||||
openFileTreeContextMenu(fileRow, point.x, point.y, fileBtn || fileRow);
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
@@ -360,7 +690,9 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
fileRow.setAttribute('data-selected', 'true');
|
||||
dispatchSidebarEvent('tree.filetree.open', { rowId: rowId, rowKind: rowKind, documentId: documentId || null, assetId: assetId || null });
|
||||
if ((rowKind === 'document' || rowKind === 'index') && documentId) {
|
||||
navigateToDocument(documentId, resolveWorkspaceId(fileRow));
|
||||
navigateToDocument(documentId, resolveWorkspaceId(fileRow), { treeView: 'filetree' });
|
||||
} else if (assetId) {
|
||||
dispatchSidebarEvent('tree.asset.open', { rowId: rowId, rowKind: rowKind, documentId: documentId || null, assetId: assetId });
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -379,7 +711,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
e.preventDefault();
|
||||
} else if (action === 'open') {
|
||||
var workspaceId = resolveWorkspaceId(btn);
|
||||
navigateToDocument(nodeId, workspaceId);
|
||||
navigateToDocument(nodeId, workspaceId, { treeView: 'page' });
|
||||
e.preventDefault();
|
||||
} else if (action === 'create') {
|
||||
e.preventDefault();
|
||||
@@ -400,10 +732,30 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
} else if (action === 'menu') {
|
||||
e.preventDefault();
|
||||
var menuPoint = rowCenter(btn);
|
||||
dispatchSidebarEvent('tree.page.context-menu', { documentId: nodeId, target: { documentId: nodeId } });
|
||||
openPageTreeContextMenu(btn.closest('.tree-row'), menuPoint.x, menuPoint.y, btn);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('contextmenu', function(event) {
|
||||
var fileRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"]');
|
||||
if (fileRow) {
|
||||
event.preventDefault();
|
||||
openFileTreeContextMenu(fileRow, event.clientX, event.clientY, fileRow);
|
||||
return;
|
||||
}
|
||||
var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"]');
|
||||
if (pageRow) {
|
||||
event.preventDefault();
|
||||
openPageTreeContextMenu(pageRow, event.clientX, event.clientY, pageRow);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') closeTreeContextMenu();
|
||||
});
|
||||
|
||||
function readPageDragNodeId(event) {
|
||||
var fromTransfer = event.dataTransfer ? event.dataTransfer.getData(PAGE_DRAG_MIME) || event.dataTransfer.getData('text/plain') : '';
|
||||
return (fromTransfer || draggingPageNodeId || '').trim();
|
||||
@@ -557,7 +909,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
window.addEventListener('tree:title-updated', function(event) {
|
||||
var detail = event.detail || {};
|
||||
updateTitleEverywhere(detail.documentId, detail.title);
|
||||
scheduleProjectionRefresh(detail.workspaceId);
|
||||
document.documentElement.setAttribute('data-mnote-title-local-applied', 'true');
|
||||
});
|
||||
|
||||
window.addEventListener('tree:snapshot', function(event) {
|
||||
@@ -593,6 +945,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
var activeRow = tree.querySelector('.tree-row[data-node-id="' + cssEscape(activeId) + '"]');
|
||||
if (activeRow instanceof HTMLElement) activeRow.setAttribute('data-active', 'true');
|
||||
}
|
||||
restoreSidebarTreeTab();
|
||||
})();
|
||||
"##;
|
||||
|
||||
@@ -793,12 +1146,12 @@ pub fn PageLayout(
|
||||
<span class="wolai-sidebar-chevron" aria-hidden="true">"⌄"</span>
|
||||
</div>
|
||||
<nav class="mnote-sidebar-nav wolai-quick-actions" aria-label="快捷操作">
|
||||
<a href="/search" class:active={current_nav == "search"} title="搜索" aria-label="搜索"><svg class="mnote-symbol nav-icon" data-icon="search" viewBox="0 0 24 24" aria-hidden="true"><circle cx="10.5" cy="10.5" r="5.5"></circle><path d="m15 15 5 5"></path></svg></a>
|
||||
<a href="/graph" title="关系图" aria-label="关系图"><svg class="mnote-symbol nav-icon" data-icon="account_tree" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6h5v5H6z"></path><path d="M15 3h5v5h-5z"></path><path d="M15 16h5v5h-5z"></path><path d="M11 8.5h2.5a4 4 0 0 0 4-4"></path><path d="M11 8.5h2.5a4 4 0 0 1 4 4V16"></path></svg></a>
|
||||
<a href="/actions" title="快捷动作" aria-label="快捷动作"><svg class="mnote-symbol nav-icon" data-icon="bolt" viewBox="0 0 24 24" aria-hidden="true"><path d="M13 2 5 14h6l-1 8 9-13h-6z"></path></svg></a>
|
||||
<a href="/help" title="帮助" aria-label="帮助"><svg class="mnote-symbol nav-icon" data-icon="help" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9"></circle><path d="M9.8 9a2.4 2.4 0 0 1 4.5 1.2c0 1.7-2.3 2-2.3 3.8"></path><path d="M12 17.5h.01"></path></svg></a>
|
||||
<a href="/files" title="文件" aria-label="文件"><svg class="mnote-symbol nav-icon" data-icon="inventory_2" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16v4H4z"></path><path d="M6 10v9h12v-9"></path><path d="M9 14h6"></path></svg></a>
|
||||
<a href="/more" title="更多" aria-label="更多"><svg class="mnote-symbol nav-icon" data-icon="more_horiz" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h.01"></path><path d="M12 12h.01"></path><path d="M19 12h.01"></path></svg></a>
|
||||
<a href="/search" class:active={current_nav == "search"} title="搜索" aria-label="搜索"><span class="material-symbols-outlined nav-icon" data-icon="search" aria-hidden="true"></span></a>
|
||||
<a href="/graph" title="关系图" aria-label="关系图"><span class="material-symbols-outlined nav-icon" data-icon="account_tree" aria-hidden="true"></span></a>
|
||||
<a href="/actions" title="快捷动作" aria-label="快捷动作"><span class="material-symbols-outlined nav-icon" data-icon="bolt" aria-hidden="true"></span></a>
|
||||
<a href="/help" title="帮助" aria-label="帮助"><span class="material-symbols-outlined nav-icon" data-icon="help" aria-hidden="true"></span></a>
|
||||
<a href="/files" title="文件" aria-label="文件"><span class="material-symbols-outlined nav-icon" data-icon="inventory_2" aria-hidden="true"></span></a>
|
||||
<a href="/more" title="更多" aria-label="更多"><span class="material-symbols-outlined nav-icon" data-icon="more_horiz" aria-hidden="true"></span></a>
|
||||
</nav>
|
||||
<div class="wolai-sidebar-body" inner_html={sidebar_sections_html}></div>
|
||||
<script id="__MNOTE_TREE_LIVE_BOOTSTRAP__" type="application/json" inner_html={tree_live_bootstrap}></script>
|
||||
@@ -808,28 +1161,28 @@ pub fn PageLayout(
|
||||
<div class="mnote-main">
|
||||
<header class="wolai-topbar" data-testid="wolai-topbar">
|
||||
<div class="wolai-topbar-left">
|
||||
<button type="button" class="wolai-icon-button wolai-menu-button" aria-label="切换侧栏">"☰"</button>
|
||||
<button type="button" class="wolai-icon-button wolai-menu-button" aria-label="切换侧栏"><span class="material-symbols-outlined" data-icon="menu" aria-hidden="true"></span></button>
|
||||
<nav class="wolai-breadcrumb" aria-label="页面路径">
|
||||
<span class="wolai-breadcrumb-root">"The Digital Atelier"</span>
|
||||
<span class="wolai-breadcrumb-separator" aria-hidden="true">"/"</span>
|
||||
<span class="wolai-breadcrumb-current"><svg class="mnote-symbol wolai-home-icon" data-icon="home" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 10.5 12 3l8 7.5"></path><path d="M6.5 9.5V21h11V9.5"></path><path d="M9.5 21v-6h5v6"></path></svg><span data-page-title-current="true">{topbar_title}</span></span>
|
||||
<span class="wolai-breadcrumb-current"><span class="material-symbols-outlined wolai-home-icon" data-icon="home" aria-hidden="true"></span><span data-page-title-current="true">{topbar_title}</span></span>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="wolai-topbar-actions" aria-label="页面操作">
|
||||
<span class="wolai-public-pill"><span aria-hidden="true">"●"</span>"Public"</span>
|
||||
<button type="button" class="wolai-icon-button" title="收藏" aria-label="收藏">"☆"</button>
|
||||
<button type="button" class="wolai-icon-button" title="历史" aria-label="历史">"◷"</button>
|
||||
<button type="button" class="wolai-icon-button wolai-ai-inline" title="AI" aria-label="AI">"✦"</button>
|
||||
<button type="button" class="wolai-icon-button" title="搜索" aria-label="搜索">"⌕"</button>
|
||||
<button type="button" class="wolai-icon-button" title="更多" aria-label="更多">"…"</button>
|
||||
<button type="button" class="wolai-icon-button" title="收藏" aria-label="收藏"><span class="material-symbols-outlined" data-icon="star" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="历史" aria-label="历史"><span class="material-symbols-outlined" data-icon="history" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button wolai-ai-inline" title="AI" aria-label="AI"><span class="material-symbols-outlined material-symbols-filled" data-icon="auto_awesome" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="搜索" aria-label="搜索"><span class="material-symbols-outlined" data-icon="search" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button" title="更多" aria-label="更多"><span class="material-symbols-outlined" data-icon="more_horiz" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
</header>
|
||||
<article class="mnote-content">
|
||||
{children()}
|
||||
</article>
|
||||
<div class="wolai-floating-actions" aria-label="浮动操作">
|
||||
<button type="button" data-testid="wolai-floating-help" class="wolai-floating-button wolai-floating-button--help" aria-label="帮助">"?"</button>
|
||||
<button type="button" data-testid="wolai-floating-ai" class="wolai-floating-button wolai-floating-button--ai" aria-label="AI 助手">"✦"</button>
|
||||
<button type="button" data-testid="wolai-floating-help" class="wolai-floating-button wolai-floating-button--help" aria-label="帮助"><span class="material-symbols-outlined" data-icon="help" aria-hidden="true"></span></button>
|
||||
<button type="button" data-testid="wolai-floating-ai" class="wolai-floating-button wolai-floating-button--ai" aria-label="AI 助手"><span class="material-symbols-outlined material-symbols-filled" data-icon="auto_awesome" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -844,6 +1197,9 @@ mod tests {
|
||||
fn sidebar_tree_runtime_handles_navigation_drag_and_filetree_actions() {
|
||||
assert!(SIDEBAR_TREE_JS.contains("mnoteNavigationInFlight"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("data-mnote-navigation-pending"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("MNOTE_SIDEBAR_TREE_MODE_KEY"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("restoreSidebarTreeTab"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("treeView"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("application/x-mnote-page-tree-node"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("dragstart"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("drop"));
|
||||
@@ -851,10 +1207,24 @@ mod tests {
|
||||
assert!(SIDEBAR_TREE_JS.contains("action: 'move'"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("sidebar-file-tree-root"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("tree.filetree.open"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("tree.asset.open"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("tree.filetree.internal-drop"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("tree.filetree.external-drop"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidebar_tree_runtime_renders_context_menu_and_scoped_title_updates() {
|
||||
assert!(SIDEBAR_TREE_JS.contains("openTreeContextMenu"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("mnote-tree-context-menu"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("复制访问链接(带标题)"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("删除到垃圾桶"));
|
||||
assert!(SIDEBAR_TREE_JS.contains(
|
||||
".tree-row[data-node-id=\"' + escaped + '\"] > .tree-link > .tree-link-title"
|
||||
));
|
||||
assert!(!SIDEBAR_TREE_JS
|
||||
.contains("[data-node-id=\"' + cssEscape(documentId) + '\"] .tree-link-title"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_live_controller_marks_transport_and_closes_source_on_pagehide() {
|
||||
assert!(TREE_LIVE_CONTROLLER_JS.contains("convex-command-log-sse"));
|
||||
|
||||
Reference in New Issue
Block a user