Files
mnote/rust/crates/mnote-web/browser/sidebar-page-settings-runtime.js
T

946 lines
44 KiB
JavaScript
Raw Normal View History

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';
const PAGE_WIDTH_TYPES = ['default', 'markdown', 'word', 'pdf', 'excel', 'ppt', 'mindmap'];
const PAGE_WIDTH_LABELS = {
default: '默认',
markdown: 'Markdown',
word: 'Word',
pdf: 'PDF',
excel: 'Excel',
ppt: 'PPT',
mindmap: 'Mindmap'
};
const DEFAULT_PAGE_WIDTH_PREFERENCES = {
default: { mode: 'comfortable', resolvedMode: 'comfortable', cssMaxWidth: '980px', source: 'system' },
markdown: { mode: 'readable', resolvedMode: 'readable', cssMaxWidth: '760px', source: 'system' },
word: { mode: 'wide', resolvedMode: 'wide', cssMaxWidth: '1180px', source: 'system' },
pdf: { mode: 'wide', resolvedMode: 'wide', cssMaxWidth: '1180px', source: 'system' },
excel: { mode: 'full', resolvedMode: 'full', cssMaxWidth: 'none', source: 'system' },
ppt: { mode: 'wide', resolvedMode: 'wide', cssMaxWidth: '1180px', source: 'system' },
mindmap: { mode: 'full', resolvedMode: 'full', cssMaxWidth: 'none', source: 'system' }
};
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 currentPageWidthPreferences() {
return Object.assign({}, DEFAULT_PAGE_WIDTH_PREFERENCES, pageUiState.pageWidthPreferences || {});
}
function pageWidthModeLabel(mode) {
if (mode === 'inherit') return '继承默认';
if (mode === 'readable') return '阅读';
if (mode === 'comfortable') return '舒适';
if (mode === 'wide') return '宽版';
if (mode === 'full') return '全宽';
return '舒适';
}
function normalizePageWidthPreferences(value) {
var input = value && typeof value === 'object' ? value : {};
var output = {};
PAGE_WIDTH_TYPES.forEach(function(type) {
var base = DEFAULT_PAGE_WIDTH_PREFERENCES[type];
var current = input[type] && typeof input[type] === 'object' ? input[type] : {};
var mode = String(current.mode || base.mode || 'comfortable');
var resolvedMode = String(current.resolvedMode || current.resolved_mode || (mode === 'inherit' ? (input.default && input.default.resolvedMode) || 'comfortable' : mode));
output[type] = {
mode: mode,
custom: current.custom || null,
resolvedMode: resolvedMode,
resolvedCustom: current.resolvedCustom || current.resolved_custom || null,
cssMaxWidth: String(current.cssMaxWidth || current.css_max_width || pageWidthCssMaxWidth(resolvedMode)),
source: String(current.source || base.source || 'system')
};
});
return output;
}
function pageWidthCssMaxWidth(mode) {
if (mode === 'readable') return '760px';
if (mode === 'comfortable') return '980px';
if (mode === 'wide') return '1180px';
if (mode === 'full') return 'none';
return '980px';
}
function markdownPageWidthPreference(options) {
var preferences = currentPageWidthPreferences();
var markdown = preferences.markdown || DEFAULT_PAGE_WIDTH_PREFERENCES.markdown;
if (markdown.source === 'system' && options && options.wideLayout) {
return {
mode: 'comfortable',
resolvedMode: 'comfortable',
cssMaxWidth: '980px',
source: 'wideLayout'
};
}
return markdown;
}
function activeResourceWidthType() {
var raw = document.documentElement.getAttribute('data-mnote-active-resource-width-type') || '';
var type = String(raw).trim();
if (type === 'sheet') return 'excel';
if (PAGE_WIDTH_TYPES.indexOf(type) >= 0 && type !== 'default') return type;
return '';
}
function activeResourceWidthPreference(options) {
var type = activeResourceWidthType();
if (!type || type === 'markdown') return markdownPageWidthPreference(options);
var preferences = currentPageWidthPreferences();
return preferences[type] || DEFAULT_PAGE_WIDTH_PREFERENCES[type] || DEFAULT_PAGE_WIDTH_PREFERENCES.default;
}
function notifyPageWidthPreferenceChanged(type) {
var detail = {
type: String(type || ''),
preferences: currentPageWidthPreferences()
};
window.dispatchEvent(new CustomEvent('mnote:page-width-preference-changed', { detail: detail }));
document.querySelectorAll('iframe.mnote-resource-tab-frame').forEach(function(frame) {
if (!(frame instanceof HTMLIFrameElement) || !frame.contentWindow) return;
try {
frame.contentWindow.postMessage({
type: 'mnote:page-width-preference-changed',
contentType: detail.type
}, window.location.origin);
} catch (_) {}
});
}
function readGlobalShowHeadingNumbers() {
return Boolean(currentPageOptions().showHeadingNumbers);
}
function writeGlobalShowHeadingNumbers(value) {
void persistPageOptionsPatch({ showHeadingNumbers: Boolean(value) });
}
function effectiveShowHeadingNumbers(options) {
return Boolean(options && options.showHeadingNumbers);
}
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 = Boolean(options.showHeadingNumbers);
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) {
var widthPreference = activeResourceWidthPreference(options);
var cssMaxWidth = String(widthPreference.cssMaxWidth || pageWidthCssMaxWidth(widthPreference.resolvedMode));
shell.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
shell.setAttribute('data-page-width-content-type', activeResourceWidthType() || 'markdown');
shell.setAttribute('data-page-width-mode', String(widthPreference.mode || 'readable'));
shell.setAttribute('data-page-width-resolved-mode', String(widthPreference.resolvedMode || widthPreference.mode || 'readable'));
shell.setAttribute('data-page-width-source', String(widthPreference.source || 'system'));
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 = cssMaxWidth === 'none' ? 'none' : cssMaxWidth;
}
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)));
var rootWidthPreference = activeResourceWidthPreference(options);
document.documentElement.setAttribute('data-page-width-content-type', activeResourceWidthType() || 'markdown');
document.documentElement.setAttribute('data-page-width-mode', String(rootWidthPreference.mode || 'readable'));
document.documentElement.setAttribute('data-page-width-resolved-mode', String(rootWidthPreference.resolvedMode || rootWidthPreference.mode || 'readable'));
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 createPageWidthSelectRow(type) {
var options = type === 'default'
? [
['readable', '阅读'],
['comfortable', '舒适'],
['wide', '宽版'],
['full', '全宽']
]
: [
['inherit', '继承默认'],
['readable', '阅读'],
['comfortable', '舒适'],
['wide', '宽版'],
['full', '全宽']
];
return '' +
'<label class="wolai-page-setting-row" data-page-width-row="' + type + '">' +
'<span class="wolai-page-setting-copy">' +
'<span class="wolai-page-setting-label">' + escapeHtml(PAGE_WIDTH_LABELS[type] || type) + '</span>' +
'<span class="wolai-page-setting-hint" data-page-width-hint="' + type + '"></span>' +
'</span>' +
'<select class="wolai-page-setting-select" data-page-width-select="' + type + '">' +
options.map(function(option) {
return '<option value="' + option[0] + '">' + option[1] + '</option>';
}).join('') +
'</select>' +
'</label>';
}
function createPageWidthRows() {
return '' +
'<div class="wolai-page-settings-global-note">页面宽度</div>' +
PAGE_WIDTH_TYPES.map(createPageWidthSelectRow).join('');
}
function renderGlobalOptions(popover) {
var globalHeadingNumbers = readGlobalShowHeadingNumbers();
popover.querySelectorAll('[data-global-option-checkbox="showHeadingNumbers"]').forEach(function(input) {
input.checked = globalHeadingNumbers;
});
var preferences = currentPageWidthPreferences();
popover.querySelectorAll('[data-page-width-select]').forEach(function(select) {
var type = select.getAttribute('data-page-width-select') || '';
var preference = preferences[type] || DEFAULT_PAGE_WIDTH_PREFERENCES[type] || DEFAULT_PAGE_WIDTH_PREFERENCES.default;
select.value = String(preference.mode || 'comfortable');
});
popover.querySelectorAll('[data-page-width-hint]').forEach(function(node) {
var type = node.getAttribute('data-page-width-hint') || '';
var preference = preferences[type] || DEFAULT_PAGE_WIDTH_PREFERENCES[type] || DEFAULT_PAGE_WIDTH_PREFERENCES.default;
var resolvedMode = String(preference.resolvedMode || preference.mode || 'comfortable');
node.textContent = pageWidthModeLabel(resolvedMode) + ' · ' + String(preference.cssMaxWidth || pageWidthCssMaxWidth(resolvedMode));
});
}
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() +
createPageWidthRows() +
'</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/ui/preferences', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
documentId: currentDocumentId(),
workspaceId: resolveWorkspaceId(document.body),
...currentWorkspaceSourcePayload(),
updates: nextOptions
})
});
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);
}
var savedOptions = payload && payload.result && payload.result.pageOptions && typeof payload.result.pageOptions === 'object'
? Object.assign(defaultPageOptions(), payload.result.pageOptions)
: nextOptions;
pageUiState.pageOptions = Object.assign({}, savedOptions);
nextOptions = Object.assign({}, pageUiState.pageOptions);
applyPageOptionsToShell();
renderPageSettingsPopover();
2026-05-26 16:47:03 +08:00
var script = document.getElementById('__MNOTE_PAGE_AGGREGATE__');
if (script) {
try {
var aggregate = JSON.parse(script.textContent || 'null');
if (aggregate && typeof aggregate === 'object') {
aggregate.layout = aggregate.layout && typeof aggregate.layout === 'object' ? aggregate.layout : {};
aggregate.layout.pageOptions = Object.assign({}, nextOptions);
aggregate.layout_options = Object.assign({}, nextOptions);
script.textContent = JSON.stringify(aggregate);
script.setAttribute('data-mnote-page-options-synced-at', String(Date.now()));
2026-05-26 17:24:05 +08:00
script.setAttribute('data-mnote-page-options-local-write-at', String(Date.now()));
2026-05-26 16:47:03 +08:00
}
} catch (_) {}
}
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));
}
}
async function loadPageWidthPreferences() {
if (!currentDocumentId()) return;
var params = new URLSearchParams();
params.set('documentId', currentDocumentId());
params.set('workspaceId', resolveWorkspaceId(document.body));
var sourcePayload = currentWorkspaceSourcePayload();
Object.keys(sourcePayload || {}).forEach(function(key) {
if (sourcePayload[key] != null && sourcePayload[key] !== '') params.set(key, sourcePayload[key]);
});
try {
var response = await fetch('/api/ui/preferences/effective?' + params.toString(), {
headers: { accept: 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) return;
pageUiState.pageWidthPreferences = normalizePageWidthPreferences(payload.result && payload.result.pageWidthPreferences);
applyPageOptionsToShell();
if (isPageSettingsOpen()) renderPageSettingsPopover();
} catch (_) {}
}
async function persistPageWidthPreference(type, mode) {
if (PAGE_WIDTH_TYPES.indexOf(type) < 0) return;
var previous = pageUiState.pageWidthPreferences;
var next = normalizePageWidthPreferences(previous);
next[type] = Object.assign({}, next[type], {
mode: mode,
resolvedMode: mode === 'inherit' ? next.default.resolvedMode : mode,
cssMaxWidth: pageWidthCssMaxWidth(mode === 'inherit' ? next.default.resolvedMode : mode),
source: 'optimistic'
});
pageUiState.pageWidthPreferences = next;
applyPageOptionsToShell();
renderPageSettingsPopover();
notifyPageWidthPreferenceChanged(type);
try {
var updates = {};
updates['pageWidth.' + type] = { mode: mode, custom: null };
var response = await fetch('/api/ui/preferences', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
documentId: currentDocumentId(),
workspaceId: resolveWorkspaceId(document.body),
...currentWorkspaceSourcePayload(),
updates: updates
})
});
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_width_save_failed_' + response.status);
}
pageUiState.pageWidthPreferences = normalizePageWidthPreferences(payload.result && payload.result.pageWidthPreferences);
applyPageOptionsToShell();
renderPageSettingsPopover();
notifyPageWidthPreferenceChanged(type);
document.documentElement.setAttribute('data-mnote-page-width-saved', 'true');
} catch (error) {
pageUiState.pageWidthPreferences = previous;
applyPageOptionsToShell();
renderPageSettingsPopover();
document.documentElement.setAttribute('data-mnote-page-width-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();
}
window.addEventListener('mnote:active-resource-tab-changed', function() {
applyPageOptionsToShell();
});
return {
applyPageOptionsToShell,
closePageHistoryDrawer,
closePageSettingsPopover,
closePageShareDialog,
currentPageOptions,
ensureHistorySnapshotsSeeded,
ensurePageHistoryDrawer,
ensurePageSettingsPopover,
ensurePageShareDialog,
isPageSettingsOpen,
openPageHistoryDrawer,
openPageSettingsPopover,
openPageShareDialog,
pageOptionIsSupported,
persistPageOptionsPatch,
persistPageWidthPreference,
recordPageHistorySnapshot,
renderPageSettingsPopover,
setActivePageSettingsTab,
togglePageSettingsPopover,
updatePageSettingsTriggerState,
writeGlobalShowHeadingNumbers,
loadPageWidthPreferences,
};
}