2026-04-29 12:24:44 +08:00
|
|
|
|
//! MNOTE 通用页面布局组件(Wolai / Notion 风格)
|
|
|
|
|
|
|
|
|
|
|
|
use leptos::prelude::*;
|
|
|
|
|
|
|
|
|
|
|
|
const SIDEBAR_TREE_JS: &str = r##"
|
|
|
|
|
|
(function(){
|
2026-04-30 05:46:36 +08:00
|
|
|
|
if (window.__mnoteSidebarTreeRuntimeStarted) return;
|
|
|
|
|
|
window.__mnoteSidebarTreeRuntimeStarted = true;
|
|
|
|
|
|
|
|
|
|
|
|
var PAGE_DRAG_MIME = 'application/x-mnote-page-tree-node';
|
|
|
|
|
|
var FILETREE_DRAG_MIME = 'application/x-mnote-filetree-row-ids';
|
2026-04-30 06:58:17 +08:00
|
|
|
|
var MNOTE_SIDEBAR_TREE_MODE_KEY = 'mnote.sidebar.tree.mode';
|
2026-04-30 05:46:36 +08:00
|
|
|
|
var mnoteNavigationInFlight = '';
|
|
|
|
|
|
var draggingPageNodeId = '';
|
|
|
|
|
|
var activePageDropRow = null;
|
|
|
|
|
|
var draggingFileTreeRowIds = [];
|
|
|
|
|
|
var activeFileTreeDropRow = null;
|
|
|
|
|
|
var projectionRefreshTimer = 0;
|
2026-04-30 06:58:17 +08:00
|
|
|
|
var activeTreeContextMenu = null;
|
2026-05-06 21:44:20 +08:00
|
|
|
|
var pageUiState = {
|
|
|
|
|
|
pageOptions: null,
|
|
|
|
|
|
historySnapshots: [],
|
|
|
|
|
|
pageSettingsOpen: false,
|
|
|
|
|
|
pageAiOpen: false,
|
|
|
|
|
|
pageAiBusy: false,
|
|
|
|
|
|
pageAiMessages: [],
|
|
|
|
|
|
pageAiSuggestionIndex: 0
|
|
|
|
|
|
};
|
2026-04-30 05:46:36 +08:00
|
|
|
|
|
2026-04-29 14:36:24 +08:00
|
|
|
|
function closestAction(target, selector) {
|
|
|
|
|
|
return target && typeof target.closest === 'function' ? target.closest(selector) : null;
|
|
|
|
|
|
}
|
2026-04-29 12:24:44 +08:00
|
|
|
|
|
2026-04-30 05:46:36 +08:00
|
|
|
|
function escapeHtml(value) {
|
|
|
|
|
|
return String(value == null ? '' : value)
|
|
|
|
|
|
.replace(/&/g, '&')
|
|
|
|
|
|
.replace(/</g, '<')
|
|
|
|
|
|
.replace(/>/g, '>')
|
|
|
|
|
|
.replace(/"/g, '"')
|
|
|
|
|
|
.replace(/'/g, ''');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function cssEscape(value) {
|
|
|
|
|
|
if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(value);
|
|
|
|
|
|
return String(value).replace(/["\\]/g, '\\$&');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 21:44:20 +08:00
|
|
|
|
function parseJsonScript(id) {
|
|
|
|
|
|
var node = document.getElementById(id);
|
|
|
|
|
|
if (!node) return null;
|
|
|
|
|
|
try {
|
|
|
|
|
|
return JSON.parse(node.textContent || 'null');
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 05:46:36 +08:00
|
|
|
|
function currentDocumentId() {
|
|
|
|
|
|
var match = window.location.pathname.match(/^\/documents\/([^\/]+)/);
|
|
|
|
|
|
return match ? decodeURIComponent(match[1]) : '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 21:44:20 +08:00
|
|
|
|
function currentPageAggregate() {
|
|
|
|
|
|
return parseJsonScript('__MNOTE_PAGE_AGGREGATE__') || {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function defaultPageOptions() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
wideLayout: false,
|
|
|
|
|
|
smallText: false,
|
|
|
|
|
|
layoutDensity: 'normal',
|
|
|
|
|
|
showHeadingNumbers: true,
|
|
|
|
|
|
showToc: false,
|
|
|
|
|
|
showStructure: false,
|
|
|
|
|
|
protectEditing: false,
|
|
|
|
|
|
showWordCount: true,
|
|
|
|
|
|
collapseBacklinks: false,
|
|
|
|
|
|
pageFont: 'default',
|
|
|
|
|
|
hideChildPages: false,
|
|
|
|
|
|
showBlockRefCount: false,
|
|
|
|
|
|
embedDefaultBlockId: null
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function currentPageOptions() {
|
|
|
|
|
|
if (pageUiState.pageOptions) return pageUiState.pageOptions;
|
|
|
|
|
|
var aggregate = currentPageAggregate();
|
|
|
|
|
|
var current = aggregate && aggregate.layout && aggregate.layout.pageOptions && typeof aggregate.layout.pageOptions === 'object'
|
|
|
|
|
|
? aggregate.layout.pageOptions
|
|
|
|
|
|
: null;
|
|
|
|
|
|
if (!current) {
|
|
|
|
|
|
return defaultPageOptions();
|
|
|
|
|
|
}
|
|
|
|
|
|
pageUiState.pageOptions = Object.assign(defaultPageOptions(), current);
|
|
|
|
|
|
return pageUiState.pageOptions;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function pageOptionIsSupported(name) {
|
|
|
|
|
|
return name === 'wideLayout'
|
|
|
|
|
|
|| name === 'smallText'
|
|
|
|
|
|
|| name === 'layoutDensity'
|
|
|
|
|
|
|| name === 'pageFont'
|
|
|
|
|
|
|| name === 'showHeadingNumbers';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function pageOptionDescription(name) {
|
|
|
|
|
|
if (name === 'wideLayout') return '自适应宽度';
|
|
|
|
|
|
if (name === 'smallText') return '小字体';
|
|
|
|
|
|
if (name === 'showToc') return '标题目录';
|
|
|
|
|
|
if (name === 'showHeadingNumbers') return '标题自动编号';
|
|
|
|
|
|
if (name === 'protectEditing') return '编辑保护';
|
|
|
|
|
|
if (name === 'collapseBacklinks') return '折叠反向引用';
|
|
|
|
|
|
if (name === 'hideChildPages') return '隐藏子页面';
|
|
|
|
|
|
if (name === 'showBlockRefCount') return '显示块引用数字';
|
|
|
|
|
|
return name;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function pageOptionHint(name) {
|
|
|
|
|
|
if (name === 'wideLayout') return '已接通:主内容列宽度立即变化';
|
|
|
|
|
|
if (name === 'smallText') return '已接通:正文排版会更紧凑';
|
|
|
|
|
|
if (name === 'showHeadingNumbers') return '已接通:标题前显示顺序编号';
|
|
|
|
|
|
if (name === 'layoutDensity') return '已接通:段落与列表间距会变化';
|
|
|
|
|
|
if (name === 'pageFont') return '已接通:当前页面字体会切换';
|
|
|
|
|
|
if (name === 'showToc') return '待接线:当前 Rust 壳还没有正式目录面板';
|
|
|
|
|
|
if (name === 'protectEditing') return '待接线:当前主编辑器只显示降级说明';
|
|
|
|
|
|
if (name === 'collapseBacklinks') return '待接线:当前 Rust 壳未挂回链面板';
|
|
|
|
|
|
if (name === 'hideChildPages') return '待接线:当前页面壳还没有子页面块显隐';
|
|
|
|
|
|
if (name === 'showBlockRefCount') return '待接线:当前页面壳未显示块引用计数';
|
|
|
|
|
|
return '待接线';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function ensureHistorySnapshotsSeeded() {
|
|
|
|
|
|
if (pageUiState.historySnapshots.length > 0) return;
|
|
|
|
|
|
var aggregate = currentPageAggregate();
|
|
|
|
|
|
var stats = aggregate && aggregate.stats && typeof aggregate.stats === 'object' ? aggregate.stats : {};
|
|
|
|
|
|
pageUiState.historySnapshots = [{
|
|
|
|
|
|
id: 'snapshot-initial',
|
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
|
stats: {
|
|
|
|
|
|
wordCount: Number(stats.wordCount || 0),
|
|
|
|
|
|
characterCount: Number(stats.characterCount || 0),
|
|
|
|
|
|
blockCount: Number(stats.blockCount || 0),
|
|
|
|
|
|
todoTotal: Number(stats.todoTotal || 0),
|
|
|
|
|
|
todoDone: Number(stats.todoDone || 0)
|
|
|
|
|
|
}
|
|
|
|
|
|
}];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function computeLivePageStats() {
|
|
|
|
|
|
var aggregate = currentPageAggregate();
|
|
|
|
|
|
var fallbackStats = aggregate && aggregate.stats && typeof aggregate.stats === 'object' ? aggregate.stats : {};
|
|
|
|
|
|
var editor = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror');
|
|
|
|
|
|
if (!(editor instanceof HTMLElement)) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
wordCount: Number(fallbackStats.wordCount || 0),
|
|
|
|
|
|
characterCount: Number(fallbackStats.characterCount || 0),
|
|
|
|
|
|
blockCount: Number(fallbackStats.blockCount || 0),
|
|
|
|
|
|
todoTotal: Number(fallbackStats.todoTotal || 0),
|
|
|
|
|
|
todoDone: Number(fallbackStats.todoDone || 0)
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
var text = (editor.textContent || '').trim();
|
|
|
|
|
|
var compact = text.replace(/\s+/g, ' ').trim();
|
|
|
|
|
|
var wordCount = compact ? compact.split(' ').filter(Boolean).length : 0;
|
|
|
|
|
|
var characterCount = text.replace(/\s/g, '').length;
|
|
|
|
|
|
var blockCount = editor.querySelectorAll(':scope > *').length;
|
|
|
|
|
|
var todos = Array.from(editor.querySelectorAll('input[type="checkbox"]'));
|
|
|
|
|
|
return {
|
|
|
|
|
|
wordCount: wordCount || Number(fallbackStats.wordCount || 0),
|
|
|
|
|
|
characterCount: characterCount || Number(fallbackStats.characterCount || 0),
|
|
|
|
|
|
blockCount: blockCount || Number(fallbackStats.blockCount || 0),
|
|
|
|
|
|
todoTotal: todos.length || Number(fallbackStats.todoTotal || 0),
|
|
|
|
|
|
todoDone: todos.filter(function(node){ return node.checked; }).length || Number(fallbackStats.todoDone || 0)
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function recordPageHistorySnapshot(reason, stats) {
|
|
|
|
|
|
ensureHistorySnapshotsSeeded();
|
|
|
|
|
|
pageUiState.historySnapshots = [{
|
|
|
|
|
|
id: 'snapshot-' + Date.now(),
|
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
|
reason: reason || 'save',
|
|
|
|
|
|
stats: stats || computeLivePageStats()
|
|
|
|
|
|
}].concat(pageUiState.historySnapshots).slice(0, 15);
|
|
|
|
|
|
renderPageHistoryDrawer();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applyPageOptionsToShell() {
|
|
|
|
|
|
var options = currentPageOptions();
|
|
|
|
|
|
var shell = document.querySelector('.document-shell');
|
|
|
|
|
|
var editorRoot = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
|
|
|
|
|
|
var editorSurface = editorRoot && editorRoot.querySelector('.editor-surface');
|
|
|
|
|
|
if (shell instanceof HTMLElement) {
|
|
|
|
|
|
shell.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
|
|
|
|
|
|
shell.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
|
|
|
|
|
|
shell.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
|
|
|
|
|
|
shell.setAttribute('data-page-font', String(options.pageFont || 'default'));
|
|
|
|
|
|
shell.setAttribute('data-page-show-heading-numbers', String(Boolean(options.showHeadingNumbers)));
|
|
|
|
|
|
shell.style.width = '100%';
|
|
|
|
|
|
shell.style.maxWidth = options.wideLayout ? '980px' : '760px';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (editorRoot instanceof HTMLElement) {
|
|
|
|
|
|
editorRoot.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
|
|
|
|
|
|
editorRoot.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
|
|
|
|
|
|
editorRoot.setAttribute('data-page-show-heading-numbers', String(Boolean(options.showHeadingNumbers)));
|
|
|
|
|
|
editorRoot.setAttribute('data-page-embed-default-block-id', options.embedDefaultBlockId == null ? '' : String(options.embedDefaultBlockId));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (editorSurface instanceof HTMLElement) {
|
|
|
|
|
|
editorSurface.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
|
|
|
|
|
|
editorSurface.setAttribute('data-page-font', String(options.pageFont || 'default'));
|
|
|
|
|
|
editorSurface.setAttribute('data-show-heading-numbers', String(Boolean(options.showHeadingNumbers)));
|
|
|
|
|
|
}
|
|
|
|
|
|
document.documentElement.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
|
|
|
|
|
|
document.documentElement.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
|
|
|
|
|
|
document.documentElement.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
|
|
|
|
|
|
document.documentElement.setAttribute('data-page-font', String(options.pageFont || 'default'));
|
|
|
|
|
|
document.documentElement.setAttribute('data-page-show-heading-numbers', String(Boolean(options.showHeadingNumbers)));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 06:58:17 +08:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 14:36:24 +08:00
|
|
|
|
function resolveWorkspaceId(trigger) {
|
|
|
|
|
|
var direct = (trigger.getAttribute('data-workspace-id') || '').trim();
|
|
|
|
|
|
if (direct) return direct;
|
|
|
|
|
|
var root = trigger.closest('[data-workspace-id]');
|
2026-04-30 05:46:36 +08:00
|
|
|
|
if (root) {
|
|
|
|
|
|
var value = (root.getAttribute('data-workspace-id') || '').trim();
|
|
|
|
|
|
if (value) return value;
|
|
|
|
|
|
}
|
|
|
|
|
|
return (new URLSearchParams(window.location.search).get('workspaceId') || 'default').trim() || 'default';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setCommandPending(trigger, pending) {
|
|
|
|
|
|
if (!(trigger instanceof HTMLElement)) return;
|
|
|
|
|
|
trigger.setAttribute('data-pending', pending ? 'true' : 'false');
|
|
|
|
|
|
if ('disabled' in trigger) {
|
|
|
|
|
|
if (pending) trigger.setAttribute('disabled', 'disabled');
|
|
|
|
|
|
else trigger.removeAttribute('disabled');
|
|
|
|
|
|
}
|
2026-04-29 14:36:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function dispatchTreeCommand(trigger, body) {
|
2026-04-30 05:46:36 +08:00
|
|
|
|
setCommandPending(trigger, true);
|
2026-04-29 14:36:24 +08:00
|
|
|
|
try {
|
|
|
|
|
|
var response = await fetch('/api/tree/commands', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'content-type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify(body)
|
|
|
|
|
|
});
|
|
|
|
|
|
var payload = await response.json().catch(function(){ return null; });
|
|
|
|
|
|
if (!response.ok || !payload || !payload.result) {
|
|
|
|
|
|
throw new Error((payload && payload.message) || 'tree_command_failed_' + response.status);
|
2026-04-29 12:24:44 +08:00
|
|
|
|
}
|
2026-04-30 05:46:36 +08:00
|
|
|
|
window.dispatchEvent(new CustomEvent('tree:local-command', { detail: { body: body, result: payload.result } }));
|
2026-04-29 14:36:24 +08:00
|
|
|
|
return payload.result;
|
|
|
|
|
|
} catch (error) {
|
2026-04-30 05:46:36 +08:00
|
|
|
|
setCommandPending(trigger, false);
|
|
|
|
|
|
if (trigger instanceof HTMLElement) {
|
|
|
|
|
|
trigger.setAttribute('data-error', error instanceof Error ? error.message : String(error));
|
|
|
|
|
|
}
|
2026-04-29 14:36:24 +08:00
|
|
|
|
throw error;
|
2026-04-29 12:24:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 06:58:17 +08:00
|
|
|
|
function navigateToDocument(nodeId, workspaceId, options) {
|
2026-04-30 05:46:36 +08:00
|
|
|
|
if (!nodeId) return;
|
2026-04-30 06:58:17 +08:00
|
|
|
|
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) {
|
2026-05-06 21:44:20 +08:00
|
|
|
|
if (row.getAttribute('data-shell-mode') === 'filetree') {
|
|
|
|
|
|
row.setAttribute('data-selected', String(row.getAttribute('data-row-id') === 'index:' + nodeId));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
row.setAttribute('data-active', 'true');
|
|
|
|
|
|
}
|
2026-04-30 06:58:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
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;
|
2026-04-30 05:46:36 +08:00
|
|
|
|
if (mnoteNavigationInFlight === url) return;
|
|
|
|
|
|
mnoteNavigationInFlight = url;
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-navigation-pending', 'true');
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-navigation-target', nodeId);
|
|
|
|
|
|
if (window.__mnoteTreeLiveEventSource && typeof window.__mnoteTreeLiveEventSource.close === 'function') {
|
|
|
|
|
|
window.__mnoteTreeLiveEventSource.close();
|
|
|
|
|
|
}
|
|
|
|
|
|
window.location.assign(url);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 14:36:24 +08:00
|
|
|
|
async function createPage(trigger, parentId) {
|
|
|
|
|
|
var workspaceId = resolveWorkspaceId(trigger);
|
|
|
|
|
|
var effectiveParentId = (parentId || trigger.getAttribute('data-parent-id') || '').trim();
|
|
|
|
|
|
if (!workspaceId) return;
|
|
|
|
|
|
var result = await dispatchTreeCommand(trigger, {
|
|
|
|
|
|
action: 'create',
|
|
|
|
|
|
workspaceId: workspaceId,
|
|
|
|
|
|
parentId: effectiveParentId || null,
|
|
|
|
|
|
title: '新页面'
|
|
|
|
|
|
});
|
|
|
|
|
|
var nextWorkspaceId = result.workspaceId || workspaceId;
|
2026-04-30 06:58:17 +08:00
|
|
|
|
navigateToDocument(result.documentId, nextWorkspaceId, { treeView: activeSidebarTreeMode() });
|
2026-04-29 14:36:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 06:58:17 +08:00
|
|
|
|
function applySidebarTreeTab(mode, shell) {
|
|
|
|
|
|
mode = persistSidebarTreeMode(mode);
|
|
|
|
|
|
shell = shell || document.querySelector('[data-testid="wolai-sidebar-page-tree-shell"]');
|
2026-04-29 16:23:49 +08:00
|
|
|
|
if (!shell) return;
|
|
|
|
|
|
var tabs = shell.querySelectorAll('[data-mnote-sidebar-tree-tab]');
|
|
|
|
|
|
for (var i = 0; i < tabs.length; i++) {
|
|
|
|
|
|
var isActive = tabs[i].getAttribute('data-mnote-sidebar-tree-tab') === mode;
|
|
|
|
|
|
tabs[i].setAttribute('aria-selected', isActive ? 'true' : 'false');
|
|
|
|
|
|
tabs[i].classList.toggle('wolai-sidebar-tab-active', isActive);
|
|
|
|
|
|
tabs[i].classList.toggle('wolai-sidebar-tab-muted', !isActive);
|
|
|
|
|
|
}
|
|
|
|
|
|
var panels = shell.querySelectorAll('[data-mnote-sidebar-tree-panel]');
|
|
|
|
|
|
for (var j = 0; j < panels.length; j++) {
|
|
|
|
|
|
var isCurrentPanel = panels[j].getAttribute('data-mnote-sidebar-tree-panel') === mode;
|
|
|
|
|
|
panels[j].hidden = !isCurrentPanel;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 06:58:17 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 05:46:36 +08:00
|
|
|
|
function updateTitleEverywhere(documentId, title) {
|
|
|
|
|
|
if (!documentId) return;
|
2026-04-30 06:58:17 +08:00
|
|
|
|
var escaped = cssEscape(documentId);
|
2026-05-06 21:44:20 +08:00
|
|
|
|
var escapedDocRowId = cssEscape('doc:' + documentId);
|
2026-04-30 05:46:36 +08:00
|
|
|
|
var selectors = [
|
2026-05-06 21:44:20 +08:00
|
|
|
|
'.tree-row[data-shell-mode="page"][data-node-id="' + escaped + '"] > .tree-link > .tree-link-title',
|
|
|
|
|
|
'.tree-row[data-shell-mode="filetree"][data-row-id="' + escapedDocRowId + '"] > .tree-link > .tree-link-title',
|
2026-04-30 06:58:17 +08:00
|
|
|
|
'.wolai-page-row[data-node-id="' + escaped + '"] > .wolai-row-title',
|
|
|
|
|
|
'a[href="/documents/' + escaped + '"] > .wolai-row-title',
|
|
|
|
|
|
'a[href^="/documents/' + escaped + '?"] > .wolai-row-title'
|
2026-04-30 05:46:36 +08:00
|
|
|
|
];
|
|
|
|
|
|
selectors.forEach(function(selector) {
|
|
|
|
|
|
document.querySelectorAll(selector).forEach(function(node) {
|
|
|
|
|
|
if (node instanceof HTMLElement) node.textContent = title;
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function readProjection(value) {
|
|
|
|
|
|
if (!value || typeof value !== 'object') return null;
|
|
|
|
|
|
if (value.result && typeof value.result === 'object') return value.result;
|
|
|
|
|
|
if (value.snapshot && value.snapshot.tree) return value.snapshot.tree;
|
|
|
|
|
|
if (value.data && value.data.tree) return value.data.tree;
|
|
|
|
|
|
if (value.tree && typeof value.tree === 'object') return value.tree;
|
|
|
|
|
|
return value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function projectionItems(projection) {
|
|
|
|
|
|
var resolved = readProjection(projection);
|
|
|
|
|
|
return resolved && Array.isArray(resolved.items) ? resolved.items : [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function nodeIdOf(item) {
|
|
|
|
|
|
return String(item && (item.nodeId || item.id || item.documentId) || '').trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function rowIdOf(item) {
|
|
|
|
|
|
return String(item && (item.rowId || item.nodeId || item.id) || '').trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parentIdOf(item) {
|
|
|
|
|
|
return String(item && (item.parentNodeId || item.parentId || '') || '').trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function titleOf(item) {
|
|
|
|
|
|
return String(item && item.title || '无标题').trim() || '无标题';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function groupRowsByParent(rows) {
|
|
|
|
|
|
var ids = new Set(rows.map(nodeIdOf).filter(Boolean));
|
|
|
|
|
|
var grouped = new Map();
|
|
|
|
|
|
rows.forEach(function(item) {
|
|
|
|
|
|
var parentId = parentIdOf(item);
|
|
|
|
|
|
if (!ids.has(parentId)) parentId = '';
|
|
|
|
|
|
if (!grouped.has(parentId)) grouped.set(parentId, []);
|
|
|
|
|
|
grouped.get(parentId).push(item);
|
|
|
|
|
|
});
|
|
|
|
|
|
return grouped;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 16:18:54 +08:00
|
|
|
|
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);
|
2026-04-30 05:46:36 +08:00
|
|
|
|
return (grouped.get(parentId) || []).map(function(item) {
|
|
|
|
|
|
var nodeId = nodeIdOf(item);
|
|
|
|
|
|
var title = titleOf(item);
|
2026-04-30 16:18:54 +08:00
|
|
|
|
var depth = computedDepth;
|
2026-04-30 05:46:36 +08:00
|
|
|
|
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
|
2026-04-30 16:18:54 +08:00
|
|
|
|
? '<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>'
|
2026-04-30 05:46:36 +08:00
|
|
|
|
: '<span class="tree-spacer" aria-hidden="true"></span>';
|
|
|
|
|
|
var childHtml = expandable && expanded
|
2026-04-30 16:18:54 +08:00
|
|
|
|
? '<ul class="tree-children">' + renderPageRows(nodeId, grouped, activeId, depth + 1) + '</ul>'
|
2026-04-30 05:46:36 +08:00
|
|
|
|
: '';
|
2026-04-30 16:18:54 +08:00
|
|
|
|
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>';
|
2026-04-30 05:46:36 +08:00
|
|
|
|
}).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderPageProjection(projection) {
|
|
|
|
|
|
var tree = document.getElementById('sidebar-tree-root');
|
|
|
|
|
|
if (!tree) return false;
|
|
|
|
|
|
var rows = projectionItems(projection).filter(function(item) {
|
|
|
|
|
|
return String(item.rowKind || 'document') === 'document';
|
|
|
|
|
|
});
|
|
|
|
|
|
var activeId = currentDocumentId();
|
2026-04-30 16:18:54 +08:00
|
|
|
|
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>';
|
2026-04-30 05:46:36 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function fileDocumentId(item) {
|
|
|
|
|
|
var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
|
|
|
|
|
|
if (meta.documentId) return String(meta.documentId).trim();
|
|
|
|
|
|
if (item && item.documentId) return String(item.documentId).trim();
|
|
|
|
|
|
if (item && item.rowKind === 'document') return nodeIdOf(item);
|
|
|
|
|
|
if (item && item.rowKind === 'index') return nodeIdOf(item).replace(/^index:/, '');
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function fileAssetId(item) {
|
|
|
|
|
|
var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
|
|
|
|
|
|
if (meta.assetId) return String(meta.assetId).trim();
|
|
|
|
|
|
if (item && item.assetId) return String(item.assetId).trim();
|
|
|
|
|
|
if (item && item.rowKind === 'asset') return nodeIdOf(item).replace(/^asset:/, '');
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function iconKindOf(item) {
|
|
|
|
|
|
return String(item && (item.iconHint || item.rowKind || 'file') || 'file').trim() || 'file';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderFileRows(parentId, grouped, activeId) {
|
|
|
|
|
|
return (grouped.get(parentId) || []).map(function(item) {
|
|
|
|
|
|
var nodeId = nodeIdOf(item);
|
|
|
|
|
|
var rowId = rowIdOf(item);
|
|
|
|
|
|
var rowKind = String(item.rowKind || 'document');
|
|
|
|
|
|
var title = titleOf(item);
|
|
|
|
|
|
var depth = Number(item.depth || 0);
|
|
|
|
|
|
var parent = parentIdOf(item);
|
|
|
|
|
|
var documentId = fileDocumentId(item);
|
|
|
|
|
|
var assetId = fileAssetId(item);
|
|
|
|
|
|
var children = grouped.get(nodeId) || [];
|
|
|
|
|
|
var expandable = Boolean(item.expandable || item.childCount > 0 || children.length);
|
|
|
|
|
|
var expanded = expandable && item.expandedByDefault !== false;
|
2026-05-06 21:44:20 +08:00
|
|
|
|
var selected = rowId === 'index:' + activeId;
|
2026-04-30 05:46:36 +08:00
|
|
|
|
var testId = rowKind === 'document' ? 'filetree-doc-row' : rowKind === 'index' ? 'filetree-index-row' : 'filetree-asset-row';
|
|
|
|
|
|
var toggle = expandable
|
|
|
|
|
|
? '<button type="button" class="tree-toggle" data-testid="filetree-toggle" data-rust-action="toggle" data-row-id="' + escapeHtml(rowId) + '" aria-label="' + (expanded ? '折叠 ' : '展开 ') + escapeHtml(title) + '">' + (expanded ? '▾' : '▸') + '</button>'
|
|
|
|
|
|
: '<span class="tree-spacer" aria-hidden="true"></span>';
|
|
|
|
|
|
var createAction = rowKind === 'document'
|
|
|
|
|
|
? '<button type="button" class="tree-action" data-testid="filetree-create" data-rust-action="create" data-row-id="' + escapeHtml(rowId) + '" data-node-id="' + escapeHtml(documentId || nodeId) + '" aria-label="新建子页面">+</button>'
|
|
|
|
|
|
: '';
|
|
|
|
|
|
var childHtml = expandable && expanded
|
|
|
|
|
|
? '<ul class="tree-children">' + renderFileRows(nodeId, grouped, activeId) + '</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="filetree" data-testid="' + testId + '" data-row-id="' + escapeHtml(rowId) + '" data-row-kind="' + escapeHtml(rowKind) + '" data-node-id="' + escapeHtml(nodeId) + '"' + (parent ? ' data-parent-id="' + escapeHtml(parent) + '"' : '') + ' data-document-id="' + escapeHtml(documentId) + '" data-doc-id="' + escapeHtml(documentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-shell-mode="filetree" data-selected="' + String(selected) + '" data-active="false" draggable="true">' + toggle + '<span class="tree-kind-badge" data-kind="' + escapeHtml(iconKindOf(item)) + '" aria-hidden="true"></span><button type="button" class="tree-link" data-rust-action="open" data-row-id="' + escapeHtml(rowId) + '" data-document-id="' + escapeHtml(documentId) + '" data-asset-id="' + escapeHtml(assetId) + '"><span class="tree-link-title">' + escapeHtml(title) + '</span></button><div class="tree-actions">' + createAction + '<button type="button" class="tree-action" data-testid="filetree-action-menu" data-rust-action="menu" data-row-id="' + escapeHtml(rowId) + '" aria-label="更多操作">…</button></div></div>' + childHtml + '</li>';
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderFileProjection(projection) {
|
|
|
|
|
|
var tree = document.getElementById('sidebar-file-tree-root');
|
|
|
|
|
|
if (!tree) return false;
|
|
|
|
|
|
var rows = projectionItems(projection);
|
|
|
|
|
|
var activeId = currentDocumentId();
|
|
|
|
|
|
tree.innerHTML = '<ul class="tree-root" role="tree" data-rust-filetree-renderer="initial_v1">' + (rows.length ? renderFileRows('', groupRowsByParent(rows), activeId) : '<li class="tree-empty" data-rust-rendered-row="filetree-empty">暂无文件或页面</li>') + '</ul>';
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function fetchProjection(path, workspaceId) {
|
|
|
|
|
|
var url = new URL(path, window.location.origin);
|
|
|
|
|
|
url.searchParams.set('workspaceId', workspaceId || 'default');
|
|
|
|
|
|
url.searchParams.set('depth', '99');
|
|
|
|
|
|
var response = await fetch(url.toString(), { cache: 'no-store' });
|
|
|
|
|
|
var payload = await response.json().catch(function(){ return null; });
|
|
|
|
|
|
if (!response.ok || !payload || !payload.result) throw new Error('tree_projection_failed_' + response.status);
|
|
|
|
|
|
return payload.result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function scheduleProjectionRefresh(workspaceId) {
|
|
|
|
|
|
if (projectionRefreshTimer) window.clearTimeout(projectionRefreshTimer);
|
|
|
|
|
|
projectionRefreshTimer = window.setTimeout(function() {
|
|
|
|
|
|
projectionRefreshTimer = 0;
|
|
|
|
|
|
var resolvedWorkspaceId = workspaceId || resolveWorkspaceId(document.body);
|
|
|
|
|
|
Promise.all([
|
|
|
|
|
|
fetchProjection('/api/tree/projections/sidebar', resolvedWorkspaceId).then(renderPageProjection),
|
|
|
|
|
|
fetchProjection('/api/tree/projections/file', resolvedWorkspaceId).then(renderFileProjection)
|
|
|
|
|
|
]).then(function() {
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'true');
|
|
|
|
|
|
}).catch(function(error) {
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-tree-live-apply-error', error instanceof Error ? error.message : String(error));
|
|
|
|
|
|
});
|
|
|
|
|
|
}, 180);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleChildren(row, button) {
|
|
|
|
|
|
var li = row && row.parentElement;
|
|
|
|
|
|
var children = li ? li.querySelector(':scope > .tree-children') : null;
|
|
|
|
|
|
if (!children) return;
|
|
|
|
|
|
children.classList.toggle('tree-children--collapsed');
|
|
|
|
|
|
var collapsed = children.classList.contains('tree-children--collapsed');
|
|
|
|
|
|
row.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
2026-04-30 16:18:54 +08:00
|
|
|
|
if (button && button.getAttribute('data-testid') === 'tree-node-toggle') {
|
|
|
|
|
|
button.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
|
|
|
|
|
} else if (button) {
|
|
|
|
|
|
button.textContent = collapsed ? '▸' : '▾';
|
|
|
|
|
|
}
|
2026-04-30 05:46:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function dispatchSidebarEvent(name, detail) {
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-last-tree-action', name);
|
|
|
|
|
|
window.dispatchEvent(new CustomEvent(name, { detail: detail }));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 06:58:17 +08:00
|
|
|
|
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' }
|
|
|
|
|
|
] : [
|
2026-04-30 16:18:54 +08:00
|
|
|
|
{ action: 'open-right', icon: 'right_panel_open', label: '在右侧边栏打开', shortcut: 'Alt+' },
|
|
|
|
|
|
{ separator: true },
|
2026-04-30 06:58:17 +08:00
|
|
|
|
{ action: 'move', icon: 'drive_file_move', label: '移动到...' },
|
|
|
|
|
|
{ action: 'embed', icon: 'account_tree', label: '嵌入到...' },
|
2026-04-30 16:18:54 +08:00
|
|
|
|
{ separator: true },
|
2026-04-30 06:58:17 +08:00
|
|
|
|
{ action: 'copy-link', icon: 'link', label: '复制访问链接' },
|
|
|
|
|
|
{ action: 'copy-reference-inline', icon: 'content_copy', label: '复制页面引用链接' },
|
2026-04-30 16:18:54 +08:00
|
|
|
|
{ action: 'copy-id', icon: 'tag', label: '复制页面ID' },
|
|
|
|
|
|
{ separator: true },
|
2026-04-30 06:58:17 +08:00
|
|
|
|
{ action: 'duplicate', icon: 'file_copy', label: '拷贝副本' },
|
|
|
|
|
|
{ separator: true },
|
|
|
|
|
|
{ action: 'rename', icon: 'edit', label: '重命名' },
|
2026-04-30 16:18:54 +08:00
|
|
|
|
{ separator: true },
|
|
|
|
|
|
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true }
|
2026-04-30 06:58:17 +08:00
|
|
|
|
];
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 16:18:54 +08:00
|
|
|
|
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>' +
|
2026-04-30 18:36:50 +08:00
|
|
|
|
'<input data-testid="wolai-search-input" class="wolai-search-input" type="search" autocomplete="off" placeholder="在当前工作区中搜索" />' +
|
2026-04-30 16:18:54 +08:00
|
|
|
|
'<button type="button" class="wolai-search-close" data-testid="wolai-search-close" aria-label="关闭搜索">×</button>' +
|
|
|
|
|
|
'</div>' +
|
2026-04-30 18:36:50 +08:00
|
|
|
|
'<div class="wolai-search-options" data-testid="wolai-search-options" aria-label="搜索选项" hidden>' +
|
2026-04-30 16:18:54 +08:00
|
|
|
|
'<div class="wolai-search-options-left">' +
|
2026-04-30 18:36:50 +08:00
|
|
|
|
'<span class="wolai-search-switch-control"><span>仅匹配标题</span><button type="button" class="wolai-search-switch" data-search-switch="title" role="switch" aria-checked="false" aria-label="仅匹配标题"></button></span>' +
|
2026-04-30 16:18:54 +08:00
|
|
|
|
'<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">' +
|
2026-04-30 18:36:50 +08:00
|
|
|
|
'<span class="wolai-search-switch-control"><span>页面内搜索</span><button type="button" class="wolai-search-switch" data-search-switch="page" role="switch" aria-checked="false" aria-label="页面内搜索"></button></span>' +
|
2026-04-30 16:18:54 +08:00
|
|
|
|
'</div>' +
|
|
|
|
|
|
'</div>' +
|
|
|
|
|
|
'<div class="wolai-search-result-meta" data-testid="wolai-search-result-meta"></div>' +
|
2026-04-30 18:36:50 +08:00
|
|
|
|
'<div class="wolai-search-results" data-testid="wolai-search-results" data-search-results-owner="rust-kernel"></div>' +
|
2026-04-30 16:18:54 +08:00
|
|
|
|
'</div>';
|
|
|
|
|
|
|
|
|
|
|
|
document.body.appendChild(overlay);
|
|
|
|
|
|
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
|
|
|
|
|
|
var closeButton = overlay.querySelector('[data-testid="wolai-search-close"]');
|
2026-04-30 18:36:50 +08:00
|
|
|
|
if (input) input.addEventListener('input', scheduleSearchResultsRender);
|
2026-04-30 16:18:54 +08:00
|
|
|
|
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);
|
2026-04-30 18:36:50 +08:00
|
|
|
|
scheduleSearchResultsRender();
|
2026-04-30 16:18:54 +08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
if (closeButton) closeButton.addEventListener('click', closeSearchModal);
|
|
|
|
|
|
overlay.addEventListener('click', function(event) {
|
|
|
|
|
|
if (event.target === overlay) closeSearchModal();
|
|
|
|
|
|
});
|
|
|
|
|
|
return overlay;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 18:36:50 +08:00
|
|
|
|
var activeSearchRequestId = 0;
|
|
|
|
|
|
var searchRenderTimer = 0;
|
|
|
|
|
|
|
2026-04-30 16:18:54 +08:00
|
|
|
|
function searchText(value) {
|
|
|
|
|
|
return String(value == null ? '' : value).replace(/\s+/g, ' ').trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 18:36:50 +08:00
|
|
|
|
function currentWorkspaceName() {
|
|
|
|
|
|
var name = document.querySelector('.sidebar-workspace-name');
|
|
|
|
|
|
return searchText(name && name.textContent) || '当前工作区';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function currentDocumentId() {
|
|
|
|
|
|
var shell = document.querySelector('.document-shell[data-document-id]');
|
|
|
|
|
|
var bodyId = document.body && document.body.getAttribute('data-document-id');
|
|
|
|
|
|
return searchText(shell && shell.getAttribute('data-document-id')) || searchText(bodyId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function searchSwitchValue(overlay, name) {
|
|
|
|
|
|
var button = overlay.querySelector('[data-search-switch="' + name + '"]');
|
|
|
|
|
|
return button instanceof HTMLElement && button.getAttribute('aria-checked') === 'true';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function highlightedHtml(value) {
|
|
|
|
|
|
return escapeHtml(value)
|
|
|
|
|
|
.replace(/<mark>/g, '<mark>')
|
|
|
|
|
|
.replace(/<\/mark>/g, '</mark>');
|
2026-04-30 16:18:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 18:36:50 +08:00
|
|
|
|
function renderSearchRecentState(overlay) {
|
|
|
|
|
|
var options = overlay.querySelector('[data-testid="wolai-search-options"]');
|
|
|
|
|
|
var meta = overlay.querySelector('[data-testid="wolai-search-result-meta"]');
|
|
|
|
|
|
var results = overlay.querySelector('[data-testid="wolai-search-results"]');
|
|
|
|
|
|
if (options instanceof HTMLElement) options.hidden = true;
|
|
|
|
|
|
if (meta) meta.innerHTML = '<span>最近浏览</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
|
|
|
|
|
|
if (results) {
|
|
|
|
|
|
results.innerHTML = '<div class="wolai-search-recent" data-testid="wolai-search-recent">暂无最近浏览</div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function scheduleSearchResultsRender() {
|
|
|
|
|
|
window.clearTimeout(searchRenderTimer);
|
|
|
|
|
|
searchRenderTimer = window.setTimeout(function() { void renderSearchResults(); }, 120);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function renderSearchResults() {
|
2026-04-30 16:18:54 +08:00
|
|
|
|
var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
|
|
|
|
|
|
if (!(overlay instanceof HTMLElement)) return;
|
|
|
|
|
|
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
|
2026-04-30 18:36:50 +08:00
|
|
|
|
var options = overlay.querySelector('[data-testid="wolai-search-options"]');
|
2026-04-30 16:18:54 +08:00
|
|
|
|
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);
|
2026-04-30 18:36:50 +08:00
|
|
|
|
if (!query) {
|
|
|
|
|
|
activeSearchRequestId += 1;
|
|
|
|
|
|
renderSearchRecentState(overlay);
|
2026-04-30 16:18:54 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-04-30 18:36:50 +08:00
|
|
|
|
if (options instanceof HTMLElement) options.hidden = false;
|
|
|
|
|
|
var requestId = ++activeSearchRequestId;
|
|
|
|
|
|
meta.innerHTML = '<span>搜索中…</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
|
|
|
|
|
|
results.innerHTML = '<div class="wolai-search-empty" data-search-loading="true">搜索中…</div>';
|
|
|
|
|
|
try {
|
|
|
|
|
|
var response = await fetch('/api/search/documents', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'content-type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
workspaceId: resolveWorkspaceId(document.body),
|
|
|
|
|
|
documentId: currentDocumentId() || null,
|
|
|
|
|
|
query: query,
|
|
|
|
|
|
limit: 30,
|
|
|
|
|
|
filters: {
|
|
|
|
|
|
titleOnly: searchSwitchValue(overlay, 'title'),
|
|
|
|
|
|
exact: searchSwitchValue(overlay, 'exact'),
|
|
|
|
|
|
includeOcr: false,
|
|
|
|
|
|
onlyCurrentPage: searchSwitchValue(overlay, 'page'),
|
|
|
|
|
|
timeRange: 'any',
|
|
|
|
|
|
timeField: 'updated'
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
var payload = await response.json();
|
|
|
|
|
|
if (requestId !== activeSearchRequestId) return;
|
|
|
|
|
|
var items = Array.isArray(payload.results) ? payload.results : [];
|
|
|
|
|
|
meta.innerHTML = '<span>共 ' + items.length + ' 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
|
|
|
|
|
|
if (!items.length) {
|
|
|
|
|
|
results.innerHTML = '<div class="wolai-search-empty" data-search-empty="true">没有搜索结果</div>';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
results.innerHTML = items.map(function(item) {
|
|
|
|
|
|
var title = searchText(item.title || item.name || item.documentTitle || '无标题');
|
|
|
|
|
|
var snippet = searchText(item.snippet || (Array.isArray(item.evidence) && item.evidence[0] && item.evidence[0].snippet) || '');
|
|
|
|
|
|
var path = searchText(item.path || item.parentTitle || item.workspaceName || currentWorkspaceName());
|
|
|
|
|
|
return '<button type="button" class="wolai-search-result-row" data-testid="wolai-search-result-row" data-search-result-owner="rust-kernel" data-document-id="' + escapeHtml(item.documentId || item.nodeId || item.id || '') + '">' +
|
|
|
|
|
|
'<span class="material-symbols-outlined wolai-search-result-icon" data-icon="article" aria-hidden="true"></span>' +
|
|
|
|
|
|
'<span class="wolai-search-result-main"><span class="wolai-search-result-title">' + highlightSearchTitle(title, query) + '</span>' +
|
|
|
|
|
|
'<span class="wolai-search-result-snippet">' + (snippet ? highlightedHtml(snippet) : escapeHtml(path)) + '</span></span>' +
|
|
|
|
|
|
'</button>';
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (requestId !== activeSearchRequestId) return;
|
|
|
|
|
|
meta.innerHTML = '<span>共 0 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
|
|
|
|
|
|
results.innerHTML = '<div class="wolai-search-empty" data-search-empty="true">没有搜索结果</div>';
|
|
|
|
|
|
}
|
2026-04-30 16:18:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
|
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
|
2026-04-30 18:36:50 +08:00
|
|
|
|
if (input) input.setAttribute('placeholder', '在 ' + currentWorkspaceName() + ' 中搜索');
|
|
|
|
|
|
void renderSearchResults();
|
2026-04-30 16:18:54 +08:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 21:44:20 +08:00
|
|
|
|
function updatePageSettingsTriggerState() {
|
|
|
|
|
|
var trigger = document.querySelector('[data-testid="wolai-page-settings-trigger"]');
|
|
|
|
|
|
if (!(trigger instanceof HTMLElement)) return;
|
|
|
|
|
|
trigger.setAttribute('data-state', pageUiState.pageSettingsOpen ? 'open' : 'closed');
|
|
|
|
|
|
trigger.setAttribute('aria-expanded', pageUiState.pageSettingsOpen ? 'true' : 'false');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updatePageAiTriggerState() {
|
|
|
|
|
|
var trigger = document.querySelector('[data-testid="wolai-floating-ai"]');
|
|
|
|
|
|
if (!(trigger instanceof HTMLElement)) return;
|
|
|
|
|
|
trigger.setAttribute('data-state', pageUiState.pageAiOpen ? 'open' : 'closed');
|
|
|
|
|
|
trigger.setAttribute('aria-expanded', pageUiState.pageAiOpen ? 'true' : 'false');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createPageOptionRow(key, type) {
|
|
|
|
|
|
var inputType = type || 'checkbox';
|
|
|
|
|
|
var supported = pageOptionIsSupported(key);
|
|
|
|
|
|
if (inputType === 'checkbox') {
|
|
|
|
|
|
return '' +
|
|
|
|
|
|
'<label class="wolai-page-setting-row' + (supported ? '' : ' is-pending') + '" data-page-setting-row="' + key + '">' +
|
|
|
|
|
|
'<span class="wolai-page-setting-copy">' +
|
|
|
|
|
|
'<span class="wolai-page-setting-label">' + escapeHtml(pageOptionDescription(key)) + '</span>' +
|
|
|
|
|
|
'<span class="wolai-page-setting-hint">' + escapeHtml(pageOptionHint(key)) + '</span>' +
|
|
|
|
|
|
'</span>' +
|
|
|
|
|
|
'<input type="checkbox" class="wolai-page-setting-checkbox" data-page-option-checkbox="' + key + '"' + (supported ? '' : ' data-setting-pending="true"') + ' />' +
|
|
|
|
|
|
'</label>';
|
|
|
|
|
|
}
|
|
|
|
|
|
return '' +
|
|
|
|
|
|
'<label class="wolai-page-setting-row" data-page-setting-row="' + key + '">' +
|
|
|
|
|
|
'<span class="wolai-page-setting-copy">' +
|
|
|
|
|
|
'<span class="wolai-page-setting-label">' + escapeHtml(pageOptionDescription(key)) + '</span>' +
|
|
|
|
|
|
'<span class="wolai-page-setting-hint">' + escapeHtml(pageOptionHint(key)) + '</span>' +
|
|
|
|
|
|
'</span>' +
|
|
|
|
|
|
'<select class="wolai-page-setting-select" data-page-option-select="' + key + '">' +
|
|
|
|
|
|
'<option value="compact">紧凑</option>' +
|
|
|
|
|
|
'<option value="normal">默认</option>' +
|
|
|
|
|
|
'<option value="spacious">宽松</option>' +
|
|
|
|
|
|
'</select>' +
|
|
|
|
|
|
'</label>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createPageFontRow() {
|
|
|
|
|
|
return '' +
|
|
|
|
|
|
'<label class="wolai-page-setting-row" data-page-setting-row="pageFont">' +
|
|
|
|
|
|
'<span class="wolai-page-setting-copy">' +
|
|
|
|
|
|
'<span class="wolai-page-setting-label">页面字体</span>' +
|
|
|
|
|
|
'<span class="wolai-page-setting-hint">已接通:仅对当前页面生效</span>' +
|
|
|
|
|
|
'</span>' +
|
|
|
|
|
|
'<select class="wolai-page-setting-select" data-page-option-select="pageFont">' +
|
|
|
|
|
|
'<option value="default">默认</option>' +
|
|
|
|
|
|
'<option value="song">宋体</option>' +
|
|
|
|
|
|
'<option value="kai">楷体</option>' +
|
|
|
|
|
|
'</select>' +
|
|
|
|
|
|
'</label>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function ensurePageHistoryDrawer() {
|
|
|
|
|
|
var existing = document.querySelector('[data-testid="wolai-page-history-drawer"]');
|
|
|
|
|
|
if (existing instanceof HTMLElement) return existing;
|
|
|
|
|
|
var drawer = document.createElement('aside');
|
|
|
|
|
|
drawer.className = 'wolai-page-history-drawer';
|
|
|
|
|
|
drawer.setAttribute('data-testid', 'wolai-page-history-drawer');
|
|
|
|
|
|
drawer.setAttribute('data-mnote-surface', 'page-history');
|
|
|
|
|
|
drawer.hidden = true;
|
|
|
|
|
|
drawer.innerHTML = '' +
|
|
|
|
|
|
'<div class="wolai-page-history-panel">' +
|
|
|
|
|
|
'<div class="wolai-page-history-header">' +
|
|
|
|
|
|
'<div><div class="wolai-page-history-title">页面历史</div><div class="wolai-page-history-subtitle">当前会话内最近保存的 15 个快照。</div></div>' +
|
|
|
|
|
|
'<button type="button" class="wolai-surface-close" data-page-history-action="close" aria-label="关闭页面历史">×</button>' +
|
|
|
|
|
|
'</div>' +
|
|
|
|
|
|
'<div class="wolai-page-history-list" data-page-history-list></div>' +
|
|
|
|
|
|
'</div>';
|
|
|
|
|
|
drawer.addEventListener('click', function(event) {
|
|
|
|
|
|
if (event.target === drawer) closePageHistoryDrawer();
|
|
|
|
|
|
});
|
|
|
|
|
|
document.body.appendChild(drawer);
|
|
|
|
|
|
return drawer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderPageHistoryDrawer() {
|
|
|
|
|
|
var drawer = ensurePageHistoryDrawer();
|
|
|
|
|
|
var list = drawer.querySelector('[data-page-history-list]');
|
|
|
|
|
|
if (!(list instanceof HTMLElement)) return;
|
|
|
|
|
|
ensureHistorySnapshotsSeeded();
|
|
|
|
|
|
list.innerHTML = pageUiState.historySnapshots.length
|
|
|
|
|
|
? pageUiState.historySnapshots.map(function(snapshot) {
|
|
|
|
|
|
var stats = snapshot.stats || {};
|
|
|
|
|
|
var label = new Date(snapshot.timestamp).toLocaleString('zh-CN', {
|
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
|
month: '2-digit',
|
|
|
|
|
|
day: '2-digit',
|
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
|
minute: '2-digit'
|
|
|
|
|
|
});
|
|
|
|
|
|
return '' +
|
|
|
|
|
|
'<div class="wolai-page-history-item">' +
|
|
|
|
|
|
'<div class="wolai-page-history-item-copy">' +
|
|
|
|
|
|
'<div class="wolai-page-history-item-title">' + escapeHtml(label) + '</div>' +
|
|
|
|
|
|
'<div class="wolai-page-history-item-meta">字数 ' + Number(stats.wordCount || 0) + ' · 字符 ' + Number(stats.characterCount || 0) + ' · 块数 ' + Number(stats.blockCount || 0) + '</div>' +
|
|
|
|
|
|
'</div>' +
|
|
|
|
|
|
'<button type="button" class="wolai-page-history-item-ghost" data-page-history-action="noop">仅查看</button>' +
|
|
|
|
|
|
'</div>';
|
|
|
|
|
|
}).join('')
|
|
|
|
|
|
: '<div class="wolai-page-history-empty">尚未产生历史快照,编辑后会自动生成。</div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openPageHistoryDrawer() {
|
|
|
|
|
|
renderPageHistoryDrawer();
|
|
|
|
|
|
var drawer = ensurePageHistoryDrawer();
|
|
|
|
|
|
drawer.hidden = false;
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-page-history-open', 'true');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closePageHistoryDrawer() {
|
|
|
|
|
|
var drawer = document.querySelector('[data-testid="wolai-page-history-drawer"]');
|
|
|
|
|
|
if (drawer instanceof HTMLElement) drawer.hidden = true;
|
|
|
|
|
|
document.documentElement.removeAttribute('data-mnote-page-history-open');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function ensurePageShareDialog() {
|
|
|
|
|
|
var existing = document.querySelector('[data-testid="wolai-page-share-dialog"]');
|
|
|
|
|
|
if (existing instanceof HTMLElement) return existing;
|
|
|
|
|
|
var dialog = document.createElement('div');
|
|
|
|
|
|
dialog.className = 'wolai-page-share-dialog';
|
|
|
|
|
|
dialog.setAttribute('data-testid', 'wolai-page-share-dialog');
|
|
|
|
|
|
dialog.setAttribute('data-mnote-surface', 'page-share');
|
|
|
|
|
|
dialog.hidden = true;
|
|
|
|
|
|
dialog.innerHTML = '' +
|
|
|
|
|
|
'<div class="wolai-page-share-card" role="dialog" aria-modal="true">' +
|
|
|
|
|
|
'<div class="wolai-page-share-header">' +
|
|
|
|
|
|
'<div><div class="wolai-page-share-title">公开分享页面</div><div class="wolai-page-share-subtitle">当前 3000 公开入口由 mnote-web 持有。</div></div>' +
|
|
|
|
|
|
'<button type="button" class="wolai-surface-close" data-page-share-action="close" aria-label="关闭公开分享页面">×</button>' +
|
|
|
|
|
|
'</div>' +
|
|
|
|
|
|
'<div class="wolai-page-share-state">' +
|
|
|
|
|
|
'<span class="wolai-public-pill wolai-public-pill--inline">全网公开</span>' +
|
|
|
|
|
|
'<span class="wolai-page-share-copy">任何拥有链接的人都可以访问当前页面。</span>' +
|
|
|
|
|
|
'</div>' +
|
|
|
|
|
|
'<div class="wolai-page-share-url-row">' +
|
|
|
|
|
|
'<input class="wolai-page-share-url" type="text" readonly data-page-share-url value="" />' +
|
|
|
|
|
|
'<button type="button" class="wolai-page-share-copy-button" data-page-share-action="copy-link">复制链接</button>' +
|
|
|
|
|
|
'</div>' +
|
|
|
|
|
|
'<div class="wolai-page-share-footer">更多共享者、群组公开和权限策略仍待接线。</div>' +
|
|
|
|
|
|
'</div>';
|
|
|
|
|
|
dialog.addEventListener('click', function(event) {
|
|
|
|
|
|
if (event.target === dialog) closePageShareDialog();
|
|
|
|
|
|
});
|
|
|
|
|
|
document.body.appendChild(dialog);
|
|
|
|
|
|
return dialog;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openPageShareDialog() {
|
|
|
|
|
|
var dialog = ensurePageShareDialog();
|
|
|
|
|
|
var input = dialog.querySelector('[data-page-share-url]');
|
|
|
|
|
|
if (input instanceof HTMLInputElement) input.value = window.location.href;
|
|
|
|
|
|
dialog.hidden = false;
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-page-share-open', 'true');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closePageShareDialog() {
|
|
|
|
|
|
var dialog = document.querySelector('[data-testid="wolai-page-share-dialog"]');
|
|
|
|
|
|
if (dialog instanceof HTMLElement) dialog.hidden = true;
|
|
|
|
|
|
document.documentElement.removeAttribute('data-mnote-page-share-open');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function pageAiSuggestions() {
|
|
|
|
|
|
var title = searchText(document.querySelector('[data-page-title-current="true"]')?.textContent) || '当前页面';
|
|
|
|
|
|
return [
|
|
|
|
|
|
'帮我总结《' + title + '》当前内容',
|
|
|
|
|
|
'把当前页面改写得更简洁一些',
|
|
|
|
|
|
'提炼当前页的关键待办和行动项',
|
|
|
|
|
|
'基于当前页内容生成一个三段式摘要'
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function ensurePageAiDrawer() {
|
|
|
|
|
|
var existing = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
|
|
|
|
|
|
if (existing instanceof HTMLElement) return existing;
|
|
|
|
|
|
var drawer = document.createElement('aside');
|
|
|
|
|
|
drawer.className = 'wolai-page-ai-drawer';
|
|
|
|
|
|
drawer.setAttribute('data-testid', 'wolai-page-ai-drawer');
|
|
|
|
|
|
drawer.setAttribute('data-mnote-surface', 'page-ai');
|
|
|
|
|
|
drawer.hidden = true;
|
|
|
|
|
|
drawer.innerHTML = '' +
|
|
|
|
|
|
'<div class="wolai-page-ai-panel" role="dialog" aria-modal="false">' +
|
|
|
|
|
|
'<div class="wolai-page-ai-header">' +
|
|
|
|
|
|
'<div class="wolai-page-ai-header-copy">' +
|
|
|
|
|
|
'<h2 class="wolai-page-ai-title" data-title="page-ai-title">智能问答</h2>' +
|
|
|
|
|
|
'<div class="wolai-page-ai-subtitle">当前页面上下文优先 · mnote-cli</div>' +
|
|
|
|
|
|
'</div>' +
|
|
|
|
|
|
'<button type="button" class="wolai-surface-close" data-page-ai-action="close" aria-label="关闭页面 AI">×</button>' +
|
|
|
|
|
|
'</div>' +
|
|
|
|
|
|
'<div class="wolai-page-ai-body">' +
|
|
|
|
|
|
'<div class="wolai-page-ai-suggestions">' +
|
|
|
|
|
|
'<div class="wolai-page-ai-suggestions-header">' +
|
|
|
|
|
|
'<span>推荐问题</span>' +
|
|
|
|
|
|
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-action="rotate">换一换</button>' +
|
|
|
|
|
|
'</div>' +
|
|
|
|
|
|
'<div class="wolai-page-ai-suggestion-list" data-page-ai-suggestion-list></div>' +
|
|
|
|
|
|
'</div>' +
|
|
|
|
|
|
'<div class="wolai-page-ai-conversation" data-page-ai-conversation></div>' +
|
|
|
|
|
|
'</div>' +
|
|
|
|
|
|
'<div class="wolai-page-ai-footer">' +
|
|
|
|
|
|
'<div class="wolai-page-ai-toolbar">' +
|
|
|
|
|
|
'<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-action="new-session" aria-label="当前已是新会话">新会话</button>' +
|
|
|
|
|
|
'<button type="button" class="wolai-page-ai-tab" data-page-ai-action="history">历史会话</button>' +
|
|
|
|
|
|
'<button type="button" class="wolai-page-ai-model-chip" data-page-ai-action="model">mnote-cli</button>' +
|
|
|
|
|
|
'</div>' +
|
|
|
|
|
|
'<div class="wolai-page-ai-input-row">' +
|
|
|
|
|
|
'<textarea class="wolai-page-ai-input" data-page-ai-input rows="3" placeholder="问我你想知道的"></textarea>' +
|
|
|
|
|
|
'<button type="button" class="wolai-page-ai-send" data-page-ai-action="send" aria-label="发送">↑</button>' +
|
|
|
|
|
|
'</div>' +
|
|
|
|
|
|
'</div>' +
|
|
|
|
|
|
'</div>';
|
|
|
|
|
|
document.body.appendChild(drawer);
|
|
|
|
|
|
return drawer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderPageAiSuggestions() {
|
|
|
|
|
|
var drawer = ensurePageAiDrawer();
|
|
|
|
|
|
var list = drawer.querySelector('[data-page-ai-suggestion-list]');
|
|
|
|
|
|
if (!(list instanceof HTMLElement)) return;
|
|
|
|
|
|
var suggestions = pageAiSuggestions();
|
|
|
|
|
|
var offset = pageUiState.pageAiSuggestionIndex % suggestions.length;
|
|
|
|
|
|
var ordered = suggestions.slice(offset).concat(suggestions.slice(0, offset)).slice(0, 3);
|
|
|
|
|
|
list.innerHTML = ordered.map(function(text) {
|
|
|
|
|
|
return '<button type="button" class="wolai-page-ai-suggestion" data-page-ai-suggestion="' + escapeHtml(text) + '">' + escapeHtml(text) + '</button>';
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderPageAiConversation() {
|
|
|
|
|
|
var drawer = ensurePageAiDrawer();
|
|
|
|
|
|
var conversation = drawer.querySelector('[data-page-ai-conversation]');
|
|
|
|
|
|
if (!(conversation instanceof HTMLElement)) return;
|
|
|
|
|
|
if (!pageUiState.pageAiMessages.length) {
|
|
|
|
|
|
conversation.innerHTML = '<div class="wolai-page-ai-empty">围绕当前页面提问,我会优先使用当前页内容、页面结构和已保存设置。</div>';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
conversation.innerHTML = pageUiState.pageAiMessages.map(function(item) {
|
|
|
|
|
|
return '' +
|
|
|
|
|
|
'<div class="wolai-page-ai-message wolai-page-ai-message--' + escapeHtml(item.role || 'assistant') + '">' +
|
|
|
|
|
|
'<div class="wolai-page-ai-message-role">' + escapeHtml(item.role === 'user' ? '你' : 'AI') + '</div>' +
|
|
|
|
|
|
'<div class="wolai-page-ai-message-text">' + escapeHtml(item.content || '') + '</div>' +
|
|
|
|
|
|
'</div>';
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
conversation.scrollTop = conversation.scrollHeight;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openPageAiDrawer() {
|
|
|
|
|
|
renderPageAiSuggestions();
|
|
|
|
|
|
renderPageAiConversation();
|
|
|
|
|
|
var drawer = ensurePageAiDrawer();
|
|
|
|
|
|
drawer.hidden = false;
|
|
|
|
|
|
pageUiState.pageAiOpen = true;
|
|
|
|
|
|
updatePageAiTriggerState();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closePageAiDrawer() {
|
|
|
|
|
|
var drawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
|
|
|
|
|
|
if (drawer instanceof HTMLElement) drawer.hidden = true;
|
|
|
|
|
|
pageUiState.pageAiOpen = false;
|
|
|
|
|
|
updatePageAiTriggerState();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isPageAiDrawerOpen() {
|
|
|
|
|
|
var drawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
|
|
|
|
|
|
return drawer instanceof HTMLElement && !drawer.hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function streamPageAiResponse(response, onEvent) {
|
|
|
|
|
|
if (!response.body || typeof response.body.getReader !== 'function') return;
|
|
|
|
|
|
var reader = response.body.getReader();
|
|
|
|
|
|
var decoder = new TextDecoder();
|
|
|
|
|
|
var buffer = '';
|
|
|
|
|
|
while (true) {
|
|
|
|
|
|
var chunk = await reader.read();
|
|
|
|
|
|
if (chunk.done) break;
|
|
|
|
|
|
buffer += decoder.decode(chunk.value, { stream: true });
|
|
|
|
|
|
var frames = buffer.split('\n\n');
|
|
|
|
|
|
buffer = frames.pop() || '';
|
|
|
|
|
|
frames.forEach(function(frame) {
|
|
|
|
|
|
var eventName = '';
|
|
|
|
|
|
var dataLines = [];
|
|
|
|
|
|
frame.split('\n').forEach(function(line) {
|
|
|
|
|
|
if (line.startsWith('event:')) eventName = line.slice(6).trim();
|
|
|
|
|
|
if (line.startsWith('data:')) dataLines.push(line.slice(5).trim());
|
|
|
|
|
|
});
|
|
|
|
|
|
if (eventName) onEvent(eventName, dataLines.join('\n'));
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function sendPageAiMessage(text) {
|
|
|
|
|
|
if (pageUiState.pageAiBusy) return;
|
|
|
|
|
|
var prompt = searchText(text);
|
|
|
|
|
|
if (!prompt) return;
|
|
|
|
|
|
var aggregate = currentPageAggregate();
|
|
|
|
|
|
var body = aggregate.body || {};
|
|
|
|
|
|
var subtree = aggregate.tree && aggregate.tree.pageSubtree ? aggregate.tree.pageSubtree : null;
|
|
|
|
|
|
var outline = subtree && subtree.outline ? subtree.outline : null;
|
|
|
|
|
|
pageUiState.pageAiBusy = true;
|
|
|
|
|
|
pageUiState.pageAiMessages.push({ role: 'user', content: prompt });
|
|
|
|
|
|
renderPageAiConversation();
|
|
|
|
|
|
try {
|
|
|
|
|
|
var response = await fetch('/api/ai-agent/run', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'content-type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
stream: true,
|
|
|
|
|
|
maxSteps: 8,
|
|
|
|
|
|
scope: 'document',
|
|
|
|
|
|
messages: [{ role: 'user', content: prompt }],
|
|
|
|
|
|
toolChoice: {
|
|
|
|
|
|
mode: 'auto',
|
|
|
|
|
|
toolSets: ['toolset.readonly', 'toolset.rag_read', 'toolset.docs_read', 'toolset.media_read', 'toolset.doc_read', 'toolset.doc_write', 'toolset.slash_write']
|
|
|
|
|
|
},
|
|
|
|
|
|
context: {
|
|
|
|
|
|
documentId: currentDocumentId(),
|
|
|
|
|
|
documentBlocks: body.content || null,
|
|
|
|
|
|
node: {
|
|
|
|
|
|
documentId: currentDocumentId(),
|
|
|
|
|
|
title: aggregate.head && aggregate.head.title ? aggregate.head.title : ''
|
|
|
|
|
|
},
|
|
|
|
|
|
subtree: subtree,
|
|
|
|
|
|
outline: outline,
|
|
|
|
|
|
evidence: null,
|
|
|
|
|
|
pageOptions: currentPageOptions()
|
|
|
|
|
|
},
|
|
|
|
|
|
options: {
|
|
|
|
|
|
searxng: true,
|
|
|
|
|
|
ai: { provider: 'hermes' }
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error('page_ai_failed_' + response.status);
|
|
|
|
|
|
}
|
|
|
|
|
|
var assistantText = '';
|
|
|
|
|
|
await streamPageAiResponse(response, function(eventName, payloadText) {
|
|
|
|
|
|
if (eventName === 'assistant_message') {
|
|
|
|
|
|
try {
|
|
|
|
|
|
var payload = JSON.parse(payloadText || 'null');
|
|
|
|
|
|
assistantText = searchText(payload && payload.text);
|
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
pageUiState.pageAiMessages.push({
|
|
|
|
|
|
role: 'assistant',
|
|
|
|
|
|
content: assistantText || '当前页面 AI 已接通 `mnote-cli`,但这次没有返回文本结果。'
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
pageUiState.pageAiMessages.push({
|
|
|
|
|
|
role: 'assistant',
|
|
|
|
|
|
content: '当前页面 AI 抽屉已接到 `mnote-cli` 主链,但本次请求失败:' + (error instanceof Error ? error.message : String(error))
|
|
|
|
|
|
});
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
pageUiState.pageAiBusy = false;
|
|
|
|
|
|
renderPageAiConversation();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function ensurePageSettingsPopover() {
|
|
|
|
|
|
var existing = document.querySelector('[data-testid="wolai-page-settings-popover"]');
|
|
|
|
|
|
if (existing instanceof HTMLElement) return existing;
|
|
|
|
|
|
var popover = document.createElement('div');
|
|
|
|
|
|
popover.className = 'wolai-page-settings-popover';
|
|
|
|
|
|
popover.setAttribute('data-testid', 'wolai-page-settings-popover');
|
|
|
|
|
|
popover.setAttribute('data-mnote-surface', 'page-settings');
|
|
|
|
|
|
popover.hidden = true;
|
|
|
|
|
|
popover.innerHTML = '' +
|
|
|
|
|
|
'<div class="wolai-page-settings-panel" role="dialog" aria-modal="false">' +
|
|
|
|
|
|
'<div class="wolai-page-settings-tabs" data-testid="wolai-page-settings-tabs" role="tablist" aria-label="页面设置分组">' +
|
|
|
|
|
|
'<button type="button" class="wolai-page-settings-tab is-active" role="tab" aria-selected="true" data-page-settings-tab="page">页面选项</button>' +
|
|
|
|
|
|
'<button type="button" class="wolai-page-settings-tab" role="tab" aria-selected="false" data-page-settings-tab="custom">自定义页面</button>' +
|
|
|
|
|
|
'<button type="button" class="wolai-page-settings-tab" role="tab" aria-selected="false" data-page-settings-tab="global">全局选项</button>' +
|
|
|
|
|
|
'</div>' +
|
|
|
|
|
|
'<div class="wolai-page-settings-section" data-page-settings-panel="page">' +
|
|
|
|
|
|
createPageOptionRow('wideLayout', 'checkbox') +
|
|
|
|
|
|
createPageOptionRow('smallText', 'checkbox') +
|
|
|
|
|
|
createPageOptionRow('showToc', 'checkbox') +
|
|
|
|
|
|
createPageOptionRow('showHeadingNumbers', 'checkbox') +
|
|
|
|
|
|
createPageOptionRow('protectEditing', 'checkbox') +
|
|
|
|
|
|
'</div>' +
|
|
|
|
|
|
'<div class="wolai-page-settings-section" data-page-settings-panel="custom" hidden>' +
|
|
|
|
|
|
createPageFontRow() +
|
|
|
|
|
|
createPageOptionRow('layoutDensity', 'select') +
|
|
|
|
|
|
createPageOptionRow('collapseBacklinks', 'checkbox') +
|
|
|
|
|
|
createPageOptionRow('hideChildPages', 'checkbox') +
|
|
|
|
|
|
createPageOptionRow('showBlockRefCount', 'checkbox') +
|
|
|
|
|
|
'</div>' +
|
|
|
|
|
|
'<div class="wolai-page-settings-section" data-page-settings-panel="global" hidden>' +
|
|
|
|
|
|
'<div class="wolai-page-settings-global-note">当前 Rust Web 壳仅保留最小全局设置入口,真正的全局偏好仍待后续统一收口。</div>' +
|
|
|
|
|
|
'</div>' +
|
|
|
|
|
|
'<div class="wolai-page-settings-actions">' +
|
|
|
|
|
|
'<button type="button" class="wolai-page-settings-action" data-page-settings-action="history">页面历史...</button>' +
|
|
|
|
|
|
'<button type="button" class="wolai-page-settings-action" data-page-settings-action="share">公开分享页面...</button>' +
|
|
|
|
|
|
'<button type="button" class="wolai-page-settings-action is-disabled" data-page-settings-action="move" disabled>移动到...</button>' +
|
|
|
|
|
|
'<button type="button" class="wolai-page-settings-action is-disabled" data-page-settings-action="embed" disabled>嵌入到...</button>' +
|
|
|
|
|
|
'<button type="button" class="wolai-page-settings-action is-danger is-disabled" data-page-settings-action="delete" disabled>删除页面</button>' +
|
|
|
|
|
|
'</div>' +
|
|
|
|
|
|
'<div class="wolai-page-settings-stats" data-testid="wolai-page-settings-stats"></div>' +
|
|
|
|
|
|
'</div>';
|
|
|
|
|
|
document.body.appendChild(popover);
|
|
|
|
|
|
return popover;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderPageSettingsPopover() {
|
|
|
|
|
|
var popover = ensurePageSettingsPopover();
|
|
|
|
|
|
var options = currentPageOptions();
|
|
|
|
|
|
popover.querySelectorAll('[data-page-option-checkbox]').forEach(function(input) {
|
|
|
|
|
|
var key = input.getAttribute('data-page-option-checkbox');
|
|
|
|
|
|
input.checked = Boolean(options[key]);
|
|
|
|
|
|
input.disabled = !pageOptionIsSupported(key);
|
|
|
|
|
|
});
|
|
|
|
|
|
popover.querySelectorAll('[data-page-option-select]').forEach(function(select) {
|
|
|
|
|
|
var key = select.getAttribute('data-page-option-select');
|
|
|
|
|
|
var value = key === 'layoutDensity' ? String(options.layoutDensity || 'normal') : String(options.pageFont || 'default');
|
|
|
|
|
|
select.value = value;
|
|
|
|
|
|
});
|
|
|
|
|
|
var statsNode = popover.querySelector('[data-testid="wolai-page-settings-stats"]');
|
|
|
|
|
|
if (statsNode instanceof HTMLElement) {
|
|
|
|
|
|
var stats = computeLivePageStats();
|
|
|
|
|
|
statsNode.innerHTML = '' +
|
|
|
|
|
|
'<span>字数 ' + Number(stats.wordCount || 0) + '</span>' +
|
|
|
|
|
|
'<span>字符 ' + Number(stats.characterCount || 0) + '</span>' +
|
|
|
|
|
|
'<span>块数 ' + Number(stats.blockCount || 0) + '</span>' +
|
|
|
|
|
|
'<span>待办 ' + Number(stats.todoDone || 0) + '/' + Number(stats.todoTotal || 0) + '</span>';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setActivePageSettingsTab(tabName) {
|
|
|
|
|
|
var popover = ensurePageSettingsPopover();
|
|
|
|
|
|
popover.querySelectorAll('[data-page-settings-tab]').forEach(function(tab) {
|
|
|
|
|
|
var active = tab.getAttribute('data-page-settings-tab') === tabName;
|
|
|
|
|
|
tab.classList.toggle('is-active', active);
|
|
|
|
|
|
tab.setAttribute('aria-selected', active ? 'true' : 'false');
|
|
|
|
|
|
});
|
|
|
|
|
|
popover.querySelectorAll('[data-page-settings-panel]').forEach(function(panel) {
|
|
|
|
|
|
panel.hidden = panel.getAttribute('data-page-settings-panel') !== tabName;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function persistPageOptionsPatch(patch) {
|
|
|
|
|
|
var previous = Object.assign({}, currentPageOptions());
|
|
|
|
|
|
pageUiState.pageOptions = Object.assign({}, previous, patch);
|
|
|
|
|
|
var nextOptions = Object.assign({}, pageUiState.pageOptions);
|
|
|
|
|
|
applyPageOptionsToShell();
|
|
|
|
|
|
renderPageSettingsPopover();
|
|
|
|
|
|
try {
|
|
|
|
|
|
var response = await fetch('/api/documents/options', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'content-type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
documentId: currentDocumentId(),
|
|
|
|
|
|
workspaceId: resolveWorkspaceId(document.body),
|
|
|
|
|
|
options: nextOptions,
|
|
|
|
|
|
commandName: 'page.layout.updateOptions'
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
var payload = await response.json().catch(function(){ return null; });
|
|
|
|
|
|
if (!response.ok || !payload || payload.ok !== true) {
|
|
|
|
|
|
throw new Error(payload && payload.error && payload.error.message ? payload.error.message : 'page_settings_save_failed_' + response.status);
|
|
|
|
|
|
}
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-page-options-saved', 'true');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
pageUiState.pageOptions = previous;
|
|
|
|
|
|
applyPageOptionsToShell();
|
|
|
|
|
|
renderPageSettingsPopover();
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-page-options-error', error instanceof Error ? error.message : String(error));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isPageSettingsOpen() {
|
|
|
|
|
|
var popover = document.querySelector('[data-testid="wolai-page-settings-popover"]');
|
|
|
|
|
|
return popover instanceof HTMLElement && !popover.hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openPageSettingsPopover() {
|
|
|
|
|
|
if (!currentDocumentId()) return;
|
|
|
|
|
|
var popover = ensurePageSettingsPopover();
|
|
|
|
|
|
renderPageSettingsPopover();
|
|
|
|
|
|
setActivePageSettingsTab('page');
|
|
|
|
|
|
popover.hidden = false;
|
|
|
|
|
|
pageUiState.pageSettingsOpen = true;
|
|
|
|
|
|
updatePageSettingsTriggerState();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closePageSettingsPopover() {
|
|
|
|
|
|
var popover = document.querySelector('[data-testid="wolai-page-settings-popover"]');
|
|
|
|
|
|
if (popover instanceof HTMLElement) popover.hidden = true;
|
|
|
|
|
|
pageUiState.pageSettingsOpen = false;
|
|
|
|
|
|
updatePageSettingsTriggerState();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function togglePageSettingsPopover() {
|
|
|
|
|
|
if (isPageSettingsOpen()) closePageSettingsPopover();
|
|
|
|
|
|
else openPageSettingsPopover();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 14:36:24 +08:00
|
|
|
|
document.addEventListener('click', function(e) {
|
2026-04-30 06:58:17 +08:00
|
|
|
|
if (activeTreeContextMenu && activeTreeContextMenu.contains(e.target)) return;
|
|
|
|
|
|
if (activeTreeContextMenu) closeTreeContextMenu();
|
|
|
|
|
|
|
2026-05-06 21:44:20 +08:00
|
|
|
|
var historyClose = closestAction(e.target, '[data-page-history-action="close"]');
|
|
|
|
|
|
if (historyClose) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
closePageHistoryDrawer();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var shareClose = closestAction(e.target, '[data-page-share-action="close"]');
|
|
|
|
|
|
if (shareClose) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
closePageShareDialog();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var shareCopy = closestAction(e.target, '[data-page-share-action="copy-link"]');
|
|
|
|
|
|
if (shareCopy) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
void copyTreeContextValue(window.location.href, 'page-share-link');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var pageHistoryTrigger = closestAction(e.target, '[data-mnote-action="open-page-history"]');
|
|
|
|
|
|
if (pageHistoryTrigger) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
openPageHistoryDrawer();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var pageSettingsTrigger = closestAction(e.target, '[data-mnote-action="open-page-settings"]');
|
|
|
|
|
|
if (pageSettingsTrigger) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
togglePageSettingsPopover();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var pageSettingsPanel = closestAction(e.target, '.wolai-page-settings-panel');
|
|
|
|
|
|
if (isPageSettingsOpen() && !pageSettingsPanel) {
|
|
|
|
|
|
closePageSettingsPopover();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var pageSettingsTab = closestAction(e.target, '[data-page-settings-tab]');
|
|
|
|
|
|
if (pageSettingsTab) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
setActivePageSettingsTab(pageSettingsTab.getAttribute('data-page-settings-tab') || 'page');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var pageSettingsAction = closestAction(e.target, '[data-page-settings-action]');
|
|
|
|
|
|
if (pageSettingsAction) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
var actionName = pageSettingsAction.getAttribute('data-page-settings-action') || '';
|
|
|
|
|
|
if (actionName === 'history') {
|
|
|
|
|
|
openPageHistoryDrawer();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (actionName === 'share') {
|
|
|
|
|
|
openPageShareDialog();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var pageAiTrigger = closestAction(e.target, '[data-mnote-action="open-page-ai"]');
|
|
|
|
|
|
if (pageAiTrigger) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
openPageAiDrawer();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var pageAiClose = closestAction(e.target, '[data-page-ai-action="close"]');
|
|
|
|
|
|
if (pageAiClose) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
closePageAiDrawer();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var pageAiRotate = closestAction(e.target, '[data-page-ai-action="rotate"]');
|
|
|
|
|
|
if (pageAiRotate) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
pageUiState.pageAiSuggestionIndex += 1;
|
|
|
|
|
|
renderPageAiSuggestions();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var pageAiSuggestion = closestAction(e.target, '[data-page-ai-suggestion]');
|
|
|
|
|
|
if (pageAiSuggestion) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
var text = pageAiSuggestion.getAttribute('data-page-ai-suggestion') || '';
|
|
|
|
|
|
var inputNode = ensurePageAiDrawer().querySelector('[data-page-ai-input]');
|
|
|
|
|
|
if (inputNode instanceof HTMLTextAreaElement) {
|
|
|
|
|
|
inputNode.value = text;
|
|
|
|
|
|
inputNode.focus();
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var pageAiNewSession = closestAction(e.target, '[data-page-ai-action="new-session"]');
|
|
|
|
|
|
if (pageAiNewSession) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
pageUiState.pageAiMessages = [];
|
|
|
|
|
|
renderPageAiConversation();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var pageAiSend = closestAction(e.target, '[data-page-ai-action="send"]');
|
|
|
|
|
|
if (pageAiSend) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
var input = ensurePageAiDrawer().querySelector('[data-page-ai-input]');
|
|
|
|
|
|
if (input instanceof HTMLTextAreaElement) {
|
|
|
|
|
|
var message = input.value;
|
|
|
|
|
|
input.value = '';
|
|
|
|
|
|
void sendPageAiMessage(message);
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 16:18:54 +08:00
|
|
|
|
var searchTrigger = closestAction(e.target, '[data-mnote-action="open-search-modal"]');
|
|
|
|
|
|
if (searchTrigger) {
|
|
|
|
|
|
e.preventDefault();
|
2026-04-30 18:36:50 +08:00
|
|
|
|
toggleSearchModal();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
var searchDialog = closestAction(e.target, '.wolai-search-dialog');
|
|
|
|
|
|
if (isSearchModalOpen() && !searchDialog) {
|
|
|
|
|
|
closeSearchModal();
|
2026-04-30 16:18:54 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 16:23:49 +08:00
|
|
|
|
var tabTrigger = closestAction(e.target, '[data-mnote-sidebar-tree-tab]');
|
|
|
|
|
|
if (tabTrigger) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
switchSidebarTreeTab(tabTrigger);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 14:36:24 +08:00
|
|
|
|
var createTrigger = closestAction(e.target, '[data-mnote-action="create-page"]');
|
|
|
|
|
|
if (createTrigger) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
void createPage(createTrigger, null);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 05:46:36 +08:00
|
|
|
|
var fileTree = document.getElementById('sidebar-file-tree-root');
|
|
|
|
|
|
if (fileTree && fileTree.contains(e.target)) {
|
|
|
|
|
|
var fileBtn = closestAction(e.target, '[data-rust-action]');
|
|
|
|
|
|
var fileRow = closestAction(e.target, '.tree-row[data-shell-mode="filetree"]');
|
|
|
|
|
|
if (!fileRow) return;
|
|
|
|
|
|
var fileAction = fileBtn ? fileBtn.getAttribute('data-rust-action') : 'open';
|
|
|
|
|
|
var rowId = fileRow.getAttribute('data-row-id') || '';
|
|
|
|
|
|
var rowKind = fileRow.getAttribute('data-row-kind') || '';
|
|
|
|
|
|
var documentId = fileRow.getAttribute('data-document-id') || fileRow.getAttribute('data-doc-id') || '';
|
|
|
|
|
|
var assetId = fileRow.getAttribute('data-asset-id') || '';
|
|
|
|
|
|
if (fileAction === 'toggle') {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
toggleChildren(fileRow, fileBtn);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (fileAction === 'create') {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
void createPage(fileBtn || fileRow, documentId || fileRow.getAttribute('data-node-id'));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (fileAction === 'menu') {
|
|
|
|
|
|
e.preventDefault();
|
2026-04-30 06:58:17 +08:00
|
|
|
|
var point = rowCenter(fileBtn || fileRow);
|
2026-04-30 05:46:36 +08:00
|
|
|
|
dispatchSidebarEvent('tree.filetree.context-menu', { rowId: rowId, rowKind: rowKind, documentId: documentId || null, assetId: assetId || null });
|
2026-04-30 06:58:17 +08:00
|
|
|
|
openFileTreeContextMenu(fileRow, point.x, point.y, fileBtn || fileRow);
|
2026-04-30 05:46:36 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
fileTree.querySelectorAll('.tree-row[data-selected="true"]').forEach(function(row) {
|
|
|
|
|
|
if (row instanceof HTMLElement) row.setAttribute('data-selected', 'false');
|
|
|
|
|
|
});
|
|
|
|
|
|
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) {
|
2026-04-30 06:58:17 +08:00
|
|
|
|
navigateToDocument(documentId, resolveWorkspaceId(fileRow), { treeView: 'filetree' });
|
|
|
|
|
|
} else if (assetId) {
|
|
|
|
|
|
dispatchSidebarEvent('tree.asset.open', { rowId: rowId, rowKind: rowKind, documentId: documentId || null, assetId: assetId });
|
2026-04-30 05:46:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 14:36:24 +08:00
|
|
|
|
var tree = document.getElementById('sidebar-tree-root');
|
|
|
|
|
|
if (!tree || !tree.contains(e.target)) return;
|
|
|
|
|
|
var btn = closestAction(e.target, '[data-rust-action]');
|
2026-04-29 12:24:44 +08:00
|
|
|
|
if (!btn) return;
|
|
|
|
|
|
var nodeId = btn.getAttribute('data-node-id');
|
|
|
|
|
|
var action = btn.getAttribute('data-rust-action');
|
|
|
|
|
|
|
|
|
|
|
|
if (action === 'toggle') {
|
|
|
|
|
|
var row = btn.closest('.tree-row');
|
|
|
|
|
|
if (!row) return;
|
2026-04-30 05:46:36 +08:00
|
|
|
|
toggleChildren(row, btn);
|
2026-04-29 12:24:44 +08:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
} else if (action === 'open') {
|
2026-04-29 14:36:24 +08:00
|
|
|
|
var workspaceId = resolveWorkspaceId(btn);
|
2026-04-30 06:58:17 +08:00
|
|
|
|
navigateToDocument(nodeId, workspaceId, { treeView: 'page' });
|
2026-04-29 12:24:44 +08:00
|
|
|
|
e.preventDefault();
|
2026-04-29 14:36:24 +08:00
|
|
|
|
} else if (action === 'create') {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
void createPage(btn, nodeId);
|
|
|
|
|
|
} else if (action === 'rename') {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
var title = window.prompt('重命名页面');
|
|
|
|
|
|
if (title && title.trim()) {
|
|
|
|
|
|
void dispatchTreeCommand(btn, {
|
|
|
|
|
|
action: 'rename',
|
|
|
|
|
|
workspaceId: resolveWorkspaceId(btn),
|
|
|
|
|
|
documentId: nodeId,
|
|
|
|
|
|
title: title.trim()
|
2026-04-30 05:46:36 +08:00
|
|
|
|
}).then(function(){
|
|
|
|
|
|
updateTitleEverywhere(nodeId, title.trim());
|
|
|
|
|
|
scheduleProjectionRefresh(resolveWorkspaceId(btn));
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (action === 'menu') {
|
|
|
|
|
|
e.preventDefault();
|
2026-04-30 06:58:17 +08:00
|
|
|
|
var menuPoint = rowCenter(btn);
|
2026-04-30 05:46:36 +08:00
|
|
|
|
dispatchSidebarEvent('tree.page.context-menu', { documentId: nodeId, target: { documentId: nodeId } });
|
2026-04-30 06:58:17 +08:00
|
|
|
|
openPageTreeContextMenu(btn.closest('.tree-row'), menuPoint.x, menuPoint.y, btn);
|
2026-04-30 05:46:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-30 06:58:17 +08:00
|
|
|
|
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) {
|
2026-05-06 21:44:20 +08:00
|
|
|
|
if (event.key === 'Escape' && isPageSettingsOpen()) {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
closePageSettingsPopover();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-04-30 16:18:54 +08:00
|
|
|
|
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && event.key && event.key.toLowerCase() === 'p') {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
toggleSearchModal();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (event.key === 'Escape') {
|
|
|
|
|
|
closeSearchModal();
|
|
|
|
|
|
closeTreeContextMenu();
|
|
|
|
|
|
}
|
2026-05-06 21:44:20 +08:00
|
|
|
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
|
|
|
|
var aiInput = closestAction(event.target, '[data-page-ai-input]');
|
|
|
|
|
|
if (aiInput instanceof HTMLTextAreaElement) {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
var text = aiInput.value;
|
|
|
|
|
|
aiInput.value = '';
|
|
|
|
|
|
void sendPageAiMessage(text);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-30 06:58:17 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-06 21:44:20 +08:00
|
|
|
|
document.addEventListener('change', function(event) {
|
|
|
|
|
|
var checkbox = closestAction(event.target, '[data-page-option-checkbox]');
|
|
|
|
|
|
if (checkbox instanceof HTMLInputElement) {
|
|
|
|
|
|
var key = checkbox.getAttribute('data-page-option-checkbox') || '';
|
|
|
|
|
|
if (!pageOptionIsSupported(key)) return;
|
|
|
|
|
|
var patch = {};
|
|
|
|
|
|
patch[key] = checkbox.checked;
|
|
|
|
|
|
void persistPageOptionsPatch(patch);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
var select = closestAction(event.target, '[data-page-option-select]');
|
|
|
|
|
|
if (select instanceof HTMLSelectElement) {
|
|
|
|
|
|
var selectKey = select.getAttribute('data-page-option-select') || '';
|
|
|
|
|
|
var next = {};
|
|
|
|
|
|
if (selectKey === 'layoutDensity') next.layoutDensity = select.value;
|
|
|
|
|
|
if (selectKey === 'pageFont') next.pageFont = select.value;
|
|
|
|
|
|
if (Object.keys(next).length) {
|
|
|
|
|
|
void persistPageOptionsPatch(next);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function initializePageUiSurfaces() {
|
|
|
|
|
|
pageUiState.pageOptions = null;
|
|
|
|
|
|
applyPageOptionsToShell();
|
|
|
|
|
|
updatePageSettingsTriggerState();
|
|
|
|
|
|
updatePageAiTriggerState();
|
|
|
|
|
|
ensureHistorySnapshotsSeeded();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
window.__mnoteRecordPageHistorySnapshot = recordPageHistorySnapshot;
|
|
|
|
|
|
window.__mnoteApplyPageOptionsToShell = applyPageOptionsToShell;
|
|
|
|
|
|
setTimeout(initializePageUiSurfaces, 0);
|
|
|
|
|
|
|
2026-04-30 05:46:36 +08:00
|
|
|
|
function readPageDragNodeId(event) {
|
|
|
|
|
|
var fromTransfer = event.dataTransfer ? event.dataTransfer.getData(PAGE_DRAG_MIME) || event.dataTransfer.getData('text/plain') : '';
|
|
|
|
|
|
return (fromTransfer || draggingPageNodeId || '').trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clearPageDropFeedback() {
|
|
|
|
|
|
if (activePageDropRow instanceof HTMLElement) {
|
|
|
|
|
|
activePageDropRow.setAttribute('data-drop-feedback', 'false');
|
|
|
|
|
|
}
|
|
|
|
|
|
activePageDropRow = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function canDropPage(sourceNodeId, targetRow) {
|
|
|
|
|
|
if (!sourceNodeId || !(targetRow instanceof HTMLElement)) return false;
|
|
|
|
|
|
var targetNodeId = targetRow.getAttribute('data-node-id') || '';
|
|
|
|
|
|
if (!targetNodeId || targetNodeId === sourceNodeId) return false;
|
|
|
|
|
|
var sourceNode = document.querySelector('#sidebar-tree-root .tree-node[data-node-id="' + cssEscape(sourceNodeId) + '"]');
|
|
|
|
|
|
return !(sourceNode instanceof HTMLElement && sourceNode.contains(targetRow));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function pageDropPosition(event, row) {
|
|
|
|
|
|
var rect = row.getBoundingClientRect();
|
|
|
|
|
|
var ratio = rect.height > 0 ? (event.clientY - rect.top) / rect.height : 0.5;
|
|
|
|
|
|
if (ratio < 0.25) return 'before';
|
|
|
|
|
|
if (ratio > 0.75) return 'after';
|
|
|
|
|
|
return 'inside';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolvePageMoveTarget(targetRow, position) {
|
|
|
|
|
|
var targetNodeId = targetRow.getAttribute('data-node-id') || '';
|
|
|
|
|
|
var parentId = targetRow.getAttribute('data-parent-id') || null;
|
|
|
|
|
|
if (position === 'inside') {
|
|
|
|
|
|
var children = targetRow.parentElement ? targetRow.parentElement.querySelectorAll(':scope > .tree-children > .tree-node > .tree-row[data-shell-mode="page"]') : [];
|
|
|
|
|
|
return { parentId: targetNodeId, sortOrder: children.length };
|
|
|
|
|
|
}
|
|
|
|
|
|
var siblings = Array.from(document.querySelectorAll('#sidebar-tree-root .tree-row[data-shell-mode="page"]')).filter(function(row) {
|
|
|
|
|
|
return (row.getAttribute('data-parent-id') || '') === (parentId || '');
|
|
|
|
|
|
});
|
|
|
|
|
|
var index = siblings.indexOf(targetRow);
|
|
|
|
|
|
return { parentId: parentId, sortOrder: Math.max(0, index + (position === 'after' ? 1 : 0)) };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('dragstart', function(event) {
|
|
|
|
|
|
var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"][draggable="true"]');
|
|
|
|
|
|
if (pageRow) {
|
|
|
|
|
|
draggingPageNodeId = pageRow.getAttribute('data-node-id') || '';
|
|
|
|
|
|
if (event.dataTransfer) {
|
|
|
|
|
|
event.dataTransfer.effectAllowed = 'move';
|
|
|
|
|
|
event.dataTransfer.setData(PAGE_DRAG_MIME, draggingPageNodeId);
|
|
|
|
|
|
event.dataTransfer.setData('text/plain', draggingPageNodeId);
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
var fileRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"][draggable="true"]');
|
|
|
|
|
|
if (fileRow) {
|
|
|
|
|
|
var rowId = fileRow.getAttribute('data-row-id') || '';
|
|
|
|
|
|
draggingFileTreeRowIds = rowId ? [rowId] : [];
|
|
|
|
|
|
if (event.dataTransfer) {
|
|
|
|
|
|
var payload = JSON.stringify({ type: 'mnote-file-tree-dnd', version: 1, rowIds: draggingFileTreeRowIds });
|
|
|
|
|
|
event.dataTransfer.effectAllowed = 'copyMove';
|
|
|
|
|
|
event.dataTransfer.setData(FILETREE_DRAG_MIME, payload);
|
|
|
|
|
|
event.dataTransfer.setData('application/x-mnote-file-tree', payload);
|
|
|
|
|
|
event.dataTransfer.setData('text/plain', payload);
|
2026-04-29 14:36:24 +08:00
|
|
|
|
}
|
2026-04-29 12:24:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-04-29 14:36:24 +08:00
|
|
|
|
|
2026-04-30 05:46:36 +08:00
|
|
|
|
document.addEventListener('dragover', function(event) {
|
|
|
|
|
|
var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"]');
|
|
|
|
|
|
var sourceNodeId = readPageDragNodeId(event);
|
|
|
|
|
|
if (pageRow && canDropPage(sourceNodeId, pageRow)) {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
clearPageDropFeedback();
|
|
|
|
|
|
pageRow.setAttribute('data-drop-feedback', 'true');
|
|
|
|
|
|
pageRow.setAttribute('data-drop-position', pageDropPosition(event, pageRow));
|
|
|
|
|
|
activePageDropRow = pageRow;
|
|
|
|
|
|
if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
|
|
|
|
|
|
return;
|
2026-04-29 14:36:24 +08:00
|
|
|
|
}
|
2026-04-30 05:46:36 +08:00
|
|
|
|
var fileTree = document.getElementById('sidebar-file-tree-root');
|
|
|
|
|
|
if (fileTree && fileTree.contains(event.target)) {
|
|
|
|
|
|
var hasFiles = event.dataTransfer && Array.prototype.indexOf.call(event.dataTransfer.types || [], 'Files') >= 0;
|
|
|
|
|
|
var hasInternal = event.dataTransfer && Array.prototype.indexOf.call(event.dataTransfer.types || [], FILETREE_DRAG_MIME) >= 0;
|
|
|
|
|
|
if (!hasFiles && !hasInternal && draggingFileTreeRowIds.length === 0) return;
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
if (activeFileTreeDropRow instanceof HTMLElement) activeFileTreeDropRow.setAttribute('data-drop-target', 'false');
|
|
|
|
|
|
activeFileTreeDropRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"]') || fileTree;
|
|
|
|
|
|
if (activeFileTreeDropRow instanceof HTMLElement) activeFileTreeDropRow.setAttribute('data-drop-target', 'true');
|
|
|
|
|
|
if (event.dataTransfer) event.dataTransfer.dropEffect = hasFiles || event.altKey ? 'copy' : 'move';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('drop', function(event) {
|
|
|
|
|
|
var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"]');
|
|
|
|
|
|
var sourceNodeId = readPageDragNodeId(event);
|
|
|
|
|
|
if (pageRow && canDropPage(sourceNodeId, pageRow)) {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
var position = pageDropPosition(event, pageRow);
|
|
|
|
|
|
var target = resolvePageMoveTarget(pageRow, position);
|
|
|
|
|
|
clearPageDropFeedback();
|
|
|
|
|
|
draggingPageNodeId = '';
|
|
|
|
|
|
void dispatchTreeCommand(pageRow, {
|
|
|
|
|
|
action: 'move',
|
|
|
|
|
|
workspaceId: resolveWorkspaceId(pageRow),
|
|
|
|
|
|
documentId: sourceNodeId,
|
|
|
|
|
|
parentId: target.parentId,
|
|
|
|
|
|
sortOrder: target.sortOrder
|
|
|
|
|
|
}).then(function(){ scheduleProjectionRefresh(resolveWorkspaceId(pageRow)); });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
var fileTree = document.getElementById('sidebar-file-tree-root');
|
|
|
|
|
|
if (fileTree && fileTree.contains(event.target)) {
|
|
|
|
|
|
var targetRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"]');
|
|
|
|
|
|
var files = event.dataTransfer ? Array.from(event.dataTransfer.files || []) : [];
|
|
|
|
|
|
var raw = event.dataTransfer ? event.dataTransfer.getData(FILETREE_DRAG_MIME) || event.dataTransfer.getData('application/x-mnote-file-tree') || '' : '';
|
|
|
|
|
|
var rowIds = draggingFileTreeRowIds.slice();
|
|
|
|
|
|
if (raw) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
var parsed = JSON.parse(raw);
|
|
|
|
|
|
if (Array.isArray(parsed.rowIds)) rowIds = parsed.rowIds;
|
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!files.length && !rowIds.length) return;
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
var detail = {
|
|
|
|
|
|
workspaceId: resolveWorkspaceId(targetRow || fileTree),
|
|
|
|
|
|
targetRowId: targetRow ? targetRow.getAttribute('data-row-id') : null,
|
|
|
|
|
|
targetRowKind: targetRow ? targetRow.getAttribute('data-row-kind') : 'root',
|
|
|
|
|
|
documentId: targetRow ? targetRow.getAttribute('data-document-id') || targetRow.getAttribute('data-doc-id') : null,
|
|
|
|
|
|
assetId: targetRow ? targetRow.getAttribute('data-asset-id') : null
|
|
|
|
|
|
};
|
|
|
|
|
|
if (activeFileTreeDropRow instanceof HTMLElement) activeFileTreeDropRow.setAttribute('data-drop-target', 'false');
|
|
|
|
|
|
activeFileTreeDropRow = null;
|
|
|
|
|
|
if (files.length) {
|
|
|
|
|
|
dispatchSidebarEvent('tree.filetree.external-drop', Object.assign({}, detail, { files: files }));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
dispatchSidebarEvent('tree.filetree.internal-drop', Object.assign({}, detail, { rowIds: rowIds, copy: event.altKey === true }));
|
|
|
|
|
|
}
|
|
|
|
|
|
draggingFileTreeRowIds = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('dragend', function() {
|
|
|
|
|
|
draggingPageNodeId = '';
|
|
|
|
|
|
draggingFileTreeRowIds = [];
|
|
|
|
|
|
clearPageDropFeedback();
|
|
|
|
|
|
if (activeFileTreeDropRow instanceof HTMLElement) activeFileTreeDropRow.setAttribute('data-drop-target', 'false');
|
|
|
|
|
|
activeFileTreeDropRow = null;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener('tree:title-updated', function(event) {
|
|
|
|
|
|
var detail = event.detail || {};
|
|
|
|
|
|
updateTitleEverywhere(detail.documentId, detail.title);
|
2026-04-30 06:58:17 +08:00
|
|
|
|
document.documentElement.setAttribute('data-mnote-title-local-applied', 'true');
|
2026-04-30 05:46:36 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener('tree:snapshot', function(event) {
|
|
|
|
|
|
var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
|
|
|
|
|
|
if (renderPageProjection(payload)) {
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'snapshot');
|
|
|
|
|
|
}
|
|
|
|
|
|
scheduleProjectionRefresh(payload && payload.workspaceId);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener('tree:delta', function(event) {
|
|
|
|
|
|
var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
|
|
|
|
|
|
var data = payload && (payload.data || payload.delta || payload);
|
|
|
|
|
|
var documents = data && (data.upsertDocuments || data.upsert_documents);
|
|
|
|
|
|
if (Array.isArray(documents)) {
|
|
|
|
|
|
documents.forEach(function(doc) {
|
|
|
|
|
|
updateTitleEverywhere(doc.id || doc.documentId, doc.title || '无标题');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'delta');
|
|
|
|
|
|
scheduleProjectionRefresh(payload && payload.workspaceId);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener('tree:resync', function(event) {
|
|
|
|
|
|
var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'resync');
|
|
|
|
|
|
scheduleProjectionRefresh(payload && payload.workspaceId);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var tree = document.getElementById('sidebar-tree-root');
|
|
|
|
|
|
var activeId = currentDocumentId();
|
|
|
|
|
|
if (tree && activeId) {
|
|
|
|
|
|
var activeRow = tree.querySelector('.tree-row[data-node-id="' + cssEscape(activeId) + '"]');
|
|
|
|
|
|
if (activeRow instanceof HTMLElement) activeRow.setAttribute('data-active', 'true');
|
|
|
|
|
|
}
|
2026-04-30 06:58:17 +08:00
|
|
|
|
restoreSidebarTreeTab();
|
2026-04-30 05:46:36 +08:00
|
|
|
|
})();
|
|
|
|
|
|
"##;
|
|
|
|
|
|
|
|
|
|
|
|
const TREE_LIVE_CONTROLLER_JS: &str = r##"
|
|
|
|
|
|
(function(){
|
|
|
|
|
|
if (window.__mnoteTreeLiveControllerStarted) return;
|
|
|
|
|
|
window.__mnoteTreeLiveControllerStarted = true;
|
|
|
|
|
|
|
|
|
|
|
|
function readBootstrap() {
|
|
|
|
|
|
var script = document.getElementById('__MNOTE_TREE_LIVE_BOOTSTRAP__');
|
|
|
|
|
|
var fallback = {
|
|
|
|
|
|
schema: 'mnote.tree_live_bootstrap.v1',
|
|
|
|
|
|
transport: 'convex-command-log-sse',
|
|
|
|
|
|
endpoint: '/api/tree/events',
|
|
|
|
|
|
resyncEndpoint: '/api/tree/projections/sidebar',
|
|
|
|
|
|
rootIds: [],
|
|
|
|
|
|
initialRevision: null
|
|
|
|
|
|
};
|
|
|
|
|
|
if (!script || !script.textContent) return fallback;
|
|
|
|
|
|
try {
|
|
|
|
|
|
return Object.assign(fallback, JSON.parse(script.textContent));
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
return fallback;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveWorkspaceId() {
|
|
|
|
|
|
var withWorkspace = document.querySelector('[data-workspace-id]');
|
|
|
|
|
|
if (withWorkspace) {
|
|
|
|
|
|
var value = (withWorkspace.getAttribute('data-workspace-id') || '').trim();
|
|
|
|
|
|
if (value) return value;
|
|
|
|
|
|
}
|
|
|
|
|
|
var params = new URLSearchParams(window.location.search);
|
|
|
|
|
|
return (params.get('workspaceId') || 'default').trim() || 'default';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function dispatchTreeEvent(name, detail) {
|
|
|
|
|
|
window.dispatchEvent(new CustomEvent(name, { detail: detail }));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applyStatus(status) {
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-tree-live-status', status);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applyTransport(transport) {
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-tree-live-transport', transport || 'convex-command-log-sse');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeActiveSource() {
|
|
|
|
|
|
var source = window.__mnoteTreeLiveEventSource;
|
|
|
|
|
|
if (source && typeof source.close === 'function') {
|
|
|
|
|
|
source.close();
|
|
|
|
|
|
window.__mnoteTreeLiveEventSource = null;
|
|
|
|
|
|
applyStatus('closed');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function start() {
|
|
|
|
|
|
if (!('EventSource' in window)) {
|
|
|
|
|
|
applyStatus('unsupported');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
var bootstrap = readBootstrap();
|
|
|
|
|
|
applyTransport(bootstrap.transport || 'convex-command-log-sse');
|
|
|
|
|
|
var workspaceId = bootstrap.workspaceId || resolveWorkspaceId();
|
|
|
|
|
|
var url = new URL(bootstrap.endpoint || '/api/tree/events', window.location.origin);
|
|
|
|
|
|
url.searchParams.set('workspaceId', workspaceId);
|
|
|
|
|
|
if (Array.isArray(bootstrap.rootIds) && bootstrap.rootIds.length === 1) {
|
|
|
|
|
|
url.searchParams.set('rootNodeId', bootstrap.rootIds[0]);
|
|
|
|
|
|
}
|
|
|
|
|
|
var failures = 0;
|
|
|
|
|
|
var source = new EventSource(url.toString());
|
|
|
|
|
|
window.__mnoteTreeLiveEventSource = source;
|
|
|
|
|
|
applyStatus('connecting');
|
|
|
|
|
|
|
|
|
|
|
|
source.addEventListener('open', function(){
|
|
|
|
|
|
failures = 0;
|
|
|
|
|
|
applyStatus('connected');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
source.addEventListener('snapshot', function(event){
|
|
|
|
|
|
var payload = JSON.parse(event.data || '{}');
|
|
|
|
|
|
var revision = payload.revision || payload.cursor || event.lastEventId || null;
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-tree-live-revision', String(revision || ''));
|
|
|
|
|
|
dispatchTreeEvent('tree:snapshot', { payload: payload, revision: revision, bootstrap: bootstrap });
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
source.addEventListener('delta', function(event){
|
|
|
|
|
|
var payload = JSON.parse(event.data || '{}');
|
|
|
|
|
|
var revision = payload.revision || payload.cursor || event.lastEventId || null;
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-tree-live-revision', String(revision || ''));
|
|
|
|
|
|
dispatchTreeEvent('tree:delta', { payload: payload, revision: revision, bootstrap: bootstrap });
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
source.addEventListener('resync', function(event){
|
|
|
|
|
|
var payload = JSON.parse(event.data || '{}');
|
|
|
|
|
|
var revision = payload.revision || payload.cursor || event.lastEventId || null;
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-tree-live-revision', String(revision || ''));
|
|
|
|
|
|
dispatchTreeEvent('tree:resync', { payload: payload, revision: revision, bootstrap: bootstrap });
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
source.onerror = function(){
|
|
|
|
|
|
failures += 1;
|
|
|
|
|
|
applyStatus(failures >= 3 ? 'resync-pending' : 'reconnecting');
|
|
|
|
|
|
if (failures >= 3) {
|
|
|
|
|
|
dispatchTreeEvent('tree:resync-requested', { endpoint: bootstrap.resyncEndpoint, workspaceId: workspaceId });
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
window.__mnoteTreeLiveClose = closeActiveSource;
|
|
|
|
|
|
window.addEventListener('pagehide', closeActiveSource, { once: true });
|
|
|
|
|
|
|
|
|
|
|
|
if (document.readyState === 'loading') {
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', start, { once: true });
|
|
|
|
|
|
} else {
|
|
|
|
|
|
start();
|
2026-04-29 14:36:24 +08:00
|
|
|
|
}
|
2026-04-29 12:24:44 +08:00
|
|
|
|
})();
|
|
|
|
|
|
"##;
|
|
|
|
|
|
|
|
|
|
|
|
/// MNOTE Wolai 风格页面布局
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 包含左侧栏 + 内容区的双栏布局。
|
|
|
|
|
|
/// 侧栏显示品牌、导航链接和可选的页面树。
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # 用法
|
|
|
|
|
|
///
|
|
|
|
|
|
/// ```ignore
|
|
|
|
|
|
/// view! {
|
|
|
|
|
|
/// <PageLayout current_nav="home" sidebar_tree_html={None}>
|
|
|
|
|
|
/// <section>...</section>
|
|
|
|
|
|
/// </PageLayout>
|
|
|
|
|
|
/// }
|
|
|
|
|
|
/// ```
|
|
|
|
|
|
#[component]
|
|
|
|
|
|
pub fn PageLayout(
|
|
|
|
|
|
children: Children,
|
|
|
|
|
|
current_nav: &'static str,
|
|
|
|
|
|
/// 侧栏页面树 HTML(可选),由路由 handler 渲染
|
|
|
|
|
|
#[prop(optional)]
|
|
|
|
|
|
sidebar_tree_html: Option<String>,
|
|
|
|
|
|
/// 工作区名称(可选),显示在侧栏顶部
|
|
|
|
|
|
#[prop(optional)]
|
|
|
|
|
|
workspace_name: Option<String>,
|
|
|
|
|
|
/// workspace shell 侧栏 sections HTML(可选),由 projection 渲染
|
|
|
|
|
|
#[prop(optional)]
|
|
|
|
|
|
workspace_sidebar_html: Option<String>,
|
2026-04-29 14:36:24 +08:00
|
|
|
|
/// 顶栏当前页面标题(可选)
|
|
|
|
|
|
#[prop(optional)]
|
|
|
|
|
|
topbar_title: Option<String>,
|
2026-04-29 12:24:44 +08:00
|
|
|
|
) -> impl IntoView {
|
|
|
|
|
|
let sidebar_tree_html = sidebar_tree_html.unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
|
|
let ws_name = workspace_name
|
|
|
|
|
|
.map(|value| value.trim().to_string())
|
|
|
|
|
|
.filter(|value| !value.is_empty())
|
|
|
|
|
|
.unwrap_or_else(|| "开发用户 的空间".to_string());
|
2026-04-29 14:36:24 +08:00
|
|
|
|
let topbar_title = topbar_title
|
|
|
|
|
|
.map(|value| value.trim().to_string())
|
|
|
|
|
|
.filter(|value| !value.is_empty())
|
|
|
|
|
|
.unwrap_or_else(|| "个人".to_string());
|
2026-04-29 12:24:44 +08:00
|
|
|
|
let sidebar_sections_html = workspace_sidebar_html
|
|
|
|
|
|
.map(|value| value.trim().to_string())
|
|
|
|
|
|
.filter(|value| !value.is_empty())
|
|
|
|
|
|
.unwrap_or_else(|| {
|
|
|
|
|
|
let dataset = serde_json::json!({
|
|
|
|
|
|
"workspaces": [{ "id": "default", "name": ws_name.clone() }],
|
|
|
|
|
|
"documents": []
|
|
|
|
|
|
});
|
|
|
|
|
|
let projection = crate::workspace_shell::build_workspace_shell_projection(
|
2026-04-29 14:36:24 +08:00
|
|
|
|
&dataset, "default", None, &ws_name,
|
2026-04-29 12:24:44 +08:00
|
|
|
|
);
|
|
|
|
|
|
crate::workspace_shell::render_workspace_shell_sidebar_html(
|
|
|
|
|
|
&projection,
|
|
|
|
|
|
Some(sidebar_tree_html.as_str()),
|
2026-04-29 14:36:24 +08:00
|
|
|
|
None,
|
2026-04-29 12:24:44 +08:00
|
|
|
|
)
|
|
|
|
|
|
});
|
2026-04-30 05:46:36 +08:00
|
|
|
|
let tree_live_bootstrap = serde_json::json!({
|
|
|
|
|
|
"schema": "mnote.tree_live_bootstrap.v1",
|
|
|
|
|
|
"transport": "convex-command-log-sse",
|
|
|
|
|
|
"workspaceId": null,
|
|
|
|
|
|
"rootIds": [],
|
|
|
|
|
|
"initialRevision": null,
|
|
|
|
|
|
"endpoint": "/api/tree/events",
|
|
|
|
|
|
"resyncEndpoint": "/api/tree/projections/sidebar",
|
|
|
|
|
|
"views": ["page-tree", "file-tree"]
|
|
|
|
|
|
})
|
|
|
|
|
|
.to_string();
|
2026-04-29 12:24:44 +08:00
|
|
|
|
|
|
|
|
|
|
view! {
|
|
|
|
|
|
<div class="mnote-shell wolai-workspace-shell" data-mnote-web-owner="mnote-web" data-mnote-shell="workspace">
|
|
|
|
|
|
<aside class="mnote-sidebar wolai-sidebar" data-testid="wolai-sidebar">
|
|
|
|
|
|
<div class="mnote-sidebar-header wolai-sidebar-header" data-testid="wolai-workspace-identity">
|
|
|
|
|
|
<a href="/" class="mnote-sidebar-brand wolai-avatar" aria-label="工作区首页">"L"</a>
|
2026-04-30 18:36:50 +08:00
|
|
|
|
<span class="sidebar-workspace-name">{ws_name.clone()}</span>
|
2026-04-29 12:24:44 +08:00
|
|
|
|
<span class="wolai-sidebar-chevron" aria-hidden="true">"⌄"</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<nav class="mnote-sidebar-nav wolai-quick-actions" aria-label="快捷操作">
|
2026-04-30 16:18:54 +08:00
|
|
|
|
<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>
|
2026-04-30 06:58:17 +08:00
|
|
|
|
<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>
|
2026-04-29 12:24:44 +08:00
|
|
|
|
</nav>
|
|
|
|
|
|
<div class="wolai-sidebar-body" inner_html={sidebar_sections_html}></div>
|
2026-04-30 05:46:36 +08:00
|
|
|
|
<script id="__MNOTE_TREE_LIVE_BOOTSTRAP__" type="application/json" inner_html={tree_live_bootstrap}></script>
|
2026-04-29 12:24:44 +08:00
|
|
|
|
<script inner_html={SIDEBAR_TREE_JS.to_string()}></script>
|
2026-04-30 05:46:36 +08:00
|
|
|
|
<script id="mnote-tree-live-controller" inner_html={TREE_LIVE_CONTROLLER_JS.to_string()}></script>
|
2026-04-29 12:24:44 +08:00
|
|
|
|
</aside>
|
|
|
|
|
|
<div class="mnote-main">
|
|
|
|
|
|
<header class="wolai-topbar" data-testid="wolai-topbar">
|
2026-04-29 16:23:49 +08:00
|
|
|
|
<div class="wolai-topbar-left">
|
2026-04-30 06:58:17 +08:00
|
|
|
|
<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>
|
2026-04-29 16:23:49 +08:00
|
|
|
|
<nav class="wolai-breadcrumb" aria-label="页面路径">
|
2026-04-30 18:36:50 +08:00
|
|
|
|
<span class="wolai-breadcrumb-root">{ws_name.clone()}</span>
|
|
|
|
|
|
<span class="wolai-breadcrumb-separator" aria-hidden="true">"›"</span>
|
2026-04-30 06:58:17 +08:00
|
|
|
|
<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>
|
2026-04-29 16:23:49 +08:00
|
|
|
|
</nav>
|
|
|
|
|
|
</div>
|
2026-04-29 12:24:44 +08:00
|
|
|
|
<div class="wolai-topbar-actions" aria-label="页面操作">
|
2026-04-30 18:36:50 +08:00
|
|
|
|
<span class="wolai-public-pill" data-testid="wolai-public-state">"全网公开"</span>
|
2026-04-30 06:58:17 +08:00
|
|
|
|
<button type="button" class="wolai-icon-button" title="收藏" aria-label="收藏"><span class="material-symbols-outlined" data-icon="star" aria-hidden="true"></span></button>
|
2026-04-30 18:36:50 +08:00
|
|
|
|
<button type="button" class="wolai-icon-button" title="演示" aria-label="演示"><span class="material-symbols-outlined" data-icon="slideshow" aria-hidden="true"></span></button>
|
|
|
|
|
|
<button type="button" class="wolai-icon-button" title="评论" aria-label="评论"><span class="material-symbols-outlined" data-icon="comment" aria-hidden="true"></span></button>
|
|
|
|
|
|
<button type="button" class="wolai-icon-button" title="分享" aria-label="分享"><span class="material-symbols-outlined" data-icon="share" aria-hidden="true"></span></button>
|
|
|
|
|
|
<button type="button" class="wolai-icon-button" title="成员" aria-label="成员"><span class="material-symbols-outlined" data-icon="person_add" aria-hidden="true"></span></button>
|
2026-05-06 21:44:20 +08:00
|
|
|
|
<button type="button" class="wolai-icon-button" title="页面历史" aria-label="历史" data-mnote-action="open-page-history"><span class="material-symbols-outlined" data-icon="history" aria-hidden="true"></span></button>
|
|
|
|
|
|
<button type="button" class="wolai-icon-button" title="页面选项和全局选项" aria-label="更多" data-testid="wolai-page-settings-trigger" data-mnote-action="open-page-settings"><span class="material-symbols-outlined" data-icon="more_horiz" aria-hidden="true"></span></button>
|
2026-04-29 12:24:44 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
<article class="mnote-content">
|
|
|
|
|
|
{children()}
|
|
|
|
|
|
</article>
|
|
|
|
|
|
<div class="wolai-floating-actions" aria-label="浮动操作">
|
2026-04-30 06:58:17 +08:00
|
|
|
|
<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>
|
2026-05-06 21:44:20 +08:00
|
|
|
|
<button type="button" data-testid="wolai-floating-ai" class="wolai-floating-button wolai-floating-button--ai" aria-label="AI 助手" title="点击 进入空间智能问答" data-mnote-action="open-page-ai"><span class="material-symbols-outlined material-symbols-filled" data-icon="auto_awesome" aria-hidden="true"></span></button>
|
2026-04-29 12:24:44 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-30 05:46:36 +08:00
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
mod tests {
|
|
|
|
|
|
use super::{SIDEBAR_TREE_JS, TREE_LIVE_CONTROLLER_JS};
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
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"));
|
2026-04-30 06:58:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("MNOTE_SIDEBAR_TREE_MODE_KEY"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("restoreSidebarTreeTab"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("treeView"));
|
2026-04-30 05:46:36 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("application/x-mnote-page-tree-node"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("dragstart"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("drop"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("data-drop-feedback"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("action: 'move'"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("sidebar-file-tree-root"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("tree.filetree.open"));
|
2026-04-30 06:58:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("tree.asset.open"));
|
2026-04-30 05:46:36 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("tree.filetree.internal-drop"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("tree.filetree.external-drop"));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 06:58:17 +08:00
|
|
|
|
#[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"));
|
2026-05-06 21:44:20 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("复制访问链接"));
|
2026-04-30 06:58:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains("删除到垃圾桶"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains(
|
2026-05-06 21:44:20 +08:00
|
|
|
|
r#".tree-row[data-shell-mode="page"][data-node-id="' + escaped + '"] > .tree-link > .tree-link-title"#
|
|
|
|
|
|
));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_JS.contains(
|
|
|
|
|
|
r#".tree-row[data-shell-mode="filetree"][data-row-id="' + escapedDocRowId + '"] > .tree-link > .tree-link-title"#
|
|
|
|
|
|
));
|
|
|
|
|
|
assert!(!SIDEBAR_TREE_JS.contains(
|
|
|
|
|
|
r#".tree-row[data-document-id="' + escaped + '"] > .tree-link > .tree-link-title"#
|
2026-04-30 06:58:17 +08:00
|
|
|
|
));
|
|
|
|
|
|
assert!(!SIDEBAR_TREE_JS
|
2026-05-06 21:44:20 +08:00
|
|
|
|
.contains(r#"[data-node-id="' + cssEscape(documentId) + '"] .tree-link-title"#));
|
2026-04-30 06:58:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 05:46:36 +08:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn tree_live_controller_marks_transport_and_closes_source_on_pagehide() {
|
|
|
|
|
|
assert!(TREE_LIVE_CONTROLLER_JS.contains("convex-command-log-sse"));
|
|
|
|
|
|
assert!(TREE_LIVE_CONTROLLER_JS.contains("data-mnote-tree-live-transport"));
|
|
|
|
|
|
assert!(TREE_LIVE_CONTROLLER_JS.contains("pagehide"));
|
|
|
|
|
|
assert!(TREE_LIVE_CONTROLLER_JS.contains("__mnoteTreeLiveEventSource"));
|
|
|
|
|
|
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:snapshot"));
|
|
|
|
|
|
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:delta"));
|
|
|
|
|
|
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:resync"));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|