@tnotesjs/ui 0.1.5 → 0.2.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 +30 -97
- package/dist/code/highlight.js +6 -15
- package/dist/components/Footprints/parse.js +1 -3
- package/dist/components/Mindmap/markdown.d.ts +1 -7
- package/dist/components/Mindmap/markdown.js +1 -15
- package/package.json +24 -8
- package/src/browser/clipboard.ts +16 -19
- package/src/browser/theme.ts +10 -15
- package/src/code/highlight.test.ts +23 -6
- package/src/code/highlight.ts +89 -105
- package/src/components/Badge/Badge.vue +4 -4
- package/src/components/CodeBlock/CodeBlock.vue +43 -53
- package/src/components/CodeGroup/CodeGroup.vue +32 -35
- package/src/components/Footprints/Footprints.vue +2 -14
- package/src/components/Footprints/parse.ts +1 -3
- package/src/components/ImagePreview/ImagePreview.vue +101 -126
- package/src/components/Mermaid/Mermaid.vue +9 -12
- package/src/components/Mindmap/FocusBreadcrumbs.test.ts +40 -30
- package/src/components/Mindmap/FocusBreadcrumbs.vue +31 -28
- package/src/components/Mindmap/InlineRuns.ts +25 -16
- package/src/components/Mindmap/Mindmap.vue +147 -81
- package/src/components/Mindmap/MindmapOutlineNode.vue +4 -1
- package/src/components/Mindmap/editor/AppIcon.vue +32 -11
- package/src/components/Mindmap/editor/CanvasContextMenu.vue +126 -49
- package/src/components/Mindmap/editor/LinkPopover.vue +101 -23
- package/src/components/Mindmap/editor/MarkdownView.vue +7 -4
- package/src/components/Mindmap/editor/MindmapView.vue +11 -3
- package/src/components/Mindmap/editor/OutlineView.vue +144 -58
- package/src/components/Mindmap/editor/RichInlineEditor.vue +93 -44
- package/src/components/Mindmap/editor/SelectionToolbar.vue +133 -41
- package/src/components/Mindmap/editor/canvasClipboard.ts +1 -5
- package/src/components/Mindmap/editor/imagePaste.ts +1 -1
- package/src/components/Mindmap/editor/mindmapClipboard.ts +17 -5
- package/src/components/Mindmap/editor/outlineDrag.ts +1 -1
- package/src/components/Mindmap/markdown.ts +10 -29
- package/src/components/WordList/RightClickMenu.vue +10 -35
- package/src/components/WordList/WordList.vue +19 -42
- package/src/components/sharedMedia.test.ts +94 -112
- package/src/entries/bilibili-video.ts +1 -1
- package/src/entries/excalidraw-editor.ts +20 -0
- package/src/entries/excalidraw-view.ts +15 -0
- package/src/entries/footprints.ts +1 -1
- package/src/entries/mermaid.ts +1 -1
- package/src/entries/mindmap.ts +1 -1
- package/src/entries/notes-table.ts +2 -2
- package/src/entries/word-list.ts +1 -1
- package/src/excalidraw/ExcalidrawSvg.vue +125 -0
- package/src/excalidraw/editorHost.ts +110 -0
- package/src/excalidraw/editorSession.test.ts +205 -0
- package/src/excalidraw/editorSession.ts +224 -0
- package/src/excalidraw/exporter.ts +50 -0
- package/src/excalidraw/focus.test.ts +60 -0
- package/src/excalidraw/focus.ts +33 -0
- package/src/excalidraw/fonts.test.ts +66 -0
- package/src/excalidraw/fonts.ts +92 -0
- package/src/excalidraw/renderer.test.ts +148 -0
- package/src/excalidraw/renderer.ts +105 -0
- package/src/excalidraw/scene.ts +93 -0
- package/src/index.ts +25 -33
- package/src/markdown/image.test.ts +1 -3
- package/src/markdown/image.ts +4 -13
- package/src/styles/code.css +5 -5
- package/src/styles/prose.css +1 -1
- package/src/styles/swiper.css +1 -1
- package/src/styles/tokens.css +5 -6
- package/src/swiper/hydrate.test.ts +37 -41
- package/src/swiper/hydrate.ts +91 -102
|
@@ -8,22 +8,25 @@ import {
|
|
|
8
8
|
replaceInlineRange,
|
|
9
9
|
richSelectionOffsets,
|
|
10
10
|
setRichSelection,
|
|
11
|
-
stripInline
|
|
11
|
+
stripInline
|
|
12
12
|
} from '@tnotesjs/mindmap-core'
|
|
13
13
|
import type { InlineLink, RichInlineEditorElement } from '@tnotesjs/mindmap-core'
|
|
14
14
|
|
|
15
|
-
const props = withDefaults(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
const props = withDefaults(
|
|
16
|
+
defineProps<{
|
|
17
|
+
editorId: string
|
|
18
|
+
raw: string
|
|
19
|
+
active: boolean
|
|
20
|
+
done?: boolean
|
|
21
|
+
placeholder?: string
|
|
22
|
+
pasteMode?: 'single-line' | 'outline'
|
|
23
|
+
}>(),
|
|
24
|
+
{
|
|
25
|
+
done: false,
|
|
26
|
+
placeholder: '',
|
|
27
|
+
pasteMode: 'single-line'
|
|
28
|
+
}
|
|
29
|
+
)
|
|
27
30
|
|
|
28
31
|
interface DraftPayload {
|
|
29
32
|
raw: string
|
|
@@ -89,7 +92,10 @@ function payload(): DraftPayload {
|
|
|
89
92
|
return { raw: draftRaw.value, text: stripInline(draftRaw.value) }
|
|
90
93
|
}
|
|
91
94
|
|
|
92
|
-
function renderDraft(
|
|
95
|
+
function renderDraft(
|
|
96
|
+
nextRaw: string,
|
|
97
|
+
selection?: { start: number; end?: number; direction?: 'forward' | 'backward' }
|
|
98
|
+
) {
|
|
93
99
|
draftRaw.value = nextRaw
|
|
94
100
|
dirty = nextRaw !== sourceRaw
|
|
95
101
|
renderInlineDom()
|
|
@@ -112,7 +118,7 @@ function replaceSelection(textToInsert: string) {
|
|
|
112
118
|
end: text.value.length,
|
|
113
119
|
anchor: text.value.length,
|
|
114
120
|
focus: text.value.length,
|
|
115
|
-
direction: 'forward' as const
|
|
121
|
+
direction: 'forward' as const
|
|
116
122
|
}
|
|
117
123
|
const nextRaw = replaceInlineRange(draftRaw.value, selection.start, selection.end, textToInsert)
|
|
118
124
|
const caret = selection.start + textToInsert.length
|
|
@@ -136,7 +142,7 @@ function onBeforeInput(event: InputEvent) {
|
|
|
136
142
|
end: text.value.length,
|
|
137
143
|
anchor: text.value.length,
|
|
138
144
|
focus: text.value.length,
|
|
139
|
-
direction: 'forward' as const
|
|
145
|
+
direction: 'forward' as const
|
|
140
146
|
}
|
|
141
147
|
|
|
142
148
|
if (event.inputType === 'insertText' || event.inputType === 'insertReplacementText') {
|
|
@@ -149,25 +155,31 @@ function onBeforeInput(event: InputEvent) {
|
|
|
149
155
|
|
|
150
156
|
if (event.inputType === 'deleteContentBackward') {
|
|
151
157
|
event.preventDefault()
|
|
152
|
-
const start =
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
const start =
|
|
159
|
+
selection.start === selection.end
|
|
160
|
+
? previousGraphemeOffset(text.value, selection.start)
|
|
161
|
+
: selection.start
|
|
155
162
|
renderDraft(replaceInlineRange(draftRaw.value, start, selection.end, ''), { start })
|
|
156
163
|
return
|
|
157
164
|
}
|
|
158
165
|
|
|
159
166
|
if (event.inputType === 'deleteContentForward') {
|
|
160
167
|
event.preventDefault()
|
|
161
|
-
const end =
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
168
|
+
const end =
|
|
169
|
+
selection.start === selection.end
|
|
170
|
+
? nextGraphemeOffset(text.value, selection.end)
|
|
171
|
+
: selection.end
|
|
172
|
+
renderDraft(replaceInlineRange(draftRaw.value, selection.start, end, ''), {
|
|
173
|
+
start: selection.start
|
|
174
|
+
})
|
|
165
175
|
return
|
|
166
176
|
}
|
|
167
177
|
|
|
168
178
|
if (event.inputType === 'deleteByCut') {
|
|
169
179
|
event.preventDefault()
|
|
170
|
-
renderDraft(replaceInlineRange(draftRaw.value, selection.start, selection.end, ''), {
|
|
180
|
+
renderDraft(replaceInlineRange(draftRaw.value, selection.start, selection.end, ''), {
|
|
181
|
+
start: selection.start
|
|
182
|
+
})
|
|
171
183
|
return
|
|
172
184
|
}
|
|
173
185
|
|
|
@@ -240,8 +252,11 @@ function onLinkClick(link: InlineLink, event: Event) {
|
|
|
240
252
|
emit('linkClick', link, event)
|
|
241
253
|
}
|
|
242
254
|
|
|
243
|
-
function linkFromTarget(
|
|
244
|
-
|
|
255
|
+
function linkFromTarget(
|
|
256
|
+
target: EventTarget | null
|
|
257
|
+
): { element: HTMLElement; link: InlineLink } | null {
|
|
258
|
+
const element =
|
|
259
|
+
target instanceof HTMLElement ? target.closest<HTMLElement>('.inline-run.link') : null
|
|
245
260
|
const root = rootRef.value
|
|
246
261
|
if (!element || !root?.contains(element) || !element.dataset.linkUrl) return null
|
|
247
262
|
return {
|
|
@@ -249,22 +264,30 @@ function linkFromTarget(target: EventTarget | null): { element: HTMLElement; lin
|
|
|
249
264
|
link: {
|
|
250
265
|
url: element.dataset.linkUrl,
|
|
251
266
|
rawStart: Number(element.dataset.linkRawStart ?? 0),
|
|
252
|
-
rawEnd: Number(element.dataset.linkRawEnd ?? 0)
|
|
253
|
-
}
|
|
267
|
+
rawEnd: Number(element.dataset.linkRawEnd ?? 0)
|
|
268
|
+
}
|
|
254
269
|
}
|
|
255
270
|
}
|
|
256
271
|
|
|
257
272
|
function onMouseOver(event: MouseEvent) {
|
|
258
273
|
if (props.active) return
|
|
259
274
|
const found = linkFromTarget(event.target)
|
|
260
|
-
if (
|
|
275
|
+
if (
|
|
276
|
+
!found ||
|
|
277
|
+
(event.relatedTarget instanceof Node && found.element.contains(event.relatedTarget))
|
|
278
|
+
)
|
|
279
|
+
return
|
|
261
280
|
emit('linkEnter', found.link, event)
|
|
262
281
|
}
|
|
263
282
|
|
|
264
283
|
function onMouseOut(event: MouseEvent) {
|
|
265
284
|
if (props.active) return
|
|
266
285
|
const found = linkFromTarget(event.target)
|
|
267
|
-
if (
|
|
286
|
+
if (
|
|
287
|
+
!found ||
|
|
288
|
+
(event.relatedTarget instanceof Node && found.element.contains(event.relatedTarget))
|
|
289
|
+
)
|
|
290
|
+
return
|
|
268
291
|
emit('linkLeave')
|
|
269
292
|
}
|
|
270
293
|
|
|
@@ -288,13 +311,13 @@ function installCompatibilityApi(root: RichInlineEditorElement) {
|
|
|
288
311
|
get: () => stripInline(draftRaw.value),
|
|
289
312
|
set: (next: string) => {
|
|
290
313
|
renderDraft(replaceInlineDisplayText(draftRaw.value, String(next)))
|
|
291
|
-
}
|
|
314
|
+
}
|
|
292
315
|
},
|
|
293
316
|
rawValue: { configurable: true, get: () => draftRaw.value },
|
|
294
317
|
selectionStart: { configurable: true, get: () => richSelectionOffsets(root)?.start ?? 0 },
|
|
295
318
|
selectionEnd: { configurable: true, get: () => richSelectionOffsets(root)?.end ?? 0 },
|
|
296
319
|
isDirty: { configurable: true, get: () => dirty },
|
|
297
|
-
isComposing: { configurable: true, get: () => composing }
|
|
320
|
+
isComposing: { configurable: true, get: () => composing }
|
|
298
321
|
})
|
|
299
322
|
root.setSelectionRange = (start: number, end: number) => setRichSelection(root, start, end)
|
|
300
323
|
root.markCommitted = () => {
|
|
@@ -320,7 +343,7 @@ watch(
|
|
|
320
343
|
draftRaw.value = raw
|
|
321
344
|
renderInlineDom()
|
|
322
345
|
}
|
|
323
|
-
}
|
|
346
|
+
}
|
|
324
347
|
)
|
|
325
348
|
|
|
326
349
|
watch(
|
|
@@ -328,7 +351,7 @@ watch(
|
|
|
328
351
|
(active) => {
|
|
329
352
|
if (!active) emitCommit()
|
|
330
353
|
if (!composing && !dirty) renderInlineDom()
|
|
331
|
-
}
|
|
354
|
+
}
|
|
332
355
|
)
|
|
333
356
|
|
|
334
357
|
onBeforeUnmount(emitCommit)
|
|
@@ -371,23 +394,49 @@ onBeforeUnmount(emitCommit)
|
|
|
371
394
|
color: var(--mm-text-dim);
|
|
372
395
|
pointer-events: none;
|
|
373
396
|
}
|
|
374
|
-
.rich-inline-editor.active {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
.rich-inline-editor :deep(.inline-run.
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
.rich-inline-editor :deep(.inline-run.
|
|
397
|
+
.rich-inline-editor.active {
|
|
398
|
+
cursor: text;
|
|
399
|
+
}
|
|
400
|
+
.rich-inline-editor :deep(.inline-run.bold) {
|
|
401
|
+
font-weight: 700;
|
|
402
|
+
}
|
|
403
|
+
.rich-inline-editor :deep(.inline-run.italic) {
|
|
404
|
+
font-style: italic;
|
|
405
|
+
}
|
|
406
|
+
.rich-inline-editor :deep(.inline-run.underline) {
|
|
407
|
+
text-decoration-line: underline;
|
|
408
|
+
text-underline-offset: 3px;
|
|
409
|
+
}
|
|
410
|
+
.rich-inline-editor :deep(.inline-run.strike) {
|
|
411
|
+
text-decoration-line: line-through;
|
|
412
|
+
}
|
|
413
|
+
.rich-inline-editor :deep(.inline-run.underline.strike) {
|
|
414
|
+
text-decoration-line: underline line-through;
|
|
415
|
+
}
|
|
416
|
+
.rich-inline-editor :deep(.inline-run.highlight:not(.code)) {
|
|
417
|
+
padding: 0 1px;
|
|
418
|
+
border-radius: 2px;
|
|
419
|
+
background: #fff36a;
|
|
420
|
+
color: #242424;
|
|
421
|
+
}
|
|
381
422
|
.rich-inline-editor :deep(.inline-run.code) {
|
|
382
423
|
padding: 2px 6px;
|
|
383
424
|
border-radius: 5px;
|
|
384
425
|
background: color-mix(in srgb, var(--mm-text) 18%, transparent);
|
|
385
426
|
color: #f08a6e;
|
|
386
427
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
387
|
-
font-size: .92em;
|
|
428
|
+
font-size: 0.92em;
|
|
388
429
|
box-decoration-break: clone;
|
|
389
430
|
-webkit-box-decoration-break: clone;
|
|
390
431
|
}
|
|
391
|
-
.rich-inline-editor :deep(.inline-run.link) {
|
|
392
|
-
|
|
432
|
+
.rich-inline-editor :deep(.inline-run.link) {
|
|
433
|
+
color: var(--mm-accent);
|
|
434
|
+
cursor: pointer;
|
|
435
|
+
text-decoration: underline;
|
|
436
|
+
text-underline-offset: 3px;
|
|
437
|
+
}
|
|
438
|
+
.rich-inline-editor.done {
|
|
439
|
+
color: var(--mm-text-dim);
|
|
440
|
+
text-decoration: line-through;
|
|
441
|
+
}
|
|
393
442
|
</style>
|
|
@@ -17,7 +17,7 @@ const props = withDefaults(
|
|
|
17
17
|
placement?: 'fixed' | 'canvas-bottom'
|
|
18
18
|
activeFormats?: Partial<Record<InlineFormat, boolean>>
|
|
19
19
|
}>(),
|
|
20
|
-
{ placement: 'fixed' }
|
|
20
|
+
{ placement: 'fixed' }
|
|
21
21
|
)
|
|
22
22
|
|
|
23
23
|
const emit = defineEmits<{
|
|
@@ -35,7 +35,7 @@ const formats: Array<{ id: InlineFormat; text: string; label: string }> = [
|
|
|
35
35
|
{ id: 'italic', text: 'I', label: `斜体 (${primaryShortcut('I')})` },
|
|
36
36
|
{ id: 'underline', text: 'U', label: `下划线 (${primaryShortcut('U')})` },
|
|
37
37
|
{ id: 'strike', text: 'S', label: `删除线 (${primaryShortcut('Enter')})` },
|
|
38
|
-
{ id: 'highlight', text: '▰', label: '高亮' }
|
|
38
|
+
{ id: 'highlight', text: '▰', label: '高亮' }
|
|
39
39
|
]
|
|
40
40
|
|
|
41
41
|
const toolbarRef = ref<HTMLElement | null>(null)
|
|
@@ -55,7 +55,10 @@ const toolbarStyle = computed(() => {
|
|
|
55
55
|
const left = Math.min(maxLeft, Math.max(VIEWPORT_MARGIN, point.left - width / 2))
|
|
56
56
|
const preferredTop = point.top - height - ANCHOR_GAP
|
|
57
57
|
const belowTop = point.top + ANCHOR_GAP
|
|
58
|
-
const top = Math.min(
|
|
58
|
+
const top = Math.min(
|
|
59
|
+
maxTop,
|
|
60
|
+
Math.max(VIEWPORT_MARGIN, preferredTop < VIEWPORT_MARGIN ? belowTop : preferredTop)
|
|
61
|
+
)
|
|
59
62
|
return { left: `${left}px`, top: `${top}px` }
|
|
60
63
|
})
|
|
61
64
|
|
|
@@ -69,7 +72,7 @@ function measureToolbar() {
|
|
|
69
72
|
|
|
70
73
|
watch(
|
|
71
74
|
() => [props.mode, props.placement, props.position?.left, props.position?.top],
|
|
72
|
-
measureToolbar
|
|
75
|
+
measureToolbar
|
|
73
76
|
)
|
|
74
77
|
onMounted(() => {
|
|
75
78
|
measureToolbar()
|
|
@@ -88,33 +91,88 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
|
|
|
88
91
|
:aria-label="mode === 'text' ? '文字格式工具栏' : '多主题工具栏'"
|
|
89
92
|
@pointerdown.prevent
|
|
90
93
|
>
|
|
94
|
+
<button
|
|
95
|
+
v-for="item in formats"
|
|
96
|
+
:key="item.id"
|
|
97
|
+
type="button"
|
|
98
|
+
class="format-button"
|
|
99
|
+
:class="[{ active: activeFormats?.[item.id] }, `is-${item.id}`]"
|
|
100
|
+
:data-tooltip="item.label"
|
|
101
|
+
:aria-label="item.label"
|
|
102
|
+
@click="emit('format', item.id)"
|
|
103
|
+
>
|
|
104
|
+
<AppIcon v-if="item.id === 'highlight'" name="highlight" :size="21" />
|
|
105
|
+
<template v-else>{{ item.text }}</template>
|
|
106
|
+
</button>
|
|
107
|
+
<span class="toolbar-divider" />
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
class="tool-button"
|
|
111
|
+
:data-tooltip="`添加/取消待办 (${primaryShortcut('L', { shift: true })})`"
|
|
112
|
+
aria-label="添加或取消待办"
|
|
113
|
+
@click="emit('task')"
|
|
114
|
+
>
|
|
115
|
+
<AppIcon name="check" :size="20" />
|
|
116
|
+
</button>
|
|
117
|
+
<template v-if="mode === 'text'">
|
|
91
118
|
<button
|
|
92
|
-
v-for="item in formats"
|
|
93
|
-
:key="item.id"
|
|
94
119
|
type="button"
|
|
95
|
-
class="
|
|
96
|
-
:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
@click="emit('format', item.id)"
|
|
120
|
+
class="tool-button"
|
|
121
|
+
:data-tooltip="`添加图片 (${altShortcut('Enter')})`"
|
|
122
|
+
aria-label="添加图片"
|
|
123
|
+
@click="emit('image')"
|
|
100
124
|
>
|
|
101
|
-
<AppIcon
|
|
102
|
-
<template v-else>{{ item.text }}</template>
|
|
125
|
+
<AppIcon name="image" :size="20" />
|
|
103
126
|
</button>
|
|
104
|
-
<
|
|
105
|
-
|
|
106
|
-
|
|
127
|
+
<button
|
|
128
|
+
type="button"
|
|
129
|
+
class="tool-button"
|
|
130
|
+
:data-tooltip="`添加链接 (${primaryShortcut('K')})`"
|
|
131
|
+
aria-label="添加链接"
|
|
132
|
+
@click="emit('link')"
|
|
133
|
+
>
|
|
134
|
+
<AppIcon name="link" :size="20" />
|
|
107
135
|
</button>
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
</
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
class="tool-button code-button"
|
|
139
|
+
:data-tooltip="`行内代码 (${primaryShortcut('E')})`"
|
|
140
|
+
aria-label="行内代码"
|
|
141
|
+
@click="emit('format', 'code')"
|
|
142
|
+
>
|
|
143
|
+
</>
|
|
144
|
+
</button>
|
|
145
|
+
</template>
|
|
146
|
+
<button
|
|
147
|
+
v-else
|
|
148
|
+
type="button"
|
|
149
|
+
class="tool-button"
|
|
150
|
+
:data-tooltip="`复制 (${primaryShortcut('C')})`"
|
|
151
|
+
aria-label="复制所选主题"
|
|
152
|
+
@click="emit('copy')"
|
|
153
|
+
>
|
|
154
|
+
<AppIcon name="copy" :size="20" />
|
|
155
|
+
</button>
|
|
156
|
+
<span class="toolbar-divider" />
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
class="tool-button"
|
|
160
|
+
:data-tooltip="`清除样式 (${primaryShortcut('\\')})`"
|
|
161
|
+
aria-label="清除样式"
|
|
162
|
+
@click="emit('clear')"
|
|
163
|
+
>
|
|
164
|
+
<AppIcon name="clearFormat" :size="20" />
|
|
165
|
+
</button>
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
class="tool-button danger"
|
|
169
|
+
:data-tooltip="`删除 (${primaryShortcut('D', { shift: true })})`"
|
|
170
|
+
aria-label="删除"
|
|
171
|
+
@click="emit('delete')"
|
|
172
|
+
>
|
|
173
|
+
<AppIcon name="trash" :size="20" />
|
|
174
|
+
</button>
|
|
175
|
+
</div>
|
|
118
176
|
</template>
|
|
119
177
|
|
|
120
178
|
<style scoped>
|
|
@@ -130,7 +188,7 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
|
|
|
130
188
|
border-radius: 11px;
|
|
131
189
|
background: color-mix(in srgb, var(--mm-panel-bg) 94%, #545760 6%);
|
|
132
190
|
color: var(--mm-text);
|
|
133
|
-
box-shadow: 0 10px 30px rgb(0 0 0 / .24);
|
|
191
|
+
box-shadow: 0 10px 30px rgb(0 0 0 / 0.24);
|
|
134
192
|
}
|
|
135
193
|
.selection-toolbar.is-canvas-anchored {
|
|
136
194
|
position: absolute;
|
|
@@ -140,7 +198,8 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
|
|
|
140
198
|
bottom: 18px;
|
|
141
199
|
transform: translateX(-50%);
|
|
142
200
|
}
|
|
143
|
-
.format-button,
|
|
201
|
+
.format-button,
|
|
202
|
+
.tool-button {
|
|
144
203
|
position: relative;
|
|
145
204
|
display: inline-flex;
|
|
146
205
|
width: 34px;
|
|
@@ -154,7 +213,8 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
|
|
|
154
213
|
cursor: pointer;
|
|
155
214
|
font-size: 21px;
|
|
156
215
|
}
|
|
157
|
-
.format-button::after,
|
|
216
|
+
.format-button::after,
|
|
217
|
+
.tool-button::after {
|
|
158
218
|
position: absolute;
|
|
159
219
|
z-index: 2;
|
|
160
220
|
bottom: calc(100% + 9px);
|
|
@@ -165,7 +225,7 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
|
|
|
165
225
|
border-radius: 7px;
|
|
166
226
|
background: #f5f5f7;
|
|
167
227
|
color: #25262b;
|
|
168
|
-
box-shadow: 0 5px 18px rgb(0 0 0 / .22);
|
|
228
|
+
box-shadow: 0 5px 18px rgb(0 0 0 / 0.22);
|
|
169
229
|
content: attr(data-tooltip);
|
|
170
230
|
font-family: system-ui, sans-serif;
|
|
171
231
|
font-size: 13px;
|
|
@@ -177,15 +237,47 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
|
|
|
177
237
|
transform: translateX(-50%);
|
|
178
238
|
white-space: nowrap;
|
|
179
239
|
}
|
|
180
|
-
.format-button:hover::after,
|
|
181
|
-
.
|
|
182
|
-
.format-button:
|
|
183
|
-
.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
.format-button
|
|
187
|
-
.
|
|
188
|
-
.
|
|
189
|
-
|
|
190
|
-
|
|
240
|
+
.format-button:hover::after,
|
|
241
|
+
.tool-button:hover::after,
|
|
242
|
+
.format-button:focus-visible::after,
|
|
243
|
+
.tool-button:focus-visible::after {
|
|
244
|
+
opacity: 1;
|
|
245
|
+
}
|
|
246
|
+
.format-button:hover,
|
|
247
|
+
.tool-button:hover,
|
|
248
|
+
.format-button.active {
|
|
249
|
+
background: var(--mm-hover);
|
|
250
|
+
color: var(--mm-accent);
|
|
251
|
+
}
|
|
252
|
+
.format-button.is-bold {
|
|
253
|
+
font-weight: 800;
|
|
254
|
+
}
|
|
255
|
+
.format-button.is-italic {
|
|
256
|
+
font-family: Georgia, serif;
|
|
257
|
+
font-style: italic;
|
|
258
|
+
}
|
|
259
|
+
.format-button.is-underline {
|
|
260
|
+
text-decoration: underline;
|
|
261
|
+
text-underline-offset: 4px;
|
|
262
|
+
}
|
|
263
|
+
.format-button.is-strike {
|
|
264
|
+
text-decoration: line-through;
|
|
265
|
+
}
|
|
266
|
+
.format-button.is-highlight {
|
|
267
|
+
color: #d3c900;
|
|
268
|
+
}
|
|
269
|
+
.tool-button.code-button {
|
|
270
|
+
font-family: ui-monospace, monospace;
|
|
271
|
+
font-size: 15px;
|
|
272
|
+
font-weight: 700;
|
|
273
|
+
}
|
|
274
|
+
.tool-button.danger {
|
|
275
|
+
color: #e14f5b;
|
|
276
|
+
}
|
|
277
|
+
.toolbar-divider {
|
|
278
|
+
width: 1px;
|
|
279
|
+
height: 25px;
|
|
280
|
+
margin: 0 3px;
|
|
281
|
+
background: var(--mm-border);
|
|
282
|
+
}
|
|
191
283
|
</style>
|
|
@@ -1,6 +1,2 @@
|
|
|
1
1
|
/** @deprecated Import from `./mindmapClipboard` — kept so Vite stale graphs do not 404. */
|
|
2
|
-
export {
|
|
3
|
-
pasteCanvasOutline,
|
|
4
|
-
readMindmapClipboard,
|
|
5
|
-
writeMindmapClipboard,
|
|
6
|
-
} from './mindmapClipboard'
|
|
2
|
+
export { pasteCanvasOutline, readMindmapClipboard, writeMindmapClipboard } from './mindmapClipboard'
|
|
@@ -7,7 +7,7 @@ export function insertImageIntoSource(
|
|
|
7
7
|
selectionStart: number,
|
|
8
8
|
selectionEnd: number,
|
|
9
9
|
relativePath: string,
|
|
10
|
-
alt = '截图'
|
|
10
|
+
alt = '截图'
|
|
11
11
|
): string {
|
|
12
12
|
const image = `![${alt.replace(/\]/g, '').trim() || '截图'}](${relativePath})`
|
|
13
13
|
const start = Math.max(0, Math.min(markdown.length, selectionStart))
|
|
@@ -69,25 +69,37 @@ export async function readMindmapClipboard(event?: ClipboardEvent | null): Promi
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/** 把剪贴板中的普通文字 / Markdown 列表作为当前主题后的同级子树插入。 */
|
|
72
|
-
export function pasteCanvasOutline(
|
|
72
|
+
export function pasteCanvasOutline(
|
|
73
|
+
session: MindmapSession,
|
|
74
|
+
anchorId: string,
|
|
75
|
+
text: string
|
|
76
|
+
): string[] {
|
|
73
77
|
const nodes = clipboardNodes(text)
|
|
74
78
|
const anchor = session.document.find(anchorId)
|
|
75
79
|
if (!anchor || nodes.length === 0) return []
|
|
76
80
|
const insertedIds: string[] = []
|
|
77
81
|
session.transact((doc) => {
|
|
78
|
-
const parent =
|
|
79
|
-
|
|
82
|
+
const parent =
|
|
83
|
+
anchor === session.focusRootNode ? anchor : (anchor.parent ?? session.focusRootNode)
|
|
84
|
+
let index =
|
|
85
|
+
anchor === session.focusRootNode
|
|
86
|
+
? parent.children.length
|
|
87
|
+
: parent.children.indexOf(anchor) + 1
|
|
80
88
|
for (const source of nodes) {
|
|
81
89
|
const inserted = doc.addNode(
|
|
82
90
|
parent,
|
|
83
91
|
{ ...source.content, image: source.content.image ? { ...source.content.image } : null },
|
|
84
|
-
index
|
|
92
|
+
index++
|
|
85
93
|
)
|
|
86
94
|
inserted.collapsed = source.collapsed
|
|
87
95
|
for (const child of [...source.children]) doc.move(child, inserted, inserted.children.length)
|
|
88
96
|
insertedIds.push(inserted.id)
|
|
89
97
|
}
|
|
90
98
|
})
|
|
91
|
-
session.selectMany(
|
|
99
|
+
session.selectMany(
|
|
100
|
+
insertedIds,
|
|
101
|
+
insertedIds[insertedIds.length - 1] ?? null,
|
|
102
|
+
insertedIds[0] ?? null
|
|
103
|
+
)
|
|
92
104
|
return insertedIds
|
|
93
105
|
}
|
|
@@ -3,13 +3,11 @@ export interface MindmapFenceOptions {
|
|
|
3
3
|
initialExpandLevel?: number
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
export interface MindmapReference {
|
|
7
|
-
path: string
|
|
8
|
-
title?: string
|
|
9
|
-
}
|
|
10
|
-
|
|
11
6
|
function cleanHeadingText(value: string): string {
|
|
12
|
-
return value
|
|
7
|
+
return value
|
|
8
|
+
.trim()
|
|
9
|
+
.replace(/\s+#+\s*$/, '')
|
|
10
|
+
.trim()
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
/** Parse the canonical `mindmap [title] 2` fence metadata. */
|
|
@@ -23,7 +21,8 @@ export function parseMindmapFence(openLine: string): MindmapFenceOptions | null
|
|
|
23
21
|
const titleMatch = rest.match(/\[([^\]]+)\]/)
|
|
24
22
|
if (titleMatch) {
|
|
25
23
|
options.title = cleanHeadingText(titleMatch[1]) || undefined
|
|
26
|
-
rest =
|
|
24
|
+
rest =
|
|
25
|
+
`${rest.slice(0, titleMatch.index)} ${rest.slice((titleMatch.index ?? 0) + titleMatch[0].length)}`.trim()
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
if (rest && !/^\d+$/.test(rest)) return null
|
|
@@ -33,23 +32,6 @@ export function parseMindmapFence(openLine: string): MindmapFenceOptions | null
|
|
|
33
32
|
return options
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
/** @deprecated Mindmap fences no longer resolve `<<<` includes; kept for body-include parsers / tests. */
|
|
37
|
-
export function parseMindmapReference(line: string): MindmapReference | null {
|
|
38
|
-
const match = line.trim().match(/^<<<\s+(.+?)\s*$/)
|
|
39
|
-
if (!match) return null
|
|
40
|
-
|
|
41
|
-
let rest = match[1].trim()
|
|
42
|
-
let title: string | undefined
|
|
43
|
-
const titleMatch = rest.match(/\s+\[([^\]]+)\]\s*$/)
|
|
44
|
-
if (titleMatch) {
|
|
45
|
-
title = cleanHeadingText(titleMatch[1]) || undefined
|
|
46
|
-
rest = rest.slice(0, titleMatch.index).trim()
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const path = rest.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/, '$1$2').trim()
|
|
50
|
-
return path ? { path, title } : null
|
|
51
|
-
}
|
|
52
|
-
|
|
53
35
|
export interface NormalizeMindmapOptions {
|
|
54
36
|
title?: string
|
|
55
37
|
defaultTitle?: string
|
|
@@ -58,7 +40,7 @@ export interface NormalizeMindmapOptions {
|
|
|
58
40
|
/** Ensure canonical mindmap Markdown has exactly one H1 root title. */
|
|
59
41
|
export function normalizeMindmapMarkdown(
|
|
60
42
|
source: string,
|
|
61
|
-
options: NormalizeMindmapOptions = {}
|
|
43
|
+
options: NormalizeMindmapOptions = {}
|
|
62
44
|
): string {
|
|
63
45
|
const lines = source.replace(/\r\n?/g, '\n').split('\n')
|
|
64
46
|
let existingTitle = ''
|
|
@@ -72,12 +54,11 @@ export function normalizeMindmapMarkdown(
|
|
|
72
54
|
break
|
|
73
55
|
}
|
|
74
56
|
|
|
75
|
-
const rootTitle =
|
|
57
|
+
const rootTitle =
|
|
58
|
+
cleanHeadingText(options.title || existingTitle || options.defaultTitle || 'root') || 'root'
|
|
76
59
|
const body = lines.filter((_, index) => index !== rootIndex)
|
|
77
60
|
while (body[0]?.trim() === '') body.shift()
|
|
78
61
|
while (body[body.length - 1]?.trim() === '') body.pop()
|
|
79
62
|
|
|
80
|
-
return body.length > 0
|
|
81
|
-
? `# ${rootTitle}\n\n${body.join('\n')}\n`
|
|
82
|
-
: `# ${rootTitle}\n`
|
|
63
|
+
return body.length > 0 ? `# ${rootTitle}\n\n${body.join('\n')}\n` : `# ${rootTitle}\n`
|
|
83
64
|
}
|