@tnotesjs/core 0.4.1 → 0.5.0
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/dist/vitepress/config/index.js +18 -189
- package/package.json +3 -3
- package/vitepress/components/MindmapPreview/MindmapPreview.vue +189 -34
- package/vitepress/components/MindmapPreview/MindmapViewIcon.vue +41 -0
- package/vitepress/components/MindmapPreview/markdown.test.ts +67 -0
- package/vitepress/components/MindmapPreview/markdown.ts +83 -0
- package/vitepress/components/MindmapPreview/wheelInteraction.test.ts +23 -0
- package/vitepress/components/MindmapPreview/wheelInteraction.ts +7 -0
- package/vitepress/components/constants.ts +0 -5
- package/vitepress/config/dependencyOptimization.test.ts +5 -2
- package/vitepress/config/dependencyOptimization.ts +1 -0
- package/vitepress/configs/markdown.config.ts +6 -199
- package/vitepress/theme/index.ts +0 -2
- package/vitepress/components/MindmapPreview/compat.test.ts +0 -93
- package/vitepress/components/MindmapPreview/compat.ts +0 -128
- package/vitepress/components/MindmapPreview/legacyCorpus.test.ts +0 -57
|
@@ -28,7 +28,8 @@ var DEFAULT_OPTIMIZE_DEPS_INCLUDE = [
|
|
|
28
28
|
// Mermaid 11 的 ESM chunk 默认导入 CommonJS fastdom。pnpm 的严格
|
|
29
29
|
// 依赖布局下 Vite 无法从知识库根目录自动发现这条嵌套依赖,开发
|
|
30
30
|
// 模式会直接把 fastdom.js 当作 ESM 加载并导致整页白屏。
|
|
31
|
-
"@tnotesjs/core > mermaid > fastdom"
|
|
31
|
+
"@tnotesjs/core > mermaid > fastdom",
|
|
32
|
+
"@tnotesjs/core > mermaid > fastdom/extensions/fastdom-promised.js"
|
|
32
33
|
];
|
|
33
34
|
|
|
34
35
|
// vitepress/configs/constants.ts
|
|
@@ -64,45 +65,24 @@ import mila from "markdown-it-link-attributes";
|
|
|
64
65
|
import markdownItTaskLists from "markdown-it-task-lists";
|
|
65
66
|
import path from "path";
|
|
66
67
|
|
|
67
|
-
// vitepress/components/MindmapPreview/
|
|
68
|
+
// vitepress/components/MindmapPreview/markdown.ts
|
|
68
69
|
function cleanHeadingText(value) {
|
|
69
70
|
return value.trim().replace(/\s+#+\s*$/, "").trim();
|
|
70
71
|
}
|
|
71
|
-
function
|
|
72
|
-
const firstContentIndex = body.findIndex((line) => line.trim() !== "");
|
|
73
|
-
if (firstContentIndex < 0) return body;
|
|
74
|
-
const firstItem = body[firstContentIndex].match(/^[-+*]\s+(.+?)\s*$/);
|
|
75
|
-
if (!firstItem || cleanHeadingText(firstItem[1]) !== rootTitle) return body;
|
|
76
|
-
const descendants = body.slice(firstContentIndex + 1);
|
|
77
|
-
if (descendants.some((line) => line.trim() !== "" && !/^\s{2,}/.test(line))) return body;
|
|
78
|
-
return [
|
|
79
|
-
...body.slice(0, firstContentIndex),
|
|
80
|
-
...descendants.map((line) => line.replace(/^ {2}/, ""))
|
|
81
|
-
];
|
|
82
|
-
}
|
|
83
|
-
function parseMarkmapFence(openLine) {
|
|
72
|
+
function parseMindmapFence(openLine) {
|
|
84
73
|
const fenceBody = openLine.trim().replace(/^`+\s*/, "");
|
|
85
|
-
const nameMatch = fenceBody.match(/^
|
|
86
|
-
if (!nameMatch) return
|
|
87
|
-
let rest = fenceBody.slice(nameMatch[
|
|
74
|
+
const nameMatch = fenceBody.match(/^mindmap(?=\s|\[|$)/);
|
|
75
|
+
if (!nameMatch) return null;
|
|
76
|
+
let rest = fenceBody.slice(nameMatch[0].length).trim();
|
|
88
77
|
const options = {};
|
|
89
78
|
const titleMatch = rest.match(/\[([^\]]+)\]/);
|
|
90
79
|
if (titleMatch) {
|
|
91
|
-
options.title = titleMatch[1]
|
|
80
|
+
options.title = cleanHeadingText(titleMatch[1]) || void 0;
|
|
92
81
|
rest = `${rest.slice(0, titleMatch.index)} ${rest.slice((titleMatch.index ?? 0) + titleMatch[0].length)}`.trim();
|
|
93
82
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
for (const [index, token] of tokens.entries()) {
|
|
98
|
-
if (/^\d+$/.test(token) && index === 0) {
|
|
99
|
-
options.initialExpandLevel = Number(token);
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
const pair = token.match(/^([^=:\s]+)\s*(?:=|:)\s*(.+)$/);
|
|
103
|
-
if (!pair || pair[1] !== "initialExpandLevel") continue;
|
|
104
|
-
const value = pair[2].replace(/^['"]|['"]$/g, "");
|
|
105
|
-
if (/^\d+$/.test(value)) options.initialExpandLevel = Number(value);
|
|
83
|
+
if (rest && !/^\d+$/.test(rest)) return null;
|
|
84
|
+
if (rest) {
|
|
85
|
+
options.initialExpandLevel = Math.max(1, Number(rest));
|
|
106
86
|
}
|
|
107
87
|
return options;
|
|
108
88
|
}
|
|
@@ -131,30 +111,12 @@ function normalizeMindmapMarkdown(source, options = {}) {
|
|
|
131
111
|
break;
|
|
132
112
|
}
|
|
133
113
|
const rootTitle = cleanHeadingText(options.title || existingTitle || options.defaultTitle || "root") || "root";
|
|
134
|
-
const body =
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const line = lines[index];
|
|
139
|
-
const heading = line.match(/^\s{0,3}(#{2,6})\s+(.+?)\s*$/);
|
|
140
|
-
if (heading) {
|
|
141
|
-
headingDepth = heading[1].length - 2;
|
|
142
|
-
body.push(`${" ".repeat(headingDepth)}- ${cleanHeadingText(heading[2])}`);
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
const listItem = line.match(/^(\s*)([-+*])\s+(.+)$/);
|
|
146
|
-
if (listItem && headingDepth !== null) {
|
|
147
|
-
body.push(`${" ".repeat(headingDepth + 1)}${line}`);
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
body.push(line);
|
|
151
|
-
}
|
|
152
|
-
const normalizedBody = promoteLegacyRootList(body, rootTitle);
|
|
153
|
-
while (normalizedBody[0]?.trim() === "") normalizedBody.shift();
|
|
154
|
-
while (normalizedBody[normalizedBody.length - 1]?.trim() === "") normalizedBody.pop();
|
|
155
|
-
return normalizedBody.length > 0 ? `# ${rootTitle}
|
|
114
|
+
const body = lines.filter((_, index) => index !== rootIndex);
|
|
115
|
+
while (body[0]?.trim() === "") body.shift();
|
|
116
|
+
while (body[body.length - 1]?.trim() === "") body.pop();
|
|
117
|
+
return body.length > 0 ? `# ${rootTitle}
|
|
156
118
|
|
|
157
|
-
${
|
|
119
|
+
${body.join("\n")}
|
|
158
120
|
` : `# ${rootTitle}
|
|
159
121
|
`;
|
|
160
122
|
}
|
|
@@ -191,145 +153,13 @@ var simpleMermaidMarkdown = (md) => {
|
|
|
191
153
|
return fence(tokens, index, options, env, slf);
|
|
192
154
|
};
|
|
193
155
|
};
|
|
194
|
-
function configureMindmapContainer(md) {
|
|
195
|
-
md.use(markdownItContainer, "markmap", {
|
|
196
|
-
marker: "`",
|
|
197
|
-
validate(params) {
|
|
198
|
-
return (params || "").trim().startsWith("markmap");
|
|
199
|
-
},
|
|
200
|
-
render() {
|
|
201
|
-
return "";
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
md.core.ruler.after("block", "tn_replace_markmap_container", (state) => {
|
|
205
|
-
const src = state.env.source || "";
|
|
206
|
-
const lines = src.split("\n");
|
|
207
|
-
const tokens = state.tokens;
|
|
208
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
209
|
-
const t = tokens[i];
|
|
210
|
-
if (t.type === "container_markmap_open") {
|
|
211
|
-
const containerName = "markmap";
|
|
212
|
-
const closeType = "container_markmap_close";
|
|
213
|
-
let j = i + 1;
|
|
214
|
-
while (j < tokens.length && tokens[j].type !== closeType)
|
|
215
|
-
j++;
|
|
216
|
-
if (j >= tokens.length) continue;
|
|
217
|
-
const open = t;
|
|
218
|
-
const startLine = open.map ? open.map[0] + 1 : null;
|
|
219
|
-
const endLine = open.map ? open.map[1] - 1 : null;
|
|
220
|
-
const params = {};
|
|
221
|
-
let explicitTitle;
|
|
222
|
-
if (open.map && typeof open.map[0] === "number") {
|
|
223
|
-
const openLine = (lines[open.map[0]] || "").trim();
|
|
224
|
-
const fenceOptions = parseMarkmapFence(openLine);
|
|
225
|
-
explicitTitle = fenceOptions.title;
|
|
226
|
-
let paramPart = "";
|
|
227
|
-
const braceMatch = openLine.match(/\{([^}]*)\}/);
|
|
228
|
-
if (braceMatch) {
|
|
229
|
-
paramPart = braceMatch[1].trim();
|
|
230
|
-
} else {
|
|
231
|
-
const after = openLine.replace(/^`+\s*/, "");
|
|
232
|
-
if (after.startsWith(containerName)) {
|
|
233
|
-
paramPart = after.slice(containerName.length).trim();
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
if (fenceOptions.initialExpandLevel !== void 0) {
|
|
237
|
-
params.initialExpandLevel = fenceOptions.initialExpandLevel;
|
|
238
|
-
}
|
|
239
|
-
if (paramPart) {
|
|
240
|
-
const tokenArr = paramPart.match(/"[^"]*"|'[^']*'|\S+/g) || [];
|
|
241
|
-
let startIdx = 0;
|
|
242
|
-
if (tokenArr.length > 0 && /^\d+$/.test(tokenArr[0])) {
|
|
243
|
-
params.initialExpandLevel = Number(tokenArr[0]);
|
|
244
|
-
startIdx = 1;
|
|
245
|
-
}
|
|
246
|
-
for (let k = startIdx; k < tokenArr.length; k++) {
|
|
247
|
-
const pair = tokenArr[k];
|
|
248
|
-
if (!pair) continue;
|
|
249
|
-
const m = pair.match(/^([^=:\s]+)\s*(=|:)\s*(.+)$/);
|
|
250
|
-
if (m) {
|
|
251
|
-
const key = m[1];
|
|
252
|
-
let val = m[3];
|
|
253
|
-
if (/^".*"$/.test(val) && val.length >= 2 || /^'.*'$/.test(val) && val.length >= 2) {
|
|
254
|
-
val = val.slice(1, -1);
|
|
255
|
-
} else if (/^\d+$/.test(val)) {
|
|
256
|
-
val = String(Number(val));
|
|
257
|
-
}
|
|
258
|
-
params[key] = val;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
let content = "";
|
|
264
|
-
if (startLine !== null && endLine !== null) {
|
|
265
|
-
for (let k = startLine; k <= endLine && k < lines.length; k++) {
|
|
266
|
-
content += lines[k] + "\n";
|
|
267
|
-
}
|
|
268
|
-
} else {
|
|
269
|
-
for (let k = i + 1; k < j; k++) {
|
|
270
|
-
content += tokens[k].content || "";
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
const firstNonEmptyLine = (content || "").split("\n").find((ln) => ln.trim() !== "") || "";
|
|
274
|
-
const reference = parseMindmapReference(firstNonEmptyLine);
|
|
275
|
-
let referencedTitle;
|
|
276
|
-
if (reference) {
|
|
277
|
-
const refRaw = reference.path;
|
|
278
|
-
referencedTitle = reference.title;
|
|
279
|
-
try {
|
|
280
|
-
const env = state.env || {};
|
|
281
|
-
const possibleRel = env.relativePath || env.path || env.filePath || env.file || "";
|
|
282
|
-
let refFullPath = refRaw;
|
|
283
|
-
if (!path.isAbsolute(refRaw)) {
|
|
284
|
-
if (possibleRel) {
|
|
285
|
-
const currentDir = path.dirname(possibleRel);
|
|
286
|
-
refFullPath = path.resolve(process.cwd(), currentDir, refRaw);
|
|
287
|
-
} else {
|
|
288
|
-
refFullPath = path.resolve(process.cwd(), refRaw);
|
|
289
|
-
}
|
|
290
|
-
} else {
|
|
291
|
-
refFullPath = refRaw;
|
|
292
|
-
}
|
|
293
|
-
const fileContent = fs.readFileSync(refFullPath, "utf-8");
|
|
294
|
-
content = fileContent;
|
|
295
|
-
} catch (err) {
|
|
296
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
297
|
-
content = `- Failed to load referenced file: ${esc(String(refRaw))}
|
|
298
|
-
- Error: ${esc(errorMsg)}`;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
content = normalizeMindmapMarkdown(content, {
|
|
302
|
-
title: explicitTitle || referencedTitle
|
|
303
|
-
});
|
|
304
|
-
const encodedContent = encodeURIComponent(content.trim());
|
|
305
|
-
let propsStr = `content="${encodedContent}"`;
|
|
306
|
-
for (const [k, v] of Object.entries(params)) {
|
|
307
|
-
if (typeof v === "number" || /^\d+$/.test(String(v))) {
|
|
308
|
-
propsStr += ` :${k}="${v}"`;
|
|
309
|
-
} else {
|
|
310
|
-
const safe = String(v).replace(/"/g, """);
|
|
311
|
-
propsStr += ` ${k}="${safe}"`;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
const html = `<MindmapPreview ${propsStr}></MindmapPreview>
|
|
315
|
-
`;
|
|
316
|
-
const htmlToken = new state.Token("html_block", "", 0);
|
|
317
|
-
htmlToken.content = html;
|
|
318
|
-
tokens.splice(i, j - i + 1, htmlToken);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
return true;
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
156
|
function configureMindmapFence(md) {
|
|
325
157
|
const fence = md.renderer.rules.fence ? md.renderer.rules.fence.bind(md.renderer.rules) : () => "";
|
|
326
158
|
md.renderer.rules.fence = (tokens, index, options, env, slf) => {
|
|
327
159
|
const token = tokens[index];
|
|
328
160
|
const info = token.info.trim();
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
const fenceOptions = parseMarkmapFence(info);
|
|
161
|
+
const fenceOptions = parseMindmapFence(info);
|
|
162
|
+
if (!fenceOptions) return fence(tokens, index, options, env, slf);
|
|
333
163
|
let content = token.content;
|
|
334
164
|
const firstNonEmptyLine = content.split("\n").find((line) => line.trim()) ?? "";
|
|
335
165
|
const reference = parseMindmapReference(firstNonEmptyLine);
|
|
@@ -423,7 +253,6 @@ function getMarkdownConfig() {
|
|
|
423
253
|
return true;
|
|
424
254
|
});
|
|
425
255
|
simpleMermaidMarkdown(md);
|
|
426
|
-
configureMindmapContainer(md);
|
|
427
256
|
configureMindmapFence(md);
|
|
428
257
|
md.use(markdownItTaskLists);
|
|
429
258
|
md.use(mila, {
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tnotesjs/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "TNotes 知识库核心框架 —— 基于 VitePress 的笔记管理系统",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.17.1",
|
|
7
7
|
"bin": {
|
|
8
|
-
"tnotes": "
|
|
8
|
+
"tnotes": "dist/cli/index.js"
|
|
9
9
|
},
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"license": "MIT",
|
|
86
86
|
"repository": {
|
|
87
87
|
"type": "git",
|
|
88
|
-
"url": "https://github.com/tnotesjs/core"
|
|
88
|
+
"url": "git+https://github.com/tnotesjs/core.git"
|
|
89
89
|
},
|
|
90
90
|
"keywords": [
|
|
91
91
|
"tnotes",
|
|
@@ -3,9 +3,12 @@ import { CanvasViewer, MindmapSession } from '@tnotesjs/mindmap-core'
|
|
|
3
3
|
import { onContentUpdated, useData } from 'vitepress'
|
|
4
4
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
|
|
5
5
|
|
|
6
|
-
import { normalizeMindmapMarkdown } from './compat'
|
|
7
6
|
import { applyInitialExpandLevel } from './expandLevel'
|
|
7
|
+
import { normalizeMindmapMarkdown } from './markdown'
|
|
8
8
|
import MindmapOutlineNode from './MindmapOutlineNode.vue'
|
|
9
|
+
import MindmapViewIcon from './MindmapViewIcon.vue'
|
|
10
|
+
import { gateMindmapWheel } from './wheelInteraction'
|
|
11
|
+
import { icon__fullscreen, icon__fullscreen_exit, icon__zoom_fit } from '../../assets/icons'
|
|
9
12
|
|
|
10
13
|
type PreviewView = 'mindmap' | 'outline' | 'source'
|
|
11
14
|
|
|
@@ -19,12 +22,21 @@ const props = withDefaults(defineProps<{
|
|
|
19
22
|
|
|
20
23
|
const { isDark } = useData()
|
|
21
24
|
const activeView = ref<PreviewView>('mindmap')
|
|
25
|
+
const previewRoot = ref<HTMLElement | null>(null)
|
|
22
26
|
const canvasHost = ref<HTMLElement | null>(null)
|
|
23
27
|
const session = shallowRef<MindmapSession | null>(null)
|
|
24
28
|
const renderVersion = ref(0)
|
|
29
|
+
const isFullscreen = ref(false)
|
|
30
|
+
const isCanvasActive = ref(false)
|
|
25
31
|
let viewer: CanvasViewer | null = null
|
|
26
32
|
let mounted = false
|
|
27
33
|
|
|
34
|
+
const viewOptions = [
|
|
35
|
+
{ value: 'mindmap', label: '脑图' },
|
|
36
|
+
{ value: 'outline', label: '大纲' },
|
|
37
|
+
{ value: 'source', label: '源码' },
|
|
38
|
+
] as const
|
|
39
|
+
|
|
28
40
|
function decodeContent(value: string): string {
|
|
29
41
|
try {
|
|
30
42
|
return decodeURIComponent(value)
|
|
@@ -61,6 +73,7 @@ function rebuildSession(): void {
|
|
|
61
73
|
|
|
62
74
|
function setView(view: PreviewView): void {
|
|
63
75
|
activeView.value = view
|
|
76
|
+
if (view !== 'mindmap') isCanvasActive.value = false
|
|
64
77
|
if (view === 'mindmap') void nextTick(() => viewer?.zoomToFit())
|
|
65
78
|
}
|
|
66
79
|
|
|
@@ -72,11 +85,51 @@ function exitFocusTo(index: number): void {
|
|
|
72
85
|
session.value?.exitFocusTo(index)
|
|
73
86
|
}
|
|
74
87
|
|
|
88
|
+
async function toggleFullscreen(): Promise<void> {
|
|
89
|
+
const root = previewRoot.value
|
|
90
|
+
if (!root) return
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
if (document.fullscreenElement === root) await document.exitFullscreen()
|
|
94
|
+
else await root.requestFullscreen()
|
|
95
|
+
} catch {
|
|
96
|
+
// Fullscreen can be rejected by the browser or an embedded document policy.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function handleFullscreenChange(): void {
|
|
101
|
+
isFullscreen.value = document.fullscreenElement === previewRoot.value
|
|
102
|
+
if (activeView.value === 'mindmap') void nextTick(() => viewer?.zoomToFit())
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function activateCanvas(): void {
|
|
106
|
+
isCanvasActive.value = true
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function handleCanvasWheelCapture(event: WheelEvent): void {
|
|
110
|
+
gateMindmapWheel(event, isCanvasActive.value)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function handleDocumentPointerDown(event: PointerEvent): void {
|
|
114
|
+
if (event.target instanceof Node && !previewRoot.value?.contains(event.target)) {
|
|
115
|
+
isCanvasActive.value = false
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function handleDocumentKeydown(event: KeyboardEvent): void {
|
|
120
|
+
if (event.key !== 'Escape' || !isCanvasActive.value) return
|
|
121
|
+
isCanvasActive.value = false
|
|
122
|
+
canvasHost.value?.blur()
|
|
123
|
+
}
|
|
124
|
+
|
|
75
125
|
watch([normalizedContent, () => props.initialExpandLevel], rebuildSession, { immediate: true })
|
|
76
126
|
watch(isDark, (dark) => viewer?.setTheme(dark ? 'dark' : 'light'))
|
|
77
127
|
|
|
78
128
|
onMounted(() => {
|
|
79
129
|
mounted = true
|
|
130
|
+
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
|
131
|
+
document.addEventListener('pointerdown', handleDocumentPointerDown, true)
|
|
132
|
+
document.addEventListener('keydown', handleDocumentKeydown, true)
|
|
80
133
|
createViewer()
|
|
81
134
|
})
|
|
82
135
|
|
|
@@ -86,29 +139,58 @@ onContentUpdated(() => {
|
|
|
86
139
|
|
|
87
140
|
onBeforeUnmount(() => {
|
|
88
141
|
mounted = false
|
|
142
|
+
document.removeEventListener('fullscreenchange', handleFullscreenChange)
|
|
143
|
+
document.removeEventListener('pointerdown', handleDocumentPointerDown, true)
|
|
144
|
+
document.removeEventListener('keydown', handleDocumentKeydown, true)
|
|
89
145
|
viewer?.destroy()
|
|
90
146
|
viewer = null
|
|
91
147
|
})
|
|
92
148
|
</script>
|
|
93
149
|
|
|
94
150
|
<template>
|
|
95
|
-
<section
|
|
96
|
-
|
|
151
|
+
<section
|
|
152
|
+
ref="previewRoot"
|
|
153
|
+
class="mindmap-preview"
|
|
154
|
+
:class="{ 'is-dark': isDark, 'is-fullscreen': isFullscreen }"
|
|
155
|
+
:data-version="renderVersion"
|
|
156
|
+
>
|
|
157
|
+
<div class="mindmap-preview-actions">
|
|
97
158
|
<nav class="mindmap-preview-tabs" aria-label="脑图预览视图">
|
|
98
159
|
<button
|
|
99
|
-
v-for="item in
|
|
100
|
-
:key="item
|
|
160
|
+
v-for="item in viewOptions"
|
|
161
|
+
:key="item.value"
|
|
101
162
|
type="button"
|
|
102
|
-
|
|
103
|
-
|
|
163
|
+
class="mindmap-preview-action"
|
|
164
|
+
:class="{ 'is-active': activeView === item.value }"
|
|
165
|
+
:aria-label="item.label"
|
|
166
|
+
:aria-pressed="activeView === item.value"
|
|
167
|
+
:title="item.label"
|
|
168
|
+
@click="setView(item.value)"
|
|
104
169
|
>
|
|
105
|
-
|
|
170
|
+
<MindmapViewIcon :view="item.value" />
|
|
106
171
|
</button>
|
|
107
172
|
</nav>
|
|
108
|
-
<
|
|
109
|
-
|
|
173
|
+
<span class="mindmap-preview-action-divider" aria-hidden="true" />
|
|
174
|
+
<button
|
|
175
|
+
v-if="activeView === 'mindmap'"
|
|
176
|
+
type="button"
|
|
177
|
+
class="mindmap-preview-action"
|
|
178
|
+
aria-label="适应视口"
|
|
179
|
+
title="适应视口"
|
|
180
|
+
@click="viewer?.zoomToFit()"
|
|
181
|
+
>
|
|
182
|
+
<img :src="icon__zoom_fit" alt="" />
|
|
110
183
|
</button>
|
|
111
|
-
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
class="mindmap-preview-action"
|
|
187
|
+
:aria-label="isFullscreen ? '退出全屏' : '全屏查看'"
|
|
188
|
+
:title="isFullscreen ? '退出全屏' : '全屏查看'"
|
|
189
|
+
@click="toggleFullscreen"
|
|
190
|
+
>
|
|
191
|
+
<img :src="isFullscreen ? icon__fullscreen_exit : icon__fullscreen" alt="" />
|
|
192
|
+
</button>
|
|
193
|
+
</div>
|
|
112
194
|
|
|
113
195
|
<nav v-if="session && session.focusPath.length > 0" class="mindmap-focus-path" aria-label="当前主题路径">
|
|
114
196
|
<button type="button" @click="exitFocusTo(0)">全部</button>
|
|
@@ -118,7 +200,14 @@ onBeforeUnmount(() => {
|
|
|
118
200
|
</template>
|
|
119
201
|
</nav>
|
|
120
202
|
|
|
121
|
-
<div
|
|
203
|
+
<div
|
|
204
|
+
v-show="activeView === 'mindmap'"
|
|
205
|
+
ref="canvasHost"
|
|
206
|
+
class="mindmap-canvas-host"
|
|
207
|
+
:class="{ 'is-interaction-active': isCanvasActive }"
|
|
208
|
+
@pointerdown.capture="activateCanvas"
|
|
209
|
+
@wheel.capture="handleCanvasWheelCapture"
|
|
210
|
+
/>
|
|
122
211
|
|
|
123
212
|
<div v-if="activeView === 'outline' && session" class="mindmap-outline" :data-version="renderVersion">
|
|
124
213
|
<ul class="mindmap-outline-root">
|
|
@@ -139,6 +228,7 @@ onBeforeUnmount(() => {
|
|
|
139
228
|
.mindmap-preview {
|
|
140
229
|
--mindmap-panel: var(--vp-c-bg-soft);
|
|
141
230
|
--mindmap-border: var(--vp-c-divider);
|
|
231
|
+
position: relative;
|
|
142
232
|
margin: 1.5rem 0;
|
|
143
233
|
overflow: hidden;
|
|
144
234
|
border: 1px solid var(--mindmap-border);
|
|
@@ -146,36 +236,75 @@ onBeforeUnmount(() => {
|
|
|
146
236
|
background: var(--vp-c-bg);
|
|
147
237
|
}
|
|
148
238
|
|
|
149
|
-
.mindmap-preview-
|
|
239
|
+
.mindmap-preview-actions {
|
|
240
|
+
position: absolute;
|
|
241
|
+
top: 8px;
|
|
242
|
+
right: 8px;
|
|
243
|
+
z-index: 20;
|
|
150
244
|
display: flex;
|
|
151
245
|
align-items: center;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
border-
|
|
156
|
-
background: var(--
|
|
246
|
+
gap: 4px;
|
|
247
|
+
padding: 2px;
|
|
248
|
+
border: .1px solid var(--vp-c-divider);
|
|
249
|
+
border-radius: 7px;
|
|
250
|
+
background-color: color-mix(in srgb, var(--vp-code-block-bg) 92%, transparent);
|
|
251
|
+
box-shadow: var(--vp-shadow-1);
|
|
252
|
+
opacity: 0;
|
|
253
|
+
pointer-events: none;
|
|
254
|
+
transition: opacity .2s;
|
|
157
255
|
}
|
|
158
256
|
|
|
159
257
|
.mindmap-preview-tabs {
|
|
160
258
|
display: flex;
|
|
161
|
-
gap:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
259
|
+
gap: 4px;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.mindmap-preview:hover > .mindmap-preview-actions,
|
|
263
|
+
.mindmap-preview-actions:focus-within {
|
|
264
|
+
opacity: 1;
|
|
265
|
+
pointer-events: auto;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.mindmap-preview-action {
|
|
269
|
+
display: inline-flex;
|
|
270
|
+
align-items: center;
|
|
271
|
+
justify-content: center;
|
|
272
|
+
width: 32px;
|
|
273
|
+
height: 32px;
|
|
274
|
+
padding: 0;
|
|
275
|
+
border: 0;
|
|
276
|
+
border-radius: 6px;
|
|
277
|
+
background: transparent;
|
|
278
|
+
color: var(--vp-c-brand-1);
|
|
279
|
+
cursor: pointer;
|
|
280
|
+
transition: background-color .2s, transform .2s;
|
|
281
|
+
|
|
282
|
+
svg,
|
|
283
|
+
img {
|
|
284
|
+
width: 18px;
|
|
285
|
+
height: 18px;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
&:hover,
|
|
289
|
+
&.is-active {
|
|
290
|
+
background-color: var(--vp-c-default-soft);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
&.is-active {
|
|
294
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--vp-c-brand-1) 35%, transparent);
|
|
175
295
|
}
|
|
296
|
+
|
|
297
|
+
&:hover { transform: scale(1.05); }
|
|
298
|
+
&:active { transform: scale(.95); }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.mindmap-preview-action-divider {
|
|
302
|
+
width: 1px;
|
|
303
|
+
height: 20px;
|
|
304
|
+
margin: 0 1px;
|
|
305
|
+
background: var(--vp-c-divider);
|
|
176
306
|
}
|
|
177
307
|
|
|
178
|
-
.mindmap-fit,
|
|
179
308
|
.mindmap-focus-path button {
|
|
180
309
|
color: var(--vp-c-text-2);
|
|
181
310
|
font-size: 12px;
|
|
@@ -202,6 +331,10 @@ onBeforeUnmount(() => {
|
|
|
202
331
|
background: var(--vp-c-bg);
|
|
203
332
|
touch-action: none;
|
|
204
333
|
user-select: none;
|
|
334
|
+
|
|
335
|
+
&.is-interaction-active {
|
|
336
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--vp-c-brand-1) 38%, transparent);
|
|
337
|
+
}
|
|
205
338
|
}
|
|
206
339
|
|
|
207
340
|
.mindmap-canvas-host:deep(.mm-canvas),
|
|
@@ -279,9 +412,31 @@ onBeforeUnmount(() => {
|
|
|
279
412
|
white-space: pre;
|
|
280
413
|
}
|
|
281
414
|
|
|
415
|
+
.mindmap-preview:fullscreen {
|
|
416
|
+
display: flex;
|
|
417
|
+
width: 100%;
|
|
418
|
+
height: 100%;
|
|
419
|
+
margin: 0;
|
|
420
|
+
border: 0;
|
|
421
|
+
border-radius: 0;
|
|
422
|
+
background: var(--vp-c-bg);
|
|
423
|
+
flex-direction: column;
|
|
424
|
+
|
|
425
|
+
.mindmap-canvas-host,
|
|
426
|
+
.mindmap-outline,
|
|
427
|
+
.mindmap-source {
|
|
428
|
+
flex: 1 1 auto;
|
|
429
|
+
max-height: none;
|
|
430
|
+
min-height: 0;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.mindmap-canvas-host { height: auto; }
|
|
434
|
+
}
|
|
435
|
+
|
|
282
436
|
@media (max-width: 768px) {
|
|
283
437
|
.mindmap-canvas-host { height: 360px; }
|
|
284
|
-
.mindmap-preview-
|
|
438
|
+
.mindmap-preview-actions { top: 6px; right: 6px; }
|
|
439
|
+
.mindmap-preview-action { width: 28px; height: 28px; }
|
|
285
440
|
.mindmap-outline { padding-inline: 12px; }
|
|
286
441
|
}
|
|
287
442
|
</style>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{
|
|
3
|
+
view: 'mindmap' | 'outline' | 'source'
|
|
4
|
+
}>()
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<svg v-if="view === 'mindmap'" aria-hidden="true" viewBox="0 0 48 48">
|
|
9
|
+
<path d="M0 0h48v48H0z" fill="none" />
|
|
10
|
+
<path
|
|
11
|
+
d="M26 24h16M26 38h16M26 10h16M18 24H6h4m8 14c-6-2-2-14-8-14m8-14c-6 2-2 14-8 14"
|
|
12
|
+
fill="none"
|
|
13
|
+
stroke="#636cff"
|
|
14
|
+
stroke-linecap="round"
|
|
15
|
+
stroke-linejoin="round"
|
|
16
|
+
stroke-width="4"
|
|
17
|
+
/>
|
|
18
|
+
</svg>
|
|
19
|
+
<svg v-else-if="view === 'outline'" aria-hidden="true" viewBox="0 0 48 48">
|
|
20
|
+
<path d="M0 0h48v48H0z" fill="none" />
|
|
21
|
+
<path
|
|
22
|
+
d="M26 24h18m-30 0h4m0 14h26M6 38h4m8-28h26M6 10h4"
|
|
23
|
+
fill="none"
|
|
24
|
+
stroke="#636cff"
|
|
25
|
+
stroke-linecap="round"
|
|
26
|
+
stroke-linejoin="round"
|
|
27
|
+
stroke-width="4"
|
|
28
|
+
/>
|
|
29
|
+
</svg>
|
|
30
|
+
<svg v-else aria-hidden="true" viewBox="0 0 24 24">
|
|
31
|
+
<path d="M0 0h24v24H0z" fill="none" />
|
|
32
|
+
<path
|
|
33
|
+
d="m17 8 1.84 1.85c.773.778 1.16 1.167 1.16 1.65s-.387.872-1.16 1.65L17 15M7 8 5.16 9.85C4.387 10.628 4 11.017 4 11.5s.387.872 1.16 1.65L7 15m7.5-11-5 16"
|
|
34
|
+
fill="none"
|
|
35
|
+
stroke="#636cff"
|
|
36
|
+
stroke-linecap="round"
|
|
37
|
+
stroke-linejoin="round"
|
|
38
|
+
stroke-width="1.5"
|
|
39
|
+
/>
|
|
40
|
+
</svg>
|
|
41
|
+
</template>
|