@tnotesjs/ui 0.3.3 → 0.4.1
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/Footprints/Footprints.vue +4 -0
- package/src/components/ImagePreview/ImagePreview.vue +1 -0
- package/src/components/Mermaid/Mermaid.vue +1 -2
- package/src/components/Mindmap/Mindmap.vue +35 -20
- package/src/components/Mindmap/editor/MindmapView.vue +91 -2
- package/src/components/Mindmap/editor/OutlineView.vue +6 -2
- package/src/components/Mindmap/editor/RichInlineEditor.vue +9 -8
- package/src/components/Mindmap/editor/SelectionToolbar.vue +38 -22
- package/src/components/Mindmap/markdown.ts +19 -4
- package/src/components/WordList/RightClickMenu.vue +12 -7
- package/src/components/WordList/WordList.vue +197 -61
- package/src/excalidraw/ExcalidrawSvg.vue +2 -1
- package/src/index.ts +7 -1
- package/src/styles/code.css +1 -1
- package/src/styles/prose.css +49 -23
- package/src/styles/swiper.css +35 -7
- package/src/styles/tokens.css +23 -4
- package/src/swiper/hydrate.ts +8 -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.1",
|
|
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.4"
|
|
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
|
+
}
|
|
@@ -186,6 +186,9 @@ watch(
|
|
|
186
186
|
<style scoped lang="scss">
|
|
187
187
|
.tn-footprints {
|
|
188
188
|
margin: 1rem 0;
|
|
189
|
+
padding: 12px 16px;
|
|
190
|
+
border: 1px solid var(--tn-c-divider);
|
|
191
|
+
border-radius: 8px;
|
|
189
192
|
color: var(--tn-c-text);
|
|
190
193
|
}
|
|
191
194
|
|
|
@@ -312,6 +315,7 @@ watch(
|
|
|
312
315
|
.tn-footprints__modal-img {
|
|
313
316
|
max-width: min(92vw, 960px);
|
|
314
317
|
max-height: 88vh;
|
|
318
|
+
border-radius: 4px;
|
|
315
319
|
object-fit: contain;
|
|
316
320
|
}
|
|
317
321
|
|
|
@@ -502,8 +502,7 @@ watch(
|
|
|
502
502
|
.tn-mermaid {
|
|
503
503
|
position: relative;
|
|
504
504
|
margin: 1rem 0;
|
|
505
|
-
|
|
506
|
-
border: 1px solid transparent;
|
|
505
|
+
border: 1px solid var(--tn-c-divider);
|
|
507
506
|
border-radius: 8px;
|
|
508
507
|
background: var(--tn-c-bg);
|
|
509
508
|
overflow: hidden;
|
|
@@ -82,6 +82,8 @@ const props = withDefaults(
|
|
|
82
82
|
editable?: boolean
|
|
83
83
|
/** Show expand-level control on the chrome (Desk). */
|
|
84
84
|
expandLevelControl?: boolean
|
|
85
|
+
/** Pane height in px; omit for the default 440px canvas. */
|
|
86
|
+
height?: number
|
|
85
87
|
/** Explicit dark mode; omit to auto-detect html.dark / data-theme=dark. */
|
|
86
88
|
isDark?: boolean
|
|
87
89
|
/**
|
|
@@ -101,6 +103,7 @@ const props = withDefaults(
|
|
|
101
103
|
initialExpandLevel: 3,
|
|
102
104
|
editable: false,
|
|
103
105
|
expandLevelControl: false,
|
|
106
|
+
height: undefined,
|
|
104
107
|
isDark: undefined,
|
|
105
108
|
resolveImageSrc: undefined,
|
|
106
109
|
writeAsset: undefined
|
|
@@ -161,6 +164,9 @@ function decodeContent(value: string): string {
|
|
|
161
164
|
}
|
|
162
165
|
}
|
|
163
166
|
|
|
167
|
+
const paneStyle = computed(() =>
|
|
168
|
+
props.height ? { '--mindmap-pane-height': `${props.height}px` } : undefined
|
|
169
|
+
)
|
|
164
170
|
const rawContent = computed(() => props.source || decodeContent(props.content || ''))
|
|
165
171
|
const normalizedContent = computed(() => normalizeMindmapMarkdown(rawContent.value))
|
|
166
172
|
|
|
@@ -508,9 +514,14 @@ function zoomToFit(): void {
|
|
|
508
514
|
viewer?.zoomToFit()
|
|
509
515
|
}
|
|
510
516
|
|
|
511
|
-
|
|
517
|
+
/** 画布上的浮层自己管焦点;被抢走会让节点编辑提前提交,工具栏点了也不生效 */
|
|
518
|
+
const CANVAS_OVERLAY_SELECTOR =
|
|
519
|
+
'.selection-toolbar, .mm-edit-input, .link-popover, .canvas-context-menu'
|
|
520
|
+
|
|
521
|
+
function activateCanvas(event: PointerEvent): void {
|
|
512
522
|
isCanvasActive.value = true
|
|
513
523
|
if (!props.editable) return
|
|
524
|
+
if (event.target instanceof Element && event.target.closest(CANVAS_OVERLAY_SELECTOR)) return
|
|
514
525
|
const editorEl = canvasHost.value?.querySelector<HTMLElement>('.mm-editor')
|
|
515
526
|
editorEl?.focus({ preventScroll: true })
|
|
516
527
|
}
|
|
@@ -729,6 +740,7 @@ onBeforeUnmount(() => {
|
|
|
729
740
|
<section
|
|
730
741
|
ref="previewRoot"
|
|
731
742
|
class="mindmap-preview"
|
|
743
|
+
:style="paneStyle"
|
|
732
744
|
:class="{
|
|
733
745
|
'is-dark': dark,
|
|
734
746
|
'is-fullscreen': isFullscreen,
|
|
@@ -1039,7 +1051,7 @@ onBeforeUnmount(() => {
|
|
|
1039
1051
|
.mindmap-canvas-host {
|
|
1040
1052
|
position: relative;
|
|
1041
1053
|
width: 100%;
|
|
1042
|
-
height: 440px;
|
|
1054
|
+
height: var(--mindmap-pane-height, 440px);
|
|
1043
1055
|
overflow: hidden;
|
|
1044
1056
|
/*
|
|
1045
1057
|
* No background of its own: the canvas is cleared to transparent, and the
|
|
@@ -1051,7 +1063,7 @@ onBeforeUnmount(() => {
|
|
|
1051
1063
|
touch-action: none;
|
|
1052
1064
|
user-select: none;
|
|
1053
1065
|
|
|
1054
|
-
/* CanvasEditor overlay + chrome tokens
|
|
1066
|
+
/* CanvasEditor overlay + chrome tokens. */
|
|
1055
1067
|
--mm-canvas-bg: var(--tn-c-bg);
|
|
1056
1068
|
--mm-panel-bg: var(--tn-c-bg-soft, var(--tn-c-bg));
|
|
1057
1069
|
--mm-text: var(--tn-c-text);
|
|
@@ -1091,7 +1103,7 @@ onBeforeUnmount(() => {
|
|
|
1091
1103
|
pointer-events: none;
|
|
1092
1104
|
}
|
|
1093
1105
|
|
|
1094
|
-
/* Marquee box — required for box-select feedback
|
|
1106
|
+
/* Marquee box — required for box-select feedback. */
|
|
1095
1107
|
.mindmap-canvas-host:deep(.mm-selection-box) {
|
|
1096
1108
|
position: absolute;
|
|
1097
1109
|
z-index: 8;
|
|
@@ -1169,16 +1181,17 @@ onBeforeUnmount(() => {
|
|
|
1169
1181
|
.mindmap-canvas-host:deep(.mm-edit-input .inline-run.highlight:not(.code)) {
|
|
1170
1182
|
padding: 0 1px;
|
|
1171
1183
|
border-radius: 2px;
|
|
1172
|
-
background:
|
|
1173
|
-
color: #242424;
|
|
1184
|
+
background: var(--tn-c-mark-bg, rgba(255, 221, 0, 0.45));
|
|
1174
1185
|
}
|
|
1175
1186
|
.mindmap-canvas-host:deep(.mm-edit-input .inline-run.code) {
|
|
1176
1187
|
padding: 0 4px;
|
|
1188
|
+
border: 1px solid var(--tn-c-inline-code-border);
|
|
1177
1189
|
border-radius: 4px;
|
|
1178
|
-
background:
|
|
1179
|
-
color:
|
|
1190
|
+
background: var(--tn-c-inline-code-bg);
|
|
1191
|
+
color: inherit;
|
|
1180
1192
|
font-family: var(--tn-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
1181
|
-
font-size:
|
|
1193
|
+
font-size: 1em;
|
|
1194
|
+
font-weight: 400;
|
|
1182
1195
|
}
|
|
1183
1196
|
.mindmap-canvas-host:deep(.mm-edit-input .inline-run.link) {
|
|
1184
1197
|
color: var(--mm-accent);
|
|
@@ -1213,13 +1226,13 @@ onBeforeUnmount(() => {
|
|
|
1213
1226
|
}
|
|
1214
1227
|
|
|
1215
1228
|
.mindmap-outline {
|
|
1216
|
-
max-height: 560px;
|
|
1229
|
+
max-height: var(--mindmap-pane-height, 560px);
|
|
1217
1230
|
padding: 18px 22px 22px;
|
|
1218
1231
|
overflow: auto;
|
|
1219
1232
|
}
|
|
1220
1233
|
|
|
1221
1234
|
.mindmap-outline.is-editable {
|
|
1222
|
-
height: 440px;
|
|
1235
|
+
height: var(--mindmap-pane-height, 440px);
|
|
1223
1236
|
max-height: none;
|
|
1224
1237
|
padding: 0;
|
|
1225
1238
|
overflow: hidden;
|
|
@@ -1228,7 +1241,7 @@ onBeforeUnmount(() => {
|
|
|
1228
1241
|
}
|
|
1229
1242
|
|
|
1230
1243
|
.mindmap-source-wrap.is-editable {
|
|
1231
|
-
height: 440px;
|
|
1244
|
+
height: var(--mindmap-pane-height, 440px);
|
|
1232
1245
|
overflow: hidden;
|
|
1233
1246
|
display: flex;
|
|
1234
1247
|
flex-direction: column;
|
|
@@ -1315,7 +1328,7 @@ onBeforeUnmount(() => {
|
|
|
1315
1328
|
max-width: min(100%, 560px);
|
|
1316
1329
|
max-height: 360px;
|
|
1317
1330
|
margin: 5px 0 12px 25px;
|
|
1318
|
-
border-radius:
|
|
1331
|
+
border-radius: 4px;
|
|
1319
1332
|
}
|
|
1320
1333
|
.mindmap-outline :deep(.is-bold) {
|
|
1321
1334
|
font-weight: 700;
|
|
@@ -1332,15 +1345,17 @@ onBeforeUnmount(() => {
|
|
|
1332
1345
|
.mindmap-outline :deep(.is-highlight) {
|
|
1333
1346
|
padding: 0 2px;
|
|
1334
1347
|
border-radius: 2px;
|
|
1335
|
-
background:
|
|
1336
|
-
color: #252525;
|
|
1348
|
+
background: var(--tn-c-mark-bg, rgba(255, 221, 0, 0.45));
|
|
1337
1349
|
}
|
|
1338
1350
|
.mindmap-outline :deep(.is-code) {
|
|
1339
|
-
padding:
|
|
1351
|
+
padding: 0 4px;
|
|
1352
|
+
border: 1px solid var(--tn-c-inline-code-border);
|
|
1340
1353
|
border-radius: 4px;
|
|
1341
|
-
background: var(--tn-c-bg
|
|
1342
|
-
color:
|
|
1354
|
+
background: var(--tn-c-inline-code-bg);
|
|
1355
|
+
color: inherit;
|
|
1343
1356
|
font-family: var(--tn-font-mono);
|
|
1357
|
+
font-size: 1em;
|
|
1358
|
+
font-weight: 400;
|
|
1344
1359
|
}
|
|
1345
1360
|
.mindmap-outline :deep(.is-link) {
|
|
1346
1361
|
color: var(--tn-c-brand);
|
|
@@ -1349,7 +1364,7 @@ onBeforeUnmount(() => {
|
|
|
1349
1364
|
}
|
|
1350
1365
|
|
|
1351
1366
|
.mindmap-source {
|
|
1352
|
-
max-height: 560px;
|
|
1367
|
+
max-height: var(--mindmap-pane-height, 560px);
|
|
1353
1368
|
margin: 0;
|
|
1354
1369
|
padding: 18px 22px;
|
|
1355
1370
|
overflow: auto;
|
|
@@ -1431,7 +1446,7 @@ onBeforeUnmount(() => {
|
|
|
1431
1446
|
|
|
1432
1447
|
@media (max-width: 768px) {
|
|
1433
1448
|
.mindmap-canvas-host {
|
|
1434
|
-
height: 360px;
|
|
1449
|
+
height: var(--mindmap-pane-height, 360px);
|
|
1435
1450
|
}
|
|
1436
1451
|
.mindmap-preview-actions {
|
|
1437
1452
|
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
|
|
@@ -2341,7 +2345,7 @@ function onDragUp() {
|
|
|
2341
2345
|
position: relative;
|
|
2342
2346
|
max-width: 100%;
|
|
2343
2347
|
line-height: 0;
|
|
2344
|
-
border-radius:
|
|
2348
|
+
border-radius: 4px;
|
|
2345
2349
|
overflow: hidden;
|
|
2346
2350
|
background: color-mix(in srgb, var(--mm-text-dim) 12%, transparent);
|
|
2347
2351
|
box-shadow: 0 0 0 1px color-mix(in srgb, var(--mm-text-dim) 25%, transparent);
|
|
@@ -416,16 +416,17 @@ 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
|
-
padding:
|
|
424
|
-
border
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
font-
|
|
422
|
+
padding: 0 4px;
|
|
423
|
+
border: 1px solid var(--tn-c-inline-code-border, #e7e9e8);
|
|
424
|
+
border-radius: 4px;
|
|
425
|
+
background: var(--tn-c-inline-code-bg, rgba(0, 0, 0, 0.06));
|
|
426
|
+
color: inherit;
|
|
427
|
+
font-family: var(--tn-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
|
|
428
|
+
font-size: 1em;
|
|
429
|
+
font-weight: 400;
|
|
429
430
|
box-decoration-break: clone;
|
|
430
431
|
-webkit-box-decoration-break: clone;
|
|
431
432
|
}
|
|
@@ -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
|
}
|