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 '' + ''; } return '' + ''; } function createGlobalHeadingNumbersRow() { return '' + ''; } function renderGlobalOptions(popover) { var globalHeadingNumbers = readGlobalShowHeadingNumbers(); popover.querySelectorAll('[data-global-option-checkbox="showHeadingNumbers"]').forEach(function(input) { input.checked = globalHeadingNumbers; }); } function createPageFontRow() { return '' + ''; } 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 = '' + '
' + '
' + '
页面历史
当前会话内最近保存的 15 个快照。
' + '' + '
' + '
' + '
'; 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 '' + '
' + '
' + '
' + escapeHtml(label) + '
' + '
字数 ' + Number(stats.wordCount || 0) + ' · 字符 ' + Number(stats.characterCount || 0) + ' · 块数 ' + Number(stats.blockCount || 0) + '
' + '
' + '' + '
'; }).join('') : '
尚未产生历史快照,编辑后会自动生成。
'; } 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 = '' + ''; 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 = '' + ''; 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 '
' + escapeHtml(message) + '
'; } 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 '' + '
' + '
' + escapeHtml(title) + '
' + (path ? '
' + escapeHtml(path) + '
' : '') + (snippet ? '
' + escapeHtml(snippet) + '
' : '') + '
'; }).join(''); } return rows.slice(0, 16).map(function(item) { var tag = searchText(item && item.tag) || 'untagged'; var count = Number(item && item.count || 0); return '' + '
' + '
#' + escapeHtml(tag) + '
' + '
' + count + ' 个页面
' + '
'; }).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 = '' + '字数 ' + Number(stats.wordCount || 0) + '' + '字符 ' + Number(stats.characterCount || 0) + '' + '块数 ' + Number(stats.blockCount || 0) + '' + '待办 ' + Number(stats.todoDone || 0) + '/' + Number(stats.todoTotal || 0) + ''; } 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, }; }