@tnotesjs/ui 0.3.2 → 0.4.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/README.md +0 -32
- package/dist/components/Mindmap/markdown.d.ts +7 -2
- package/dist/components/Mindmap/markdown.js +17 -3
- package/package.json +5 -9
- package/src/components/Mindmap/Mindmap.vue +238 -48
- package/src/components/Mindmap/editor/MindmapView.vue +91 -2
- package/src/components/Mindmap/editor/OutlineView.vue +5 -1
- package/src/components/Mindmap/editor/RichInlineEditor.vue +1 -2
- package/src/components/Mindmap/editor/SelectionToolbar.vue +38 -22
- package/src/components/Mindmap/expandLevel.ts +40 -1
- package/src/components/Mindmap/fullscreenFlag.ts +40 -0
- package/src/components/Mindmap/markdown.ts +19 -4
- package/src/index.ts +7 -1
- package/src/styles/code.css +1 -1
- package/src/styles/prose.css +22 -15
- package/src/styles/tokens.css +3 -0
- package/src/entries/mindmap-editor.ts +0 -37
package/README.md
CHANGED
|
@@ -100,35 +100,3 @@ console.log('a')
|
|
|
100
100
|
| `enableCopy` | `true` |
|
|
101
101
|
| `enableFullscreen` | `true` |
|
|
102
102
|
| `enableCenterToggle` | `true` |
|
|
103
|
-
|
|
104
|
-
### `MindmapEditor`(`@tnotesjs/ui/mindmap-editor`)
|
|
105
|
-
|
|
106
|
-
思维导图编辑器的**唯一实现**。`@tnotesjs/ui/mindmap` 那个块(`Mindmap.vue`)是给 SSG / Desk 笔记
|
|
107
|
-
内嵌用的「预览 + 可选编辑」;本入口导出它内部复用的编辑器组件,供自带页面外壳的宿主直接组装。
|
|
108
|
-
当前宿主:`apps/mindmap-web`、`apps/mindmap-vscode`。
|
|
109
|
-
|
|
110
|
-
| 导出 | 说明 |
|
|
111
|
-
| ----------------------------------------------------------------------------- | ---------------------------------------------- |
|
|
112
|
-
| `MindmapView` | 脑图画布(含选区工具条 / 右键菜单 / 链接浮层) |
|
|
113
|
-
| `OutlineView` | 大纲视图(拖拽、行内富文本、折叠) |
|
|
114
|
-
| `MarkdownView` | 源码视图(防抖回流 + 诊断定位) |
|
|
115
|
-
| `SearchBar` | 三视图共用的搜索条 |
|
|
116
|
-
| `FocusBreadcrumbs` | 进入主题后的面包屑(与主 barrel 同源) |
|
|
117
|
-
| `AppIcon` / `IconButton` / `CollapseMenu` | 外壳按钮与图标 |
|
|
118
|
-
| `CanvasContextMenu` / `LinkPopover` / `RichInlineEditor` / `SelectionToolbar` | 上面几个组件的组成件,供未来宿主自行组装 |
|
|
119
|
-
| `insertImageIntoSource` | 源码视图的整节点图片插入 |
|
|
120
|
-
| `pasteCanvasOutline` / `readMindmapClipboard` / `writeMindmapClipboard` | 脑图剪贴板(含无权限时的进程内回退) |
|
|
121
|
-
| `resolveAfterDropLevel` / `DropLevel` | 大纲拖拽落点解析 |
|
|
122
|
-
| `primaryShortcut` / `altShortcut` / `isApplePlatform` | 平台快捷键文案 |
|
|
123
|
-
|
|
124
|
-
**浮层挂载点(`teleportTo`)**:`MindmapView`(并透传给 `CanvasContextMenu` / `LinkPopover`)接受
|
|
125
|
-
`teleportTo?: string`。默认 `undefined` = 就地渲染——浮层是 `position: fixed`,嵌在笔记里的宿主
|
|
126
|
-
(Desk / SSG)teleport 到 `body` 会脱离笔记的层叠上下文。整页宿主传 `'body'`。
|
|
127
|
-
|
|
128
|
-
**宿主职责(不属于本包)**:`MindmapSession` 的归属与生命周期、`--mm-*` 主题变量
|
|
129
|
-
(`--mm-canvas-bg` / `--mm-panel-bg` / `--mm-border` / `--mm-text` / `--mm-text-dim` / `--mm-hover` /
|
|
130
|
-
`--mm-selected-bg` / `--mm-accent` / `--mm-edit-bg`)、落盘、图片写入、宿主快捷键到组件
|
|
131
|
-
`defineExpose` 钩子的桥接(`OutlineView` 的 `locateNode` / `selectAllFromHost` / `undoFromHost` /
|
|
132
|
-
`redoFromHost`,`MarkdownView` 的 `selectAllFromHost` / `flushDraft`)。
|
|
133
|
-
|
|
134
|
-
改公共交互只改这里一处,三个宿主同时生效。
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
interface MindmapFenceOptions {
|
|
2
2
|
title?: string;
|
|
3
3
|
initialExpandLevel?: number;
|
|
4
|
+
/** Pane height in px (`h=480` in the fence info). */
|
|
5
|
+
height?: number;
|
|
4
6
|
}
|
|
5
|
-
|
|
7
|
+
declare const MINDMAP_MIN_HEIGHT = 200;
|
|
8
|
+
declare const MINDMAP_MAX_HEIGHT = 1600;
|
|
9
|
+
declare function clampMindmapHeight(value: number): number;
|
|
10
|
+
/** Parse the canonical `mindmap [title] 2 h=480` fence metadata. */
|
|
6
11
|
declare function parseMindmapFence(openLine: string): MindmapFenceOptions | null;
|
|
7
12
|
interface NormalizeMindmapOptions {
|
|
8
13
|
title?: string;
|
|
@@ -11,4 +16,4 @@ interface NormalizeMindmapOptions {
|
|
|
11
16
|
/** Ensure canonical mindmap Markdown has exactly one H1 root title. */
|
|
12
17
|
declare function normalizeMindmapMarkdown(source: string, options?: NormalizeMindmapOptions): string;
|
|
13
18
|
|
|
14
|
-
export { type MindmapFenceOptions, type NormalizeMindmapOptions, normalizeMindmapMarkdown, parseMindmapFence };
|
|
19
|
+
export { MINDMAP_MAX_HEIGHT, MINDMAP_MIN_HEIGHT, type MindmapFenceOptions, type NormalizeMindmapOptions, clampMindmapHeight, normalizeMindmapMarkdown, parseMindmapFence };
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
// src/components/Mindmap/markdown.ts
|
|
2
|
+
var MINDMAP_MIN_HEIGHT = 200;
|
|
3
|
+
var MINDMAP_MAX_HEIGHT = 1600;
|
|
4
|
+
function clampMindmapHeight(value) {
|
|
5
|
+
return Math.round(Math.min(MINDMAP_MAX_HEIGHT, Math.max(MINDMAP_MIN_HEIGHT, value)));
|
|
6
|
+
}
|
|
2
7
|
function cleanHeadingText(value) {
|
|
3
8
|
return value.trim().replace(/\s+#+\s*$/, "").trim();
|
|
4
9
|
}
|
|
@@ -13,9 +18,15 @@ function parseMindmapFence(openLine) {
|
|
|
13
18
|
options.title = cleanHeadingText(titleMatch[1]) || void 0;
|
|
14
19
|
rest = `${rest.slice(0, titleMatch.index)} ${rest.slice((titleMatch.index ?? 0) + titleMatch[0].length)}`.trim();
|
|
15
20
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
for (const token of rest.split(/\s+/).filter(Boolean)) {
|
|
22
|
+
const height = token.match(/^h=(\d+)$/);
|
|
23
|
+
if (height) {
|
|
24
|
+
options.height = clampMindmapHeight(Number(height[1]));
|
|
25
|
+
} else if (/^\d+$/.test(token) && options.initialExpandLevel === void 0) {
|
|
26
|
+
options.initialExpandLevel = Math.max(1, Number(token));
|
|
27
|
+
} else {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
19
30
|
}
|
|
20
31
|
return options;
|
|
21
32
|
}
|
|
@@ -41,6 +52,9 @@ ${body.join("\n")}
|
|
|
41
52
|
`;
|
|
42
53
|
}
|
|
43
54
|
export {
|
|
55
|
+
MINDMAP_MAX_HEIGHT,
|
|
56
|
+
MINDMAP_MIN_HEIGHT,
|
|
57
|
+
clampMindmapHeight,
|
|
44
58
|
normalizeMindmapMarkdown,
|
|
45
59
|
parseMindmapFence
|
|
46
60
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tnotesjs/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Shared Vue UI for TNotes built-in blocks — consumed by the TNotes SSG and Desk.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -60,11 +60,6 @@
|
|
|
60
60
|
"types": "./src/entries/excalidraw-editor.ts",
|
|
61
61
|
"import": "./src/entries/excalidraw-editor.ts",
|
|
62
62
|
"default": "./src/entries/excalidraw-editor.ts"
|
|
63
|
-
},
|
|
64
|
-
"./mindmap-editor": {
|
|
65
|
-
"types": "./src/entries/mindmap-editor.ts",
|
|
66
|
-
"import": "./src/entries/mindmap-editor.ts",
|
|
67
|
-
"default": "./src/entries/mindmap-editor.ts"
|
|
68
63
|
}
|
|
69
64
|
},
|
|
70
65
|
"files": [
|
|
@@ -88,7 +83,7 @@
|
|
|
88
83
|
"react-dom": "18.3.1",
|
|
89
84
|
"sass": "^1.90.0",
|
|
90
85
|
"shiki": "4.4.2",
|
|
91
|
-
"@tnotesjs/mindmap-core": "^0.2.
|
|
86
|
+
"@tnotesjs/mindmap-core": "^0.2.3"
|
|
92
87
|
},
|
|
93
88
|
"devDependencies": {
|
|
94
89
|
"@eslint/js": "^10.0.1",
|
|
@@ -111,7 +106,8 @@
|
|
|
111
106
|
},
|
|
112
107
|
"publishConfig": {
|
|
113
108
|
"access": "public",
|
|
114
|
-
"registry": "https://registry.npmjs.org/"
|
|
109
|
+
"registry": "https://registry.npmjs.org/",
|
|
110
|
+
"provenance": true
|
|
115
111
|
},
|
|
116
112
|
"repository": {
|
|
117
113
|
"type": "git",
|
|
@@ -136,4 +132,4 @@
|
|
|
136
132
|
"lint": "eslint . --max-warnings=0",
|
|
137
133
|
"lint:fix": "eslint . --fix"
|
|
138
134
|
}
|
|
139
|
-
}
|
|
135
|
+
}
|
|
@@ -2,7 +2,16 @@
|
|
|
2
2
|
import { CanvasEditor, CanvasViewer, MindmapSession } from '@tnotesjs/mindmap-core'
|
|
3
3
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
MAX_EXPAND_LEVEL,
|
|
7
|
+
MIN_EXPAND_LEVEL,
|
|
8
|
+
acceptsExpandLevelEdit,
|
|
9
|
+
applyInitialExpandLevel,
|
|
10
|
+
normalizeExpandLevel,
|
|
11
|
+
resolvedExpandLevel,
|
|
12
|
+
stepExpandLevel
|
|
13
|
+
} from './expandLevel'
|
|
14
|
+
import { FORCE_EXIT_FULLSCREEN_EVENT, reconcileMindmapFullscreen } from './fullscreenFlag'
|
|
6
15
|
import { normalizeMindmapMarkdown } from './markdown'
|
|
7
16
|
import MindmapOutlineNode from './MindmapOutlineNode.vue'
|
|
8
17
|
import MindmapViewIcon from './MindmapViewIcon.vue'
|
|
@@ -20,8 +29,6 @@ type PreviewView = 'mindmap' | 'outline' | 'source'
|
|
|
20
29
|
|
|
21
30
|
/** One fullscreen mindmap at a time across preview instances on the page. */
|
|
22
31
|
const FS_GATE_KEY = '__tnotesjs_mindmap_fullscreen_gate__'
|
|
23
|
-
const FS_BODY_ATTR = 'tnMindmapFs'
|
|
24
|
-
const FORCE_EXIT_FULLSCREEN_EVENT = 'tnotes-mindmap-force-exit-fullscreen'
|
|
25
32
|
|
|
26
33
|
type FsGateState = { owners: Map<symbol, () => void> }
|
|
27
34
|
|
|
@@ -52,15 +59,7 @@ function claimMindmapFullscreen(id: symbol): void {
|
|
|
52
59
|
|
|
53
60
|
function syncBodyFullscreenAttr(activeRoot: HTMLElement | null): void {
|
|
54
61
|
if (typeof document === 'undefined') return
|
|
55
|
-
|
|
56
|
-
document.body.dataset[FS_BODY_ATTR] = '1'
|
|
57
|
-
document.documentElement.dataset[FS_BODY_ATTR] = '1'
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
if (!document.querySelector('.mindmap-preview.is-fullscreen')) {
|
|
61
|
-
delete document.body.dataset[FS_BODY_ATTR]
|
|
62
|
-
delete document.documentElement.dataset[FS_BODY_ATTR]
|
|
63
|
-
}
|
|
62
|
+
reconcileMindmapFullscreen(activeRoot, document.querySelectorAll<HTMLElement>('.mindmap-preview'))
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
function forceExitPeerFullscreen(activeRoot: HTMLElement | null): void {
|
|
@@ -83,6 +82,8 @@ const props = withDefaults(
|
|
|
83
82
|
editable?: boolean
|
|
84
83
|
/** Show expand-level control on the chrome (Desk). */
|
|
85
84
|
expandLevelControl?: boolean
|
|
85
|
+
/** Pane height in px; omit for the default 440px canvas. */
|
|
86
|
+
height?: number
|
|
86
87
|
/** Explicit dark mode; omit to auto-detect html.dark / data-theme=dark. */
|
|
87
88
|
isDark?: boolean
|
|
88
89
|
/**
|
|
@@ -102,6 +103,7 @@ const props = withDefaults(
|
|
|
102
103
|
initialExpandLevel: 3,
|
|
103
104
|
editable: false,
|
|
104
105
|
expandLevelControl: false,
|
|
106
|
+
height: undefined,
|
|
105
107
|
isDark: undefined,
|
|
106
108
|
resolveImageSrc: undefined,
|
|
107
109
|
writeAsset: undefined
|
|
@@ -162,6 +164,9 @@ function decodeContent(value: string): string {
|
|
|
162
164
|
}
|
|
163
165
|
}
|
|
164
166
|
|
|
167
|
+
const paneStyle = computed(() =>
|
|
168
|
+
props.height ? { '--mindmap-pane-height': `${props.height}px` } : undefined
|
|
169
|
+
)
|
|
165
170
|
const rawContent = computed(() => props.source || decodeContent(props.content || ''))
|
|
166
171
|
const normalizedContent = computed(() => normalizeMindmapMarkdown(rawContent.value))
|
|
167
172
|
|
|
@@ -274,9 +279,127 @@ function setExpandLevel(raw: number): void {
|
|
|
274
279
|
emit('expandLevelChange', level)
|
|
275
280
|
}
|
|
276
281
|
|
|
282
|
+
/** The value the field held when it was focused — what ESC / an empty blur restores. */
|
|
283
|
+
let expandLevelBeforeFocus: number | null = null
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Refuse a non-digit before it ever reaches the field. Cancelling `beforeinput`
|
|
287
|
+
* leaves the value and the selection untouched, which is what keeps the field
|
|
288
|
+
* usable after a rejected key — restoring `input.value` after the fact leaves
|
|
289
|
+
* the browser's editing state wedged, and nothing types into the field again.
|
|
290
|
+
*
|
|
291
|
+
* The first character cannot be `0` (the range starts at 1); a second character
|
|
292
|
+
* completes a 1–99 number, so `0` is allowed there (`10`, `20`, …).
|
|
293
|
+
*/
|
|
294
|
+
function onExpandLevelBeforeInput(event: InputEvent): void {
|
|
295
|
+
// Only text being inserted is judged; deletions and history carry no `data`.
|
|
296
|
+
if (event.data === null) return
|
|
297
|
+
if (event.inputType !== 'insertText') return
|
|
298
|
+
if (!/^\d+$/.test(event.data)) {
|
|
299
|
+
event.preventDefault()
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
const input = event.target as HTMLInputElement
|
|
303
|
+
const replacingAll = input.selectionStart === 0 && input.selectionEnd === input.value.length
|
|
304
|
+
const next = replacingAll
|
|
305
|
+
? event.data
|
|
306
|
+
: input.value.slice(0, input.selectionStart ?? input.value.length) +
|
|
307
|
+
event.data +
|
|
308
|
+
input.value.slice(input.selectionEnd ?? input.value.length)
|
|
309
|
+
if (!acceptsExpandLevelEdit(next)) event.preventDefault()
|
|
310
|
+
}
|
|
311
|
+
|
|
277
312
|
function onExpandLevelInput(event: Event): void {
|
|
278
|
-
const
|
|
279
|
-
|
|
313
|
+
const input = event.target as HTMLInputElement
|
|
314
|
+
// Safety net for input that skips `beforeinput` (paste, IME, drag-and-drop, a
|
|
315
|
+
// programmatic value): anything that cannot be a 1–99 number is dropped.
|
|
316
|
+
if (!acceptsExpandLevelEdit(input.value)) {
|
|
317
|
+
input.value = String(expandLevel.value)
|
|
318
|
+
input.select()
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
if (input.value === '') return
|
|
322
|
+
const typed = Number(input.value)
|
|
323
|
+
// `0` (and anything above the cap) stays in the field uncommitted; blur clamps it.
|
|
324
|
+
if (typed >= MIN_EXPAND_LEVEL && typed <= MAX_EXPAND_LEVEL) setExpandLevel(typed)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Editing ends: an empty field means "no input", so the pre-focus value comes
|
|
329
|
+
* back; otherwise whatever was typed is clamped into 1–99 and committed.
|
|
330
|
+
*/
|
|
331
|
+
function onExpandLevelBlur(event: Event): void {
|
|
332
|
+
const input = event.target as HTMLInputElement
|
|
333
|
+
const restore = expandLevelBeforeFocus
|
|
334
|
+
expandLevelBeforeFocus = null
|
|
335
|
+
const level = resolvedExpandLevel(input.value, restore ?? expandLevel.value)
|
|
336
|
+
input.value = String(level)
|
|
337
|
+
setExpandLevel(level)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** ESC abandons the edit outright, wherever the field happens to be. */
|
|
341
|
+
function onExpandLevelEscape(event: KeyboardEvent): boolean {
|
|
342
|
+
if (event.key !== 'Escape') return false
|
|
343
|
+
const restore = expandLevelBeforeFocus ?? expandLevel.value
|
|
344
|
+
event.preventDefault()
|
|
345
|
+
event.stopPropagation()
|
|
346
|
+
const input = event.target as HTMLInputElement
|
|
347
|
+
input.value = String(restore)
|
|
348
|
+
setExpandLevel(restore)
|
|
349
|
+
input.blur()
|
|
350
|
+
return true
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Backspace clears the field in one press. Editing a single number digit by digit
|
|
355
|
+
* has no use: an empty field here means "no input" — it shows the hint, and blur
|
|
356
|
+
* puts the pre-focus value back.
|
|
357
|
+
*/
|
|
358
|
+
function onExpandLevelClearKey(event: KeyboardEvent): boolean {
|
|
359
|
+
if (event.key !== 'Backspace' && event.key !== 'Delete') return false
|
|
360
|
+
const input = event.target as HTMLInputElement
|
|
361
|
+
if (input.value === '') return false
|
|
362
|
+
event.preventDefault()
|
|
363
|
+
input.value = ''
|
|
364
|
+
return true
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* All four arrows step the level. The field is a text input (see
|
|
369
|
+
* `onExpandLevelFocus`), so ↑/↓ no longer come free from the number input and
|
|
370
|
+
* have to be handled here alongside ←/→ — either hand stays on the arrows.
|
|
371
|
+
* A caret inside one number has nothing to scroll, which is what leaves the
|
|
372
|
+
* horizontal arrows free to take.
|
|
373
|
+
*/
|
|
374
|
+
function onExpandLevelKeydown(event: KeyboardEvent): void {
|
|
375
|
+
if (onExpandLevelEscape(event)) return
|
|
376
|
+
if (onExpandLevelClearKey(event)) return
|
|
377
|
+
const step = { ArrowUp: 1, ArrowRight: 1, ArrowDown: -1, ArrowLeft: -1 }[event.key]
|
|
378
|
+
if (step === undefined) return
|
|
379
|
+
// Leave modifiers alone: Cmd/Alt+← are word-jump / back shortcuts.
|
|
380
|
+
if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) return
|
|
381
|
+
const input = event.target as HTMLInputElement
|
|
382
|
+
const current = Number(input.value)
|
|
383
|
+
const base = /^\d{1,2}$/.test(input.value) ? current : expandLevel.value
|
|
384
|
+
// Clamp rather than lean on `setExpandLevel`: a clamped step is still a step,
|
|
385
|
+
// and the value stays inside the documented 1–99.
|
|
386
|
+
const bounded = stepExpandLevel(base, step as -1 | 1)
|
|
387
|
+
event.preventDefault()
|
|
388
|
+
input.value = String(bounded)
|
|
389
|
+
setExpandLevel(bounded)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Two digits mean "type the level, not append to it": select the current value on
|
|
394
|
+
* focus so a keystroke replaces it. This is also why the field is a `text` input
|
|
395
|
+
* with `inputmode="numeric"` rather than `type=number` — selection APIs are not
|
|
396
|
+
* available on the latter (the digit would be appended, giving `39`).
|
|
397
|
+
* The pre-focus value is remembered for ESC and for an empty blur.
|
|
398
|
+
*/
|
|
399
|
+
function onExpandLevelFocus(event: FocusEvent): void {
|
|
400
|
+
const input = event.target as HTMLInputElement
|
|
401
|
+
expandLevelBeforeFocus = expandLevel.value
|
|
402
|
+
input.select()
|
|
280
403
|
}
|
|
281
404
|
|
|
282
405
|
function setView(view: PreviewView): void {
|
|
@@ -378,6 +501,9 @@ function handleFullscreenChange(): void {
|
|
|
378
501
|
} else if (fullscreenMode === 'native') {
|
|
379
502
|
fullscreenMode = null
|
|
380
503
|
isFullscreen.value = false
|
|
504
|
+
// `syncBodyFullscreenAttr` also drops the stale `is-fullscreen` class, so
|
|
505
|
+
// ESC (which only fires `fullscreenchange`) lands in the same state as the
|
|
506
|
+
// button path and the toolbar comes back.
|
|
381
507
|
syncBodyFullscreenAttr(null)
|
|
382
508
|
}
|
|
383
509
|
if (activeView.value === 'mindmap') void nextTick(() => zoomToFit())
|
|
@@ -388,9 +514,14 @@ function zoomToFit(): void {
|
|
|
388
514
|
viewer?.zoomToFit()
|
|
389
515
|
}
|
|
390
516
|
|
|
391
|
-
|
|
517
|
+
/** 画布上的浮层自己管焦点;被抢走会让节点编辑提前提交,工具栏点了也不生效 */
|
|
518
|
+
const CANVAS_OVERLAY_SELECTOR =
|
|
519
|
+
'.selection-toolbar, .mm-edit-input, .link-popover, .canvas-context-menu'
|
|
520
|
+
|
|
521
|
+
function activateCanvas(event: PointerEvent): void {
|
|
392
522
|
isCanvasActive.value = true
|
|
393
523
|
if (!props.editable) return
|
|
524
|
+
if (event.target instanceof Element && event.target.closest(CANVAS_OVERLAY_SELECTOR)) return
|
|
394
525
|
const editorEl = canvasHost.value?.querySelector<HTMLElement>('.mm-editor')
|
|
395
526
|
editorEl?.focus({ preventScroll: true })
|
|
396
527
|
}
|
|
@@ -609,6 +740,7 @@ onBeforeUnmount(() => {
|
|
|
609
740
|
<section
|
|
610
741
|
ref="previewRoot"
|
|
611
742
|
class="mindmap-preview"
|
|
743
|
+
:style="paneStyle"
|
|
612
744
|
:class="{
|
|
613
745
|
'is-dark': dark,
|
|
614
746
|
'is-fullscreen': isFullscreen,
|
|
@@ -637,16 +769,24 @@ onBeforeUnmount(() => {
|
|
|
637
769
|
</button>
|
|
638
770
|
</nav>
|
|
639
771
|
<span class="mindmap-preview-action-divider" aria-hidden="true" />
|
|
640
|
-
<label
|
|
641
|
-
|
|
772
|
+
<label
|
|
773
|
+
v-if="expandLevelControl"
|
|
774
|
+
class="mindmap-preview-expand"
|
|
775
|
+
:title="`默认展开层级(${MIN_EXPAND_LEVEL}~${MAX_EXPAND_LEVEL})`"
|
|
776
|
+
>
|
|
642
777
|
<input
|
|
643
778
|
class="mindmap-preview-expand-input"
|
|
644
|
-
type="
|
|
645
|
-
|
|
646
|
-
|
|
779
|
+
type="text"
|
|
780
|
+
inputmode="numeric"
|
|
781
|
+
maxlength="2"
|
|
782
|
+
:placeholder="`${MIN_EXPAND_LEVEL}~${MAX_EXPAND_LEVEL}`"
|
|
647
783
|
:value="expandLevel"
|
|
648
|
-
aria-label="
|
|
649
|
-
@
|
|
784
|
+
:aria-label="`默认展开层级,${MIN_EXPAND_LEVEL} 到 ${MAX_EXPAND_LEVEL}`"
|
|
785
|
+
@focus="onExpandLevelFocus"
|
|
786
|
+
@beforeinput="onExpandLevelBeforeInput"
|
|
787
|
+
@input="onExpandLevelInput"
|
|
788
|
+
@blur="onExpandLevelBlur"
|
|
789
|
+
@keydown="onExpandLevelKeydown"
|
|
650
790
|
/>
|
|
651
791
|
</label>
|
|
652
792
|
<button
|
|
@@ -865,32 +1005,65 @@ onBeforeUnmount(() => {
|
|
|
865
1005
|
cursor: default;
|
|
866
1006
|
}
|
|
867
1007
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1008
|
+
/*
|
|
1009
|
+
* Just the number: no label, no box, no fill — an underline is enough to say
|
|
1010
|
+
* "you can type here". The field is as wide as its placeholder (`1~99`) needs,
|
|
1011
|
+
* so the hint is never clipped, and the value is centred so the caret and the
|
|
1012
|
+
* number do not jump as it changes.
|
|
1013
|
+
*/
|
|
872
1014
|
.mindmap-preview-expand-input {
|
|
873
|
-
width: 2.
|
|
874
|
-
height:
|
|
875
|
-
padding: 0
|
|
876
|
-
border:
|
|
877
|
-
border-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1015
|
+
width: 2.9rem;
|
|
1016
|
+
height: 20px;
|
|
1017
|
+
padding: 0;
|
|
1018
|
+
border: 0;
|
|
1019
|
+
border-bottom: 1px solid var(--tn-c-divider);
|
|
1020
|
+
border-radius: 0;
|
|
1021
|
+
background: transparent;
|
|
1022
|
+
color: var(--tn-c-brand);
|
|
1023
|
+
font-size: 13px;
|
|
1024
|
+
font-weight: 600;
|
|
1025
|
+
line-height: 20px;
|
|
1026
|
+
text-align: center;
|
|
1027
|
+
cursor: text;
|
|
1028
|
+
appearance: textfield;
|
|
1029
|
+
-moz-appearance: textfield;
|
|
1030
|
+
|
|
1031
|
+
&::placeholder {
|
|
1032
|
+
color: var(--tn-c-text-3, var(--tn-c-text-2));
|
|
1033
|
+
font-weight: 400;
|
|
1034
|
+
opacity: 0.75;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
&:focus {
|
|
1038
|
+
border-bottom-color: var(--tn-c-brand);
|
|
1039
|
+
outline: none;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/* Chromium keeps a spinner on text-ish numeric fields; we step via keys. */
|
|
1043
|
+
&::-webkit-outer-spin-button,
|
|
1044
|
+
&::-webkit-inner-spin-button {
|
|
1045
|
+
margin: 0;
|
|
1046
|
+
appearance: none;
|
|
1047
|
+
-webkit-appearance: none;
|
|
1048
|
+
}
|
|
882
1049
|
}
|
|
883
1050
|
|
|
884
1051
|
.mindmap-canvas-host {
|
|
885
1052
|
position: relative;
|
|
886
1053
|
width: 100%;
|
|
887
|
-
height: 440px;
|
|
1054
|
+
height: var(--mindmap-pane-height, 440px);
|
|
888
1055
|
overflow: hidden;
|
|
889
|
-
|
|
1056
|
+
/*
|
|
1057
|
+
* No background of its own: the canvas is cleared to transparent, and the
|
|
1058
|
+
* container above (`.mindmap-preview`) already paints `--tn-c-bg` with the
|
|
1059
|
+
* panel border. Painting it again here only made the canvas read as a second,
|
|
1060
|
+
* inset panel inside the editor.
|
|
1061
|
+
*/
|
|
1062
|
+
background: transparent;
|
|
890
1063
|
touch-action: none;
|
|
891
1064
|
user-select: none;
|
|
892
1065
|
|
|
893
|
-
/* CanvasEditor overlay + chrome tokens
|
|
1066
|
+
/* CanvasEditor overlay + chrome tokens. */
|
|
894
1067
|
--mm-canvas-bg: var(--tn-c-bg);
|
|
895
1068
|
--mm-panel-bg: var(--tn-c-bg-soft, var(--tn-c-bg));
|
|
896
1069
|
--mm-text: var(--tn-c-text);
|
|
@@ -930,7 +1103,7 @@ onBeforeUnmount(() => {
|
|
|
930
1103
|
pointer-events: none;
|
|
931
1104
|
}
|
|
932
1105
|
|
|
933
|
-
/* Marquee box — required for box-select feedback
|
|
1106
|
+
/* Marquee box — required for box-select feedback. */
|
|
934
1107
|
.mindmap-canvas-host:deep(.mm-selection-box) {
|
|
935
1108
|
position: absolute;
|
|
936
1109
|
z-index: 8;
|
|
@@ -1008,8 +1181,7 @@ onBeforeUnmount(() => {
|
|
|
1008
1181
|
.mindmap-canvas-host:deep(.mm-edit-input .inline-run.highlight:not(.code)) {
|
|
1009
1182
|
padding: 0 1px;
|
|
1010
1183
|
border-radius: 2px;
|
|
1011
|
-
background:
|
|
1012
|
-
color: #242424;
|
|
1184
|
+
background: var(--tn-c-mark-bg, rgba(255, 221, 0, 0.45));
|
|
1013
1185
|
}
|
|
1014
1186
|
.mindmap-canvas-host:deep(.mm-edit-input .inline-run.code) {
|
|
1015
1187
|
padding: 0 4px;
|
|
@@ -1052,13 +1224,13 @@ onBeforeUnmount(() => {
|
|
|
1052
1224
|
}
|
|
1053
1225
|
|
|
1054
1226
|
.mindmap-outline {
|
|
1055
|
-
max-height: 560px;
|
|
1227
|
+
max-height: var(--mindmap-pane-height, 560px);
|
|
1056
1228
|
padding: 18px 22px 22px;
|
|
1057
1229
|
overflow: auto;
|
|
1058
1230
|
}
|
|
1059
1231
|
|
|
1060
1232
|
.mindmap-outline.is-editable {
|
|
1061
|
-
height: 440px;
|
|
1233
|
+
height: var(--mindmap-pane-height, 440px);
|
|
1062
1234
|
max-height: none;
|
|
1063
1235
|
padding: 0;
|
|
1064
1236
|
overflow: hidden;
|
|
@@ -1067,7 +1239,7 @@ onBeforeUnmount(() => {
|
|
|
1067
1239
|
}
|
|
1068
1240
|
|
|
1069
1241
|
.mindmap-source-wrap.is-editable {
|
|
1070
|
-
height: 440px;
|
|
1242
|
+
height: var(--mindmap-pane-height, 440px);
|
|
1071
1243
|
overflow: hidden;
|
|
1072
1244
|
display: flex;
|
|
1073
1245
|
flex-direction: column;
|
|
@@ -1105,9 +1277,28 @@ onBeforeUnmount(() => {
|
|
|
1105
1277
|
|
|
1106
1278
|
.mindmap-outline :deep(.mindmap-outline-toggle),
|
|
1107
1279
|
.mindmap-outline :deep(.mindmap-outline-leaf) {
|
|
1280
|
+
display: flex;
|
|
1108
1281
|
flex: 0 0 18px;
|
|
1282
|
+
align-items: center;
|
|
1283
|
+
justify-content: center;
|
|
1109
1284
|
width: 18px;
|
|
1285
|
+
height: 24px;
|
|
1110
1286
|
color: var(--tn-c-text-2);
|
|
1287
|
+
/*
|
|
1288
|
+
* The toggle is a real `<button>`, so it arrives with the browser's default
|
|
1289
|
+
* chrome — 2px outset border, button-face background, 1px 6px padding and a
|
|
1290
|
+
* 13.33px font. Desk never saw it because the preview renders inside
|
|
1291
|
+
* `.milkdown`, whose theme reset (`.milkdown button { border/background: none }`)
|
|
1292
|
+
* swallowed it; the SSG site has no such ancestor, so the box showed up and
|
|
1293
|
+
* the chevron read as a filled square next to the plain `•` leaf. Reset it
|
|
1294
|
+
* here so the outline looks the same wherever the component is mounted.
|
|
1295
|
+
*/
|
|
1296
|
+
padding: 0;
|
|
1297
|
+
border: 0;
|
|
1298
|
+
background: none;
|
|
1299
|
+
box-shadow: none;
|
|
1300
|
+
font: inherit;
|
|
1301
|
+
line-height: inherit;
|
|
1111
1302
|
text-align: center;
|
|
1112
1303
|
}
|
|
1113
1304
|
|
|
@@ -1152,8 +1343,7 @@ onBeforeUnmount(() => {
|
|
|
1152
1343
|
.mindmap-outline :deep(.is-highlight) {
|
|
1153
1344
|
padding: 0 2px;
|
|
1154
1345
|
border-radius: 2px;
|
|
1155
|
-
background:
|
|
1156
|
-
color: #252525;
|
|
1346
|
+
background: var(--tn-c-mark-bg, rgba(255, 221, 0, 0.45));
|
|
1157
1347
|
}
|
|
1158
1348
|
.mindmap-outline :deep(.is-code) {
|
|
1159
1349
|
padding: 1px 5px;
|
|
@@ -1169,7 +1359,7 @@ onBeforeUnmount(() => {
|
|
|
1169
1359
|
}
|
|
1170
1360
|
|
|
1171
1361
|
.mindmap-source {
|
|
1172
|
-
max-height: 560px;
|
|
1362
|
+
max-height: var(--mindmap-pane-height, 560px);
|
|
1173
1363
|
margin: 0;
|
|
1174
1364
|
padding: 18px 22px;
|
|
1175
1365
|
overflow: auto;
|
|
@@ -1251,7 +1441,7 @@ onBeforeUnmount(() => {
|
|
|
1251
1441
|
|
|
1252
1442
|
@media (max-width: 768px) {
|
|
1253
1443
|
.mindmap-canvas-host {
|
|
1254
|
-
height: 360px;
|
|
1444
|
+
height: var(--mindmap-pane-height, 360px);
|
|
1255
1445
|
}
|
|
1256
1446
|
.mindmap-preview-actions {
|
|
1257
1447
|
top: 6px;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
|
2
|
+
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
3
3
|
import { CanvasEditor, serializeSubtree } from '@tnotesjs/mindmap-core'
|
|
4
4
|
import type {
|
|
5
5
|
CanvasContextRequest,
|
|
6
|
+
CanvasEditState,
|
|
6
7
|
CanvasLinkHover,
|
|
8
|
+
CanvasLinkRequest,
|
|
7
9
|
InlineFormat,
|
|
8
10
|
MindmapNode,
|
|
9
11
|
MindmapSession
|
|
@@ -33,6 +35,27 @@ const emit = defineEmits<{
|
|
|
33
35
|
const host = ref<HTMLElement>()
|
|
34
36
|
let editor: CanvasEditor | null = null
|
|
35
37
|
const selectedCount = ref(0)
|
|
38
|
+
/** 选中的不全是图片节点;图片说明是纯文本,不出格式按钮 */
|
|
39
|
+
const selectionFormattable = ref(true)
|
|
40
|
+
/** 内联编辑态:编辑框里有文字选区时,弹出与大纲视图同一套文字格式工具栏 */
|
|
41
|
+
const editState = ref<CanvasEditState | null>(null)
|
|
42
|
+
const selectionLink = ref<CanvasLinkRequest | null>(null)
|
|
43
|
+
const textSelection = computed(() => {
|
|
44
|
+
const state = editState.value
|
|
45
|
+
return state && !state.image ? state.selection : null
|
|
46
|
+
})
|
|
47
|
+
const textFormats = computed<Partial<Record<InlineFormat, boolean>>>(() => {
|
|
48
|
+
const state = editState.value
|
|
49
|
+
const selection = textSelection.value
|
|
50
|
+
if (!state || !selection) return {}
|
|
51
|
+
const formats: InlineFormat[] = ['bold', 'italic', 'underline', 'strike', 'highlight', 'code']
|
|
52
|
+
return Object.fromEntries(
|
|
53
|
+
formats.map((format) => [
|
|
54
|
+
format,
|
|
55
|
+
props.session.inlineFormatActive(state.nodeId, selection.start, selection.end, format)
|
|
56
|
+
])
|
|
57
|
+
)
|
|
58
|
+
})
|
|
36
59
|
const linkEditor = ref<CanvasLinkHover | null>(null)
|
|
37
60
|
const contextMenu = ref<CanvasContextRequest | null>(null)
|
|
38
61
|
let linkCloseTimer: ReturnType<typeof setTimeout> | null = null
|
|
@@ -87,6 +110,41 @@ function applyNodeFormat(format: InlineFormat) {
|
|
|
87
110
|
props.session.formatSelectedNodes(format)
|
|
88
111
|
}
|
|
89
112
|
|
|
113
|
+
function formatEditing(format: InlineFormat) {
|
|
114
|
+
editor?.formatEditing(format)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function clearEditingFormats() {
|
|
118
|
+
editor?.clearEditingFormats()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function linkEditing() {
|
|
122
|
+
editor?.linkEditing()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function toggleEditingTask() {
|
|
126
|
+
editor?.commitEditing()
|
|
127
|
+
props.session.toggleTaskSelectedNodes()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function removeEditingNode() {
|
|
131
|
+
editor?.commitEditing()
|
|
132
|
+
props.session.removeSelectedNodes()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function saveSelectionLink(url: string) {
|
|
136
|
+
const request = selectionLink.value
|
|
137
|
+
selectionLink.value = null
|
|
138
|
+
if (!request) return
|
|
139
|
+
props.session.setNodeInlineLink(request.nodeId, request.start, request.end, url.trim() || null)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function removeSelectionLink() {
|
|
143
|
+
const request = selectionLink.value
|
|
144
|
+
selectionLink.value = null
|
|
145
|
+
if (request) props.session.setNodeInlineLink(request.nodeId, request.start, request.end, null)
|
|
146
|
+
}
|
|
147
|
+
|
|
90
148
|
function selectedMarkdown() {
|
|
91
149
|
return selectedRoots()
|
|
92
150
|
.map((node) => serializeSubtree(node))
|
|
@@ -167,6 +225,14 @@ onMounted(() => {
|
|
|
167
225
|
onSelectionPositionChange: (_position, count) => {
|
|
168
226
|
// Canvas toolbar is absolutely anchored; only the selection count gates visibility.
|
|
169
227
|
selectedCount.value = count
|
|
228
|
+
selectionFormattable.value = !props.session.selectedNodes.every((node) => node.content.image)
|
|
229
|
+
},
|
|
230
|
+
onEditStateChange: (state) => {
|
|
231
|
+
editState.value = state
|
|
232
|
+
},
|
|
233
|
+
onRequestLink: (request) => {
|
|
234
|
+
linkEditor.value = null
|
|
235
|
+
selectionLink.value = request
|
|
170
236
|
},
|
|
171
237
|
onLinkHover: onCanvasLinkHover,
|
|
172
238
|
onContextMenu: (request) => {
|
|
@@ -204,9 +270,22 @@ onBeforeUnmount(() => {
|
|
|
204
270
|
<div class="mindmap-view-root">
|
|
205
271
|
<div ref="host" class="mindmap-view-host" />
|
|
206
272
|
<SelectionToolbar
|
|
207
|
-
v-if="
|
|
273
|
+
v-if="textSelection && !selectionLink"
|
|
274
|
+
mode="text"
|
|
275
|
+
:position="textSelection.position"
|
|
276
|
+
:active-formats="textFormats"
|
|
277
|
+
:can-insert-image="false"
|
|
278
|
+
@format="formatEditing"
|
|
279
|
+
@task="toggleEditingTask"
|
|
280
|
+
@link="linkEditing"
|
|
281
|
+
@clear="clearEditingFormats"
|
|
282
|
+
@delete="removeEditingNode"
|
|
283
|
+
/>
|
|
284
|
+
<SelectionToolbar
|
|
285
|
+
v-else-if="selectedCount > 0 && !editState"
|
|
208
286
|
mode="nodes"
|
|
209
287
|
placement="canvas-bottom"
|
|
288
|
+
:formattable="selectionFormattable"
|
|
210
289
|
@format="applyNodeFormat"
|
|
211
290
|
@task="props.session.toggleTaskSelectedNodes()"
|
|
212
291
|
@copy="copySelected"
|
|
@@ -224,6 +303,16 @@ onBeforeUnmount(() => {
|
|
|
224
303
|
@leave="closeLinkPopoverSoon"
|
|
225
304
|
@close="linkEditor = null"
|
|
226
305
|
/>
|
|
306
|
+
<LinkPopover
|
|
307
|
+
v-if="selectionLink"
|
|
308
|
+
:url="selectionLink.url"
|
|
309
|
+
:position="selectionLink.position"
|
|
310
|
+
:teleport-to="teleportTo"
|
|
311
|
+
start-editing
|
|
312
|
+
@save="saveSelectionLink"
|
|
313
|
+
@remove="removeSelectionLink"
|
|
314
|
+
@close="selectionLink = null"
|
|
315
|
+
/>
|
|
227
316
|
<CanvasContextMenu
|
|
228
317
|
v-if="contextMenu"
|
|
229
318
|
:position="contextMenu.position"
|
|
@@ -361,7 +361,7 @@ function selectionPosition(input: RichInlineEditorElement): { left: number; top:
|
|
|
361
361
|
function syncTextSelection(nodeId: string, input: RichInlineEditorElement) {
|
|
362
362
|
const start = input.selectionStart ?? 0
|
|
363
363
|
const end = input.selectionEnd ?? 0
|
|
364
|
-
if (start === end) {
|
|
364
|
+
if (start === end || props.session.document.find(nodeId)?.content.image) {
|
|
365
365
|
if (textSelection.value?.nodeId === nodeId) textSelection.value = null
|
|
366
366
|
return
|
|
367
367
|
}
|
|
@@ -713,6 +713,8 @@ function applyInputFormatShortcut(
|
|
|
713
713
|
input: RichInlineEditorElement,
|
|
714
714
|
format: InlineFormat
|
|
715
715
|
) {
|
|
716
|
+
// 图片节点的文字是图片说明:纯文本,不接受行内格式
|
|
717
|
+
if (node.content.image) return
|
|
716
718
|
const caretStart = input.selectionStart ?? 0
|
|
717
719
|
const caretEnd = input.selectionEnd ?? 0
|
|
718
720
|
if (input.value.length === 0) return
|
|
@@ -741,6 +743,8 @@ function applyInputFormatShortcut(
|
|
|
741
743
|
}
|
|
742
744
|
|
|
743
745
|
function applyInputClearFormats(node: MindmapNode, input: RichInlineEditorElement) {
|
|
746
|
+
// 图片节点的文字是图片说明:纯文本,不接受行内格式
|
|
747
|
+
if (node.content.image) return
|
|
744
748
|
const caretStart = input.selectionStart ?? 0
|
|
745
749
|
const caretEnd = input.selectionEnd ?? 0
|
|
746
750
|
if (input.value.length === 0) return
|
|
@@ -416,8 +416,7 @@ onBeforeUnmount(emitCommit)
|
|
|
416
416
|
.rich-inline-editor :deep(.inline-run.highlight:not(.code)) {
|
|
417
417
|
padding: 0 1px;
|
|
418
418
|
border-radius: 2px;
|
|
419
|
-
background:
|
|
420
|
-
color: #242424;
|
|
419
|
+
background: var(--tn-c-mark-bg, rgba(255, 221, 0, 0.45));
|
|
421
420
|
}
|
|
422
421
|
.rich-inline-editor :deep(.inline-run.code) {
|
|
423
422
|
padding: 2px 6px;
|
|
@@ -16,8 +16,18 @@ const props = withDefaults(
|
|
|
16
16
|
*/
|
|
17
17
|
placement?: 'fixed' | 'canvas-bottom'
|
|
18
18
|
activeFormats?: Partial<Record<InlineFormat, boolean>>
|
|
19
|
+
/** 宿主没有插入图片入口时隐藏(脑图编辑框) */
|
|
20
|
+
canInsertImage?: boolean
|
|
21
|
+
/** 选中的全是图片节点时为 false:图片说明是纯文本,不出格式按钮 */
|
|
22
|
+
formattable?: boolean
|
|
19
23
|
}>(),
|
|
20
|
-
{
|
|
24
|
+
{
|
|
25
|
+
placement: 'fixed',
|
|
26
|
+
position: undefined,
|
|
27
|
+
activeFormats: undefined,
|
|
28
|
+
canInsertImage: true,
|
|
29
|
+
formattable: true
|
|
30
|
+
}
|
|
21
31
|
)
|
|
22
32
|
|
|
23
33
|
const emit = defineEmits<{
|
|
@@ -90,21 +100,24 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
|
|
|
90
100
|
role="toolbar"
|
|
91
101
|
:aria-label="mode === 'text' ? '文字格式工具栏' : '多主题工具栏'"
|
|
92
102
|
@pointerdown.prevent
|
|
103
|
+
@mousedown.prevent
|
|
93
104
|
>
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
<template v-if="formattable">
|
|
106
|
+
<button
|
|
107
|
+
v-for="item in formats"
|
|
108
|
+
:key="item.id"
|
|
109
|
+
type="button"
|
|
110
|
+
class="format-button"
|
|
111
|
+
:class="[{ active: activeFormats?.[item.id] }, `is-${item.id}`]"
|
|
112
|
+
:data-tooltip="item.label"
|
|
113
|
+
:aria-label="item.label"
|
|
114
|
+
@click="emit('format', item.id)"
|
|
115
|
+
>
|
|
116
|
+
<AppIcon v-if="item.id === 'highlight'" name="highlight" :size="21" />
|
|
117
|
+
<template v-else>{{ item.text }}</template>
|
|
118
|
+
</button>
|
|
119
|
+
<span class="toolbar-divider" />
|
|
120
|
+
</template>
|
|
108
121
|
<button
|
|
109
122
|
type="button"
|
|
110
123
|
class="tool-button"
|
|
@@ -116,6 +129,7 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
|
|
|
116
129
|
</button>
|
|
117
130
|
<template v-if="mode === 'text'">
|
|
118
131
|
<button
|
|
132
|
+
v-if="canInsertImage"
|
|
119
133
|
type="button"
|
|
120
134
|
class="tool-button"
|
|
121
135
|
:data-tooltip="`添加图片 (${altShortcut('Enter')})`"
|
|
@@ -135,12 +149,12 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
|
|
|
135
149
|
</button>
|
|
136
150
|
<button
|
|
137
151
|
type="button"
|
|
138
|
-
class="tool-button
|
|
152
|
+
class="tool-button"
|
|
139
153
|
:data-tooltip="`行内代码 (${primaryShortcut('E')})`"
|
|
140
154
|
aria-label="行内代码"
|
|
141
155
|
@click="emit('format', 'code')"
|
|
142
156
|
>
|
|
143
|
-
|
|
157
|
+
<AppIcon name="code" :size="20" />
|
|
144
158
|
</button>
|
|
145
159
|
</template>
|
|
146
160
|
<button
|
|
@@ -155,6 +169,7 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
|
|
|
155
169
|
</button>
|
|
156
170
|
<span class="toolbar-divider" />
|
|
157
171
|
<button
|
|
172
|
+
v-if="formattable"
|
|
158
173
|
type="button"
|
|
159
174
|
class="tool-button"
|
|
160
175
|
:data-tooltip="`清除样式 (${primaryShortcut('\\')})`"
|
|
@@ -217,7 +232,13 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
|
|
|
217
232
|
.tool-button::after {
|
|
218
233
|
position: absolute;
|
|
219
234
|
z-index: 2;
|
|
235
|
+
/* 宿主可能有全局 [data-tooltip]::after(例如 Desk 往下弹),这里把方向和显隐都定死 */
|
|
236
|
+
top: auto;
|
|
220
237
|
bottom: calc(100% + 9px);
|
|
238
|
+
min-width: 0;
|
|
239
|
+
border: 0;
|
|
240
|
+
visibility: visible;
|
|
241
|
+
transition: none;
|
|
221
242
|
left: 50%;
|
|
222
243
|
width: max-content;
|
|
223
244
|
max-width: min(260px, calc(100vw - 16px));
|
|
@@ -266,11 +287,6 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
|
|
|
266
287
|
.format-button.is-highlight {
|
|
267
288
|
color: #d3c900;
|
|
268
289
|
}
|
|
269
|
-
.tool-button.code-button {
|
|
270
|
-
font-family: ui-monospace, monospace;
|
|
271
|
-
font-size: 15px;
|
|
272
|
-
font-weight: 700;
|
|
273
|
-
}
|
|
274
290
|
.tool-button.danger {
|
|
275
291
|
color: #e14f5b;
|
|
276
292
|
}
|
|
@@ -1,7 +1,46 @@
|
|
|
1
1
|
import type { MindmapNode, MindmapSession } from '@tnotesjs/mindmap-core'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* The level field holds at most two digits: 1–99. Kept here as the one source of
|
|
5
|
+
* truth for the clamp, the key handler and the field's own width/placeholder.
|
|
6
|
+
*/
|
|
7
|
+
export const MIN_EXPAND_LEVEL = 1
|
|
8
|
+
export const MAX_EXPAND_LEVEL = 99
|
|
9
|
+
|
|
3
10
|
export function normalizeExpandLevel(value: number): number {
|
|
4
|
-
|
|
11
|
+
const level = Math.trunc(Number(value) || MIN_EXPAND_LEVEL)
|
|
12
|
+
return Math.min(MAX_EXPAND_LEVEL, Math.max(MIN_EXPAND_LEVEL, level))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* What the level field accepts as its complete text: one or two digits, no
|
|
17
|
+
* leading zero (`0` is below the range, `07` is not how a level is written).
|
|
18
|
+
* Empty is allowed while editing — it means "no input yet".
|
|
19
|
+
*/
|
|
20
|
+
export function isValidExpandLevelText(text: string): boolean {
|
|
21
|
+
return text === '' || /^[1-9]\d?$/.test(text)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whether an edit would leave the field in an acceptable state. `beforeinput`
|
|
26
|
+
* asks this before the browser applies the change, so a rejected key leaves the
|
|
27
|
+
* value *and* the selection untouched — restoring the value afterwards wedges the
|
|
28
|
+
* browser's editing state and the field stops accepting input.
|
|
29
|
+
*
|
|
30
|
+
* `next` is what the field would read after the edit.
|
|
31
|
+
*/
|
|
32
|
+
export function acceptsExpandLevelEdit(next: string): boolean {
|
|
33
|
+
return isValidExpandLevelText(next)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** One arrow-key step, clamped to the range. */
|
|
37
|
+
export function stepExpandLevel(value: number, direction: -1 | 1): number {
|
|
38
|
+
return Math.min(MAX_EXPAND_LEVEL, Math.max(MIN_EXPAND_LEVEL, value + direction))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Editing ended: an empty field drops the current value back. */
|
|
42
|
+
export function resolvedExpandLevel(text: string, previous: number): number {
|
|
43
|
+
return text === '' ? previous : normalizeExpandLevel(Number(text))
|
|
5
44
|
}
|
|
6
45
|
|
|
7
46
|
function childLevel(node: MindmapNode, root: MindmapNode): number {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The document-wide "a mindmap owns fullscreen" flag.
|
|
3
|
+
*
|
|
4
|
+
* The flag drives an **unscoped**, `!important` rule (in `Mindmap.vue`) that
|
|
5
|
+
* hides the child buttons (`.mindmap-preview-actions`) of every *non*-fullscreen
|
|
6
|
+
* mindmap. So the flag and the `is-fullscreen` classes have to be reconciled
|
|
7
|
+
* together — a stale flag, or a stale class left behind by a path that only
|
|
8
|
+
* observed `fullscreenchange` (ESC) or unmounted a preview, hides every toolbar
|
|
9
|
+
* with no way back short of a page reload.
|
|
10
|
+
*
|
|
11
|
+
* Kept as plain DOM code in its own module so the invariant is unit-testable
|
|
12
|
+
* without mounting the canvas-backed component.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const FS_BODY_ATTR = 'tnMindmapFs'
|
|
16
|
+
|
|
17
|
+
/** Custom event peers dispatch to make each other leave fullscreen. */
|
|
18
|
+
export const FORCE_EXIT_FULLSCREEN_EVENT = 'tnotes-mindmap-force-exit-fullscreen'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Reconcile the document flag with the fullscreen classes.
|
|
22
|
+
*
|
|
23
|
+
* - `activeRoot` still marked fullscreen → keep the flag (a peer's stray
|
|
24
|
+
* `.is-fullscreen` must not be mistaken for the owner);
|
|
25
|
+
* - otherwise → drop the class from every tracked preview and delete the flag.
|
|
26
|
+
*/
|
|
27
|
+
export function reconcileMindmapFullscreen(
|
|
28
|
+
activeRoot: HTMLElement | null,
|
|
29
|
+
previews: ArrayLike<HTMLElement>
|
|
30
|
+
): void {
|
|
31
|
+
if (typeof document === 'undefined') return
|
|
32
|
+
if (activeRoot?.classList.contains('is-fullscreen')) {
|
|
33
|
+
document.body.dataset[FS_BODY_ATTR] = '1'
|
|
34
|
+
document.documentElement.dataset[FS_BODY_ATTR] = '1'
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
for (const el of Array.from(previews)) el.classList.remove('is-fullscreen')
|
|
38
|
+
delete document.body.dataset[FS_BODY_ATTR]
|
|
39
|
+
delete document.documentElement.dataset[FS_BODY_ATTR]
|
|
40
|
+
}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
export interface MindmapFenceOptions {
|
|
2
2
|
title?: string
|
|
3
3
|
initialExpandLevel?: number
|
|
4
|
+
/** Pane height in px (`h=480` in the fence info). */
|
|
5
|
+
height?: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const MINDMAP_MIN_HEIGHT = 200
|
|
9
|
+
export const MINDMAP_MAX_HEIGHT = 1600
|
|
10
|
+
|
|
11
|
+
export function clampMindmapHeight(value: number): number {
|
|
12
|
+
return Math.round(Math.min(MINDMAP_MAX_HEIGHT, Math.max(MINDMAP_MIN_HEIGHT, value)))
|
|
4
13
|
}
|
|
5
14
|
|
|
6
15
|
function cleanHeadingText(value: string): string {
|
|
@@ -10,7 +19,7 @@ function cleanHeadingText(value: string): string {
|
|
|
10
19
|
.trim()
|
|
11
20
|
}
|
|
12
21
|
|
|
13
|
-
/** Parse the canonical `mindmap [title] 2` fence metadata. */
|
|
22
|
+
/** Parse the canonical `mindmap [title] 2 h=480` fence metadata. */
|
|
14
23
|
export function parseMindmapFence(openLine: string): MindmapFenceOptions | null {
|
|
15
24
|
const fenceBody = openLine.trim().replace(/^`+\s*/, '')
|
|
16
25
|
const nameMatch = fenceBody.match(/^mindmap(?=\s|\[|$)/)
|
|
@@ -25,9 +34,15 @@ export function parseMindmapFence(openLine: string): MindmapFenceOptions | null
|
|
|
25
34
|
`${rest.slice(0, titleMatch.index)} ${rest.slice((titleMatch.index ?? 0) + titleMatch[0].length)}`.trim()
|
|
26
35
|
}
|
|
27
36
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
for (const token of rest.split(/\s+/).filter(Boolean)) {
|
|
38
|
+
const height = token.match(/^h=(\d+)$/)
|
|
39
|
+
if (height) {
|
|
40
|
+
options.height = clampMindmapHeight(Number(height[1]))
|
|
41
|
+
} else if (/^\d+$/.test(token) && options.initialExpandLevel === undefined) {
|
|
42
|
+
options.initialExpandLevel = Math.max(1, Number(token))
|
|
43
|
+
} else {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
31
46
|
}
|
|
32
47
|
return options
|
|
33
48
|
}
|
package/src/index.ts
CHANGED
|
@@ -11,7 +11,13 @@ export {
|
|
|
11
11
|
resolveWordListFeatures
|
|
12
12
|
} from './components/WordList/wordListFeatures'
|
|
13
13
|
export type { WordListFeatures } from './components/WordList/wordListFeatures'
|
|
14
|
-
export {
|
|
14
|
+
export {
|
|
15
|
+
clampMindmapHeight,
|
|
16
|
+
MINDMAP_MAX_HEIGHT,
|
|
17
|
+
MINDMAP_MIN_HEIGHT,
|
|
18
|
+
normalizeMindmapMarkdown,
|
|
19
|
+
parseMindmapFence
|
|
20
|
+
} from './components/Mindmap/markdown'
|
|
15
21
|
export type { MindmapFenceOptions, NormalizeMindmapOptions } from './components/Mindmap/markdown'
|
|
16
22
|
export type { NotesTableRow } from './components/NotesTable/types'
|
|
17
23
|
export {
|
package/src/styles/code.css
CHANGED
package/src/styles/prose.css
CHANGED
|
@@ -112,6 +112,12 @@
|
|
|
112
112
|
font: 0.875em var(--tn-font-mono);
|
|
113
113
|
padding: 3px 6px;
|
|
114
114
|
}
|
|
115
|
+
:is(.tn-prose, .vp-doc) :where(mark) {
|
|
116
|
+
color: inherit;
|
|
117
|
+
background: var(--tn-c-mark-bg);
|
|
118
|
+
border-radius: 2px;
|
|
119
|
+
padding: 0 2px;
|
|
120
|
+
}
|
|
115
121
|
:is(.tn-prose, .vp-doc) :where(blockquote) {
|
|
116
122
|
margin: 16px 0;
|
|
117
123
|
border-left: 2px solid var(--tn-c-divider);
|
|
@@ -185,7 +191,7 @@
|
|
|
185
191
|
}
|
|
186
192
|
|
|
187
193
|
/* Callouts — same soft-tint cards as Desk visual (no left accent bar). */
|
|
188
|
-
:is(.tn-prose, .vp-doc) :where(.tn-custom-block) {
|
|
194
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block) {
|
|
189
195
|
box-sizing: border-box;
|
|
190
196
|
margin: 16px 0;
|
|
191
197
|
padding: 12px 16px;
|
|
@@ -195,37 +201,37 @@
|
|
|
195
201
|
font-size: inherit;
|
|
196
202
|
line-height: 1.5;
|
|
197
203
|
}
|
|
198
|
-
:is(.tn-prose, .vp-doc) :where(.tn-custom-block.tip) {
|
|
204
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block.tip) {
|
|
199
205
|
background-color: var(--tn-c-highlight);
|
|
200
206
|
}
|
|
201
|
-
:is(.tn-prose, .vp-doc) :where(.tn-custom-block.info),
|
|
202
|
-
:is(.tn-prose, .vp-doc) :where(.tn-custom-block.note),
|
|
203
|
-
:is(.tn-prose, .vp-doc) :where(.tn-custom-block.details) {
|
|
207
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block.info),
|
|
208
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block.note),
|
|
209
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block.details) {
|
|
204
210
|
background-color: var(--tn-c-hover);
|
|
205
211
|
}
|
|
206
|
-
:is(.tn-prose, .vp-doc) :where(.tn-custom-block.warning) {
|
|
212
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block.warning) {
|
|
207
213
|
background-color: var(--tn-c-warning-soft);
|
|
208
214
|
}
|
|
209
|
-
:is(.tn-prose, .vp-doc) :where(.tn-custom-block.danger) {
|
|
215
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block.danger) {
|
|
210
216
|
background-color: var(--tn-c-danger-soft);
|
|
211
217
|
}
|
|
212
|
-
:is(.tn-prose, .vp-doc) :where(.tn-custom-block-title) {
|
|
218
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block-title) {
|
|
213
219
|
margin: 0 0 2px;
|
|
214
220
|
font-size: inherit;
|
|
215
221
|
font-weight: 600;
|
|
216
222
|
line-height: 1.5;
|
|
217
223
|
color: var(--tn-c-text);
|
|
218
224
|
}
|
|
219
|
-
:is(.tn-prose, .vp-doc) :where(.tn-custom-block > p:not(.tn-custom-block-title)) {
|
|
225
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block > p:not(.tn-custom-block-title)) {
|
|
220
226
|
margin: 0;
|
|
221
227
|
padding: 4px 0;
|
|
222
228
|
font-size: 1em;
|
|
223
229
|
line-height: 1.5;
|
|
224
230
|
}
|
|
225
|
-
:is(.tn-prose, .vp-doc) :where(.tn-custom-block > :last-child) {
|
|
231
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block > :last-child) {
|
|
226
232
|
margin-bottom: 0;
|
|
227
233
|
}
|
|
228
|
-
:is(.tn-prose, .vp-doc) :where(details.tn-custom-block > summary) {
|
|
234
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(details.tn-custom-block > summary) {
|
|
229
235
|
display: flex;
|
|
230
236
|
align-items: center;
|
|
231
237
|
gap: 8px;
|
|
@@ -236,13 +242,14 @@
|
|
|
236
242
|
font-weight: 600;
|
|
237
243
|
line-height: 1.5;
|
|
238
244
|
}
|
|
239
|
-
:is(.tn-prose, .vp-doc) :where(details.tn-custom-block:not([open]) > summary) {
|
|
245
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(details.tn-custom-block:not([open]) > summary) {
|
|
240
246
|
margin-bottom: 0;
|
|
241
247
|
}
|
|
242
|
-
:is(.tn-prose, .vp-doc
|
|
248
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card)
|
|
249
|
+
:where(details.tn-custom-block > summary::-webkit-details-marker) {
|
|
243
250
|
display: none;
|
|
244
251
|
}
|
|
245
|
-
:is(.tn-prose, .vp-doc) :where(details.tn-custom-block > summary::before) {
|
|
252
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(details.tn-custom-block > summary::before) {
|
|
246
253
|
content: '';
|
|
247
254
|
flex: none;
|
|
248
255
|
box-sizing: border-box;
|
|
@@ -256,7 +263,7 @@
|
|
|
256
263
|
transform-origin: 50% 50%;
|
|
257
264
|
transition: transform 120ms ease;
|
|
258
265
|
}
|
|
259
|
-
:is(.tn-prose, .vp-doc) :where(details.tn-custom-block[open] > summary::before) {
|
|
266
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(details.tn-custom-block[open] > summary::before) {
|
|
260
267
|
margin-top: -0.05em;
|
|
261
268
|
transform: rotate(45deg);
|
|
262
269
|
}
|
package/src/styles/tokens.css
CHANGED
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
--tn-c-warning-soft: rgba(234, 179, 8, 0.14);
|
|
23
23
|
--tn-c-danger-soft: rgba(244, 63, 94, 0.14);
|
|
24
24
|
--tn-c-default-soft: #e8e8e9;
|
|
25
|
+
/* ==高亮== 的底色:正文、Desk 编辑器、SSG 共用 */
|
|
26
|
+
--tn-c-mark-bg: rgba(255, 221, 0, 0.45);
|
|
25
27
|
/* 任务列表复选框(对齐语雀):16px 方框、圆角 4px、选中填充品牌绿 + 白色对勾。 */
|
|
26
28
|
--tn-c-task-check: #00b96b;
|
|
27
29
|
--tn-c-task-check-border: #d9d9d9;
|
|
@@ -69,6 +71,7 @@
|
|
|
69
71
|
--tn-c-warning-soft: rgba(234, 179, 8, 0.16);
|
|
70
72
|
--tn-c-danger-soft: rgba(244, 63, 94, 0.14);
|
|
71
73
|
--tn-c-default-soft: #2e2e32;
|
|
74
|
+
--tn-c-mark-bg: rgba(250, 204, 21, 0.32);
|
|
72
75
|
--tn-c-task-check-border: #4a4d54;
|
|
73
76
|
--tn-c-task-check-bg: transparent;
|
|
74
77
|
--tn-shadow-2: 0 4px 12px rgb(0 0 0 / 0.35);
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 思维导图编辑器外壳入口:Web 与 VS Code 扩展共用同一套三视图 UI。
|
|
3
|
-
*
|
|
4
|
-
* 与 `./mindmap` 的分工:`./mindmap` 是 SSG / Desk 笔记里内嵌的「预览 + 可选编辑」块
|
|
5
|
-
* (`components/Mindmap/Mindmap.vue`,自带 `--mm-*` 主题变量与只读渲染);本入口导出它
|
|
6
|
-
* 内部复用的编辑器组件,供自带页面外壳的宿主(Web 整页、VS Code webview)直接组装。
|
|
7
|
-
*
|
|
8
|
-
* 宿主职责:`MindmapSession` 归属、`--mm-*` 变量定义、落盘 / 剪贴板 / 图片写入
|
|
9
|
-
* (`apps/mindmap-web`、`apps/mindmap-vscode`)。浮层默认不 teleport,嵌入笔记的宿主
|
|
10
|
-
* 保持默认即可;整页宿主传 `teleport-to="body"`。
|
|
11
|
-
*/
|
|
12
|
-
export { default as AppIcon } from '../components/Mindmap/editor/AppIcon.vue'
|
|
13
|
-
export { default as CanvasContextMenu } from '../components/Mindmap/editor/CanvasContextMenu.vue'
|
|
14
|
-
export { default as CollapseMenu } from '../components/Mindmap/editor/CollapseMenu.vue'
|
|
15
|
-
export { default as IconButton } from '../components/Mindmap/editor/IconButton.vue'
|
|
16
|
-
export { default as LinkPopover } from '../components/Mindmap/editor/LinkPopover.vue'
|
|
17
|
-
export { default as MarkdownView } from '../components/Mindmap/editor/MarkdownView.vue'
|
|
18
|
-
export { default as MindmapView } from '../components/Mindmap/editor/MindmapView.vue'
|
|
19
|
-
export { default as OutlineView } from '../components/Mindmap/editor/OutlineView.vue'
|
|
20
|
-
export { default as RichInlineEditor } from '../components/Mindmap/editor/RichInlineEditor.vue'
|
|
21
|
-
export { default as SearchBar } from '../components/Mindmap/editor/SearchBar.vue'
|
|
22
|
-
export { default as SelectionToolbar } from '../components/Mindmap/editor/SelectionToolbar.vue'
|
|
23
|
-
export { default as FocusBreadcrumbs } from '../components/Mindmap/FocusBreadcrumbs.vue'
|
|
24
|
-
|
|
25
|
-
export { insertImageIntoSource } from '../components/Mindmap/editor/imagePaste'
|
|
26
|
-
export {
|
|
27
|
-
pasteCanvasOutline,
|
|
28
|
-
readMindmapClipboard,
|
|
29
|
-
writeMindmapClipboard
|
|
30
|
-
} from '../components/Mindmap/editor/mindmapClipboard'
|
|
31
|
-
export { resolveAfterDropLevel } from '../components/Mindmap/editor/outlineDrag'
|
|
32
|
-
export type { DropLevel } from '../components/Mindmap/editor/outlineDrag'
|
|
33
|
-
export {
|
|
34
|
-
altShortcut,
|
|
35
|
-
isApplePlatform,
|
|
36
|
-
primaryShortcut
|
|
37
|
-
} from '../components/Mindmap/editor/platform'
|