md2ui 1.0.18 → 1.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -58
- package/bin/build.js +95 -9
- package/bin/md2ui.js +102 -13
- package/package.json +24 -10
- package/public/docs/00-/345/277/253/351/200/237/345/274/200/345/247/213.md +48 -28
- package/public/docs/01-/345/212/237/350/203/275/347/211/271/346/200/247.md +55 -40
- package/public/docs/02-Markdown/346/270/262/346/237/223/00-/345/237/272/347/241/200/350/257/255/346/263/225.md +88 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/01-/344/273/243/347/240/201/345/235/227.md +91 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/02-/350/241/250/346/240/274.md +187 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/03-Mermaid/345/233/276/350/241/250.md +101 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/04-Frontmatter.md +32 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/05-/346/225/260/345/255/246/345/205/254/345/274/217.md +47 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/06-Mermaid/345/244/215/346/235/202/345/233/276/350/241/250/346/265/213/350/257/225.md +1376 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/assets/img-1777383093712.png +0 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/00-/344/270/211/346/240/217/345/270/203/345/261/200.md +33 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/01-/347/233/256/345/275/225/346/240/221/345/257/274/350/210/252.md +43 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/02-/346/226/207/346/241/243/345/244/247/347/272/262.md +51 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/03-/344/270/212/344/270/213/347/257/207/345/257/274/350/210/252.md +29 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/04-/347/253/231/345/206/205/351/223/276/346/216/245.md +39 -0
- package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/05-/345/244/247/347/272/262/345/216/213/345/212/233/346/265/213/350/257/225.md +340 -0
- package/public/docs/04-/346/220/234/347/264/242/345/212/237/350/203/275/00-/345/205/250/346/226/207/346/220/234/347/264/242.md +46 -0
- package/public/docs/05-/347/274/226/350/276/221/345/212/237/350/203/275/00-/347/274/226/350/276/221/345/231/250/345/237/272/347/241/200.md +65 -0
- package/public/docs/05-/347/274/226/350/276/221/345/212/237/350/203/275/01-/350/207/252/345/212/250/344/277/235/345/255/230.md +38 -0
- package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/00-/351/230/205/350/257/273/350/277/233/345/272/246.md +43 -0
- package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/01-/345/233/276/347/211/207/346/224/276/345/244/247.md +40 -0
- package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/02-/350/277/224/345/233/236/351/241/266/351/203/250.md +38 -0
- package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/assets/img-1777261394722.png +0 -0
- package/public/docs/07-/347/247/273/345/212/250/347/253/257/351/200/202/351/205/215/00-/345/223/215/345/272/224/345/274/217/345/270/203/345/261/200.md +37 -0
- package/public/docs/08-/346/226/207/346/241/243/347/256/241/347/220/206/00-/346/226/260/345/273/272/344/270/216/345/210/240/351/231/244.md +47 -0
- package/public/docs/09-/345/257/274/345/207/272/345/212/237/350/203/275/00-/345/257/274/345/207/272Word.md +77 -0
- package/public/docs/10-/351/203/250/347/275/262/344/270/216/351/205/215/347/275/256/00-CLI/345/267/245/345/205/267.md +52 -0
- package/public/docs/10-/351/203/250/347/275/262/344/270/216/351/205/215/347/275/256/01-SSG/351/235/231/346/200/201/346/236/204/345/273/272.md +44 -0
- package/public/docs/10-/351/203/250/347/275/262/344/270/216/351/205/215/347/275/256/02-/350/207/252/345/256/232/344/271/211/351/205/215/347/275/256.md +58 -0
- package/public/docs/11-/345/244/232/347/272/247/347/233/256/345/275/225/346/265/213/350/257/225/00-/344/270/200/347/272/247/346/226/207/346/241/243.md +20 -0
- package/public/docs/11-/345/244/232/347/272/247/347/233/256/345/275/225/346/265/213/350/257/225/01-/345/255/220/347/233/256/345/275/225/00-/344/272/214/347/272/247/346/226/207/346/241/243.md +13 -0
- package/public/docs/11-/345/244/232/347/272/247/347/233/256/345/275/225/346/265/213/350/257/225/01-/345/255/220/347/233/256/345/275/225/01-/346/267/261/345/261/202/345/265/214/345/245/227/00-/344/270/211/347/272/247/346/226/207/346/241/243.md +23 -0
- package/src/App.vue +111 -12
- package/src/components/AppSidebar.vue +181 -21
- package/src/components/CodeBlockNodeView.vue +72 -0
- package/src/components/DocContent.vue +25 -14
- package/src/components/EditorContent.vue +257 -0
- package/src/components/EditorToolbar.vue +264 -0
- package/src/components/ImageZoom.vue +88 -5
- package/src/components/MathBlockNodeView.vue +160 -0
- package/src/components/MathInlineNodeView.vue +145 -0
- package/src/components/MermaidNodeView.vue +157 -0
- package/src/components/MobileSearch.vue +97 -0
- package/src/components/TableOfContents.vue +174 -32
- package/src/components/TopBar.vue +69 -4
- package/src/components/TreeNode.vue +232 -39
- package/src/components/WelcomePage.vue +2 -2
- package/src/composables/useDocHash.js +9 -1
- package/src/composables/useDocManager.js +452 -105
- package/src/composables/useDocTree.js +33 -2
- package/src/composables/useExportWord.js +73 -10
- package/src/composables/useFileWatcher.js +45 -0
- package/src/composables/useFrontmatter.js +2 -2
- package/src/composables/useMarkdown.js +450 -52
- package/src/composables/useMermaidCache.js +15 -0
- package/src/composables/useScroll.js +354 -27
- package/src/composables/useSearch.js +12 -11
- package/src/config.js +1 -4
- package/src/extensions/CodeBlockCustom.js +113 -0
- package/src/extensions/MathBlock.js +107 -0
- package/src/extensions/MathInline.js +100 -0
- package/src/extensions/MermaidBlock.js +73 -0
- package/src/extensions/TableControls.js +670 -0
- package/src/services/DocService.js +168 -0
- package/src/style.css +2416 -36
- package/src/utils/imageConverter.js +129 -0
- package/vite-plugin-doc-api.js +369 -0
- package/vite.config.js +7 -2
- package/public/docs/02-Mermaid/345/233/276/350/241/250.md +0 -102
- package/public/docs/03-/350/277/233/351/230/266/346/214/207/345/215/227/01-/347/233/256/345/275/225/347/273/223/346/236/204.md +0 -55
- package/public/docs/03-/350/277/233/351/230/266/346/214/207/345/215/227/02-/350/207/252/345/256/232/344/271/211/351/205/215/347/275/256.md +0 -63
- package/public/docs/03-/350/277/233/351/230/266/346/214/207/345/215/227/03-/351/203/250/347/275/262/346/226/271/346/241/210.md +0 -73
- package/public/docs/04-API/345/217/202/350/200/203/01-/347/273/204/344/273/266API.md +0 -80
- package/public/docs/04-API/345/217/202/350/200/203/02-Composables.md +0 -92
- package/src/api/docs.js +0 -106
- package/src/components/SearchPanel.vue +0 -90
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { ref, computed, nextTick } from 'vue'
|
|
2
|
-
import
|
|
1
|
+
import { ref, computed, watch, nextTick } from 'vue'
|
|
2
|
+
import * as docService from '../services/DocService.js'
|
|
3
3
|
import { useMarkdown } from './useMarkdown.js'
|
|
4
4
|
import { useSearch } from './useSearch.js'
|
|
5
5
|
import { useScroll } from './useScroll.js'
|
|
6
6
|
import { useMobile } from './useMobile.js'
|
|
7
|
-
import { findDoc, findFirstDoc, findReadmeDoc, findDocByHash, expandParents, flattenDocsList, expandAll, collapseAll } from './useDocTree.js'
|
|
7
|
+
import { findDoc, findFirstDoc, findReadmeDoc, findDocByHash, expandParents, flattenDocsList, expandAll, collapseAll, buildHashIndex } from './useDocTree.js'
|
|
8
|
+
import { stripOrderPrefix } from './useDocHash.js'
|
|
9
|
+
import { clearMermaidCache } from './useMermaidCache.js'
|
|
8
10
|
|
|
9
11
|
// 等待内容区域图片加载完成
|
|
10
12
|
async function waitForContentImages(timeoutMs = 3000) {
|
|
@@ -21,88 +23,121 @@ export function useDocManager() {
|
|
|
21
23
|
// 文档状态
|
|
22
24
|
const docsList = ref([])
|
|
23
25
|
const currentDoc = ref('')
|
|
24
|
-
// 如果 URL 有路径,说明是刷新已有文档页面,初始不显示欢迎页,避免闪烁
|
|
25
26
|
const hasInitialPath = window.location.pathname.replace(/^\//, '') !== ''
|
|
26
27
|
const showWelcome = ref(!hasInitialPath)
|
|
28
|
+
const lastModified = ref('')
|
|
29
|
+
|
|
30
|
+
// 编辑模式(从 sessionStorage 恢复)
|
|
31
|
+
const editMode = ref(sessionStorage.getItem('editMode') === 'true')
|
|
32
|
+
const rawMarkdown = ref('')
|
|
33
|
+
const currentDocFilePath = ref('')
|
|
27
34
|
|
|
28
35
|
// composables
|
|
29
|
-
const { htmlContent, tocItems, renderMarkdown, docHash } = useMarkdown()
|
|
36
|
+
const { htmlContent, tocItems, renderMarkdown, extractTOCFromMarkdown, docHash } = useMarkdown()
|
|
30
37
|
const { buildIndex } = useSearch()
|
|
31
38
|
const {
|
|
32
39
|
scrollProgress, showBackToTop, activeHeading,
|
|
33
40
|
handleScroll: _handleScroll,
|
|
34
41
|
scrollToHeading: _scrollToHeading,
|
|
35
|
-
scrollToTop
|
|
42
|
+
scrollToTop,
|
|
43
|
+
setTocItems,
|
|
44
|
+
rebuildHeadingsCache,
|
|
45
|
+
clearActiveHeading,
|
|
46
|
+
isSuppressHashClear,
|
|
47
|
+
lockHeading
|
|
36
48
|
} = useScroll()
|
|
37
49
|
const { isMobile, mobileDrawerOpen } = useMobile()
|
|
38
50
|
|
|
39
|
-
|
|
51
|
+
setTocItems(tocItems)
|
|
52
|
+
|
|
53
|
+
// ===== 滚动 & 历史 =====
|
|
40
54
|
function getScrollTop() {
|
|
41
55
|
const el = document.querySelector('.content')
|
|
42
56
|
return el ? el.scrollTop : 0
|
|
43
57
|
}
|
|
44
58
|
|
|
45
|
-
// 构造 history state,保留滚动位置
|
|
46
59
|
function makeState(scrollTop) {
|
|
47
60
|
return { scrollTop: scrollTop ?? getScrollTop() }
|
|
48
61
|
}
|
|
49
62
|
|
|
50
|
-
//
|
|
63
|
+
// URL 锚点同步:统一由 watch 驱动,scrollToHeading 不再直接操作 URL
|
|
64
|
+
// _pendingPush: 标记下一次 activeHeading 变化是否需要 pushState(点击目录项)
|
|
65
|
+
let _pendingPush = false
|
|
66
|
+
let _hashUpdateTimer = null
|
|
67
|
+
|
|
68
|
+
watch(activeHeading, (id, oldId) => {
|
|
69
|
+
if (!currentDoc.value) return
|
|
70
|
+
|
|
71
|
+
// 文档切换导致的清空,不动 URL
|
|
72
|
+
if (!id && isSuppressHashClear()) return
|
|
73
|
+
|
|
74
|
+
// 取出并重置 push 标记
|
|
75
|
+
const shouldPush = _pendingPush
|
|
76
|
+
_pendingPush = false
|
|
77
|
+
|
|
78
|
+
if (_hashUpdateTimer) clearTimeout(_hashUpdateTimer)
|
|
79
|
+
_hashUpdateTimer = setTimeout(() => {
|
|
80
|
+
_hashUpdateTimer = null
|
|
81
|
+
const base = `/${docHash(currentDoc.value)}`
|
|
82
|
+
const url = id ? `${base}#${id}` : base
|
|
83
|
+
const current = window.location.pathname + window.location.hash
|
|
84
|
+
if (current === url) return
|
|
85
|
+
|
|
86
|
+
if (shouldPush) {
|
|
87
|
+
// 先保存当前滚动位置到旧条目
|
|
88
|
+
history.replaceState(makeState(), '', current)
|
|
89
|
+
history.pushState(makeState(), '', url)
|
|
90
|
+
} else {
|
|
91
|
+
history.replaceState(makeState(), '', url)
|
|
92
|
+
}
|
|
93
|
+
}, 100)
|
|
94
|
+
})
|
|
95
|
+
|
|
51
96
|
function handleScroll(e) {
|
|
52
97
|
_handleScroll(e)
|
|
53
|
-
if (activeHeading.value && currentDoc.value) {
|
|
54
|
-
history.replaceState(makeState(), '', `/${docHash(currentDoc.value)}#${activeHeading.value}`)
|
|
55
|
-
}
|
|
56
98
|
}
|
|
57
99
|
|
|
58
|
-
// push: true 表示用户主动点击锚点,产生可回退的历史条目
|
|
59
100
|
function scrollToHeading(id, { push = false } = {}) {
|
|
60
|
-
if (push
|
|
61
|
-
// 先把当前滚动位置写入即将被覆盖的历史条目
|
|
62
|
-
history.replaceState(makeState(), '', window.location.href)
|
|
63
|
-
}
|
|
101
|
+
if (push) _pendingPush = true
|
|
64
102
|
_scrollToHeading(id)
|
|
65
|
-
if (currentDoc.value) {
|
|
66
|
-
const url = `/${docHash(currentDoc.value)}#${id}`
|
|
67
|
-
if (push) {
|
|
68
|
-
history.pushState(makeState(), '', url)
|
|
69
|
-
} else {
|
|
70
|
-
history.replaceState(makeState(), '', url)
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
103
|
}
|
|
74
104
|
|
|
75
|
-
//
|
|
105
|
+
// ===== 文档列表加载 =====
|
|
76
106
|
async function loadDocsList() {
|
|
77
|
-
docsList.value = await
|
|
107
|
+
docsList.value = await docService.fetchDocsList()
|
|
108
|
+
buildHashIndex(docsList.value, docHash)
|
|
109
|
+
restoreExpandedState()
|
|
78
110
|
buildIndex(docsList.value)
|
|
79
111
|
}
|
|
80
112
|
|
|
81
|
-
//
|
|
113
|
+
// ===== 导航 =====
|
|
82
114
|
function goHome({ isPopstate = false } = {}) {
|
|
83
115
|
currentDoc.value = ''
|
|
84
116
|
showWelcome.value = true
|
|
85
117
|
htmlContent.value = ''
|
|
86
118
|
tocItems.value = []
|
|
119
|
+
editMode.value = false
|
|
120
|
+
lastModified.value = ''
|
|
121
|
+
sessionStorage.setItem('editMode', 'false')
|
|
122
|
+
document.title = 'md2ui'
|
|
87
123
|
if (!isPopstate) {
|
|
88
|
-
// 用户主动点击,保存旧条目滚动位置后 push
|
|
89
124
|
history.replaceState(makeState(), '', window.location.href)
|
|
90
125
|
history.pushState(makeState(0), '', '/')
|
|
91
126
|
}
|
|
92
127
|
if (isMobile.value) mobileDrawerOpen.value = false
|
|
93
128
|
}
|
|
94
129
|
|
|
95
|
-
// 加载文档
|
|
96
130
|
async function loadDoc(key, { replace = false, anchor = '', keepState = false } = {}) {
|
|
97
131
|
currentDoc.value = key
|
|
98
132
|
showWelcome.value = false
|
|
133
|
+
lastContentHash = ''
|
|
134
|
+
clearMermaidCache()
|
|
135
|
+
docService.resetContentEtag()
|
|
99
136
|
const hash = docHash(key)
|
|
100
137
|
const url = anchor ? `/${hash}#${anchor}` : `/${hash}`
|
|
101
138
|
if (replace) {
|
|
102
|
-
// keepState: popstate 回退时保留浏览器已有的 state(含 scrollTop)
|
|
103
139
|
if (!keepState) history.replaceState(makeState(0), '', url)
|
|
104
140
|
} else {
|
|
105
|
-
// push 前先保存当前滚动位置到旧条目
|
|
106
141
|
history.replaceState(makeState(), '', window.location.href)
|
|
107
142
|
history.pushState(makeState(0), '', url)
|
|
108
143
|
}
|
|
@@ -112,42 +147,155 @@ export function useDocManager() {
|
|
|
112
147
|
const response = await fetch(doc.path)
|
|
113
148
|
if (response.ok) {
|
|
114
149
|
const content = await response.text()
|
|
115
|
-
|
|
150
|
+
rawMarkdown.value = content
|
|
151
|
+
// 捕获最后修改时间
|
|
152
|
+
const lm = response.headers.get('x-last-modified')
|
|
153
|
+
lastModified.value = lm || ''
|
|
154
|
+
// 提取文件路径供轮询使用
|
|
155
|
+
currentDocFilePath.value = doc.path.replace(/^\/@user-docs\//, '')
|
|
156
|
+
if (editMode.value) {
|
|
157
|
+
extractTOCFromMarkdown(content, tocItems)
|
|
158
|
+
} else {
|
|
159
|
+
await renderMarkdown(content, key, docsList.value)
|
|
160
|
+
}
|
|
161
|
+
// 渲染完成后重建标题缓存(编辑模式需等 tiptap 渲染完成)
|
|
162
|
+
await nextTick()
|
|
163
|
+
rebuildHeadingsCache()
|
|
164
|
+
// 切换文档时清空 activeHeading(标记为文档切换,不清 URL hash)
|
|
165
|
+
// 同时取消 pending 的 hash 同步 timer
|
|
166
|
+
if (_hashUpdateTimer) { clearTimeout(_hashUpdateTimer); _hashUpdateTimer = null }
|
|
167
|
+
// 如果有锚点参数,先锁定再清空,防止 scrollTop=0 触发的滚动检测覆盖锚点
|
|
168
|
+
if (anchor) {
|
|
169
|
+
lockHeading(anchor)
|
|
170
|
+
} else {
|
|
171
|
+
clearActiveHeading()
|
|
172
|
+
}
|
|
116
173
|
const contentEl = document.querySelector('.content')
|
|
117
174
|
if (contentEl) contentEl.scrollTop = 0
|
|
175
|
+
// 动态更新页面标题(SEO + 浏览器标签页)
|
|
176
|
+
const docTitle = findDoc(docsList.value, key)?.label || ''
|
|
177
|
+
document.title = docTitle ? `${docTitle} - md2ui` : 'md2ui'
|
|
118
178
|
}
|
|
119
179
|
} catch (error) {
|
|
120
180
|
console.error('加载文档失败:', error)
|
|
121
181
|
}
|
|
122
182
|
}
|
|
123
183
|
|
|
124
|
-
// 加载第一篇文档
|
|
125
184
|
function loadFirstDoc() {
|
|
126
185
|
const first = findFirstDoc(docsList.value)
|
|
127
186
|
if (first) loadDoc(first.key)
|
|
128
187
|
}
|
|
129
188
|
|
|
130
|
-
// 选择文档(移动端自动关闭抽屉)
|
|
131
189
|
function handleDocSelect(key) {
|
|
132
190
|
loadDoc(key)
|
|
133
191
|
if (isMobile.value) mobileDrawerOpen.value = false
|
|
134
192
|
}
|
|
135
193
|
|
|
136
|
-
// 文件夹操作
|
|
137
|
-
function toggleFolder(item) { item.expanded = !item.expanded }
|
|
138
|
-
function onExpandAll() { expandAll(docsList.value) }
|
|
139
|
-
function onCollapseAll() { collapseAll(docsList.value) }
|
|
194
|
+
// ===== 文件夹操作 =====
|
|
195
|
+
function toggleFolder(item) { item.expanded = !item.expanded; saveExpandedState() }
|
|
196
|
+
function onExpandAll() { expandAll(docsList.value); saveExpandedState() }
|
|
197
|
+
function onCollapseAll() { collapseAll(docsList.value); saveExpandedState() }
|
|
198
|
+
|
|
199
|
+
function saveExpandedState() {
|
|
200
|
+
const expanded = []
|
|
201
|
+
function collect(items) {
|
|
202
|
+
for (const item of items) {
|
|
203
|
+
if (item.type === 'folder') {
|
|
204
|
+
if (item.expanded) expanded.push(item.key)
|
|
205
|
+
if (item.children) collect(item.children)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
collect(docsList.value)
|
|
210
|
+
sessionStorage.setItem('expandedFolders', JSON.stringify(expanded))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function restoreExpandedState() {
|
|
214
|
+
const raw = sessionStorage.getItem('expandedFolders')
|
|
215
|
+
if (!raw) return
|
|
216
|
+
try {
|
|
217
|
+
const expanded = new Set(JSON.parse(raw))
|
|
218
|
+
function apply(items) {
|
|
219
|
+
for (const item of items) {
|
|
220
|
+
if (item.type === 'folder') {
|
|
221
|
+
if (expanded.has(item.key)) item.expanded = true
|
|
222
|
+
if (item.children) apply(item.children)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
apply(docsList.value)
|
|
227
|
+
} catch { /* ignore */ }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ===== 内容区点击处理 =====
|
|
231
|
+
|
|
232
|
+
// 复制锚点链接到剪贴板(通过 class 切换锚点旁提示文字)
|
|
233
|
+
async function copyAnchorLink(anchorId, anchorEl) {
|
|
234
|
+
const base = currentDoc.value ? `/${docHash(currentDoc.value)}` : window.location.pathname
|
|
235
|
+
const url = `${window.location.origin}${base}#${anchorId}`
|
|
236
|
+
try {
|
|
237
|
+
await navigator.clipboard.writeText(url)
|
|
238
|
+
anchorEl.classList.add('anchor-copied')
|
|
239
|
+
setTimeout(() => anchorEl.classList.remove('anchor-copied'), 1500)
|
|
240
|
+
} catch {
|
|
241
|
+
anchorEl.classList.add('anchor-copy-error')
|
|
242
|
+
setTimeout(() => anchorEl.classList.remove('anchor-copy-error'), 1500)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
140
245
|
|
|
141
|
-
// 内容区点击处理(链接跳转 + 图片放大)
|
|
142
246
|
function handleContentClick(event, { onZoom }) {
|
|
143
247
|
const target = event.target
|
|
248
|
+
|
|
249
|
+
// 优先判断图片放大(即使图片在链接内,也走放大逻辑而非链接跳转)
|
|
250
|
+
const isImg = target.tagName === 'IMG' && target.classList.contains('zoomable-image')
|
|
251
|
+
const mermaidEl = target.closest('.mermaid') || target.closest('.mermaid-svg')
|
|
252
|
+
const isMermaid = mermaidEl && mermaidEl.classList.contains('zoomable-image')
|
|
253
|
+
if (isImg || isMermaid) {
|
|
254
|
+
event.preventDefault()
|
|
255
|
+
const container = document.querySelector('.markdown-content')
|
|
256
|
+
if (!container) return
|
|
257
|
+
const allZoomable = [...container.querySelectorAll('.zoomable-image')]
|
|
258
|
+
const images = allZoomable.map(el => {
|
|
259
|
+
if (el.tagName === 'IMG') {
|
|
260
|
+
return `<img src="${el.src}" alt="${el.alt || ''}" style="max-width: 100%; height: auto;" />`
|
|
261
|
+
}
|
|
262
|
+
const clone = el.cloneNode(true)
|
|
263
|
+
clone.querySelectorAll('.mermaid-copy-btn, .image-copy-btn').forEach(btn => btn.remove())
|
|
264
|
+
return clone.innerHTML
|
|
265
|
+
})
|
|
266
|
+
const clickedEl = isImg ? target : mermaidEl
|
|
267
|
+
const index = allZoomable.indexOf(clickedEl)
|
|
268
|
+
onZoom({ images, index: Math.max(index, 0) })
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 链接处理
|
|
144
273
|
const link = target.closest('a')
|
|
145
274
|
if (link) {
|
|
275
|
+
// 标题锚点点击:复制链接 + 跳转 + 高亮
|
|
276
|
+
if (link.classList.contains('heading-anchor')) {
|
|
277
|
+
event.preventDefault()
|
|
278
|
+
const anchorId = link.dataset.anchor
|
|
279
|
+
if (anchorId) {
|
|
280
|
+
copyAnchorLink(anchorId, link)
|
|
281
|
+
scrollToHeading(anchorId, { push: true })
|
|
282
|
+
// 触发高亮闪烁动画
|
|
283
|
+
const heading = document.getElementById(anchorId)
|
|
284
|
+
if (heading) {
|
|
285
|
+
heading.classList.remove('heading-flash')
|
|
286
|
+
void heading.offsetWidth // 强制 reflow 重置动画
|
|
287
|
+
heading.classList.add('heading-flash')
|
|
288
|
+
heading.addEventListener('animationend', () => heading.classList.remove('heading-flash'), { once: true })
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return
|
|
292
|
+
}
|
|
146
293
|
const docKey = link.dataset.docKey
|
|
147
294
|
if (docKey) {
|
|
148
295
|
event.preventDefault()
|
|
149
296
|
const anchor = link.dataset.anchor || ''
|
|
150
297
|
expandParents(docsList.value, docKey)
|
|
298
|
+
saveExpandedState()
|
|
151
299
|
loadDoc(docKey).then(async () => {
|
|
152
300
|
if (anchor) { await nextTick(); await waitForContentImages(); scrollToHeading(anchor) }
|
|
153
301
|
})
|
|
@@ -160,69 +308,169 @@ export function useDocManager() {
|
|
|
160
308
|
}
|
|
161
309
|
return
|
|
162
310
|
}
|
|
163
|
-
// 图片/Mermaid 放大(收集所有可放大元素,支持左右切换)
|
|
164
|
-
const isImg = target.tagName === 'IMG' && target.classList.contains('zoomable-image')
|
|
165
|
-
const mermaidEl = target.closest('.mermaid')
|
|
166
|
-
const isMermaid = mermaidEl && mermaidEl.classList.contains('zoomable-image')
|
|
167
|
-
if (isImg || isMermaid) {
|
|
168
|
-
const container = document.querySelector('.markdown-content')
|
|
169
|
-
if (!container) return
|
|
170
|
-
// 收集所有可放大元素
|
|
171
|
-
const allZoomable = [...container.querySelectorAll('.zoomable-image')]
|
|
172
|
-
const images = allZoomable.map(el => {
|
|
173
|
-
if (el.tagName === 'IMG') {
|
|
174
|
-
return `<img src="${el.src}" alt="${el.alt || ''}" style="max-width: 100%; height: auto;" />`
|
|
175
|
-
}
|
|
176
|
-
return el.innerHTML
|
|
177
|
-
})
|
|
178
|
-
const clickedEl = isImg ? target : mermaidEl
|
|
179
|
-
const index = allZoomable.indexOf(clickedEl)
|
|
180
|
-
onZoom({ images, index: Math.max(index, 0) })
|
|
181
|
-
}
|
|
182
311
|
}
|
|
183
312
|
|
|
184
|
-
//
|
|
313
|
+
// ===== 计算属性 =====
|
|
185
314
|
const currentDocTitle = computed(() => {
|
|
186
315
|
if (!currentDoc.value) return '文档'
|
|
187
316
|
const doc = findDoc(docsList.value, currentDoc.value)
|
|
188
317
|
return doc?.label || '文档'
|
|
189
318
|
})
|
|
190
319
|
|
|
191
|
-
//
|
|
320
|
+
// 扁平化文档列表(缓存,供 prevDoc / nextDoc 共享)
|
|
321
|
+
const flatList = computed(() => flattenDocsList(docsList.value))
|
|
322
|
+
|
|
192
323
|
const prevDoc = computed(() => {
|
|
193
324
|
if (!currentDoc.value) return null
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
return idx > 0 ? flat[idx - 1] : null
|
|
325
|
+
const idx = flatList.value.findIndex(d => d.key === currentDoc.value)
|
|
326
|
+
return idx > 0 ? flatList.value[idx - 1] : null
|
|
197
327
|
})
|
|
198
328
|
|
|
199
329
|
const nextDoc = computed(() => {
|
|
200
330
|
if (!currentDoc.value) return null
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
return idx >= 0 && idx < flat.length - 1 ? flat[idx + 1] : null
|
|
331
|
+
const idx = flatList.value.findIndex(d => d.key === currentDoc.value)
|
|
332
|
+
return idx >= 0 && idx < flatList.value.length - 1 ? flatList.value[idx + 1] : null
|
|
204
333
|
})
|
|
205
334
|
|
|
206
|
-
// 搜索结果选中
|
|
207
335
|
function handleSearchSelect(key) {
|
|
208
336
|
expandParents(docsList.value, key)
|
|
337
|
+
saveExpandedState()
|
|
209
338
|
loadDoc(key)
|
|
210
339
|
}
|
|
211
340
|
|
|
212
|
-
//
|
|
341
|
+
// ===== 编辑模式 =====
|
|
342
|
+
function toggleEditMode() {
|
|
343
|
+
editMode.value = !editMode.value
|
|
344
|
+
sessionStorage.setItem('editMode', editMode.value)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
watch(editMode, async (newVal, oldVal) => {
|
|
348
|
+
if (oldVal && !newVal && rawMarkdown.value && currentDoc.value) {
|
|
349
|
+
await renderMarkdown(rawMarkdown.value, currentDoc.value, docsList.value)
|
|
350
|
+
await nextTick()
|
|
351
|
+
rebuildHeadingsCache()
|
|
352
|
+
}
|
|
353
|
+
if (newVal && rawMarkdown.value) {
|
|
354
|
+
extractTOCFromMarkdown(rawMarkdown.value, tocItems)
|
|
355
|
+
await nextTick()
|
|
356
|
+
rebuildHeadingsCache()
|
|
357
|
+
}
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
watch(rawMarkdown, (md) => {
|
|
361
|
+
if (editMode.value && md) {
|
|
362
|
+
extractTOCFromMarkdown(md, tocItems)
|
|
363
|
+
}
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
// ===== 轮询回调(供 useFileWatcher 调用) =====
|
|
367
|
+
|
|
368
|
+
// 树结构指纹,快速判断是否有变化
|
|
369
|
+
function treeFingerprint(items) {
|
|
370
|
+
const parts = []
|
|
371
|
+
for (const item of items) {
|
|
372
|
+
parts.push(item.key)
|
|
373
|
+
if (item.type === 'folder' && item.children) {
|
|
374
|
+
parts.push('{', treeFingerprint(item.children), '}')
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return parts.join(',')
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function getExpandedKeys(items) {
|
|
381
|
+
const keys = new Set()
|
|
382
|
+
for (const item of items) {
|
|
383
|
+
if (item.type === 'folder') {
|
|
384
|
+
if (item.expanded) keys.add(item.key)
|
|
385
|
+
if (item.children) for (const k of getExpandedKeys(item.children)) keys.add(k)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return keys
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function applyExpandedState(items, expandedKeys) {
|
|
392
|
+
for (const item of items) {
|
|
393
|
+
if (item.type === 'folder') {
|
|
394
|
+
if (expandedKeys.has(item.key)) item.expanded = true
|
|
395
|
+
if (item.children) applyExpandedState(item.children, expandedKeys)
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// 刷新文档列表(轮询回调 或 手动调用)
|
|
401
|
+
async function reloadDocsList(newTree) {
|
|
402
|
+
if (!newTree) {
|
|
403
|
+
// 手动调用(创建/删除/重命名后),强制拉取最新
|
|
404
|
+
docService.resetListEtag()
|
|
405
|
+
newTree = await docService.fetchDocsList()
|
|
406
|
+
}
|
|
407
|
+
// 结构没变就跳过
|
|
408
|
+
if (docsList.value.length > 0 && treeFingerprint(docsList.value) === treeFingerprint(newTree)) {
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
// 保存展开状态 → 替换 → 恢复
|
|
412
|
+
const expandedKeys = getExpandedKeys(docsList.value)
|
|
413
|
+
docsList.value = newTree
|
|
414
|
+
buildHashIndex(docsList.value, docHash)
|
|
415
|
+
applyExpandedState(docsList.value, expandedKeys)
|
|
416
|
+
if (currentDoc.value) expandParents(docsList.value, currentDoc.value)
|
|
417
|
+
buildIndex(docsList.value)
|
|
418
|
+
saveExpandedState()
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 刷新当前文档内容(轮询回调)
|
|
422
|
+
let lastContentHash = ''
|
|
423
|
+
async function reloadCurrentDoc(content) {
|
|
424
|
+
if (!currentDoc.value) return
|
|
425
|
+
// 哈希比对,内容没变就跳过
|
|
426
|
+
let hash = 0
|
|
427
|
+
for (let i = 0; i < content.length; i++) {
|
|
428
|
+
hash = ((hash << 5) - hash + content.charCodeAt(i)) | 0
|
|
429
|
+
}
|
|
430
|
+
const hashStr = String(hash)
|
|
431
|
+
if (hashStr === lastContentHash) return
|
|
432
|
+
lastContentHash = hashStr
|
|
433
|
+
// 编辑模式下编辑器是内容权威来源,不回写 rawMarkdown 避免循环抖动
|
|
434
|
+
if (editMode.value) return
|
|
435
|
+
rawMarkdown.value = content
|
|
436
|
+
await renderMarkdown(content, currentDoc.value, docsList.value)
|
|
437
|
+
rebuildHeadingsCache()
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ===== 写操作 =====
|
|
441
|
+
async function saveDocContent({ path: filePath, content }) {
|
|
442
|
+
try {
|
|
443
|
+
const ok = await docService.saveDoc(filePath, content)
|
|
444
|
+
if (ok) {
|
|
445
|
+
rawMarkdown.value = content
|
|
446
|
+
if (!editMode.value) {
|
|
447
|
+
await renderMarkdown(content, currentDoc.value, docsList.value)
|
|
448
|
+
}
|
|
449
|
+
return true
|
|
450
|
+
}
|
|
451
|
+
return false
|
|
452
|
+
} catch (e) {
|
|
453
|
+
console.error('保存失败:', e)
|
|
454
|
+
return false
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function getCurrentDocPath() {
|
|
459
|
+
return currentDocFilePath.value
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ===== URL 路由 =====
|
|
213
463
|
async function loadFromUrl() {
|
|
214
464
|
const pathname = window.location.pathname.replace(/^\//, '')
|
|
215
465
|
const anchor = window.location.hash.replace('#', '')
|
|
216
466
|
const savedScroll = history.state?.scrollTop
|
|
217
467
|
if (!pathname) {
|
|
218
468
|
if (currentDoc.value) {
|
|
219
|
-
// 浏览器回退到首页,不操作 history
|
|
220
469
|
goHome({ isPopstate: true })
|
|
221
470
|
} else if (docsList.value.length === 0) {
|
|
222
471
|
showWelcome.value = false
|
|
223
472
|
renderMarkdown('# 当前目录没有 Markdown 文档\n\n请在当前目录下添加 `.md` 文件,然后刷新页面。')
|
|
224
473
|
} else {
|
|
225
|
-
// 优先定位到 README,没有则定位到第一篇文档
|
|
226
474
|
const readme = findReadmeDoc(docsList.value)
|
|
227
475
|
const target = readme || findFirstDoc(docsList.value)
|
|
228
476
|
if (target) {
|
|
@@ -234,14 +482,13 @@ export function useDocManager() {
|
|
|
234
482
|
}
|
|
235
483
|
const doc = findDocByHash(docsList.value, pathname, docHash)
|
|
236
484
|
if (!doc) return
|
|
237
|
-
// 同一文档内的锚点变化(含回退)
|
|
238
485
|
if (doc.key === currentDoc.value) {
|
|
239
486
|
await nextTick()
|
|
240
487
|
if (savedScroll != null) {
|
|
241
|
-
// 有保存的滚动位置,直接恢复(回退场景)
|
|
242
488
|
const contentEl = document.querySelector('.content')
|
|
243
489
|
if (contentEl) contentEl.scrollTo({ top: savedScroll, behavior: 'smooth' })
|
|
244
490
|
} else if (anchor) {
|
|
491
|
+
// popstate 同文档锚点跳转,_scrollToHeading 内部有锁
|
|
245
492
|
_scrollToHeading(decodeURIComponent(anchor))
|
|
246
493
|
} else {
|
|
247
494
|
const contentEl = document.querySelector('.content')
|
|
@@ -250,48 +497,148 @@ export function useDocManager() {
|
|
|
250
497
|
return
|
|
251
498
|
}
|
|
252
499
|
expandParents(docsList.value, doc.key)
|
|
500
|
+
saveExpandedState()
|
|
253
501
|
await loadDoc(doc.key, { replace: true, keepState: savedScroll != null, anchor: anchor ? decodeURIComponent(anchor) : '' })
|
|
254
502
|
if (savedScroll != null) {
|
|
255
503
|
await nextTick()
|
|
504
|
+
await waitForContentImages()
|
|
256
505
|
const contentEl = document.querySelector('.content')
|
|
257
506
|
if (contentEl) contentEl.scrollTo({ top: savedScroll })
|
|
258
507
|
} else if (anchor) {
|
|
259
|
-
|
|
508
|
+
const decodedAnchor = decodeURIComponent(anchor)
|
|
509
|
+
await nextTick(); await waitForContentImages()
|
|
510
|
+
// 复用 scrollToHeading,编辑模式下通过 _syncHeadingIds 注入的 id 或文本匹配均可定位
|
|
511
|
+
_scrollToHeading(decodedAnchor)
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ===== 文档管理操作 =====
|
|
516
|
+
async function createDoc({ parentKey, name, type }) {
|
|
517
|
+
const relativePath = parentKey ? `${parentKey}/${name}` : name
|
|
518
|
+
const apiPath = type === 'file' ? `${relativePath}.md` : relativePath
|
|
519
|
+
try {
|
|
520
|
+
await docService.createDoc(apiPath, type)
|
|
521
|
+
await reloadDocsList()
|
|
522
|
+
if (parentKey) expandParents(docsList.value, relativePath)
|
|
523
|
+
if (type === 'file') await loadDoc(relativePath)
|
|
524
|
+
return { ok: true }
|
|
525
|
+
} catch (e) {
|
|
526
|
+
return { ok: false, message: e.message }
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async function renameDoc(item, newName) {
|
|
531
|
+
const parts = item.key.split('/')
|
|
532
|
+
parts[parts.length - 1] = newName
|
|
533
|
+
const newKey = parts.join('/')
|
|
534
|
+
const oldPath = item.type === 'file' ? `${item.key}.md` : item.key
|
|
535
|
+
const newPath = item.type === 'file' ? `${newKey}.md` : newKey
|
|
536
|
+
try {
|
|
537
|
+
await docService.renameDoc(oldPath, newPath)
|
|
538
|
+
const wasCurrentDoc = item.type === 'file' && item.key === currentDoc.value
|
|
539
|
+
const wasInFolder = item.type === 'folder' && currentDoc.value.startsWith(item.key + '/')
|
|
540
|
+
await reloadDocsList()
|
|
541
|
+
if (wasCurrentDoc) {
|
|
542
|
+
expandParents(docsList.value, newKey)
|
|
543
|
+
await loadDoc(newKey, { replace: true })
|
|
544
|
+
}
|
|
545
|
+
if (wasInFolder) {
|
|
546
|
+
const newDocKey = currentDoc.value.replace(item.key + '/', newKey + '/')
|
|
547
|
+
expandParents(docsList.value, newDocKey)
|
|
548
|
+
await loadDoc(newDocKey, { replace: true })
|
|
549
|
+
}
|
|
550
|
+
return { ok: true }
|
|
551
|
+
} catch (e) {
|
|
552
|
+
return { ok: false, message: e.message }
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async function deleteDoc(item) {
|
|
557
|
+
const apiPath = item.type === 'file' ? `${item.key}.md` : item.key
|
|
558
|
+
try {
|
|
559
|
+
await docService.deleteDoc(apiPath)
|
|
560
|
+
if (item.type === 'file' && item.key === currentDoc.value) goHome()
|
|
561
|
+
if (item.type === 'folder' && currentDoc.value.startsWith(item.key + '/')) goHome()
|
|
562
|
+
await reloadDocsList()
|
|
563
|
+
return { ok: true }
|
|
564
|
+
} catch (e) {
|
|
565
|
+
return { ok: false, message: e.message }
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ===== 拖拽排序 =====
|
|
570
|
+
function calcPadWidth(count) { return count < 100 ? 2 : String(count - 1).length }
|
|
571
|
+
function orderPrefix(index, padWidth) { return String(index).padStart(padWidth, '0') }
|
|
572
|
+
function stripPrefix(name) { return name.replace(/^\d+-/, '') }
|
|
573
|
+
|
|
574
|
+
function collectReorderItems(items, parentPath = '') {
|
|
575
|
+
const result = []
|
|
576
|
+
const padWidth = calcPadWidth(items.length)
|
|
577
|
+
items.forEach((item, index) => {
|
|
578
|
+
const prefix = orderPrefix(index, padWidth)
|
|
579
|
+
const pureName = stripPrefix(item.label || item.key.split('/').pop())
|
|
580
|
+
const newName = `${prefix}-${pureName}`
|
|
581
|
+
const oldName = item.key.split('/').pop()
|
|
582
|
+
const oldFsName = item.type === 'file' ? `${oldName}.md` : oldName
|
|
583
|
+
const newFsName = item.type === 'file' ? `${newName}.md` : newName
|
|
584
|
+
const oldPath = parentPath ? `${parentPath}/${oldFsName}` : oldFsName
|
|
585
|
+
const newPath = parentPath ? `${parentPath}/${newFsName}` : newFsName
|
|
586
|
+
if (oldPath !== newPath) result.push({ oldPath, newPath })
|
|
587
|
+
})
|
|
588
|
+
return result
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function collectAllReorderLevels(items, parentPath = '') {
|
|
592
|
+
const levels = []
|
|
593
|
+
items.forEach((item) => {
|
|
594
|
+
if (item.type === 'folder' && item.children) {
|
|
595
|
+
const folderPath = parentPath ? `${parentPath}/${item.key.split('/').pop()}` : item.key.split('/').pop()
|
|
596
|
+
levels.push(...collectAllReorderLevels(item.children, folderPath))
|
|
597
|
+
}
|
|
598
|
+
})
|
|
599
|
+
const currentLevelItems = collectReorderItems(items, parentPath)
|
|
600
|
+
if (currentLevelItems.length > 0) levels.push(currentLevelItems)
|
|
601
|
+
return levels
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function reorderDocs() {
|
|
605
|
+
const levels = collectAllReorderLevels(docsList.value)
|
|
606
|
+
if (levels.length === 0) return { ok: true }
|
|
607
|
+
const currentPureName = currentDoc.value ? stripOrderPrefix(currentDoc.value) : ''
|
|
608
|
+
try {
|
|
609
|
+
for (const levelItems of levels) {
|
|
610
|
+
if (levelItems.length === 0) continue
|
|
611
|
+
await docService.reorderDocs(levelItems)
|
|
612
|
+
}
|
|
613
|
+
await reloadDocsList()
|
|
614
|
+
if (currentPureName) {
|
|
615
|
+
const flat = flattenDocsList(docsList.value)
|
|
616
|
+
const match = flat.find(d => stripOrderPrefix(d.key) === currentPureName)
|
|
617
|
+
if (match) {
|
|
618
|
+
expandParents(docsList.value, match.key)
|
|
619
|
+
await loadDoc(match.key, { replace: true })
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return { ok: true }
|
|
623
|
+
} catch (e) {
|
|
624
|
+
console.error('重编号失败:', e)
|
|
625
|
+
return { ok: false, message: e.message }
|
|
260
626
|
}
|
|
261
627
|
}
|
|
262
628
|
|
|
629
|
+
// ===== 导出 =====
|
|
263
630
|
return {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
handleScroll,
|
|
276
|
-
scrollToHeading,
|
|
277
|
-
scrollToTop,
|
|
278
|
-
// 文档操作
|
|
279
|
-
loadDocsList,
|
|
280
|
-
loadFromUrl,
|
|
281
|
-
goHome,
|
|
282
|
-
loadDoc,
|
|
283
|
-
loadFirstDoc,
|
|
284
|
-
handleDocSelect,
|
|
285
|
-
handleContentClick,
|
|
286
|
-
handleSearchSelect,
|
|
287
|
-
// 文件夹操作
|
|
288
|
-
toggleFolder,
|
|
289
|
-
onExpandAll,
|
|
290
|
-
onCollapseAll,
|
|
291
|
-
// 导航
|
|
292
|
-
prevDoc,
|
|
293
|
-
nextDoc,
|
|
294
|
-
// 工具
|
|
631
|
+
docsList, currentDoc, currentDocTitle, showWelcome, htmlContent, tocItems,
|
|
632
|
+
editMode, rawMarkdown, currentDocFilePath, lastModified,
|
|
633
|
+
scrollProgress, showBackToTop, activeHeading,
|
|
634
|
+
handleScroll, scrollToHeading, scrollToTop,
|
|
635
|
+
loadDocsList, loadFromUrl, goHome, loadDoc, loadFirstDoc,
|
|
636
|
+
handleDocSelect, handleContentClick, handleSearchSelect,
|
|
637
|
+
toggleEditMode, reloadDocsList, reloadCurrentDoc,
|
|
638
|
+
saveDoc: saveDocContent, getCurrentDocPath,
|
|
639
|
+
toggleFolder, onExpandAll, onCollapseAll,
|
|
640
|
+
prevDoc, nextDoc,
|
|
641
|
+
createDoc, deleteDoc, renameDoc, reorderDocs,
|
|
295
642
|
docHash
|
|
296
643
|
}
|
|
297
644
|
}
|