对齐 Wolai 侧栏体验并收拢设计入库
This commit is contained in:
@@ -252,22 +252,28 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
return grouped;
|
||||
}
|
||||
|
||||
function renderPageRows(parentId, grouped, activeId) {
|
||||
function pageTreeChevronSvg() {
|
||||
return '<svg class="tree-toggle-icon" viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><path d="M7.84 14.955c.206 0 .37-.07.505-.21l4.277-4.179a.79.79 0 0 0 .264-.574.78.78 0 0 0-.258-.574L8.35 5.24a.7.7 0 0 0-.51-.21.721.721 0 0 0-.498 1.247l3.814 3.721-3.814 3.709a.721.721 0 0 0 .498 1.248"></path></svg>';
|
||||
}
|
||||
|
||||
function renderPageRows(parentId, grouped, activeId, inheritedDepth) {
|
||||
var computedDepth = Number(inheritedDepth || 0);
|
||||
return (grouped.get(parentId) || []).map(function(item) {
|
||||
var nodeId = nodeIdOf(item);
|
||||
var title = titleOf(item);
|
||||
var depth = Number(item.depth || 0);
|
||||
var depth = computedDepth;
|
||||
var parent = parentIdOf(item);
|
||||
var children = grouped.get(nodeId) || [];
|
||||
var expandable = Boolean(item.expandable || item.childCount > 0 || children.length);
|
||||
var expanded = expandable && item.expandedByDefault !== false;
|
||||
var toggle = expandable
|
||||
? '<button type="button" class="tree-toggle" data-testid="tree-node-toggle" data-rust-action="toggle" data-node-id="' + escapeHtml(nodeId) + '" aria-label="' + (expanded ? '折叠 ' : '展开 ') + escapeHtml(title) + '">' + (expanded ? '▾' : '▸') + '</button>'
|
||||
? '<button type="button" class="tree-toggle" data-testid="tree-node-toggle" data-rust-action="toggle" data-node-id="' + escapeHtml(nodeId) + '" aria-label="' + (expanded ? '折叠 ' : '展开 ') + escapeHtml(title) + '" aria-expanded="' + String(expanded) + '">' + pageTreeChevronSvg() + '</button>'
|
||||
: '<span class="tree-spacer" aria-hidden="true"></span>';
|
||||
var childHtml = expandable && expanded
|
||||
? '<ul class="tree-children">' + renderPageRows(nodeId, grouped, activeId) + '</ul>'
|
||||
? '<ul class="tree-children">' + renderPageRows(nodeId, grouped, activeId, depth + 1) + '</ul>'
|
||||
: '';
|
||||
return '<li class="tree-node" data-node-id="' + escapeHtml(nodeId) + '"><div class="tree-row" role="treeitem" aria-level="' + (depth + 1) + '" aria-expanded="' + String(expandable && expanded) + '" data-rust-rendered-row="page" data-testid="wolai-sidebar-row" data-tree-testid="tree-node-open" data-node-id="' + escapeHtml(nodeId) + '"' + (parent ? ' data-parent-id="' + escapeHtml(parent) + '"' : '') + ' data-depth="' + depth + '" data-shell-mode="page" data-active="' + String(nodeId === activeId) + '" data-focused="false" data-draggable="true" draggable="true" tabindex="-1">' + toggle + '<span class="tree-kind-badge" data-kind="page" aria-hidden="true"></span><button type="button" class="tree-link" data-testid="tree-node-open" data-rust-action="open" data-node-id="' + escapeHtml(nodeId) + '"><span class="tree-link-title">' + escapeHtml(title) + '</span></button><div class="tree-actions"><button type="button" class="tree-action" data-testid="tree-action-create" data-rust-action="create" data-node-id="' + escapeHtml(nodeId) + '" aria-label="新建子页面">+</button><button type="button" class="tree-action" data-testid="tree-action-rename" data-rust-action="rename" data-node-id="' + escapeHtml(nodeId) + '" aria-label="重命名">✎</button><button type="button" class="tree-action" data-testid="tree-action-menu" data-rust-action="menu" data-node-id="' + escapeHtml(nodeId) + '" aria-label="更多操作">…</button></div></div>' + childHtml + '</li>';
|
||||
var currentAttr = nodeId === activeId ? ' aria-current="page" aria-selected="true"' : ' aria-selected="false"';
|
||||
return '<li class="tree-node" data-node-id="' + escapeHtml(nodeId) + '"><div class="tree-row" role="treeitem" aria-level="' + (depth + 1) + '" aria-expanded="' + String(expandable && expanded) + '" data-rust-rendered-row="page" data-testid="wolai-sidebar-row" data-tree-testid="tree-node-open" data-node-id="' + escapeHtml(nodeId) + '"' + (parent ? ' data-parent-id="' + escapeHtml(parent) + '"' : '') + ' data-depth="' + depth + '" data-shell-mode="page" data-active="' + String(nodeId === activeId) + '"' + currentAttr + ' data-focused="false" data-draggable="true" draggable="true" tabindex="-1">' + toggle + '<button type="button" class="tree-link" data-testid="tree-node-open" data-rust-action="open" data-node-id="' + escapeHtml(nodeId) + '"><span class="tree-link-title">' + escapeHtml(title) + '</span></button><div class="tree-actions"><button type="button" class="tree-action" data-testid="tree-action-menu" data-rust-action="menu" data-node-id="' + escapeHtml(nodeId) + '" aria-label="更多操作">…</button><button type="button" class="tree-action" data-testid="tree-action-create" data-rust-action="create" data-node-id="' + escapeHtml(nodeId) + '" aria-label="新建子页面">+</button></div></div>' + childHtml + '</li>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
@@ -278,7 +284,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
return String(item.rowKind || 'document') === 'document';
|
||||
});
|
||||
var activeId = currentDocumentId();
|
||||
tree.innerHTML = '<ul class="tree-root" role="tree" data-rust-page-renderer="initial_v1">' + (rows.length ? renderPageRows('', groupRowsByParent(rows), activeId) : '<li class="tree-empty" data-rust-rendered-row="page-empty">暂无页面</li>') + '</ul>';
|
||||
tree.innerHTML = '<ul class="tree-root" role="tree" data-rust-page-renderer="initial_v1">' + (rows.length ? renderPageRows('', groupRowsByParent(rows), activeId, 0) : '<li class="tree-empty" data-rust-rendered-row="page-empty">暂无页面</li>') + '</ul>';
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -373,7 +379,11 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
children.classList.toggle('tree-children--collapsed');
|
||||
var collapsed = children.classList.contains('tree-children--collapsed');
|
||||
row.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
||||
if (button) button.textContent = collapsed ? '▸' : '▾';
|
||||
if (button && button.getAttribute('data-testid') === 'tree-node-toggle') {
|
||||
button.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
||||
} else if (button) {
|
||||
button.textContent = collapsed ? '▸' : '▾';
|
||||
}
|
||||
}
|
||||
|
||||
function dispatchSidebarEvent(name, detail) {
|
||||
@@ -588,20 +598,20 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
{ 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: 'open-right', icon: 'right_panel_open', label: '在右侧边栏打开', shortcut: 'Alt+' },
|
||||
{ separator: true },
|
||||
{ action: 'move', icon: 'drive_file_move', label: '移动到...' },
|
||||
{ action: 'embed', icon: 'account_tree', label: '嵌入到...' },
|
||||
{ separator: true },
|
||||
{ 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: 'copy-id', icon: 'tag', label: '复制页面ID' },
|
||||
{ separator: true },
|
||||
{ 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 }
|
||||
{ separator: true },
|
||||
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true }
|
||||
];
|
||||
items.forEach(function(item) { appendTreeContextMenuButton(menu, item, detail, trigger); });
|
||||
document.body.appendChild(menu);
|
||||
@@ -638,10 +648,155 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}, x, y, trigger || row);
|
||||
}
|
||||
|
||||
function ensureSearchModal() {
|
||||
var existing = document.querySelector('[data-testid="wolai-search-modal"]');
|
||||
if (existing instanceof HTMLElement) return existing;
|
||||
|
||||
var overlay = document.createElement('div');
|
||||
overlay.className = 'wolai-search-overlay';
|
||||
overlay.setAttribute('data-testid', 'wolai-search-modal');
|
||||
overlay.setAttribute('role', 'dialog');
|
||||
overlay.setAttribute('aria-modal', 'true');
|
||||
overlay.hidden = true;
|
||||
|
||||
overlay.innerHTML = '' +
|
||||
'<div class="wolai-search-dialog">' +
|
||||
'<div class="wolai-search-input-row">' +
|
||||
'<span class="material-symbols-outlined wolai-search-input-icon" data-icon="search" aria-hidden="true"></span>' +
|
||||
'<input data-testid="wolai-search-input" class="wolai-search-input" type="text" autocomplete="off" placeholder="在当前工作区中搜索" />' +
|
||||
'<button type="button" class="wolai-search-close" data-testid="wolai-search-close" aria-label="关闭搜索">×</button>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-search-options" aria-label="搜索选项">' +
|
||||
'<div class="wolai-search-options-left">' +
|
||||
'<span class="wolai-search-switch-control"><span>仅匹配标题</span><button type="button" class="wolai-search-switch is-on" data-search-switch="title" role="switch" aria-checked="true" aria-label="仅匹配标题"></button></span>' +
|
||||
'<span class="wolai-search-switch-control"><span>精确匹配</span><button type="button" class="wolai-search-switch" data-search-switch="exact" role="switch" aria-checked="false" aria-label="精确匹配"></button></span>' +
|
||||
'<span class="wolai-search-sort-control"><span>按编辑时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="updated" aria-label="按编辑时间范围">所有</button></span>' +
|
||||
'<span class="wolai-search-sort-control"><span>按创建时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="created" aria-label="按创建时间范围">所有</button></span>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-search-options-right">' +
|
||||
'<span class="wolai-search-switch-control"><span>页面内搜索</span><button type="button" class="wolai-search-switch is-on" data-search-switch="page" role="switch" aria-checked="true" aria-label="页面内搜索"></button></span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-search-result-meta" data-testid="wolai-search-result-meta"></div>' +
|
||||
'<div class="wolai-search-results" data-testid="wolai-search-results"></div>' +
|
||||
'</div>';
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
|
||||
var closeButton = overlay.querySelector('[data-testid="wolai-search-close"]');
|
||||
if (input) input.addEventListener('input', renderSearchResults);
|
||||
overlay.querySelectorAll('[data-search-switch]').forEach(function(button) {
|
||||
button.addEventListener('click', function() {
|
||||
var isOn = button.getAttribute('aria-checked') !== 'true';
|
||||
button.setAttribute('aria-checked', isOn ? 'true' : 'false');
|
||||
button.classList.toggle('is-on', isOn);
|
||||
});
|
||||
});
|
||||
if (closeButton) closeButton.addEventListener('click', closeSearchModal);
|
||||
overlay.addEventListener('click', function(event) {
|
||||
if (event.target === overlay) closeSearchModal();
|
||||
});
|
||||
return overlay;
|
||||
}
|
||||
|
||||
function searchText(value) {
|
||||
return String(value == null ? '' : value).replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function collectSearchCandidates() {
|
||||
var seen = Object.create(null);
|
||||
var candidates = [];
|
||||
function add(title, source) {
|
||||
title = searchText(title);
|
||||
if (!title || seen[title]) return;
|
||||
seen[title] = true;
|
||||
candidates.push({ title: title, source: source || '当前工作区' });
|
||||
}
|
||||
document.querySelectorAll('.tree-link-title, .wolai-row-title, [data-page-title-current="true"], .document-title-input, .document-read-view h1, .document-shell h1').forEach(function(node) {
|
||||
add(node.textContent, '页面');
|
||||
});
|
||||
document.querySelectorAll('.mnote-content h1, .mnote-content h2, .mnote-content h3, .mnote-content p, .mnote-content a').forEach(function(node) {
|
||||
add(node.textContent, '当前页面');
|
||||
});
|
||||
return candidates.slice(0, 120);
|
||||
}
|
||||
|
||||
function highlightSearchTitle(title, query) {
|
||||
var cleanTitle = searchText(title);
|
||||
var cleanQuery = searchText(query);
|
||||
if (!cleanQuery) return escapeHtml(cleanTitle);
|
||||
var index = cleanTitle.toLowerCase().indexOf(cleanQuery.toLowerCase());
|
||||
if (index < 0) return escapeHtml(cleanTitle);
|
||||
return escapeHtml(cleanTitle.slice(0, index)) +
|
||||
'<mark>' + escapeHtml(cleanTitle.slice(index, index + cleanQuery.length)) + '</mark>' +
|
||||
escapeHtml(cleanTitle.slice(index + cleanQuery.length));
|
||||
}
|
||||
|
||||
function renderSearchResults() {
|
||||
var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
|
||||
if (!(overlay instanceof HTMLElement)) return;
|
||||
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
|
||||
var meta = overlay.querySelector('[data-testid="wolai-search-result-meta"]');
|
||||
var results = overlay.querySelector('[data-testid="wolai-search-results"]');
|
||||
if (!input || !meta || !results) return;
|
||||
var query = searchText(input.value);
|
||||
var candidates = collectSearchCandidates();
|
||||
var filtered = query
|
||||
? candidates.filter(function(item) { return item.title.toLowerCase().indexOf(query.toLowerCase()) >= 0; })
|
||||
: candidates.slice(0, 8);
|
||||
filtered = filtered.slice(0, 100);
|
||||
meta.innerHTML = '<span>共 ' + filtered.length + ' 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
|
||||
if (!filtered.length) {
|
||||
results.innerHTML = '<div class="wolai-search-empty">暂无匹配结果</div>';
|
||||
return;
|
||||
}
|
||||
results.innerHTML = filtered.map(function(item) {
|
||||
return '<button type="button" class="wolai-search-result-row">' +
|
||||
'<span class="material-symbols-outlined wolai-search-result-icon" data-icon="link" aria-hidden="true"></span>' +
|
||||
'<span class="wolai-search-result-main"><span class="wolai-search-result-title">' + highlightSearchTitle(item.title, query) + '</span>' +
|
||||
'<span class="wolai-search-result-snippet">' + escapeHtml(item.source) + ' · 当前工作区</span></span>' +
|
||||
'</button>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function isSearchModalOpen() {
|
||||
var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
|
||||
return overlay instanceof HTMLElement && !overlay.hidden;
|
||||
}
|
||||
|
||||
function openSearchModal() {
|
||||
var overlay = ensureSearchModal();
|
||||
overlay.hidden = false;
|
||||
document.documentElement.setAttribute('data-mnote-search-modal-open', 'true');
|
||||
renderSearchResults();
|
||||
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
|
||||
if (input) {
|
||||
setTimeout(function() { input.focus(); input.select(); }, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function closeSearchModal() {
|
||||
var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
|
||||
if (overlay instanceof HTMLElement) overlay.hidden = true;
|
||||
document.documentElement.removeAttribute('data-mnote-search-modal-open');
|
||||
}
|
||||
|
||||
function toggleSearchModal() {
|
||||
if (isSearchModalOpen()) closeSearchModal();
|
||||
else openSearchModal();
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if (activeTreeContextMenu && activeTreeContextMenu.contains(e.target)) return;
|
||||
if (activeTreeContextMenu) closeTreeContextMenu();
|
||||
|
||||
var searchTrigger = closestAction(e.target, '[data-mnote-action="open-search-modal"]');
|
||||
if (searchTrigger) {
|
||||
e.preventDefault();
|
||||
openSearchModal();
|
||||
return;
|
||||
}
|
||||
|
||||
var tabTrigger = closestAction(e.target, '[data-mnote-sidebar-tree-tab]');
|
||||
if (tabTrigger) {
|
||||
e.preventDefault();
|
||||
@@ -753,7 +908,15 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') closeTreeContextMenu();
|
||||
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && event.key && event.key.toLowerCase() === 'p') {
|
||||
event.preventDefault();
|
||||
toggleSearchModal();
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
closeSearchModal();
|
||||
closeTreeContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
function readPageDragNodeId(event) {
|
||||
@@ -1146,7 +1309,7 @@ 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="搜索"><span class="material-symbols-outlined nav-icon" data-icon="search" aria-hidden="true"></span></a>
|
||||
<a href="/search" class:active={current_nav == "search"} title="搜索" aria-label="搜索" data-mnote-action="open-search-modal"><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>
|
||||
@@ -1173,7 +1336,7 @@ pub fn PageLayout(
|
||||
<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="搜索" data-mnote-action="open-search-modal"><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>
|
||||
|
||||
Reference in New Issue
Block a user