@tnotesjs/core 0.4.2 → 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 +16 -188
- 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/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
|
@@ -65,45 +65,24 @@ import mila from "markdown-it-link-attributes";
|
|
|
65
65
|
import markdownItTaskLists from "markdown-it-task-lists";
|
|
66
66
|
import path from "path";
|
|
67
67
|
|
|
68
|
-
// vitepress/components/MindmapPreview/
|
|
68
|
+
// vitepress/components/MindmapPreview/markdown.ts
|
|
69
69
|
function cleanHeadingText(value) {
|
|
70
70
|
return value.trim().replace(/\s+#+\s*$/, "").trim();
|
|
71
71
|
}
|
|
72
|
-
function
|
|
73
|
-
const firstContentIndex = body.findIndex((line) => line.trim() !== "");
|
|
74
|
-
if (firstContentIndex < 0) return body;
|
|
75
|
-
const firstItem = body[firstContentIndex].match(/^[-+*]\s+(.+?)\s*$/);
|
|
76
|
-
if (!firstItem || cleanHeadingText(firstItem[1]) !== rootTitle) return body;
|
|
77
|
-
const descendants = body.slice(firstContentIndex + 1);
|
|
78
|
-
if (descendants.some((line) => line.trim() !== "" && !/^\s{2,}/.test(line))) return body;
|
|
79
|
-
return [
|
|
80
|
-
...body.slice(0, firstContentIndex),
|
|
81
|
-
...descendants.map((line) => line.replace(/^ {2}/, ""))
|
|
82
|
-
];
|
|
83
|
-
}
|
|
84
|
-
function parseMarkmapFence(openLine) {
|
|
72
|
+
function parseMindmapFence(openLine) {
|
|
85
73
|
const fenceBody = openLine.trim().replace(/^`+\s*/, "");
|
|
86
|
-
const nameMatch = fenceBody.match(/^
|
|
87
|
-
if (!nameMatch) return
|
|
88
|
-
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();
|
|
89
77
|
const options = {};
|
|
90
78
|
const titleMatch = rest.match(/\[([^\]]+)\]/);
|
|
91
79
|
if (titleMatch) {
|
|
92
|
-
options.title = titleMatch[1]
|
|
80
|
+
options.title = cleanHeadingText(titleMatch[1]) || void 0;
|
|
93
81
|
rest = `${rest.slice(0, titleMatch.index)} ${rest.slice((titleMatch.index ?? 0) + titleMatch[0].length)}`.trim();
|
|
94
82
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
for (const [index, token] of tokens.entries()) {
|
|
99
|
-
if (/^\d+$/.test(token) && index === 0) {
|
|
100
|
-
options.initialExpandLevel = Number(token);
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
const pair = token.match(/^([^=:\s]+)\s*(?:=|:)\s*(.+)$/);
|
|
104
|
-
if (!pair || pair[1] !== "initialExpandLevel") continue;
|
|
105
|
-
const value = pair[2].replace(/^['"]|['"]$/g, "");
|
|
106
|
-
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));
|
|
107
86
|
}
|
|
108
87
|
return options;
|
|
109
88
|
}
|
|
@@ -132,30 +111,12 @@ function normalizeMindmapMarkdown(source, options = {}) {
|
|
|
132
111
|
break;
|
|
133
112
|
}
|
|
134
113
|
const rootTitle = cleanHeadingText(options.title || existingTitle || options.defaultTitle || "root") || "root";
|
|
135
|
-
const body =
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const line = lines[index];
|
|
140
|
-
const heading = line.match(/^\s{0,3}(#{2,6})\s+(.+?)\s*$/);
|
|
141
|
-
if (heading) {
|
|
142
|
-
headingDepth = heading[1].length - 2;
|
|
143
|
-
body.push(`${" ".repeat(headingDepth)}- ${cleanHeadingText(heading[2])}`);
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
const listItem = line.match(/^(\s*)([-+*])\s+(.+)$/);
|
|
147
|
-
if (listItem && headingDepth !== null) {
|
|
148
|
-
body.push(`${" ".repeat(headingDepth + 1)}${line}`);
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
body.push(line);
|
|
152
|
-
}
|
|
153
|
-
const normalizedBody = promoteLegacyRootList(body, rootTitle);
|
|
154
|
-
while (normalizedBody[0]?.trim() === "") normalizedBody.shift();
|
|
155
|
-
while (normalizedBody[normalizedBody.length - 1]?.trim() === "") normalizedBody.pop();
|
|
156
|
-
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}
|
|
157
118
|
|
|
158
|
-
${
|
|
119
|
+
${body.join("\n")}
|
|
159
120
|
` : `# ${rootTitle}
|
|
160
121
|
`;
|
|
161
122
|
}
|
|
@@ -192,145 +153,13 @@ var simpleMermaidMarkdown = (md) => {
|
|
|
192
153
|
return fence(tokens, index, options, env, slf);
|
|
193
154
|
};
|
|
194
155
|
};
|
|
195
|
-
function configureMindmapContainer(md) {
|
|
196
|
-
md.use(markdownItContainer, "markmap", {
|
|
197
|
-
marker: "`",
|
|
198
|
-
validate(params) {
|
|
199
|
-
return (params || "").trim().startsWith("markmap");
|
|
200
|
-
},
|
|
201
|
-
render() {
|
|
202
|
-
return "";
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
md.core.ruler.after("block", "tn_replace_markmap_container", (state) => {
|
|
206
|
-
const src = state.env.source || "";
|
|
207
|
-
const lines = src.split("\n");
|
|
208
|
-
const tokens = state.tokens;
|
|
209
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
210
|
-
const t = tokens[i];
|
|
211
|
-
if (t.type === "container_markmap_open") {
|
|
212
|
-
const containerName = "markmap";
|
|
213
|
-
const closeType = "container_markmap_close";
|
|
214
|
-
let j = i + 1;
|
|
215
|
-
while (j < tokens.length && tokens[j].type !== closeType)
|
|
216
|
-
j++;
|
|
217
|
-
if (j >= tokens.length) continue;
|
|
218
|
-
const open = t;
|
|
219
|
-
const startLine = open.map ? open.map[0] + 1 : null;
|
|
220
|
-
const endLine = open.map ? open.map[1] - 1 : null;
|
|
221
|
-
const params = {};
|
|
222
|
-
let explicitTitle;
|
|
223
|
-
if (open.map && typeof open.map[0] === "number") {
|
|
224
|
-
const openLine = (lines[open.map[0]] || "").trim();
|
|
225
|
-
const fenceOptions = parseMarkmapFence(openLine);
|
|
226
|
-
explicitTitle = fenceOptions.title;
|
|
227
|
-
let paramPart = "";
|
|
228
|
-
const braceMatch = openLine.match(/\{([^}]*)\}/);
|
|
229
|
-
if (braceMatch) {
|
|
230
|
-
paramPart = braceMatch[1].trim();
|
|
231
|
-
} else {
|
|
232
|
-
const after = openLine.replace(/^`+\s*/, "");
|
|
233
|
-
if (after.startsWith(containerName)) {
|
|
234
|
-
paramPart = after.slice(containerName.length).trim();
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
if (fenceOptions.initialExpandLevel !== void 0) {
|
|
238
|
-
params.initialExpandLevel = fenceOptions.initialExpandLevel;
|
|
239
|
-
}
|
|
240
|
-
if (paramPart) {
|
|
241
|
-
const tokenArr = paramPart.match(/"[^"]*"|'[^']*'|\S+/g) || [];
|
|
242
|
-
let startIdx = 0;
|
|
243
|
-
if (tokenArr.length > 0 && /^\d+$/.test(tokenArr[0])) {
|
|
244
|
-
params.initialExpandLevel = Number(tokenArr[0]);
|
|
245
|
-
startIdx = 1;
|
|
246
|
-
}
|
|
247
|
-
for (let k = startIdx; k < tokenArr.length; k++) {
|
|
248
|
-
const pair = tokenArr[k];
|
|
249
|
-
if (!pair) continue;
|
|
250
|
-
const m = pair.match(/^([^=:\s]+)\s*(=|:)\s*(.+)$/);
|
|
251
|
-
if (m) {
|
|
252
|
-
const key = m[1];
|
|
253
|
-
let val = m[3];
|
|
254
|
-
if (/^".*"$/.test(val) && val.length >= 2 || /^'.*'$/.test(val) && val.length >= 2) {
|
|
255
|
-
val = val.slice(1, -1);
|
|
256
|
-
} else if (/^\d+$/.test(val)) {
|
|
257
|
-
val = String(Number(val));
|
|
258
|
-
}
|
|
259
|
-
params[key] = val;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
let content = "";
|
|
265
|
-
if (startLine !== null && endLine !== null) {
|
|
266
|
-
for (let k = startLine; k <= endLine && k < lines.length; k++) {
|
|
267
|
-
content += lines[k] + "\n";
|
|
268
|
-
}
|
|
269
|
-
} else {
|
|
270
|
-
for (let k = i + 1; k < j; k++) {
|
|
271
|
-
content += tokens[k].content || "";
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
const firstNonEmptyLine = (content || "").split("\n").find((ln) => ln.trim() !== "") || "";
|
|
275
|
-
const reference = parseMindmapReference(firstNonEmptyLine);
|
|
276
|
-
let referencedTitle;
|
|
277
|
-
if (reference) {
|
|
278
|
-
const refRaw = reference.path;
|
|
279
|
-
referencedTitle = reference.title;
|
|
280
|
-
try {
|
|
281
|
-
const env = state.env || {};
|
|
282
|
-
const possibleRel = env.relativePath || env.path || env.filePath || env.file || "";
|
|
283
|
-
let refFullPath = refRaw;
|
|
284
|
-
if (!path.isAbsolute(refRaw)) {
|
|
285
|
-
if (possibleRel) {
|
|
286
|
-
const currentDir = path.dirname(possibleRel);
|
|
287
|
-
refFullPath = path.resolve(process.cwd(), currentDir, refRaw);
|
|
288
|
-
} else {
|
|
289
|
-
refFullPath = path.resolve(process.cwd(), refRaw);
|
|
290
|
-
}
|
|
291
|
-
} else {
|
|
292
|
-
refFullPath = refRaw;
|
|
293
|
-
}
|
|
294
|
-
const fileContent = fs.readFileSync(refFullPath, "utf-8");
|
|
295
|
-
content = fileContent;
|
|
296
|
-
} catch (err) {
|
|
297
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
298
|
-
content = `- Failed to load referenced file: ${esc(String(refRaw))}
|
|
299
|
-
- Error: ${esc(errorMsg)}`;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
content = normalizeMindmapMarkdown(content, {
|
|
303
|
-
title: explicitTitle || referencedTitle
|
|
304
|
-
});
|
|
305
|
-
const encodedContent = encodeURIComponent(content.trim());
|
|
306
|
-
let propsStr = `content="${encodedContent}"`;
|
|
307
|
-
for (const [k, v] of Object.entries(params)) {
|
|
308
|
-
if (typeof v === "number" || /^\d+$/.test(String(v))) {
|
|
309
|
-
propsStr += ` :${k}="${v}"`;
|
|
310
|
-
} else {
|
|
311
|
-
const safe = String(v).replace(/"/g, """);
|
|
312
|
-
propsStr += ` ${k}="${safe}"`;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
const html = `<MindmapPreview ${propsStr}></MindmapPreview>
|
|
316
|
-
`;
|
|
317
|
-
const htmlToken = new state.Token("html_block", "", 0);
|
|
318
|
-
htmlToken.content = html;
|
|
319
|
-
tokens.splice(i, j - i + 1, htmlToken);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
return true;
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
156
|
function configureMindmapFence(md) {
|
|
326
157
|
const fence = md.renderer.rules.fence ? md.renderer.rules.fence.bind(md.renderer.rules) : () => "";
|
|
327
158
|
md.renderer.rules.fence = (tokens, index, options, env, slf) => {
|
|
328
159
|
const token = tokens[index];
|
|
329
160
|
const info = token.info.trim();
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
const fenceOptions = parseMarkmapFence(info);
|
|
161
|
+
const fenceOptions = parseMindmapFence(info);
|
|
162
|
+
if (!fenceOptions) return fence(tokens, index, options, env, slf);
|
|
334
163
|
let content = token.content;
|
|
335
164
|
const firstNonEmptyLine = content.split("\n").find((line) => line.trim()) ?? "";
|
|
336
165
|
const reference = parseMindmapReference(firstNonEmptyLine);
|
|
@@ -424,7 +253,6 @@ function getMarkdownConfig() {
|
|
|
424
253
|
return true;
|
|
425
254
|
});
|
|
426
255
|
simpleMermaidMarkdown(md);
|
|
427
|
-
configureMindmapContainer(md);
|
|
428
256
|
configureMindmapFence(md);
|
|
429
257
|
md.use(markdownItTaskLists);
|
|
430
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>
|