@tnotesjs/ui 0.2.1 → 0.3.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 +81 -0
- package/dist/code/highlight.d.ts +39 -1
- package/dist/code/highlight.js +62 -4
- package/package.json +23 -9
- package/src/code/collapse.ts +79 -0
- package/src/code/highlight.ts +29 -3
- package/src/components/CodeBlock/CodeBlock.vue +39 -5
- package/src/components/CodeGroup/CodeGroup.vue +70 -7
- package/src/components/Mindmap/Mindmap.vue +7 -6
- package/src/components/Mindmap/editor/CanvasContextMenu.vue +77 -68
- package/src/components/Mindmap/editor/CollapseMenu.vue +113 -0
- package/src/components/Mindmap/editor/IconButton.vue +63 -0
- package/src/components/Mindmap/editor/LinkPopover.vue +50 -46
- package/src/components/Mindmap/editor/MarkdownView.vue +5 -3
- package/src/components/Mindmap/editor/MindmapView.vue +11 -1
- package/src/components/Mindmap/editor/SearchBar.vue +672 -0
- package/src/components/Mindmap/editor/SelectionToolbar.vue +1 -1
- package/src/entries/excalidraw-view.ts +5 -0
- package/src/entries/mindmap-editor.ts +37 -0
- package/src/excalidraw/exporter.ts +1 -0
- package/src/excalidraw/fonts.ts +21 -1
- package/src/styles/code.css +82 -4
- package/src/styles/prose.css +45 -0
- package/src/styles/tokens.css +6 -0
- package/src/components/Mindmap/editor/canvasClipboard.ts +0 -2
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
import { nextTick, onMounted, ref, watch } from 'vue'
|
|
3
3
|
import CodeBlock from '../CodeBlock/CodeBlock.vue'
|
|
4
4
|
import { parseCodeMeta } from '../../code/highlight'
|
|
5
|
+
import {
|
|
6
|
+
applyCollapseChrome,
|
|
7
|
+
codeBlockIn,
|
|
8
|
+
expandCollapsedCodeBlocks,
|
|
9
|
+
isCodeBlockCollapsed,
|
|
10
|
+
toggleCodeBlockCollapsed
|
|
11
|
+
} from '../../code/collapse'
|
|
5
12
|
|
|
6
13
|
export interface CodeGroupItem {
|
|
7
14
|
code?: string
|
|
@@ -11,16 +18,47 @@ export interface CodeGroupItem {
|
|
|
11
18
|
}
|
|
12
19
|
const props = defineProps<{ items: CodeGroupItem[] }>()
|
|
13
20
|
const root = ref<HTMLElement>()
|
|
21
|
+
const collapseButton = ref<HTMLButtonElement>()
|
|
14
22
|
const active = ref(0)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
|
|
24
|
+
function panels(): HTMLElement[] {
|
|
25
|
+
return [
|
|
26
|
+
...(root.value?.querySelectorAll<HTMLElement>(
|
|
18
27
|
':scope > .tn-code-group__panels > .tn-code-group__panel'
|
|
19
|
-
) ?? []
|
|
20
|
-
|
|
28
|
+
) ?? [])
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function activePanel(): HTMLElement | null {
|
|
33
|
+
return panels()[active.value] ?? null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function activeBlock(): HTMLElement | null {
|
|
37
|
+
return codeBlockIn(activePanel())
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toggleCollapse(): void {
|
|
41
|
+
const block = activeBlock()
|
|
42
|
+
if (!block) return
|
|
43
|
+
toggleCodeBlockCollapsed(block)
|
|
44
|
+
applyCollapseChrome(collapseButton.value ?? null, isCodeBlockCollapsed(block))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 面板自带的那颗折叠按钮在分组里会被 tab 行盖住,所以 chrome 挂在 tab 行这颗上。 */
|
|
48
|
+
function syncCollapseButton(): void {
|
|
49
|
+
applyCollapseChrome(collapseButton.value ?? null, isCodeBlockCollapsed(activeBlock()))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function syncPanels(): void {
|
|
53
|
+
const list = panels()
|
|
54
|
+
list.forEach((panel, index) => {
|
|
21
55
|
panel.hidden = index !== active.value
|
|
22
56
|
})
|
|
57
|
+
// 切到的面板如果处于收起状态就自动展开:读者不该看到一片被裁掉的代码。
|
|
58
|
+
expandCollapsedCodeBlocks(activePanel())
|
|
59
|
+
syncCollapseButton()
|
|
23
60
|
}
|
|
61
|
+
|
|
24
62
|
watch(active, syncPanels, { flush: 'post' })
|
|
25
63
|
watch(
|
|
26
64
|
() => props.items.length,
|
|
@@ -30,6 +68,7 @@ watch(
|
|
|
30
68
|
}
|
|
31
69
|
)
|
|
32
70
|
onMounted(syncPanels)
|
|
71
|
+
|
|
33
72
|
function navigate(event: KeyboardEvent, index: number): void {
|
|
34
73
|
let next = index
|
|
35
74
|
if (event.key === 'ArrowRight') next = (index + 1) % props.items.length
|
|
@@ -40,19 +79,43 @@ function navigate(event: KeyboardEvent, index: number): void {
|
|
|
40
79
|
event.preventDefault()
|
|
41
80
|
active.value = next
|
|
42
81
|
;(event.currentTarget as HTMLElement).parentElement
|
|
43
|
-
?.querySelectorAll<HTMLButtonElement>('button')
|
|
82
|
+
?.querySelectorAll<HTMLButtonElement>('button[role="tab"]')
|
|
44
83
|
[next]?.focus()
|
|
45
84
|
}
|
|
46
85
|
</script>
|
|
47
86
|
|
|
48
87
|
<template>
|
|
49
|
-
<section ref="root" class="tn-code-group">
|
|
88
|
+
<section ref="root" class="tn-code-group" :class="{ 'has-tabs': items.length > 1 }">
|
|
50
89
|
<div
|
|
51
90
|
v-if="items.length > 1"
|
|
52
91
|
class="tn-code-group__tabs code-group-tabs"
|
|
53
92
|
role="tablist"
|
|
54
93
|
aria-label="代码分组"
|
|
55
94
|
>
|
|
95
|
+
<button
|
|
96
|
+
ref="collapseButton"
|
|
97
|
+
type="button"
|
|
98
|
+
class="tn-code-block__icon-btn tn-code-group__collapse-btn"
|
|
99
|
+
aria-expanded="true"
|
|
100
|
+
aria-label="收起代码"
|
|
101
|
+
title="收起代码"
|
|
102
|
+
@click="toggleCollapse"
|
|
103
|
+
>
|
|
104
|
+
<svg
|
|
105
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
106
|
+
width="16"
|
|
107
|
+
height="16"
|
|
108
|
+
viewBox="0 0 24 24"
|
|
109
|
+
fill="none"
|
|
110
|
+
stroke="currentColor"
|
|
111
|
+
stroke-width="2"
|
|
112
|
+
stroke-linecap="round"
|
|
113
|
+
stroke-linejoin="round"
|
|
114
|
+
aria-hidden="true"
|
|
115
|
+
>
|
|
116
|
+
<path d="m6 9 6 6 6-6" />
|
|
117
|
+
</svg>
|
|
118
|
+
</button>
|
|
56
119
|
<button
|
|
57
120
|
v-for="(item, index) in items"
|
|
58
121
|
:key="item.key || index"
|
|
@@ -786,19 +786,20 @@ onBeforeUnmount(() => {
|
|
|
786
786
|
transition: opacity 0.2s;
|
|
787
787
|
}
|
|
788
788
|
|
|
789
|
-
/*
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
789
|
+
/* 工具条按需出现:hover、聚焦(键盘 Tab 进来也算)、画布交互中、全屏。
|
|
790
|
+
Desk 的可编辑岛原本额外钉了 `opacity: 1`(旧注释:“chrome must stay hittable”),
|
|
791
|
+
结果每张脑图右上角常驻一排按钮,既像噪声又占掉画布右上角的拖拽区;
|
|
792
|
+
可点击性并没丢 —— 指针进入即 hover 显现,`:focus-within` 保住键盘路径。
|
|
793
|
+
只读岛(站点)本来就是这套行为,这里让两边一致。 */
|
|
795
794
|
.mindmap-preview-tabs {
|
|
796
795
|
display: flex;
|
|
797
796
|
gap: 4px;
|
|
798
797
|
}
|
|
799
798
|
|
|
800
799
|
.mindmap-preview:hover > .mindmap-preview-actions,
|
|
800
|
+
.mindmap-preview:focus-within > .mindmap-preview-actions,
|
|
801
801
|
.mindmap-preview-actions:focus-within,
|
|
802
|
+
.mindmap-preview.is-interaction-active > .mindmap-preview-actions,
|
|
802
803
|
.mindmap-preview.is-fullscreen > .mindmap-preview-actions,
|
|
803
804
|
.mindmap-preview:fullscreen > .mindmap-preview-actions {
|
|
804
805
|
opacity: 1;
|
|
@@ -5,6 +5,12 @@ import { isApplePlatform, primaryShortcut } from './platform'
|
|
|
5
5
|
const props = withDefaults(
|
|
6
6
|
defineProps<{
|
|
7
7
|
position: { left: number; top: number }
|
|
8
|
+
/**
|
|
9
|
+
* 浮层挂载点。默认 `undefined` = 就地渲染,供嵌在笔记里的宿主(Desk / SSG)
|
|
10
|
+
* 使用——菜单是 `position: fixed`,teleport 到 body 会脱离笔记的层叠上下文。
|
|
11
|
+
* 自带整页外壳的宿主(Web / VS Code webview)传 `'body'`。
|
|
12
|
+
*/
|
|
13
|
+
teleportTo?: string
|
|
8
14
|
multiple?: boolean
|
|
9
15
|
canInsertSibling?: boolean
|
|
10
16
|
canInsertParent?: boolean
|
|
@@ -16,6 +22,7 @@ const props = withDefaults(
|
|
|
16
22
|
canFocus?: boolean
|
|
17
23
|
}>(),
|
|
18
24
|
{
|
|
25
|
+
teleportTo: undefined,
|
|
19
26
|
multiple: false,
|
|
20
27
|
canInsertSibling: true,
|
|
21
28
|
canInsertParent: true,
|
|
@@ -96,85 +103,87 @@ onBeforeUnmount(() => {
|
|
|
96
103
|
</script>
|
|
97
104
|
|
|
98
105
|
<template>
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
<Teleport :to="teleportTo" :disabled="!teleportTo">
|
|
107
|
+
<div
|
|
108
|
+
ref="menu"
|
|
109
|
+
class="canvas-context-menu"
|
|
110
|
+
:style="style"
|
|
111
|
+
role="menu"
|
|
112
|
+
:aria-label="multiple ? '多主题右键菜单' : '主题右键菜单'"
|
|
113
|
+
@contextmenu.prevent
|
|
114
|
+
>
|
|
115
|
+
<template v-if="!multiple">
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
role="menuitem"
|
|
119
|
+
:disabled="!canInsertSibling"
|
|
120
|
+
@click="act('insertSibling')"
|
|
121
|
+
>
|
|
122
|
+
<span>插入同级主题</span><kbd>Enter</kbd>
|
|
123
|
+
</button>
|
|
124
|
+
<button type="button" role="menuitem" @click="act('insertChild')">
|
|
125
|
+
<span>插入下级主题</span><kbd>Tab</kbd>
|
|
126
|
+
</button>
|
|
127
|
+
<button
|
|
128
|
+
type="button"
|
|
129
|
+
role="menuitem"
|
|
130
|
+
:disabled="!canInsertParent"
|
|
131
|
+
@click="act('insertParent')"
|
|
132
|
+
>
|
|
133
|
+
<span>插入上级主题</span><kbd>{{ shiftTab }}</kbd>
|
|
134
|
+
</button>
|
|
135
|
+
<span class="context-divider" />
|
|
136
|
+
</template>
|
|
137
|
+
|
|
138
|
+
<button type="button" role="menuitem" @click="act('copy')">
|
|
139
|
+
<span>复制</span><kbd>{{ primaryShortcut('C') }}</kbd>
|
|
140
|
+
</button>
|
|
141
|
+
<button type="button" role="menuitem" :disabled="!canCut" @click="act('cut')">
|
|
142
|
+
<span>剪切</span><kbd>{{ primaryShortcut('X') }}</kbd>
|
|
143
|
+
</button>
|
|
144
|
+
|
|
145
|
+
<template v-if="!multiple">
|
|
146
|
+
<button type="button" role="menuitem" @click="act('paste')">
|
|
147
|
+
<span>粘贴</span><kbd>{{ primaryShortcut('V') }}</kbd>
|
|
148
|
+
</button>
|
|
149
|
+
<button type="button" role="menuitem" :disabled="!canDuplicate" @click="act('duplicate')">
|
|
150
|
+
<span>创建副本</span><kbd>{{ primaryShortcut('D') }}</kbd>
|
|
151
|
+
</button>
|
|
152
|
+
<button type="button" role="menuitem" :disabled="!canDeleteOnly" @click="act('deleteOnly')">
|
|
153
|
+
<span>仅删除当前主题</span>
|
|
154
|
+
</button>
|
|
155
|
+
</template>
|
|
156
|
+
|
|
108
157
|
<button
|
|
109
158
|
type="button"
|
|
110
159
|
role="menuitem"
|
|
111
|
-
|
|
112
|
-
|
|
160
|
+
class="danger"
|
|
161
|
+
:disabled="!canDeleteTree"
|
|
162
|
+
@click="act('deleteTree')"
|
|
113
163
|
>
|
|
114
|
-
<span
|
|
115
|
-
|
|
116
|
-
<button type="button" role="menuitem" @click="act('insertChild')">
|
|
117
|
-
<span>插入下级主题</span><kbd>Tab</kbd>
|
|
164
|
+
<span>{{ multiple ? '删除所选主题及下级主题' : '删除当前主题及下级主题' }}</span
|
|
165
|
+
><kbd>Delete</kbd>
|
|
118
166
|
</button>
|
|
167
|
+
<span class="context-divider" />
|
|
119
168
|
<button
|
|
120
169
|
type="button"
|
|
121
170
|
role="menuitem"
|
|
122
|
-
:disabled="!
|
|
123
|
-
@click="act('
|
|
171
|
+
:disabled="!canToggleSiblings"
|
|
172
|
+
@click="act('toggleSiblings')"
|
|
124
173
|
>
|
|
125
|
-
<span
|
|
174
|
+
<span>展开/折叠同级主题</span><kbd>{{ primaryShortcut('.', { shift: true }) }}</kbd>
|
|
126
175
|
</button>
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
</button>
|
|
136
|
-
|
|
137
|
-
<template v-if="!multiple">
|
|
138
|
-
<button type="button" role="menuitem" @click="act('paste')">
|
|
139
|
-
<span>粘贴</span><kbd>{{ primaryShortcut('V') }}</kbd>
|
|
140
|
-
</button>
|
|
141
|
-
<button type="button" role="menuitem" :disabled="!canDuplicate" @click="act('duplicate')">
|
|
142
|
-
<span>创建副本</span><kbd>{{ primaryShortcut('D') }}</kbd>
|
|
143
|
-
</button>
|
|
144
|
-
<button type="button" role="menuitem" :disabled="!canDeleteOnly" @click="act('deleteOnly')">
|
|
145
|
-
<span>仅删除当前主题</span>
|
|
176
|
+
<button
|
|
177
|
+
v-if="!multiple"
|
|
178
|
+
type="button"
|
|
179
|
+
role="menuitem"
|
|
180
|
+
:disabled="!canFocus"
|
|
181
|
+
@click="act('focus')"
|
|
182
|
+
>
|
|
183
|
+
<span>进入此主题</span><kbd>{{ primaryShortcut(']') }}</kbd>
|
|
146
184
|
</button>
|
|
147
|
-
</
|
|
148
|
-
|
|
149
|
-
<button
|
|
150
|
-
type="button"
|
|
151
|
-
role="menuitem"
|
|
152
|
-
class="danger"
|
|
153
|
-
:disabled="!canDeleteTree"
|
|
154
|
-
@click="act('deleteTree')"
|
|
155
|
-
>
|
|
156
|
-
<span>{{ multiple ? '删除所选主题及下级主题' : '删除当前主题及下级主题' }}</span
|
|
157
|
-
><kbd>Delete</kbd>
|
|
158
|
-
</button>
|
|
159
|
-
<span class="context-divider" />
|
|
160
|
-
<button
|
|
161
|
-
type="button"
|
|
162
|
-
role="menuitem"
|
|
163
|
-
:disabled="!canToggleSiblings"
|
|
164
|
-
@click="act('toggleSiblings')"
|
|
165
|
-
>
|
|
166
|
-
<span>展开/折叠同级主题</span><kbd>{{ primaryShortcut('.', { shift: true }) }}</kbd>
|
|
167
|
-
</button>
|
|
168
|
-
<button
|
|
169
|
-
v-if="!multiple"
|
|
170
|
-
type="button"
|
|
171
|
-
role="menuitem"
|
|
172
|
-
:disabled="!canFocus"
|
|
173
|
-
@click="act('focus')"
|
|
174
|
-
>
|
|
175
|
-
<span>进入此主题</span><kbd>{{ primaryShortcut(']') }}</kbd>
|
|
176
|
-
</button>
|
|
177
|
-
</div>
|
|
185
|
+
</div>
|
|
186
|
+
</Teleport>
|
|
178
187
|
</template>
|
|
179
188
|
|
|
180
189
|
<style scoped>
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
3
|
+
import IconButton from './IconButton.vue'
|
|
4
|
+
import { isApplePlatform } from './platform'
|
|
5
|
+
|
|
6
|
+
const emit = defineEmits<{
|
|
7
|
+
all: []
|
|
8
|
+
level: [level: 1 | 2 | 3]
|
|
9
|
+
}>()
|
|
10
|
+
|
|
11
|
+
const root = ref<HTMLElement>()
|
|
12
|
+
const open = ref(false)
|
|
13
|
+
const modifier = computed(() => (isApplePlatform() ? '⌘⌥' : 'Ctrl+Alt+'))
|
|
14
|
+
const allShortcut = computed(() => (isApplePlatform() ? '⌘⌥⇧.' : 'Ctrl+Alt+Shift+.'))
|
|
15
|
+
|
|
16
|
+
function chooseAll() {
|
|
17
|
+
emit('all')
|
|
18
|
+
open.value = false
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function chooseLevel(level: 1 | 2 | 3) {
|
|
22
|
+
emit('level', level)
|
|
23
|
+
open.value = false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function onDocumentPointerDown(event: PointerEvent) {
|
|
27
|
+
if (open.value && !root.value?.contains(event.target as Node)) open.value = false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function onDocumentKeydown(event: KeyboardEvent) {
|
|
31
|
+
if (event.key === 'Escape') open.value = false
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
onMounted(() => {
|
|
35
|
+
document.addEventListener('pointerdown', onDocumentPointerDown)
|
|
36
|
+
document.addEventListener('keydown', onDocumentKeydown)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
onBeforeUnmount(() => {
|
|
40
|
+
document.removeEventListener('pointerdown', onDocumentPointerDown)
|
|
41
|
+
document.removeEventListener('keydown', onDocumentKeydown)
|
|
42
|
+
})
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<div ref="root" class="collapse-menu">
|
|
47
|
+
<IconButton icon="collapse" label="展开/折叠主题" :active="open" @click="open = !open" />
|
|
48
|
+
<div v-if="open" class="collapse-menu-popover" role="menu" aria-label="展开/折叠主题">
|
|
49
|
+
<div class="collapse-menu-title">展开/折叠主题</div>
|
|
50
|
+
<button type="button" role="menuitem" @click="chooseAll">
|
|
51
|
+
<span>全部主题</span><kbd>{{ allShortcut }}</kbd>
|
|
52
|
+
</button>
|
|
53
|
+
<button
|
|
54
|
+
v-for="level in [1, 2, 3] as const"
|
|
55
|
+
:key="level"
|
|
56
|
+
type="button"
|
|
57
|
+
role="menuitem"
|
|
58
|
+
@click="chooseLevel(level)"
|
|
59
|
+
>
|
|
60
|
+
<span>{{ level }} 级主题</span><kbd>{{ modifier }}{{ level }}</kbd>
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
65
|
+
|
|
66
|
+
<style scoped>
|
|
67
|
+
.collapse-menu {
|
|
68
|
+
position: relative;
|
|
69
|
+
}
|
|
70
|
+
.collapse-menu-popover {
|
|
71
|
+
position: absolute;
|
|
72
|
+
z-index: 85;
|
|
73
|
+
top: calc(100% + 8px);
|
|
74
|
+
right: 0;
|
|
75
|
+
width: 260px;
|
|
76
|
+
padding: 8px;
|
|
77
|
+
border: 1px solid var(--mm-border);
|
|
78
|
+
border-radius: 12px;
|
|
79
|
+
background: var(--mm-panel-bg);
|
|
80
|
+
color: var(--mm-text);
|
|
81
|
+
box-shadow: 0 14px 38px rgb(0 0 0 / 0.24);
|
|
82
|
+
}
|
|
83
|
+
.collapse-menu-title {
|
|
84
|
+
padding: 7px 11px 9px;
|
|
85
|
+
color: var(--mm-text-dim);
|
|
86
|
+
font-size: 12px;
|
|
87
|
+
}
|
|
88
|
+
.collapse-menu-popover button {
|
|
89
|
+
display: flex;
|
|
90
|
+
width: 100%;
|
|
91
|
+
min-height: 42px;
|
|
92
|
+
align-items: center;
|
|
93
|
+
justify-content: space-between;
|
|
94
|
+
padding: 0 11px;
|
|
95
|
+
border: 0;
|
|
96
|
+
border-radius: 7px;
|
|
97
|
+
background: transparent;
|
|
98
|
+
color: inherit;
|
|
99
|
+
font: inherit;
|
|
100
|
+
text-align: left;
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
}
|
|
103
|
+
.collapse-menu-popover button:hover,
|
|
104
|
+
.collapse-menu-popover button:focus-visible {
|
|
105
|
+
background: var(--mm-hover);
|
|
106
|
+
outline: none;
|
|
107
|
+
}
|
|
108
|
+
.collapse-menu-popover kbd {
|
|
109
|
+
color: var(--mm-text-dim);
|
|
110
|
+
font: inherit;
|
|
111
|
+
font-size: 12px;
|
|
112
|
+
}
|
|
113
|
+
</style>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import AppIcon from './AppIcon.vue'
|
|
3
|
+
|
|
4
|
+
withDefaults(
|
|
5
|
+
defineProps<{
|
|
6
|
+
icon: string
|
|
7
|
+
label: string
|
|
8
|
+
size?: number
|
|
9
|
+
active?: boolean
|
|
10
|
+
danger?: boolean
|
|
11
|
+
disabled?: boolean
|
|
12
|
+
}>(),
|
|
13
|
+
{ size: 18, active: false, danger: false, disabled: false }
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
defineEmits<{ click: [event: MouseEvent] }>()
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<button
|
|
21
|
+
type="button"
|
|
22
|
+
class="icon-button"
|
|
23
|
+
:class="{ active, danger }"
|
|
24
|
+
:title="label"
|
|
25
|
+
:aria-label="label"
|
|
26
|
+
:disabled="disabled"
|
|
27
|
+
@click="$emit('click', $event)"
|
|
28
|
+
>
|
|
29
|
+
<AppIcon :name="icon" :size="size" />
|
|
30
|
+
</button>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<style scoped>
|
|
34
|
+
.icon-button {
|
|
35
|
+
display: inline-flex;
|
|
36
|
+
width: 32px;
|
|
37
|
+
height: 32px;
|
|
38
|
+
align-items: center;
|
|
39
|
+
justify-content: center;
|
|
40
|
+
padding: 0;
|
|
41
|
+
border: 0;
|
|
42
|
+
border-radius: 7px;
|
|
43
|
+
background: transparent;
|
|
44
|
+
color: var(--mm-text-dim);
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
}
|
|
47
|
+
.icon-button:hover:not(:disabled),
|
|
48
|
+
.icon-button.active {
|
|
49
|
+
background: var(--mm-hover);
|
|
50
|
+
color: var(--mm-text);
|
|
51
|
+
}
|
|
52
|
+
.icon-button.danger {
|
|
53
|
+
color: #d84d57;
|
|
54
|
+
}
|
|
55
|
+
.icon-button:focus-visible {
|
|
56
|
+
outline: 2px solid var(--mm-accent);
|
|
57
|
+
outline-offset: 1px;
|
|
58
|
+
}
|
|
59
|
+
.icon-button:disabled {
|
|
60
|
+
cursor: default;
|
|
61
|
+
opacity: 0.38;
|
|
62
|
+
}
|
|
63
|
+
</style>
|
|
@@ -7,8 +7,10 @@ const props = withDefaults(
|
|
|
7
7
|
url: string
|
|
8
8
|
position: { left: number; top: number }
|
|
9
9
|
startEditing?: boolean
|
|
10
|
+
/** 见 `CanvasContextMenu`:默认就地渲染,整页宿主传 `'body'`。 */
|
|
11
|
+
teleportTo?: string
|
|
10
12
|
}>(),
|
|
11
|
-
{ startEditing: false }
|
|
13
|
+
{ startEditing: false, teleportTo: undefined }
|
|
12
14
|
)
|
|
13
15
|
const emit = defineEmits<{ save: [url: string]; remove: []; keep: []; leave: []; close: [] }>()
|
|
14
16
|
const draft = ref(props.url)
|
|
@@ -50,52 +52,54 @@ defineExpose({ focusInput })
|
|
|
50
52
|
</script>
|
|
51
53
|
|
|
52
54
|
<template>
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
<input
|
|
60
|
-
ref="input"
|
|
61
|
-
v-model="draft"
|
|
62
|
-
class="link-input"
|
|
63
|
-
:class="{ readonly: !editing }"
|
|
64
|
-
:readonly="!editing"
|
|
65
|
-
aria-label="链接地址"
|
|
66
|
-
@dblclick="beginEdit"
|
|
67
|
-
@keydown="onKeydown"
|
|
68
|
-
/>
|
|
69
|
-
<button
|
|
70
|
-
v-if="editing"
|
|
71
|
-
type="button"
|
|
72
|
-
class="link-action primary"
|
|
73
|
-
title="更新链接 (Enter)"
|
|
74
|
-
aria-label="更新链接"
|
|
75
|
-
@click="submit"
|
|
55
|
+
<Teleport :to="teleportTo" :disabled="!teleportTo">
|
|
56
|
+
<div
|
|
57
|
+
class="link-popover"
|
|
58
|
+
:style="{ left: `${position.left}px`, top: `${position.top}px` }"
|
|
59
|
+
@mouseenter="emit('keep')"
|
|
60
|
+
@mouseleave="emit('leave')"
|
|
76
61
|
>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
62
|
+
<input
|
|
63
|
+
ref="input"
|
|
64
|
+
v-model="draft"
|
|
65
|
+
class="link-input"
|
|
66
|
+
:class="{ readonly: !editing }"
|
|
67
|
+
:readonly="!editing"
|
|
68
|
+
aria-label="链接地址"
|
|
69
|
+
@dblclick="beginEdit"
|
|
70
|
+
@keydown="onKeydown"
|
|
71
|
+
/>
|
|
72
|
+
<button
|
|
73
|
+
v-if="editing"
|
|
74
|
+
type="button"
|
|
75
|
+
class="link-action primary"
|
|
76
|
+
title="更新链接 (Enter)"
|
|
77
|
+
aria-label="更新链接"
|
|
78
|
+
@click="submit"
|
|
79
|
+
>
|
|
80
|
+
更新
|
|
81
|
+
</button>
|
|
82
|
+
<button
|
|
83
|
+
v-else
|
|
84
|
+
type="button"
|
|
85
|
+
class="icon-action"
|
|
86
|
+
title="编辑链接地址"
|
|
87
|
+
aria-label="编辑链接地址"
|
|
88
|
+
@click="beginEdit"
|
|
89
|
+
>
|
|
90
|
+
<AppIcon name="edit" :size="18" />
|
|
91
|
+
</button>
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
class="icon-action"
|
|
95
|
+
title="移除链接,保留文案"
|
|
96
|
+
aria-label="移除链接"
|
|
97
|
+
@click="emit('remove')"
|
|
98
|
+
>
|
|
99
|
+
<AppIcon name="removeLink" :size="18" />
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
</Teleport>
|
|
99
103
|
</template>
|
|
100
104
|
|
|
101
105
|
<style scoped>
|
|
@@ -37,12 +37,14 @@ function onInput(e: Event) {
|
|
|
37
37
|
}, 300)
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
/** 有未提交的防抖输入时立即回流,返回被 flush 的草稿;没有则返回 `null`。 */
|
|
41
|
+
function flushDraft(): string | null {
|
|
42
|
+
if (!typing) return null
|
|
42
43
|
if (timer) clearTimeout(timer)
|
|
43
44
|
timer = null
|
|
44
45
|
typing = false
|
|
45
46
|
emit('update:modelValue', draft.value)
|
|
47
|
+
return draft.value
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
function goToDiagnostic(item: MarkdownDiagnostic) {
|
|
@@ -63,7 +65,7 @@ function selectAllFromHost(): void {
|
|
|
63
65
|
input.setSelectionRange(0, input.value.length)
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
defineExpose({ selectAllFromHost })
|
|
68
|
+
defineExpose({ selectAllFromHost, flushDraft })
|
|
67
69
|
|
|
68
70
|
function onPaste(e: ClipboardEvent) {
|
|
69
71
|
const image = [...(e.clipboardData?.items ?? [])]
|
|
@@ -13,7 +13,15 @@ import LinkPopover from './LinkPopover.vue'
|
|
|
13
13
|
import SelectionToolbar from './SelectionToolbar.vue'
|
|
14
14
|
import { pasteCanvasOutline, readMindmapClipboard, writeMindmapClipboard } from './mindmapClipboard'
|
|
15
15
|
|
|
16
|
-
const props = defineProps<{
|
|
16
|
+
const props = defineProps<{
|
|
17
|
+
session: MindmapSession
|
|
18
|
+
resolveImageSrc?: (src: string) => string
|
|
19
|
+
/**
|
|
20
|
+
* 浮层挂载点,透传给 `CanvasContextMenu` / `LinkPopover`。
|
|
21
|
+
* 默认 `undefined` = 就地渲染(嵌在笔记里的 Desk / SSG);整页宿主传 `'body'`。
|
|
22
|
+
*/
|
|
23
|
+
teleportTo?: string
|
|
24
|
+
}>()
|
|
17
25
|
|
|
18
26
|
const emit = defineEmits<{
|
|
19
27
|
ready: [editor: CanvasEditor]
|
|
@@ -209,6 +217,7 @@ onBeforeUnmount(() => {
|
|
|
209
217
|
v-if="linkEditor"
|
|
210
218
|
:url="linkEditor.url"
|
|
211
219
|
:position="linkEditor.position"
|
|
220
|
+
:teleport-to="teleportTo"
|
|
212
221
|
@save="saveLink"
|
|
213
222
|
@remove="removeLink"
|
|
214
223
|
@keep="keepLinkPopover"
|
|
@@ -227,6 +236,7 @@ onBeforeUnmount(() => {
|
|
|
227
236
|
:can-delete-tree="contextMenu.canDeleteTree"
|
|
228
237
|
:can-toggle-siblings="contextMenu.canToggleSiblings"
|
|
229
238
|
:can-focus="contextMenu.canFocus"
|
|
239
|
+
:teleport-to="teleportTo"
|
|
230
240
|
@insert-sibling="insertSibling"
|
|
231
241
|
@insert-child="insertChild"
|
|
232
242
|
@insert-parent="insertParent"
|