@tnotesjs/ui 0.1.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/LICENSE +21 -0
- package/README.md +97 -0
- package/package.json +68 -0
- package/src/components/BilibiliVideo/BilibiliVideo.vue +55 -0
- package/src/components/Footprints/Footprints.vue +339 -0
- package/src/components/Footprints/parse.ts +122 -0
- package/src/components/Mermaid/Mermaid.vue +553 -0
- package/src/components/Mermaid/icons/icon__center_off.svg +1 -0
- package/src/components/Mermaid/icons/icon__center_on.svg +1 -0
- package/src/components/Mermaid/icons/icon__check.svg +3 -0
- package/src/components/Mermaid/icons/icon__clipboard.svg +8 -0
- package/src/components/Mermaid/icons/icon__fullscreen.svg +1 -0
- package/src/components/Mermaid/icons/icon__fullscreen_exit.svg +1 -0
- package/src/components/Mindmap/FocusBreadcrumbs.test.ts +265 -0
- package/src/components/Mindmap/FocusBreadcrumbs.vue +436 -0
- package/src/components/Mindmap/InlineRuns.ts +25 -0
- package/src/components/Mindmap/Mindmap.vue +1210 -0
- package/src/components/Mindmap/MindmapOutlineNode.vue +62 -0
- package/src/components/Mindmap/MindmapViewIcon.vue +41 -0
- package/src/components/Mindmap/editor/AppIcon.vue +107 -0
- package/src/components/Mindmap/editor/CanvasContextMenu.vue +157 -0
- package/src/components/Mindmap/editor/LinkPopover.vue +83 -0
- package/src/components/Mindmap/editor/MarkdownView.vue +163 -0
- package/src/components/Mindmap/editor/MindmapView.vue +253 -0
- package/src/components/Mindmap/editor/OutlineView.vue +2494 -0
- package/src/components/Mindmap/editor/RichInlineEditor.vue +393 -0
- package/src/components/Mindmap/editor/SelectionToolbar.vue +191 -0
- package/src/components/Mindmap/editor/canvasClipboard.ts +6 -0
- package/src/components/Mindmap/editor/imagePaste.ts +32 -0
- package/src/components/Mindmap/editor/mindmapClipboard.ts +93 -0
- package/src/components/Mindmap/editor/outlineDrag.ts +29 -0
- package/src/components/Mindmap/editor/platform.ts +18 -0
- package/src/components/Mindmap/expandLevel.ts +28 -0
- package/src/components/Mindmap/icons/icon__fullscreen.svg +1 -0
- package/src/components/Mindmap/icons/icon__fullscreen_exit.svg +1 -0
- package/src/components/Mindmap/icons/icon__zoom_fit.svg +1 -0
- package/src/components/Mindmap/markdown.ts +83 -0
- package/src/components/Mindmap/wheelInteraction.ts +7 -0
- package/src/components/NotesTable/NotesTable.vue +119 -0
- package/src/components/NotesTable/types.ts +6 -0
- package/src/components/WordList/RightClickMenu.vue +106 -0
- package/src/components/WordList/WordList.vue +692 -0
- package/src/components/WordList/wordListFeatures.ts +38 -0
- package/src/index.ts +30 -0
- package/src/styles/tokens.css +35 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
|
3
|
+
import { CanvasEditor, serializeSubtree } from '@tnotesjs/mindmap-core'
|
|
4
|
+
import type { CanvasContextRequest, CanvasLinkHover, InlineFormat, MindmapNode, MindmapSession } from '@tnotesjs/mindmap-core'
|
|
5
|
+
import CanvasContextMenu from './CanvasContextMenu.vue'
|
|
6
|
+
import LinkPopover from './LinkPopover.vue'
|
|
7
|
+
import SelectionToolbar from './SelectionToolbar.vue'
|
|
8
|
+
import { pasteCanvasOutline, readMindmapClipboard, writeMindmapClipboard } from './mindmapClipboard'
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{ session: MindmapSession; resolveImageSrc?: (src: string) => string }>()
|
|
11
|
+
|
|
12
|
+
const emit = defineEmits<{
|
|
13
|
+
ready: [editor: CanvasEditor]
|
|
14
|
+
requestSearch: []
|
|
15
|
+
imagePreview: [src: string]
|
|
16
|
+
pasteImage: [anchorId: string, blob: Blob]
|
|
17
|
+
}>()
|
|
18
|
+
|
|
19
|
+
const host = ref<HTMLElement>()
|
|
20
|
+
let editor: CanvasEditor | null = null
|
|
21
|
+
const selectedCount = ref(0)
|
|
22
|
+
const linkEditor = ref<CanvasLinkHover | null>(null)
|
|
23
|
+
const contextMenu = ref<CanvasContextRequest | null>(null)
|
|
24
|
+
let linkCloseTimer: ReturnType<typeof setTimeout> | null = null
|
|
25
|
+
|
|
26
|
+
function keepLinkPopover() {
|
|
27
|
+
if (linkCloseTimer) clearTimeout(linkCloseTimer)
|
|
28
|
+
linkCloseTimer = null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function closeLinkPopoverSoon() {
|
|
32
|
+
keepLinkPopover()
|
|
33
|
+
linkCloseTimer = setTimeout(() => (linkEditor.value = null), 180)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function onCanvasLinkHover(link: CanvasLinkHover | null) {
|
|
37
|
+
if (!link) {
|
|
38
|
+
closeLinkPopoverSoon()
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
keepLinkPopover()
|
|
42
|
+
linkEditor.value = link
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function saveLink(url: string) {
|
|
46
|
+
const link = linkEditor.value
|
|
47
|
+
if (!link) return
|
|
48
|
+
props.session.updateNodeInlineLink(link.nodeId, link.rawStart, link.rawEnd, url)
|
|
49
|
+
// URL 长度变化会让原始 Markdown 范围失效;关闭后按下一次 hover 的新范围重开。
|
|
50
|
+
linkEditor.value = null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function removeLink() {
|
|
54
|
+
const link = linkEditor.value
|
|
55
|
+
if (!link) return
|
|
56
|
+
props.session.updateNodeInlineLink(link.nodeId, link.rawStart, link.rawEnd, null)
|
|
57
|
+
linkEditor.value = null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function selectedRoots(): MindmapNode[] {
|
|
61
|
+
const selected = props.session.selectionIds
|
|
62
|
+
return props.session.selectedNodes.filter((node) => {
|
|
63
|
+
let parent = node.parent
|
|
64
|
+
while (parent) {
|
|
65
|
+
if (selected.has(parent.id)) return false
|
|
66
|
+
parent = parent.parent
|
|
67
|
+
}
|
|
68
|
+
return true
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function applyNodeFormat(format: InlineFormat) {
|
|
73
|
+
props.session.formatSelectedNodes(format)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function selectedMarkdown() {
|
|
77
|
+
return selectedRoots().map((node) => serializeSubtree(node)).join('\n')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function copySelected(event?: ClipboardEvent | null) {
|
|
81
|
+
const text = selectedMarkdown()
|
|
82
|
+
if (!text) return
|
|
83
|
+
writeMindmapClipboard(text, event)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function cutSelected(event?: ClipboardEvent | null) {
|
|
87
|
+
const text = selectedMarkdown()
|
|
88
|
+
if (!text) return
|
|
89
|
+
const ids = [...props.session.selectionIds]
|
|
90
|
+
writeMindmapClipboard(text, event)
|
|
91
|
+
props.session.removeNodesByIds(ids)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function pasteAtSelection(anchorId?: string, event?: ClipboardEvent | null) {
|
|
95
|
+
const targetId = anchorId ?? props.session.selectedNode?.id ?? props.session.focusRootNode.id
|
|
96
|
+
const text = await readMindmapClipboard(event)
|
|
97
|
+
if (text) pasteCanvasOutline(props.session, targetId, text)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function pasteAtContextNode() {
|
|
101
|
+
const target = contextMenu.value
|
|
102
|
+
if (!target) return
|
|
103
|
+
await pasteAtSelection(target.nodeId)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function pasteTextAt(text: string, anchorId: string) {
|
|
107
|
+
writeMindmapClipboard(text)
|
|
108
|
+
pasteCanvasOutline(props.session, anchorId, text)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function editCreated(node: MindmapNode | null) {
|
|
112
|
+
if (node) editor?.startEdit(node.id)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function insertSibling() {
|
|
116
|
+
const target = contextMenu.value
|
|
117
|
+
if (target) editCreated(props.session.insertSiblingOf(target.nodeId))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function insertChild() {
|
|
121
|
+
const target = contextMenu.value
|
|
122
|
+
if (target) editCreated(props.session.insertChildOf(target.nodeId))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function insertParent() {
|
|
126
|
+
const target = contextMenu.value
|
|
127
|
+
if (target) editCreated(props.session.insertParentOf(target.nodeId))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function deleteOnly() {
|
|
131
|
+
const target = contextMenu.value
|
|
132
|
+
if (target) props.session.removeNodeOnly(target.nodeId)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function toggleContextSiblings() {
|
|
136
|
+
const target = contextMenu.value
|
|
137
|
+
if (target) props.session.toggleCollapseSiblings(target.nodeId)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function focusContextNode() {
|
|
141
|
+
const target = contextMenu.value
|
|
142
|
+
if (target) props.session.focusNode(target.nodeId)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
onMounted(() => {
|
|
146
|
+
editor = new CanvasEditor(host.value!, props.session, {
|
|
147
|
+
onRequestSearch: () => emit('requestSearch'),
|
|
148
|
+
onImagePreview: (src) => emit('imagePreview', src),
|
|
149
|
+
onPasteImage: (anchorId, blob) => emit('pasteImage', anchorId, blob),
|
|
150
|
+
resolveImageSrc: (src) => props.resolveImageSrc?.(src) ?? src,
|
|
151
|
+
onSelectionPositionChange: (_position, count) => {
|
|
152
|
+
// Canvas toolbar is absolutely anchored; only the selection count gates visibility.
|
|
153
|
+
selectedCount.value = count
|
|
154
|
+
},
|
|
155
|
+
onLinkHover: onCanvasLinkHover,
|
|
156
|
+
onContextMenu: (request) => {
|
|
157
|
+
contextMenu.value = request
|
|
158
|
+
if (request) linkEditor.value = null
|
|
159
|
+
},
|
|
160
|
+
onCopySelection: (event) => {
|
|
161
|
+
void copySelected(event)
|
|
162
|
+
},
|
|
163
|
+
onCutSelection: (event) => {
|
|
164
|
+
void cutSelected(event)
|
|
165
|
+
},
|
|
166
|
+
onPasteSelection: (event) => {
|
|
167
|
+
void pasteAtSelection(undefined, event)
|
|
168
|
+
},
|
|
169
|
+
onPasteText: pasteTextAt,
|
|
170
|
+
})
|
|
171
|
+
emit('ready', editor)
|
|
172
|
+
// 切回脑图视图:居中当前选中节点(无选中则保持 zoomToFit)
|
|
173
|
+
const sel = props.session.selectedNode
|
|
174
|
+
if (sel) {
|
|
175
|
+
requestAnimationFrame(() => editor?.centerOnNode(sel.id, false))
|
|
176
|
+
setTimeout(() => editor?.centerOnNode(sel.id, false), 80)
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
onBeforeUnmount(() => {
|
|
181
|
+
if (linkCloseTimer) clearTimeout(linkCloseTimer)
|
|
182
|
+
editor?.destroy()
|
|
183
|
+
editor = null
|
|
184
|
+
})
|
|
185
|
+
</script>
|
|
186
|
+
|
|
187
|
+
<template>
|
|
188
|
+
<div class="mindmap-view-root">
|
|
189
|
+
<div ref="host" class="mindmap-view-host" />
|
|
190
|
+
<SelectionToolbar
|
|
191
|
+
v-if="selectedCount > 0"
|
|
192
|
+
mode="nodes"
|
|
193
|
+
placement="canvas-bottom"
|
|
194
|
+
@format="applyNodeFormat"
|
|
195
|
+
@task="props.session.toggleTaskSelectedNodes()"
|
|
196
|
+
@copy="copySelected"
|
|
197
|
+
@clear="props.session.clearSelectedNodeFormats()"
|
|
198
|
+
@delete="props.session.removeSelectedNodes()"
|
|
199
|
+
/>
|
|
200
|
+
<LinkPopover
|
|
201
|
+
v-if="linkEditor"
|
|
202
|
+
:url="linkEditor.url"
|
|
203
|
+
:position="linkEditor.position"
|
|
204
|
+
@save="saveLink"
|
|
205
|
+
@remove="removeLink"
|
|
206
|
+
@keep="keepLinkPopover"
|
|
207
|
+
@leave="closeLinkPopoverSoon"
|
|
208
|
+
@close="linkEditor = null"
|
|
209
|
+
/>
|
|
210
|
+
<CanvasContextMenu
|
|
211
|
+
v-if="contextMenu"
|
|
212
|
+
:position="contextMenu.position"
|
|
213
|
+
:multiple="contextMenu.multiple"
|
|
214
|
+
:can-insert-sibling="contextMenu.canInsertSibling"
|
|
215
|
+
:can-insert-parent="contextMenu.canInsertParent"
|
|
216
|
+
:can-cut="contextMenu.canCut"
|
|
217
|
+
:can-duplicate="contextMenu.canDuplicate"
|
|
218
|
+
:can-delete-only="contextMenu.canDeleteOnly"
|
|
219
|
+
:can-delete-tree="contextMenu.canDeleteTree"
|
|
220
|
+
:can-toggle-siblings="contextMenu.canToggleSiblings"
|
|
221
|
+
:can-focus="contextMenu.canFocus"
|
|
222
|
+
@insert-sibling="insertSibling"
|
|
223
|
+
@insert-child="insertChild"
|
|
224
|
+
@insert-parent="insertParent"
|
|
225
|
+
@copy="copySelected"
|
|
226
|
+
@cut="cutSelected"
|
|
227
|
+
@paste="pasteAtContextNode"
|
|
228
|
+
@duplicate="props.session.duplicateSelectedNodes()"
|
|
229
|
+
@delete-only="deleteOnly"
|
|
230
|
+
@delete-tree="props.session.removeSelectedNodes()"
|
|
231
|
+
@toggle-siblings="toggleContextSiblings"
|
|
232
|
+
@focus="focusContextNode"
|
|
233
|
+
@close="contextMenu = null"
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
</template>
|
|
237
|
+
|
|
238
|
+
<style scoped>
|
|
239
|
+
.mindmap-view-root {
|
|
240
|
+
position: relative;
|
|
241
|
+
width: 100%;
|
|
242
|
+
height: 100%;
|
|
243
|
+
min-height: 0;
|
|
244
|
+
}
|
|
245
|
+
.mindmap-view-host {
|
|
246
|
+
position: relative;
|
|
247
|
+
width: 100%;
|
|
248
|
+
height: 100%;
|
|
249
|
+
overflow: hidden;
|
|
250
|
+
outline: none;
|
|
251
|
+
background: var(--mm-canvas-bg);
|
|
252
|
+
}
|
|
253
|
+
</style>
|