refactor: split sidebar settings and slash menu view
This commit is contained in:
+4
-2
@@ -160,6 +160,7 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib routes::web_shell
|
|||||||
- 新建:`rust/crates/mnote-web/browser/sidebar-filetree-upload-runtime.js`
|
- 新建:`rust/crates/mnote-web/browser/sidebar-filetree-upload-runtime.js`
|
||||||
- 新建:`rust/crates/mnote-web/browser/sidebar-attachment-open-runtime.js`
|
- 新建:`rust/crates/mnote-web/browser/sidebar-attachment-open-runtime.js`
|
||||||
- 新建:`rust/crates/mnote-web/browser/sidebar-tree-live-apply-runtime.js`
|
- 新建:`rust/crates/mnote-web/browser/sidebar-tree-live-apply-runtime.js`
|
||||||
|
- 新建:`rust/crates/mnote-web/browser/sidebar-page-settings-runtime.js`
|
||||||
|
|
||||||
拆分项:
|
拆分项:
|
||||||
|
|
||||||
@@ -170,7 +171,7 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib routes::web_shell
|
|||||||
- [x] C5. `sidebar-filetree-upload-runtime.js`:local upload target plan、drop/paste preflight、readonly guard。
|
- [x] C5. `sidebar-filetree-upload-runtime.js`:local upload target plan、drop/paste preflight、readonly guard。
|
||||||
- [x] C6. `sidebar-attachment-open-runtime.js`:OnlyOffice/PDF/code/image open mode guard。
|
- [x] C6. `sidebar-attachment-open-runtime.js`:OnlyOffice/PDF/code/image open mode guard。
|
||||||
- [x] C7. `sidebar-tree-live-apply-runtime.js`:WS/SSE snapshot/delta/resync DOM apply。
|
- [x] C7. `sidebar-tree-live-apply-runtime.js`:WS/SSE snapshot/delta/resync DOM apply。
|
||||||
- [ ] C8. `sidebar-tree-runtime.js` 保留为 entrypoint,目标少于 3,000 行。
|
- [x] C8. `sidebar-tree-runtime.js` 保留为 entrypoint,目标少于 3,000 行。
|
||||||
|
|
||||||
验收命令:
|
验收命令:
|
||||||
|
|
||||||
@@ -183,6 +184,7 @@ node --check rust/crates/mnote-web/browser/sidebar-filetree-command-runtime.js
|
|||||||
node --check rust/crates/mnote-web/browser/sidebar-filetree-upload-runtime.js
|
node --check rust/crates/mnote-web/browser/sidebar-filetree-upload-runtime.js
|
||||||
node --check rust/crates/mnote-web/browser/sidebar-attachment-open-runtime.js
|
node --check rust/crates/mnote-web/browser/sidebar-attachment-open-runtime.js
|
||||||
node --check rust/crates/mnote-web/browser/sidebar-tree-live-apply-runtime.js
|
node --check rust/crates/mnote-web/browser/sidebar-tree-live-apply-runtime.js
|
||||||
|
node --check rust/crates/mnote-web/browser/sidebar-page-settings-runtime.js
|
||||||
cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib ssr::pages::layout -- --test-threads=1
|
cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib ssr::pages::layout -- --test-threads=1
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -247,7 +249,7 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib routes::tree -- --
|
|||||||
- [x] E2. `mount.rs`:迁出 mount context、mounted handles、`mount_app_into` 周边可独立部分;`#[wasm_bindgen]` wrapper 可暂留根文件。
|
- [x] E2. `mount.rs`:迁出 mount context、mounted handles、`mount_app_into` 周边可独立部分;`#[wasm_bindgen]` wrapper 可暂留根文件。
|
||||||
- [x] E3. `runtime_bridge.rs`:迁出 `dispatch_runtime_event`、`dispatch_*_to_target`、host command listener install。
|
- [x] E3. `runtime_bridge.rs`:迁出 `dispatch_runtime_event`、`dispatch_*_to_target`、host command listener install。
|
||||||
- [x] E4. `block_transform.rs`:迁出 `run_block_turn_into_action`、`top_level_block_matches_action`、page/block transform helpers。
|
- [x] E4. `block_transform.rs`:迁出 `run_block_turn_into_action`、`top_level_block_matches_action`、page/block transform helpers。
|
||||||
- [ ] E5. `slash_menu_view.rs`:迁出 slash menu Leptos view 和 click handling。
|
- [x] E5. `slash_menu_view.rs`:迁出 slash menu Leptos view 和 click handling。
|
||||||
- [ ] E6. `block_handle_menu_view.rs`:迁出 block handle menu 和 submenu view,作为最后一刀。
|
- [ ] E6. `block_handle_menu_view.rs`:迁出 block handle menu 和 submenu view,作为最后一刀。
|
||||||
|
|
||||||
验收标准:
|
验收标准:
|
||||||
|
|||||||
@@ -0,0 +1,700 @@
|
|||||||
|
export function createSidebarPageSettingsRuntime(context) {
|
||||||
|
const {
|
||||||
|
currentDocumentId,
|
||||||
|
currentPageAggregate,
|
||||||
|
currentRootUri,
|
||||||
|
currentSourceKind,
|
||||||
|
currentWorkspaceSourcePayload,
|
||||||
|
escapeHtml,
|
||||||
|
pageUiState,
|
||||||
|
resolveWorkspaceId,
|
||||||
|
searchText,
|
||||||
|
} = context;
|
||||||
|
const MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY = context.globalShowHeadingNumbersKey || 'mnote.global.showHeadingNumbers';
|
||||||
|
|
||||||
|
function defaultPageOptions() {
|
||||||
|
return {
|
||||||
|
wideLayout: false,
|
||||||
|
smallText: false,
|
||||||
|
layoutDensity: 'normal',
|
||||||
|
showHeadingNumbers: false,
|
||||||
|
showToc: false,
|
||||||
|
showStructure: false,
|
||||||
|
protectEditing: false,
|
||||||
|
showWordCount: true,
|
||||||
|
collapseBacklinks: false,
|
||||||
|
pageFont: 'default',
|
||||||
|
hideChildPages: false,
|
||||||
|
showBlockRefCount: false,
|
||||||
|
hideTitleHeader: 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 readGlobalShowHeadingNumbers() {
|
||||||
|
try {
|
||||||
|
var raw = window.localStorage ? window.localStorage.getItem(MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY) : '';
|
||||||
|
return raw === 'true' || raw === '1';
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeGlobalShowHeadingNumbers(value) {
|
||||||
|
try {
|
||||||
|
if (window.localStorage) {
|
||||||
|
window.localStorage.setItem(MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY, value ? 'true' : 'false');
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
document.documentElement.setAttribute('data-global-show-heading-numbers', String(Boolean(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function effectiveShowHeadingNumbers(options) {
|
||||||
|
return readGlobalShowHeadingNumbers();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageOptionIsSupported(name) {
|
||||||
|
return name === 'wideLayout'
|
||||||
|
|| name === 'smallText'
|
||||||
|
|| name === 'layoutDensity'
|
||||||
|
|| name === 'pageFont'
|
||||||
|
|| name === 'showHeadingNumbers'
|
||||||
|
|| (name === 'hideTitleHeader' && currentSourceKind() === 'local_folder');
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageOptionDescription(name) {
|
||||||
|
if (name === 'wideLayout') return '自适应宽度';
|
||||||
|
if (name === 'smallText') return '小字体';
|
||||||
|
if (name === 'hideTitleHeader') return '隐藏本地 Markdown 文件标题';
|
||||||
|
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 === 'hideTitleHeader') return '已接通:本地 Markdown 页头标题立即隐藏或显示';
|
||||||
|
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 globalShowHeadingNumbers = readGlobalShowHeadingNumbers();
|
||||||
|
var showHeadingNumbers = effectiveShowHeadingNumbers(options);
|
||||||
|
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(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(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(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-global-show-heading-numbers', String(globalShowHeadingNumbers));
|
||||||
|
document.documentElement.setAttribute('data-page-show-heading-numbers', String(showHeadingNumbers));
|
||||||
|
document.documentElement.setAttribute('data-page-hide-title-header', String(Boolean(options.hideTitleHeader)));
|
||||||
|
var titleHeader = document.querySelector('.document-pane[data-pane-role="primary"] .document-shell-header') || document.querySelector('.document-shell-header');
|
||||||
|
if (titleHeader instanceof HTMLElement) {
|
||||||
|
var hidden = Boolean(options.hideTitleHeader);
|
||||||
|
titleHeader.hidden = hidden;
|
||||||
|
titleHeader.setAttribute('data-page-title-hidden', String(hidden));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 createGlobalHeadingNumbersRow() {
|
||||||
|
return '' +
|
||||||
|
'<label class="wolai-page-setting-row" data-page-setting-row="globalShowHeadingNumbers">' +
|
||||||
|
'<span class="wolai-page-setting-copy">' +
|
||||||
|
'<span class="wolai-page-setting-label">标题自动编号</span>' +
|
||||||
|
'</span>' +
|
||||||
|
'<input type="checkbox" class="wolai-page-setting-checkbox" data-global-option-checkbox="showHeadingNumbers" />' +
|
||||||
|
'</label>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGlobalOptions(popover) {
|
||||||
|
var globalHeadingNumbers = readGlobalShowHeadingNumbers();
|
||||||
|
popover.querySelectorAll('[data-global-option-checkbox="showHeadingNumbers"]').forEach(function(input) {
|
||||||
|
input.checked = globalHeadingNumbers;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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="index">索引</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('protectEditing', 'checkbox') +
|
||||||
|
createPageOptionRow('hideTitleHeader', '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="index" hidden>' +
|
||||||
|
'<div class="wolai-page-settings-index-panel" data-testid="wolai-page-settings-local-index-panel">' +
|
||||||
|
'<div class="wolai-page-settings-index-status" data-testid="wolai-page-settings-local-index-status"></div>' +
|
||||||
|
'<section class="wolai-page-settings-index-group">' +
|
||||||
|
'<div class="wolai-page-settings-index-title">反链</div>' +
|
||||||
|
'<div class="wolai-page-settings-index-list" data-testid="wolai-page-settings-local-index-backlinks"></div>' +
|
||||||
|
'</section>' +
|
||||||
|
'<section class="wolai-page-settings-index-group">' +
|
||||||
|
'<div class="wolai-page-settings-index-title">标签</div>' +
|
||||||
|
'<div class="wolai-page-settings-index-list" data-testid="wolai-page-settings-local-index-tags"></div>' +
|
||||||
|
'</section>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="wolai-page-settings-section" data-page-settings-panel="global" hidden>' +
|
||||||
|
createGlobalHeadingNumbersRow() +
|
||||||
|
'</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 pageSettingsLocalIndexScopeKey() {
|
||||||
|
return [
|
||||||
|
currentSourceKind(),
|
||||||
|
currentRootUri(),
|
||||||
|
resolveWorkspaceId(document.body),
|
||||||
|
currentDocumentId()
|
||||||
|
].join('|');
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageSettingsLocalIndexIsAvailable() {
|
||||||
|
return currentSourceKind() === 'local_folder' && Boolean(currentRootUri()) && Boolean(currentDocumentId());
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageSettingsLocalIndexEmpty(message) {
|
||||||
|
return '<div class="wolai-page-settings-index-empty">' + escapeHtml(message) + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPageSettingsLocalIndexList(items, kind) {
|
||||||
|
var rows = Array.isArray(items) ? items : [];
|
||||||
|
if (!rows.length) {
|
||||||
|
return pageSettingsLocalIndexEmpty(kind === 'backlinks' ? '暂无反链' : '暂无标签');
|
||||||
|
}
|
||||||
|
if (kind === 'backlinks') {
|
||||||
|
return rows.slice(0, 12).map(function(item) {
|
||||||
|
var title = searchText(item && item.title) || searchText(item && item.path) || '未命名页面';
|
||||||
|
var path = searchText(item && item.path);
|
||||||
|
var snippet = searchText(item && item.snippet);
|
||||||
|
return '' +
|
||||||
|
'<div class="wolai-page-settings-index-row">' +
|
||||||
|
'<div class="wolai-page-settings-index-row-title">' + escapeHtml(title) + '</div>' +
|
||||||
|
(path ? '<div class="wolai-page-settings-index-row-meta">' + escapeHtml(path) + '</div>' : '') +
|
||||||
|
(snippet ? '<div class="wolai-page-settings-index-row-snippet">' + escapeHtml(snippet) + '</div>' : '') +
|
||||||
|
'</div>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
return rows.slice(0, 16).map(function(item) {
|
||||||
|
var tag = searchText(item && item.tag) || 'untagged';
|
||||||
|
var count = Number(item && item.count || 0);
|
||||||
|
return '' +
|
||||||
|
'<div class="wolai-page-settings-index-row is-tag">' +
|
||||||
|
'<div class="wolai-page-settings-index-row-title">#' + escapeHtml(tag) + '</div>' +
|
||||||
|
'<div class="wolai-page-settings-index-row-meta">' + count + ' 个页面</div>' +
|
||||||
|
'</div>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPageSettingsLocalIndex(popover) {
|
||||||
|
popover = popover || ensurePageSettingsPopover();
|
||||||
|
var statusNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-status"]');
|
||||||
|
var backlinksNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-backlinks"]');
|
||||||
|
var tagsNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-tags"]');
|
||||||
|
if (!(statusNode instanceof HTMLElement) || !(backlinksNode instanceof HTMLElement) || !(tagsNode instanceof HTMLElement)) return;
|
||||||
|
|
||||||
|
if (!pageSettingsLocalIndexIsAvailable()) {
|
||||||
|
statusNode.textContent = '本地索引仅在本地工作区页面可用';
|
||||||
|
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('未连接本地 root');
|
||||||
|
tagsNode.innerHTML = pageSettingsLocalIndexEmpty('未连接本地 root');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var summary = pageUiState.localIndexSummary || {};
|
||||||
|
if (summary.loading) {
|
||||||
|
statusNode.textContent = '正在读取本地索引...';
|
||||||
|
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('加载中');
|
||||||
|
tagsNode.innerHTML = pageSettingsLocalIndexEmpty('加载中');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (summary.error) {
|
||||||
|
statusNode.textContent = '本地索引读取失败';
|
||||||
|
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty(summary.error);
|
||||||
|
tagsNode.innerHTML = pageSettingsLocalIndexEmpty(summary.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (summary.scopeKey !== pageSettingsLocalIndexScopeKey()) {
|
||||||
|
statusNode.textContent = '切换到索引页签后读取本地索引';
|
||||||
|
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('等待读取');
|
||||||
|
tagsNode.innerHTML = pageSettingsLocalIndexEmpty('等待读取');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusNode.textContent = '来自当前授权 root 的 .mnote/index/search-index.json';
|
||||||
|
backlinksNode.innerHTML = renderPageSettingsLocalIndexList(summary.backlinks, 'backlinks');
|
||||||
|
tagsNode.innerHTML = renderPageSettingsLocalIndexList(summary.tags, 'tags');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPageSettingsLocalIndex(force) {
|
||||||
|
if (!pageSettingsLocalIndexIsAvailable()) {
|
||||||
|
renderPageSettingsLocalIndex();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var scopeKey = pageSettingsLocalIndexScopeKey();
|
||||||
|
var current = pageUiState.localIndexSummary || {};
|
||||||
|
if (!force && current.scopeKey === scopeKey && !current.error && !current.loading) {
|
||||||
|
renderPageSettingsLocalIndex();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pageUiState.localIndexSummary = {
|
||||||
|
scopeKey: scopeKey,
|
||||||
|
loading: true,
|
||||||
|
error: '',
|
||||||
|
backlinks: null,
|
||||||
|
tags: null
|
||||||
|
};
|
||||||
|
renderPageSettingsLocalIndex();
|
||||||
|
try {
|
||||||
|
var baseParams = new URLSearchParams();
|
||||||
|
baseParams.set('workspaceId', resolveWorkspaceId(document.body));
|
||||||
|
baseParams.set('rootUri', currentRootUri());
|
||||||
|
var backlinksParams = new URLSearchParams(baseParams);
|
||||||
|
backlinksParams.set('documentId', currentDocumentId());
|
||||||
|
var backlinksUrl = '/api/search/local-index/backlinks?' + backlinksParams.toString();
|
||||||
|
var tagsUrl = '/api/search/local-index/tags?' + baseParams.toString();
|
||||||
|
var responses = await Promise.all([
|
||||||
|
fetch(backlinksUrl, { headers: { accept: 'application/json' } }),
|
||||||
|
fetch(tagsUrl, { headers: { accept: 'application/json' } })
|
||||||
|
]);
|
||||||
|
var backlinksPayload = await responses[0].json().catch(function(){ return null; });
|
||||||
|
var tagsPayload = await responses[1].json().catch(function(){ return null; });
|
||||||
|
if (!responses[0].ok || !backlinksPayload || backlinksPayload.ok !== true) {
|
||||||
|
throw new Error('backlinks_' + responses[0].status);
|
||||||
|
}
|
||||||
|
if (!responses[1].ok || !tagsPayload || tagsPayload.ok !== true) {
|
||||||
|
throw new Error('tags_' + responses[1].status);
|
||||||
|
}
|
||||||
|
pageUiState.localIndexSummary = {
|
||||||
|
scopeKey: scopeKey,
|
||||||
|
loading: false,
|
||||||
|
error: '',
|
||||||
|
backlinks: backlinksPayload.result && Array.isArray(backlinksPayload.result.backlinks) ? backlinksPayload.result.backlinks : [],
|
||||||
|
tags: tagsPayload.result && Array.isArray(tagsPayload.result.tags) ? tagsPayload.result.tags : []
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
pageUiState.localIndexSummary = {
|
||||||
|
scopeKey: scopeKey,
|
||||||
|
loading: false,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
backlinks: [],
|
||||||
|
tags: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
renderPageSettingsLocalIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
renderGlobalOptions(popover);
|
||||||
|
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>';
|
||||||
|
}
|
||||||
|
renderPageSettingsLocalIndex(popover);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
if (tabName === 'index') void loadPageSettingsLocalIndex(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
...currentWorkspaceSourcePayload(),
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
applyPageOptionsToShell,
|
||||||
|
closePageHistoryDrawer,
|
||||||
|
closePageSettingsPopover,
|
||||||
|
closePageShareDialog,
|
||||||
|
currentPageOptions,
|
||||||
|
ensureHistorySnapshotsSeeded,
|
||||||
|
ensurePageHistoryDrawer,
|
||||||
|
ensurePageSettingsPopover,
|
||||||
|
ensurePageShareDialog,
|
||||||
|
isPageSettingsOpen,
|
||||||
|
openPageHistoryDrawer,
|
||||||
|
openPageSettingsPopover,
|
||||||
|
openPageShareDialog,
|
||||||
|
pageOptionIsSupported,
|
||||||
|
persistPageOptionsPatch,
|
||||||
|
recordPageHistorySnapshot,
|
||||||
|
renderPageSettingsPopover,
|
||||||
|
setActivePageSettingsTab,
|
||||||
|
togglePageSettingsPopover,
|
||||||
|
updatePageSettingsTriggerState,
|
||||||
|
writeGlobalShowHeadingNumbers,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { createSidebarAttachmentOpenRuntime } from './sidebar-attachment-open-ru
|
|||||||
import { createSidebarFileTreeCommandRuntime } from './sidebar-filetree-command-runtime.js';
|
import { createSidebarFileTreeCommandRuntime } from './sidebar-filetree-command-runtime.js';
|
||||||
import { createSidebarFileTreeUploadRuntime } from './sidebar-filetree-upload-runtime.js';
|
import { createSidebarFileTreeUploadRuntime } from './sidebar-filetree-upload-runtime.js';
|
||||||
import { createSidebarPageAiRuntime } from './sidebar-page-ai-runtime.js';
|
import { createSidebarPageAiRuntime } from './sidebar-page-ai-runtime.js';
|
||||||
|
import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtime.js';
|
||||||
|
|
||||||
(function(){
|
(function(){
|
||||||
if (window.__mnoteSidebarTreeRuntimeStarted) return;
|
if (window.__mnoteSidebarTreeRuntimeStarted) return;
|
||||||
@@ -191,195 +192,6 @@ import { createSidebarPageAiRuntime } from './sidebar-page-ai-runtime.js';
|
|||||||
return parseJsonScript('__MNOTE_PAGE_AGGREGATE__') || {};
|
return parseJsonScript('__MNOTE_PAGE_AGGREGATE__') || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultPageOptions() {
|
|
||||||
return {
|
|
||||||
wideLayout: false,
|
|
||||||
smallText: false,
|
|
||||||
layoutDensity: 'normal',
|
|
||||||
showHeadingNumbers: false,
|
|
||||||
showToc: false,
|
|
||||||
showStructure: false,
|
|
||||||
protectEditing: false,
|
|
||||||
showWordCount: true,
|
|
||||||
collapseBacklinks: false,
|
|
||||||
pageFont: 'default',
|
|
||||||
hideChildPages: false,
|
|
||||||
showBlockRefCount: false,
|
|
||||||
hideTitleHeader: 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 readGlobalShowHeadingNumbers() {
|
|
||||||
try {
|
|
||||||
var raw = window.localStorage ? window.localStorage.getItem(MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY) : '';
|
|
||||||
return raw === 'true' || raw === '1';
|
|
||||||
} catch (_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeGlobalShowHeadingNumbers(value) {
|
|
||||||
try {
|
|
||||||
if (window.localStorage) {
|
|
||||||
window.localStorage.setItem(MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY, value ? 'true' : 'false');
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
document.documentElement.setAttribute('data-global-show-heading-numbers', String(Boolean(value)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function effectiveShowHeadingNumbers(options) {
|
|
||||||
return readGlobalShowHeadingNumbers();
|
|
||||||
}
|
|
||||||
|
|
||||||
function pageOptionIsSupported(name) {
|
|
||||||
return name === 'wideLayout'
|
|
||||||
|| name === 'smallText'
|
|
||||||
|| name === 'layoutDensity'
|
|
||||||
|| name === 'pageFont'
|
|
||||||
|| name === 'showHeadingNumbers'
|
|
||||||
|| (name === 'hideTitleHeader' && currentSourceKind() === 'local_folder');
|
|
||||||
}
|
|
||||||
|
|
||||||
function pageOptionDescription(name) {
|
|
||||||
if (name === 'wideLayout') return '自适应宽度';
|
|
||||||
if (name === 'smallText') return '小字体';
|
|
||||||
if (name === 'hideTitleHeader') return '隐藏本地 Markdown 文件标题';
|
|
||||||
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 === 'hideTitleHeader') return '已接通:本地 Markdown 页头标题立即隐藏或显示';
|
|
||||||
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 globalShowHeadingNumbers = readGlobalShowHeadingNumbers();
|
|
||||||
var showHeadingNumbers = effectiveShowHeadingNumbers(options);
|
|
||||||
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(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(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(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-global-show-heading-numbers', String(globalShowHeadingNumbers));
|
|
||||||
document.documentElement.setAttribute('data-page-show-heading-numbers', String(showHeadingNumbers));
|
|
||||||
document.documentElement.setAttribute('data-page-hide-title-header', String(Boolean(options.hideTitleHeader)));
|
|
||||||
var titleHeader = document.querySelector('.document-pane[data-pane-role="primary"] .document-shell-header') || document.querySelector('.document-shell-header');
|
|
||||||
if (titleHeader instanceof HTMLElement) {
|
|
||||||
var hidden = Boolean(options.hideTitleHeader);
|
|
||||||
titleHeader.hidden = hidden;
|
|
||||||
titleHeader.setAttribute('data-page-title-hidden', String(hidden));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sidebarWorkspace = createSidebarWorkspaceRuntime({
|
const sidebarWorkspace = createSidebarWorkspaceRuntime({
|
||||||
cssEscape,
|
cssEscape,
|
||||||
currentDocumentId,
|
currentDocumentId,
|
||||||
@@ -409,6 +221,36 @@ import { createSidebarPageAiRuntime } from './sidebar-page-ai-runtime.js';
|
|||||||
const openAccountMenu = (...args) => sidebarWorkspace.openAccountMenu(...args);
|
const openAccountMenu = (...args) => sidebarWorkspace.openAccountMenu(...args);
|
||||||
sidebarWorkspace.autoOpenRecentLocalRootOnHome();
|
sidebarWorkspace.autoOpenRecentLocalRootOnHome();
|
||||||
|
|
||||||
|
const sidebarPageSettings = createSidebarPageSettingsRuntime({
|
||||||
|
currentDocumentId,
|
||||||
|
currentPageAggregate,
|
||||||
|
currentRootUri,
|
||||||
|
currentSourceKind,
|
||||||
|
currentWorkspaceSourcePayload,
|
||||||
|
escapeHtml,
|
||||||
|
globalShowHeadingNumbersKey: MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY,
|
||||||
|
pageUiState,
|
||||||
|
resolveWorkspaceId,
|
||||||
|
searchText,
|
||||||
|
});
|
||||||
|
const applyPageOptionsToShell = (...args) => sidebarPageSettings.applyPageOptionsToShell(...args);
|
||||||
|
const closePageHistoryDrawer = (...args) => sidebarPageSettings.closePageHistoryDrawer(...args);
|
||||||
|
const closePageSettingsPopover = (...args) => sidebarPageSettings.closePageSettingsPopover(...args);
|
||||||
|
const closePageShareDialog = (...args) => sidebarPageSettings.closePageShareDialog(...args);
|
||||||
|
const currentPageOptions = (...args) => sidebarPageSettings.currentPageOptions(...args);
|
||||||
|
const ensureHistorySnapshotsSeeded = (...args) => sidebarPageSettings.ensureHistorySnapshotsSeeded(...args);
|
||||||
|
const isPageSettingsOpen = (...args) => sidebarPageSettings.isPageSettingsOpen(...args);
|
||||||
|
const openPageHistoryDrawer = (...args) => sidebarPageSettings.openPageHistoryDrawer(...args);
|
||||||
|
const openPageShareDialog = (...args) => sidebarPageSettings.openPageShareDialog(...args);
|
||||||
|
const pageOptionIsSupported = (...args) => sidebarPageSettings.pageOptionIsSupported(...args);
|
||||||
|
const persistPageOptionsPatch = (...args) => sidebarPageSettings.persistPageOptionsPatch(...args);
|
||||||
|
const recordPageHistorySnapshot = (...args) => sidebarPageSettings.recordPageHistorySnapshot(...args);
|
||||||
|
const renderPageSettingsPopover = (...args) => sidebarPageSettings.renderPageSettingsPopover(...args);
|
||||||
|
const setActivePageSettingsTab = (...args) => sidebarPageSettings.setActivePageSettingsTab(...args);
|
||||||
|
const togglePageSettingsPopover = (...args) => sidebarPageSettings.togglePageSettingsPopover(...args);
|
||||||
|
const updatePageSettingsTriggerState = (...args) => sidebarPageSettings.updatePageSettingsTriggerState(...args);
|
||||||
|
const writeGlobalShowHeadingNumbers = (...args) => sidebarPageSettings.writeGlobalShowHeadingNumbers(...args);
|
||||||
|
|
||||||
function setCommandPending(trigger, pending) {
|
function setCommandPending(trigger, pending) {
|
||||||
if (!(trigger instanceof HTMLElement)) return;
|
if (!(trigger instanceof HTMLElement)) return;
|
||||||
trigger.setAttribute('data-pending', pending ? 'true' : 'false');
|
trigger.setAttribute('data-pending', pending ? 'true' : 'false');
|
||||||
@@ -1994,180 +1836,6 @@ import { createSidebarPageAiRuntime } from './sidebar-page-ai-runtime.js';
|
|||||||
else openSearchModal();
|
else openSearchModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
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 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 createGlobalHeadingNumbersRow() {
|
|
||||||
return '' +
|
|
||||||
'<label class="wolai-page-setting-row" data-page-setting-row="globalShowHeadingNumbers">' +
|
|
||||||
'<span class="wolai-page-setting-copy">' +
|
|
||||||
'<span class="wolai-page-setting-label">标题自动编号</span>' +
|
|
||||||
'</span>' +
|
|
||||||
'<input type="checkbox" class="wolai-page-setting-checkbox" data-global-option-checkbox="showHeadingNumbers" />' +
|
|
||||||
'</label>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderGlobalOptions(popover) {
|
|
||||||
var globalHeadingNumbers = readGlobalShowHeadingNumbers();
|
|
||||||
popover.querySelectorAll('[data-global-option-checkbox="showHeadingNumbers"]').forEach(function(input) {
|
|
||||||
input.checked = globalHeadingNumbers;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
const sidebarPageAi = createSidebarPageAiRuntime({
|
const sidebarPageAi = createSidebarPageAiRuntime({
|
||||||
buildLocalFileOpenUrl,
|
buildLocalFileOpenUrl,
|
||||||
currentDocumentId,
|
currentDocumentId,
|
||||||
@@ -2216,305 +1884,6 @@ import { createSidebarPageAiRuntime } from './sidebar-page-ai-runtime.js';
|
|||||||
const pageAiSetContextScope = (...args) => sidebarPageAi.pageAiSetContextScope(...args);
|
const pageAiSetContextScope = (...args) => sidebarPageAi.pageAiSetContextScope(...args);
|
||||||
const updatePageAiTriggerState = (...args) => sidebarPageAi.updatePageAiTriggerState(...args);
|
const updatePageAiTriggerState = (...args) => sidebarPageAi.updatePageAiTriggerState(...args);
|
||||||
|
|
||||||
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="index">索引</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('protectEditing', 'checkbox') +
|
|
||||||
createPageOptionRow('hideTitleHeader', '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="index" hidden>' +
|
|
||||||
'<div class="wolai-page-settings-index-panel" data-testid="wolai-page-settings-local-index-panel">' +
|
|
||||||
'<div class="wolai-page-settings-index-status" data-testid="wolai-page-settings-local-index-status"></div>' +
|
|
||||||
'<section class="wolai-page-settings-index-group">' +
|
|
||||||
'<div class="wolai-page-settings-index-title">反链</div>' +
|
|
||||||
'<div class="wolai-page-settings-index-list" data-testid="wolai-page-settings-local-index-backlinks"></div>' +
|
|
||||||
'</section>' +
|
|
||||||
'<section class="wolai-page-settings-index-group">' +
|
|
||||||
'<div class="wolai-page-settings-index-title">标签</div>' +
|
|
||||||
'<div class="wolai-page-settings-index-list" data-testid="wolai-page-settings-local-index-tags"></div>' +
|
|
||||||
'</section>' +
|
|
||||||
'</div>' +
|
|
||||||
'</div>' +
|
|
||||||
'<div class="wolai-page-settings-section" data-page-settings-panel="global" hidden>' +
|
|
||||||
createGlobalHeadingNumbersRow() +
|
|
||||||
'</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 pageSettingsLocalIndexScopeKey() {
|
|
||||||
return [
|
|
||||||
currentSourceKind(),
|
|
||||||
currentRootUri(),
|
|
||||||
resolveWorkspaceId(document.body),
|
|
||||||
currentDocumentId()
|
|
||||||
].join('|');
|
|
||||||
}
|
|
||||||
|
|
||||||
function pageSettingsLocalIndexIsAvailable() {
|
|
||||||
return currentSourceKind() === 'local_folder' && Boolean(currentRootUri()) && Boolean(currentDocumentId());
|
|
||||||
}
|
|
||||||
|
|
||||||
function pageSettingsLocalIndexEmpty(message) {
|
|
||||||
return '<div class="wolai-page-settings-index-empty">' + escapeHtml(message) + '</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPageSettingsLocalIndexList(items, kind) {
|
|
||||||
var rows = Array.isArray(items) ? items : [];
|
|
||||||
if (!rows.length) {
|
|
||||||
return pageSettingsLocalIndexEmpty(kind === 'backlinks' ? '暂无反链' : '暂无标签');
|
|
||||||
}
|
|
||||||
if (kind === 'backlinks') {
|
|
||||||
return rows.slice(0, 12).map(function(item) {
|
|
||||||
var title = searchText(item && item.title) || searchText(item && item.path) || '未命名页面';
|
|
||||||
var path = searchText(item && item.path);
|
|
||||||
var snippet = searchText(item && item.snippet);
|
|
||||||
return '' +
|
|
||||||
'<div class="wolai-page-settings-index-row">' +
|
|
||||||
'<div class="wolai-page-settings-index-row-title">' + escapeHtml(title) + '</div>' +
|
|
||||||
(path ? '<div class="wolai-page-settings-index-row-meta">' + escapeHtml(path) + '</div>' : '') +
|
|
||||||
(snippet ? '<div class="wolai-page-settings-index-row-snippet">' + escapeHtml(snippet) + '</div>' : '') +
|
|
||||||
'</div>';
|
|
||||||
}).join('');
|
|
||||||
}
|
|
||||||
return rows.slice(0, 16).map(function(item) {
|
|
||||||
var tag = searchText(item && item.tag) || 'untagged';
|
|
||||||
var count = Number(item && item.count || 0);
|
|
||||||
return '' +
|
|
||||||
'<div class="wolai-page-settings-index-row is-tag">' +
|
|
||||||
'<div class="wolai-page-settings-index-row-title">#' + escapeHtml(tag) + '</div>' +
|
|
||||||
'<div class="wolai-page-settings-index-row-meta">' + count + ' 个页面</div>' +
|
|
||||||
'</div>';
|
|
||||||
}).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPageSettingsLocalIndex(popover) {
|
|
||||||
popover = popover || ensurePageSettingsPopover();
|
|
||||||
var statusNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-status"]');
|
|
||||||
var backlinksNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-backlinks"]');
|
|
||||||
var tagsNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-tags"]');
|
|
||||||
if (!(statusNode instanceof HTMLElement) || !(backlinksNode instanceof HTMLElement) || !(tagsNode instanceof HTMLElement)) return;
|
|
||||||
|
|
||||||
if (!pageSettingsLocalIndexIsAvailable()) {
|
|
||||||
statusNode.textContent = '本地索引仅在本地工作区页面可用';
|
|
||||||
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('未连接本地 root');
|
|
||||||
tagsNode.innerHTML = pageSettingsLocalIndexEmpty('未连接本地 root');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var summary = pageUiState.localIndexSummary || {};
|
|
||||||
if (summary.loading) {
|
|
||||||
statusNode.textContent = '正在读取本地索引...';
|
|
||||||
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('加载中');
|
|
||||||
tagsNode.innerHTML = pageSettingsLocalIndexEmpty('加载中');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (summary.error) {
|
|
||||||
statusNode.textContent = '本地索引读取失败';
|
|
||||||
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty(summary.error);
|
|
||||||
tagsNode.innerHTML = pageSettingsLocalIndexEmpty(summary.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (summary.scopeKey !== pageSettingsLocalIndexScopeKey()) {
|
|
||||||
statusNode.textContent = '切换到索引页签后读取本地索引';
|
|
||||||
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('等待读取');
|
|
||||||
tagsNode.innerHTML = pageSettingsLocalIndexEmpty('等待读取');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
statusNode.textContent = '来自当前授权 root 的 .mnote/index/search-index.json';
|
|
||||||
backlinksNode.innerHTML = renderPageSettingsLocalIndexList(summary.backlinks, 'backlinks');
|
|
||||||
tagsNode.innerHTML = renderPageSettingsLocalIndexList(summary.tags, 'tags');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadPageSettingsLocalIndex(force) {
|
|
||||||
if (!pageSettingsLocalIndexIsAvailable()) {
|
|
||||||
renderPageSettingsLocalIndex();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var scopeKey = pageSettingsLocalIndexScopeKey();
|
|
||||||
var current = pageUiState.localIndexSummary || {};
|
|
||||||
if (!force && current.scopeKey === scopeKey && !current.error && !current.loading) {
|
|
||||||
renderPageSettingsLocalIndex();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pageUiState.localIndexSummary = {
|
|
||||||
scopeKey: scopeKey,
|
|
||||||
loading: true,
|
|
||||||
error: '',
|
|
||||||
backlinks: null,
|
|
||||||
tags: null
|
|
||||||
};
|
|
||||||
renderPageSettingsLocalIndex();
|
|
||||||
try {
|
|
||||||
var baseParams = new URLSearchParams();
|
|
||||||
baseParams.set('workspaceId', resolveWorkspaceId(document.body));
|
|
||||||
baseParams.set('rootUri', currentRootUri());
|
|
||||||
var backlinksParams = new URLSearchParams(baseParams);
|
|
||||||
backlinksParams.set('documentId', currentDocumentId());
|
|
||||||
var backlinksUrl = '/api/search/local-index/backlinks?' + backlinksParams.toString();
|
|
||||||
var tagsUrl = '/api/search/local-index/tags?' + baseParams.toString();
|
|
||||||
var responses = await Promise.all([
|
|
||||||
fetch(backlinksUrl, { headers: { accept: 'application/json' } }),
|
|
||||||
fetch(tagsUrl, { headers: { accept: 'application/json' } })
|
|
||||||
]);
|
|
||||||
var backlinksPayload = await responses[0].json().catch(function(){ return null; });
|
|
||||||
var tagsPayload = await responses[1].json().catch(function(){ return null; });
|
|
||||||
if (!responses[0].ok || !backlinksPayload || backlinksPayload.ok !== true) {
|
|
||||||
throw new Error('backlinks_' + responses[0].status);
|
|
||||||
}
|
|
||||||
if (!responses[1].ok || !tagsPayload || tagsPayload.ok !== true) {
|
|
||||||
throw new Error('tags_' + responses[1].status);
|
|
||||||
}
|
|
||||||
pageUiState.localIndexSummary = {
|
|
||||||
scopeKey: scopeKey,
|
|
||||||
loading: false,
|
|
||||||
error: '',
|
|
||||||
backlinks: backlinksPayload.result && Array.isArray(backlinksPayload.result.backlinks) ? backlinksPayload.result.backlinks : [],
|
|
||||||
tags: tagsPayload.result && Array.isArray(tagsPayload.result.tags) ? tagsPayload.result.tags : []
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
pageUiState.localIndexSummary = {
|
|
||||||
scopeKey: scopeKey,
|
|
||||||
loading: false,
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
backlinks: [],
|
|
||||||
tags: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
renderPageSettingsLocalIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
renderGlobalOptions(popover);
|
|
||||||
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>';
|
|
||||||
}
|
|
||||||
renderPageSettingsLocalIndex(popover);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
if (tabName === 'index') void loadPageSettingsLocalIndex(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
...currentWorkspaceSourcePayload(),
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
const sidebarAttachmentOpen = createSidebarAttachmentOpenRuntime({
|
const sidebarAttachmentOpen = createSidebarAttachmentOpenRuntime({
|
||||||
attachmentClassForFileName: (...args) => attachmentClassForFileName(...args),
|
attachmentClassForFileName: (...args) => attachmentClassForFileName(...args),
|
||||||
buildLocalFileOpenUrl,
|
buildLocalFileOpenUrl,
|
||||||
|
|||||||
@@ -158,6 +158,10 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
"/api/mnote-browser-runtime/sidebar-page-ai-runtime.js",
|
"/api/mnote-browser-runtime/sidebar-page-ai-runtime.js",
|
||||||
get(web_shell::sidebar_page_ai_runtime_asset),
|
get(web_shell::sidebar_page_ai_runtime_asset),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/mnote-browser-runtime/sidebar-page-settings-runtime.js",
|
||||||
|
get(web_shell::sidebar_page_settings_runtime_asset),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/mnote-browser-runtime/sidebar-tree-runtime.js",
|
"/api/mnote-browser-runtime/sidebar-tree-runtime.js",
|
||||||
get(web_shell::sidebar_tree_runtime_asset),
|
get(web_shell::sidebar_tree_runtime_asset),
|
||||||
@@ -655,6 +659,7 @@ mod tests {
|
|||||||
"/api/mnote-browser-runtime/sidebar-filetree-upload-runtime.js",
|
"/api/mnote-browser-runtime/sidebar-filetree-upload-runtime.js",
|
||||||
"/api/mnote-browser-runtime/sidebar-attachment-open-runtime.js",
|
"/api/mnote-browser-runtime/sidebar-attachment-open-runtime.js",
|
||||||
"/api/mnote-browser-runtime/sidebar-page-ai-runtime.js",
|
"/api/mnote-browser-runtime/sidebar-page-ai-runtime.js",
|
||||||
|
"/api/mnote-browser-runtime/sidebar-page-settings-runtime.js",
|
||||||
"/api/mnote-browser-runtime/sidebar-tree-runtime.js",
|
"/api/mnote-browser-runtime/sidebar-tree-runtime.js",
|
||||||
"/api/mnote-browser-runtime/tree-live-controller.js",
|
"/api/mnote-browser-runtime/tree-live-controller.js",
|
||||||
"/api/mnote-browser-runtime/tree-shell-runtime.js",
|
"/api/mnote-browser-runtime/tree-shell-runtime.js",
|
||||||
|
|||||||
@@ -742,6 +742,20 @@ pub async fn sidebar_page_ai_runtime_asset() -> Response {
|
|||||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn sidebar_page_settings_runtime_asset() -> Response {
|
||||||
|
const JS: &str = include_str!("../../browser/sidebar-page-settings-runtime.js");
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
"application/javascript; charset=utf-8",
|
||||||
|
)
|
||||||
|
.header(header::CACHE_CONTROL, "no-store")
|
||||||
|
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||||
|
.body(Body::from(JS))
|
||||||
|
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn sidebar_shell_runtime_asset() -> Response {
|
pub async fn sidebar_shell_runtime_asset() -> Response {
|
||||||
const JS: &str = include_str!("../../browser/sidebar-shell-runtime.js");
|
const JS: &str = include_str!("../../browser/sidebar-shell-runtime.js");
|
||||||
Response::builder()
|
Response::builder()
|
||||||
|
|||||||
@@ -217,6 +217,8 @@ mod tests {
|
|||||||
include_str!("../../../browser/sidebar-page-tree-runtime.js");
|
include_str!("../../../browser/sidebar-page-tree-runtime.js");
|
||||||
const SIDEBAR_PAGE_AI_RUNTIME_JS: &str =
|
const SIDEBAR_PAGE_AI_RUNTIME_JS: &str =
|
||||||
include_str!("../../../browser/sidebar-page-ai-runtime.js");
|
include_str!("../../../browser/sidebar-page-ai-runtime.js");
|
||||||
|
const SIDEBAR_PAGE_SETTINGS_RUNTIME_JS: &str =
|
||||||
|
include_str!("../../../browser/sidebar-page-settings-runtime.js");
|
||||||
const FILETREE_CONTEXT_MENU_RUNTIME_JS: &str =
|
const FILETREE_CONTEXT_MENU_RUNTIME_JS: &str =
|
||||||
include_str!("../../../browser/filetree-context-menu-runtime.js");
|
include_str!("../../../browser/filetree-context-menu-runtime.js");
|
||||||
const FILETREE_DND_RUNTIME_JS: &str = include_str!("../../../browser/filetree-dnd-runtime.js");
|
const FILETREE_DND_RUNTIME_JS: &str = include_str!("../../../browser/filetree-dnd-runtime.js");
|
||||||
@@ -255,9 +257,8 @@ mod tests {
|
|||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("MNOTE_SIDEBAR_TREE_MODE_KEY"));
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("MNOTE_SIDEBAR_TREE_MODE_KEY"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY"));
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("MNOTE_LAST_CLOUD_WORKSPACE_KEY"));
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("MNOTE_LAST_CLOUD_WORKSPACE_KEY"));
|
||||||
assert!(
|
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS
|
||||||
SIDEBAR_TREE_RUNTIME_JS.contains("data-global-option-checkbox=\"showHeadingNumbers\"")
|
.contains("data-global-option-checkbox=\"showHeadingNumbers\""));
|
||||||
);
|
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("restoreSidebarTreeTab"));
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("restoreSidebarTreeTab"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("treeView"));
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("treeView"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree:local-command"));
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree:local-command"));
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ pub(crate) mod overlays;
|
|||||||
pub(crate) mod persistence;
|
pub(crate) mod persistence;
|
||||||
pub(crate) mod runtime_bridge;
|
pub(crate) mod runtime_bridge;
|
||||||
pub(crate) mod slash_actions;
|
pub(crate) mod slash_actions;
|
||||||
|
pub(crate) mod slash_menu_view;
|
||||||
pub(crate) mod style;
|
pub(crate) mod style;
|
||||||
pub(crate) mod table_commands;
|
pub(crate) mod table_commands;
|
||||||
pub(crate) mod table_toolbar_view;
|
pub(crate) mod table_toolbar_view;
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
use crate::editor_runtime::{
|
||||||
|
block_hover_state::HoveredBlockState,
|
||||||
|
block_transform::run_slash_action,
|
||||||
|
bridge_events::{ChangeMetaPayload, ChangePayload},
|
||||||
|
command_sync::read_editor_snapshot,
|
||||||
|
overlays::toolbar_overlay_locked,
|
||||||
|
persistence::{persist_document_state, persisted_document_identity, PersistedDocumentIdentity},
|
||||||
|
runtime_bridge::dispatch_change_event_to_target,
|
||||||
|
slash_actions::{SlashActionKind, SLASH_ACTIONS},
|
||||||
|
};
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use leptos_tiptap::TiptapEditorHandle;
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use web_sys::EventTarget;
|
||||||
|
|
||||||
|
pub(crate) struct SlashMenuViewProps {
|
||||||
|
pub(crate) editor: TiptapEditorHandle,
|
||||||
|
pub(crate) signals: SlashMenuSignals,
|
||||||
|
pub(crate) runtime: SlashMenuRuntimeContext,
|
||||||
|
pub(crate) anchor_style: Arc<dyn Fn() -> String + Send + Sync>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(crate) struct SlashMenuSignals {
|
||||||
|
pub(crate) slash_open: ReadSignal<bool>,
|
||||||
|
pub(crate) set_slash_open: WriteSignal<bool>,
|
||||||
|
pub(crate) slash_index: ReadSignal<usize>,
|
||||||
|
pub(crate) slash_query: ReadSignal<String>,
|
||||||
|
pub(crate) dirty_count: ReadSignal<u32>,
|
||||||
|
pub(crate) set_dirty_count: WriteSignal<u32>,
|
||||||
|
pub(crate) document_json: WriteSignal<Value>,
|
||||||
|
pub(crate) html_output: WriteSignal<String>,
|
||||||
|
pub(crate) json_output: WriteSignal<String>,
|
||||||
|
pub(crate) document_id: ReadSignal<Option<String>>,
|
||||||
|
pub(crate) workspace_id: ReadSignal<Option<String>>,
|
||||||
|
pub(crate) title: ReadSignal<String>,
|
||||||
|
pub(crate) editor_focused: ReadSignal<bool>,
|
||||||
|
pub(crate) hovered_block: ReadSignal<Option<HoveredBlockState>>,
|
||||||
|
pub(crate) turn_into_open: ReadSignal<bool>,
|
||||||
|
pub(crate) color_menu_open: ReadSignal<bool>,
|
||||||
|
pub(crate) more_menu_open: ReadSignal<bool>,
|
||||||
|
pub(crate) revision: ReadSignal<Option<i64>>,
|
||||||
|
pub(crate) conflict_detection_key: ReadSignal<Option<String>>,
|
||||||
|
pub(crate) read_only: ReadSignal<bool>,
|
||||||
|
pub(crate) command_feedback: WriteSignal<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct SlashMenuRuntimeContext {
|
||||||
|
pub(crate) change_event_target: Option<EventTarget>,
|
||||||
|
pub(crate) dispatch_runtime_state: Arc<dyn Fn(&EventTarget) + Send + Sync>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runtime_persisted_identity(
|
||||||
|
document_id: ReadSignal<Option<String>>,
|
||||||
|
workspace_id: ReadSignal<Option<String>>,
|
||||||
|
) -> PersistedDocumentIdentity {
|
||||||
|
persisted_document_identity(document_id.get_untracked(), workspace_id.get_untracked())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filtered_slash_intro_view(
|
||||||
|
query: &str,
|
||||||
|
command_feedback: WriteSignal<String>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let show_turn_into = !query.is_empty()
|
||||||
|
&& ("zhw".contains(query) || "转换为".contains(query));
|
||||||
|
let show_page = !query.is_empty() && ("ym".contains(query) || "页面".contains(query));
|
||||||
|
|
||||||
|
if show_turn_into || show_page {
|
||||||
|
view! {
|
||||||
|
<>
|
||||||
|
{if show_turn_into {
|
||||||
|
view! {
|
||||||
|
<>
|
||||||
|
<div class="slash-section-label">"转换为"</div>
|
||||||
|
<button
|
||||||
|
class="slash-item"
|
||||||
|
data-active="false"
|
||||||
|
data-testid="slash-item-turn-into"
|
||||||
|
on:click=move |_| {
|
||||||
|
command_feedback.set("转换为入口请通过块菜单或选区工具栏执行".to_string());
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span class="slash-item-row">
|
||||||
|
<span class="slash-item-icon">"↻"</span>
|
||||||
|
<span class="slash-item-copy">
|
||||||
|
<strong>"转换为"</strong>
|
||||||
|
<span>"打开块类型转换入口"</span>
|
||||||
|
</span>
|
||||||
|
<span class="slash-item-shortcut">"/zhw"</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
}.into_any()
|
||||||
|
} else {
|
||||||
|
().into_any()
|
||||||
|
}}
|
||||||
|
{if show_page {
|
||||||
|
view! {
|
||||||
|
<>
|
||||||
|
<div class="slash-section-label">"基础块列表"</div>
|
||||||
|
<button
|
||||||
|
class="slash-item"
|
||||||
|
data-active="false"
|
||||||
|
data-testid="slash-item-page"
|
||||||
|
on:click=move |_| {
|
||||||
|
command_feedback.set("页面入口请通过转换为页面真源链路执行".to_string());
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span class="slash-item-row">
|
||||||
|
<span class="slash-item-icon">"▣"</span>
|
||||||
|
<span class="slash-item-copy">
|
||||||
|
<strong>"页面"</strong>
|
||||||
|
<span>"创建或转换为页面块"</span>
|
||||||
|
</span>
|
||||||
|
<span class="slash-item-shortcut">"/ym"</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
}.into_any()
|
||||||
|
} else {
|
||||||
|
().into_any()
|
||||||
|
}}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
|
} else {
|
||||||
|
().into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_slash_item_click(
|
||||||
|
editor: TiptapEditorHandle,
|
||||||
|
kind: SlashActionKind,
|
||||||
|
signals: SlashMenuSignals,
|
||||||
|
runtime: &SlashMenuRuntimeContext,
|
||||||
|
) {
|
||||||
|
match run_slash_action(editor, kind) {
|
||||||
|
Ok(message) => {
|
||||||
|
signals.set_slash_open.set(false);
|
||||||
|
signals.set_dirty_count.update(|count| *count += 1);
|
||||||
|
let (html, snapshot, json_text) = read_editor_snapshot(editor);
|
||||||
|
signals.html_output.set(html.clone());
|
||||||
|
signals.document_json.set(snapshot.clone());
|
||||||
|
signals.json_output.set(json_text);
|
||||||
|
if let Some(target) = runtime.change_event_target.as_ref() {
|
||||||
|
dispatch_change_event_to_target(
|
||||||
|
target,
|
||||||
|
&ChangePayload {
|
||||||
|
document_id: signals.document_id.get_untracked(),
|
||||||
|
workspace_id: signals.workspace_id.get_untracked(),
|
||||||
|
title: signals.title.get_untracked(),
|
||||||
|
content: snapshot.clone(),
|
||||||
|
meta: ChangeMetaPayload {
|
||||||
|
dirty_count: signals.dirty_count.get_untracked(),
|
||||||
|
editor_focused: signals.editor_focused.get_untracked(),
|
||||||
|
slash_open: signals.slash_open.get_untracked(),
|
||||||
|
toolbar_open: toolbar_overlay_locked(
|
||||||
|
signals.turn_into_open.get_untracked(),
|
||||||
|
signals.color_menu_open.get_untracked(),
|
||||||
|
signals.more_menu_open.get_untracked(),
|
||||||
|
),
|
||||||
|
selected_block_index: signals
|
||||||
|
.hovered_block
|
||||||
|
.get_untracked()
|
||||||
|
.map(|block| block.index),
|
||||||
|
revision: signals.revision.get_untracked(),
|
||||||
|
conflict_detection_key: signals.conflict_detection_key.get_untracked(),
|
||||||
|
read_only: signals.read_only.get_untracked(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
(runtime.dispatch_runtime_state)(target);
|
||||||
|
}
|
||||||
|
match persist_document_state(
|
||||||
|
&runtime_persisted_identity(signals.document_id, signals.workspace_id),
|
||||||
|
&signals.title.get_untracked(),
|
||||||
|
&snapshot,
|
||||||
|
Some(html),
|
||||||
|
) {
|
||||||
|
Ok(()) => signals
|
||||||
|
.command_feedback
|
||||||
|
.set(format!("{message},并已写入本地草稿")),
|
||||||
|
Err(err) => signals
|
||||||
|
.command_feedback
|
||||||
|
.set(format!("{message},但本地保存失败:{err}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
signals.set_slash_open.set(false);
|
||||||
|
signals
|
||||||
|
.command_feedback
|
||||||
|
.set(format!("命令执行失败:{err}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn slash_menu_view(props: SlashMenuViewProps) -> impl IntoView {
|
||||||
|
let SlashMenuViewProps {
|
||||||
|
editor,
|
||||||
|
signals,
|
||||||
|
runtime,
|
||||||
|
anchor_style,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
view! {
|
||||||
|
{move || {
|
||||||
|
if signals.slash_open.get() {
|
||||||
|
let anchor_style = anchor_style.clone();
|
||||||
|
let runtime = runtime.clone();
|
||||||
|
view! {
|
||||||
|
<div
|
||||||
|
class="slash-menu"
|
||||||
|
data-testid="mnote-leptos-tiptap-slash-menu"
|
||||||
|
style=anchor_style()
|
||||||
|
>
|
||||||
|
<div class="slash-list">
|
||||||
|
{move || {
|
||||||
|
let query = signals.slash_query.get().trim().to_ascii_lowercase();
|
||||||
|
filtered_slash_intro_view(&query, signals.command_feedback)
|
||||||
|
}}
|
||||||
|
{SLASH_ACTIONS.iter().enumerate().map(|(index, action)| {
|
||||||
|
let is_selected = move || signals.slash_index.get() == index;
|
||||||
|
let kind = action.kind;
|
||||||
|
let label = action.label;
|
||||||
|
let description = action.description;
|
||||||
|
let shortcut = action.shortcut;
|
||||||
|
let icon = action.icon;
|
||||||
|
let category = action.category;
|
||||||
|
let show_category = index == 0
|
||||||
|
|| SLASH_ACTIONS[index - 1].category != category;
|
||||||
|
let testid = format!("slash-item-{}", action.id);
|
||||||
|
let runtime = runtime.clone();
|
||||||
|
view! {
|
||||||
|
<>
|
||||||
|
{if show_category {
|
||||||
|
view! { <div class="slash-section-label">{category}</div> }.into_any()
|
||||||
|
} else {
|
||||||
|
().into_any()
|
||||||
|
}}
|
||||||
|
<button
|
||||||
|
class="slash-item"
|
||||||
|
data-active=move || is_selected().to_string()
|
||||||
|
data-testid=testid
|
||||||
|
on:click=move |_| {
|
||||||
|
handle_slash_item_click(editor, kind, signals, &runtime);
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span class="slash-item-row">
|
||||||
|
<span class="slash-item-icon">{icon}</span>
|
||||||
|
<span class="slash-item-copy">
|
||||||
|
<strong>{label}</strong>
|
||||||
|
<span>{description}</span>
|
||||||
|
</span>
|
||||||
|
<span class="slash-item-shortcut">{shortcut}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}).collect_view()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
|
} else {
|
||||||
|
().into_any()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,6 +80,9 @@ use editor_runtime::runtime_bridge::{
|
|||||||
dispatch_status_event_to_target, register_runtime_listener, take_unmount_handle,
|
dispatch_status_event_to_target, register_runtime_listener, take_unmount_handle,
|
||||||
};
|
};
|
||||||
use editor_runtime::slash_actions::{SlashActionKind, SLASH_ACTIONS};
|
use editor_runtime::slash_actions::{SlashActionKind, SLASH_ACTIONS};
|
||||||
|
use editor_runtime::slash_menu_view::{
|
||||||
|
slash_menu_view, SlashMenuRuntimeContext, SlashMenuSignals, SlashMenuViewProps,
|
||||||
|
};
|
||||||
use editor_runtime::style::SPIKE_STYLE;
|
use editor_runtime::style::SPIKE_STYLE;
|
||||||
use editor_runtime::table_toolbar_view::table_toolbar_view;
|
use editor_runtime::table_toolbar_view::table_toolbar_view;
|
||||||
|
|
||||||
@@ -2240,6 +2243,54 @@ fn App(mount_options: MountOptions) -> impl IntoView {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let slash_menu_runtime = SlashMenuRuntimeContext {
|
||||||
|
change_event_target: slash_change_event_target.clone(),
|
||||||
|
dispatch_runtime_state: std::sync::Arc::new(move |target: &EventTarget| {
|
||||||
|
dispatch_runtime_state_to_target(
|
||||||
|
target,
|
||||||
|
document_id.get_untracked(),
|
||||||
|
workspace_id.get_untracked(),
|
||||||
|
title.get_untracked(),
|
||||||
|
dirty_count.get_untracked(),
|
||||||
|
hovered_block.get_untracked(),
|
||||||
|
editor_focused.get_untracked(),
|
||||||
|
slash_open.get_untracked(),
|
||||||
|
turn_into_open.get_untracked(),
|
||||||
|
color_menu_open.get_untracked(),
|
||||||
|
more_menu_open.get_untracked(),
|
||||||
|
read_only.get_untracked(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let slash_menu_props = SlashMenuViewProps {
|
||||||
|
editor,
|
||||||
|
signals: SlashMenuSignals {
|
||||||
|
slash_open,
|
||||||
|
set_slash_open,
|
||||||
|
slash_index,
|
||||||
|
slash_query,
|
||||||
|
dirty_count,
|
||||||
|
set_dirty_count,
|
||||||
|
document_json: set_document_json,
|
||||||
|
html_output: set_html_output,
|
||||||
|
json_output: set_json_output,
|
||||||
|
document_id,
|
||||||
|
workspace_id,
|
||||||
|
title,
|
||||||
|
editor_focused,
|
||||||
|
hovered_block,
|
||||||
|
turn_into_open,
|
||||||
|
color_menu_open,
|
||||||
|
more_menu_open,
|
||||||
|
revision,
|
||||||
|
conflict_detection_key,
|
||||||
|
read_only,
|
||||||
|
command_feedback: set_command_feedback,
|
||||||
|
},
|
||||||
|
runtime: slash_menu_runtime,
|
||||||
|
anchor_style: std::sync::Arc::new(slash_menu_anchor_style),
|
||||||
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<style>{SPIKE_STYLE}</style>
|
<style>{SPIKE_STYLE}</style>
|
||||||
<main
|
<main
|
||||||
@@ -3828,192 +3879,7 @@ fn App(mount_options: MountOptions) -> impl IntoView {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
{move || {
|
{slash_menu_view(slash_menu_props)}
|
||||||
if slash_open.get() {
|
|
||||||
view! {
|
|
||||||
<div
|
|
||||||
class="slash-menu"
|
|
||||||
data-testid="mnote-leptos-tiptap-slash-menu"
|
|
||||||
style=slash_menu_anchor_style()
|
|
||||||
>
|
|
||||||
<div class="slash-list">
|
|
||||||
{move || {
|
|
||||||
let query = slash_query.get().trim().to_ascii_lowercase();
|
|
||||||
let show_turn_into = !query.is_empty()
|
|
||||||
&& ("zhw".contains(query.as_str())
|
|
||||||
|| "转换为".contains(query.as_str()));
|
|
||||||
let show_page = !query.is_empty()
|
|
||||||
&& ("ym".contains(query.as_str())
|
|
||||||
|| "页面".contains(query.as_str()));
|
|
||||||
if show_turn_into || show_page {
|
|
||||||
view! {
|
|
||||||
<>
|
|
||||||
{if show_turn_into {
|
|
||||||
view! {
|
|
||||||
<>
|
|
||||||
<div class="slash-section-label">"转换为"</div>
|
|
||||||
<button
|
|
||||||
class="slash-item"
|
|
||||||
data-active="false"
|
|
||||||
data-testid="slash-item-turn-into"
|
|
||||||
on:click=move |_| {
|
|
||||||
set_command_feedback.set("转换为入口请通过块菜单或选区工具栏执行".to_string());
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span class="slash-item-row">
|
|
||||||
<span class="slash-item-icon">"↻"</span>
|
|
||||||
<span class="slash-item-copy">
|
|
||||||
<strong>"转换为"</strong>
|
|
||||||
<span>"打开块类型转换入口"</span>
|
|
||||||
</span>
|
|
||||||
<span class="slash-item-shortcut">"/zhw"</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
}.into_any()
|
|
||||||
} else {
|
|
||||||
().into_any()
|
|
||||||
}}
|
|
||||||
{if show_page {
|
|
||||||
view! {
|
|
||||||
<>
|
|
||||||
<div class="slash-section-label">"基础块列表"</div>
|
|
||||||
<button
|
|
||||||
class="slash-item"
|
|
||||||
data-active="false"
|
|
||||||
data-testid="slash-item-page"
|
|
||||||
on:click=move |_| {
|
|
||||||
set_command_feedback.set("页面入口请通过转换为页面真源链路执行".to_string());
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span class="slash-item-row">
|
|
||||||
<span class="slash-item-icon">"▣"</span>
|
|
||||||
<span class="slash-item-copy">
|
|
||||||
<strong>"页面"</strong>
|
|
||||||
<span>"创建或转换为页面块"</span>
|
|
||||||
</span>
|
|
||||||
<span class="slash-item-shortcut">"/ym"</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
}.into_any()
|
|
||||||
} else {
|
|
||||||
().into_any()
|
|
||||||
}}
|
|
||||||
</>
|
|
||||||
}.into_any()
|
|
||||||
} else {
|
|
||||||
().into_any()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
{SLASH_ACTIONS.iter().enumerate().map(|(index, action)| {
|
|
||||||
let is_selected = move || slash_index.get() == index;
|
|
||||||
let kind = action.kind;
|
|
||||||
let label = action.label;
|
|
||||||
let description = action.description;
|
|
||||||
let shortcut = action.shortcut;
|
|
||||||
let icon = action.icon;
|
|
||||||
let category = action.category;
|
|
||||||
let show_category = index == 0
|
|
||||||
|| SLASH_ACTIONS[index - 1].category != category;
|
|
||||||
let testid = format!("slash-item-{}", action.id);
|
|
||||||
let slash_change_event_target = slash_change_event_target.clone();
|
|
||||||
view! {
|
|
||||||
<>
|
|
||||||
{if show_category {
|
|
||||||
view! { <div class="slash-section-label">{category}</div> }.into_any()
|
|
||||||
} else {
|
|
||||||
().into_any()
|
|
||||||
}}
|
|
||||||
<button
|
|
||||||
class="slash-item"
|
|
||||||
data-active=move || is_selected().to_string()
|
|
||||||
data-testid=testid
|
|
||||||
on:click=move |_| {
|
|
||||||
match run_slash_action(editor, kind) {
|
|
||||||
Ok(message) => {
|
|
||||||
set_slash_open.set(false);
|
|
||||||
set_dirty_count.update(|count| *count += 1);
|
|
||||||
let (html, snapshot, json_text) = read_editor_snapshot(editor);
|
|
||||||
set_html_output.set(html.clone());
|
|
||||||
set_document_json.set(snapshot.clone());
|
|
||||||
set_json_output.set(json_text);
|
|
||||||
if let Some(target) = slash_change_event_target.as_ref() {
|
|
||||||
dispatch_change_event_to_target(
|
|
||||||
target,
|
|
||||||
&ChangePayload {
|
|
||||||
document_id: document_id.get_untracked(),
|
|
||||||
workspace_id: workspace_id.get_untracked(),
|
|
||||||
title: title.get_untracked(),
|
|
||||||
content: snapshot.clone(),
|
|
||||||
meta: ChangeMetaPayload {
|
|
||||||
dirty_count: dirty_count.get_untracked(),
|
|
||||||
editor_focused: editor_focused.get_untracked(),
|
|
||||||
slash_open: slash_open.get_untracked(),
|
|
||||||
toolbar_open: toolbar_overlay_locked(
|
|
||||||
turn_into_open.get_untracked(),
|
|
||||||
color_menu_open.get_untracked(),
|
|
||||||
more_menu_open.get_untracked(),
|
|
||||||
),
|
|
||||||
selected_block_index: hovered_block.get_untracked().map(|block| block.index),
|
|
||||||
revision: revision.get_untracked(),
|
|
||||||
conflict_detection_key: conflict_detection_key.get_untracked(),
|
|
||||||
read_only: read_only.get_untracked(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
dispatch_runtime_state_to_target(
|
|
||||||
target,
|
|
||||||
document_id.get_untracked(),
|
|
||||||
workspace_id.get_untracked(),
|
|
||||||
title.get_untracked(),
|
|
||||||
dirty_count.get_untracked(),
|
|
||||||
hovered_block.get_untracked(),
|
|
||||||
editor_focused.get_untracked(),
|
|
||||||
slash_open.get_untracked(),
|
|
||||||
turn_into_open.get_untracked(),
|
|
||||||
color_menu_open.get_untracked(),
|
|
||||||
more_menu_open.get_untracked(),
|
|
||||||
read_only.get_untracked(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
match persist_document_state(
|
|
||||||
&runtime_persisted_identity(document_id, workspace_id),
|
|
||||||
&title.get_untracked(),
|
|
||||||
&snapshot,
|
|
||||||
Some(html),
|
|
||||||
) {
|
|
||||||
Ok(()) => set_command_feedback.set(format!("{message},并已写入本地草稿")),
|
|
||||||
Err(err) => set_command_feedback.set(format!("{message},但本地保存失败:{err}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
set_slash_open.set(false);
|
|
||||||
set_command_feedback.set(format!("命令执行失败:{err}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span class="slash-item-row">
|
|
||||||
<span class="slash-item-icon">{icon}</span>
|
|
||||||
<span class="slash-item-copy">
|
|
||||||
<strong>{label}</strong>
|
|
||||||
<span>{description}</span>
|
|
||||||
</span>
|
|
||||||
<span class="slash-item-shortcut">{shortcut}</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
}).collect_view()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
.into_any()
|
|
||||||
} else {
|
|
||||||
().into_any()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
|
|
||||||
<TiptapEditor
|
<TiptapEditor
|
||||||
id=editor_instance_id.clone()
|
id=editor_instance_id.clone()
|
||||||
|
|||||||
Reference in New Issue
Block a user