@tnotesjs/ui 0.1.5 → 0.2.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 +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 +26 -9
- package/src/browser/clipboard.ts +16 -19
- package/src/browser/theme.ts +10 -15
- 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.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/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.ts +224 -0
- package/src/excalidraw/exporter.ts +50 -0
- package/src/excalidraw/focus.ts +33 -0
- package/src/excalidraw/fonts.ts +92 -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.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.ts +91 -102
- package/src/code/highlight.test.ts +0 -55
- package/src/components/Mermaid/Mermaid.test.ts +0 -54
- package/src/components/Mermaid/adoptLiveSvg.test.ts +0 -16
- package/src/components/Mindmap/FocusBreadcrumbs.test.ts +0 -265
- package/src/components/sharedMedia.test.ts +0 -139
- package/src/markdown/image.test.ts +0 -88
- package/src/swiper/hydrate.test.ts +0 -65
|
@@ -1,185 +1,162 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from
|
|
2
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
3
3
|
|
|
4
4
|
const props = withDefaults(defineProps<{ selector?: string }>(), {
|
|
5
|
-
selector:
|
|
6
|
-
})
|
|
7
|
-
const visible = ref(false)
|
|
8
|
-
const images = ref<string[]>([])
|
|
9
|
-
const index = ref(0)
|
|
10
|
-
const scale = ref(1)
|
|
11
|
-
const x = ref(0)
|
|
12
|
-
const y = ref(0)
|
|
13
|
-
const dragging = ref(false)
|
|
14
|
-
const overlay = ref<HTMLElement>()
|
|
15
|
-
let pointerStart = { x: 0, y: 0, baseX: 0, baseY: 0 }
|
|
16
|
-
let swiperDownX = 0
|
|
17
|
-
let previousOverflow =
|
|
18
|
-
let previousFocus: HTMLElement | null = null
|
|
5
|
+
selector: '.tn-prose img, .vp-doc img'
|
|
6
|
+
})
|
|
7
|
+
const visible = ref(false)
|
|
8
|
+
const images = ref<string[]>([])
|
|
9
|
+
const index = ref(0)
|
|
10
|
+
const scale = ref(1)
|
|
11
|
+
const x = ref(0)
|
|
12
|
+
const y = ref(0)
|
|
13
|
+
const dragging = ref(false)
|
|
14
|
+
const overlay = ref<HTMLElement>()
|
|
15
|
+
let pointerStart = { x: 0, y: 0, baseX: 0, baseY: 0 }
|
|
16
|
+
let swiperDownX = 0
|
|
17
|
+
let previousOverflow = ''
|
|
18
|
+
let previousFocus: HTMLElement | null = null
|
|
19
19
|
|
|
20
|
-
const source = computed(() => images.value[index.value] ||
|
|
21
|
-
const transform = computed(
|
|
22
|
-
() => `translate(${x.value}px, ${y.value}px) scale(${scale.value})`,
|
|
23
|
-
);
|
|
20
|
+
const source = computed(() => images.value[index.value] || '')
|
|
21
|
+
const transform = computed(() => `translate(${x.value}px, ${y.value}px) scale(${scale.value})`)
|
|
24
22
|
|
|
25
23
|
function eligible(image: HTMLImageElement): boolean {
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
image.closest(".tn-preview-ignore, button")
|
|
29
|
-
)
|
|
30
|
-
return false;
|
|
31
|
-
return !(
|
|
32
|
-
image.naturalWidth > 0 &&
|
|
33
|
-
image.naturalWidth < 50 &&
|
|
34
|
-
image.naturalHeight < 50
|
|
35
|
-
);
|
|
24
|
+
if (image.dataset.preview === 'false' || image.closest('.tn-preview-ignore, button')) return false
|
|
25
|
+
return !(image.naturalWidth > 0 && image.naturalWidth < 50 && image.naturalHeight < 50)
|
|
36
26
|
}
|
|
37
27
|
|
|
38
28
|
function reset(): void {
|
|
39
|
-
scale.value = 1
|
|
40
|
-
x.value = 0
|
|
41
|
-
y.value = 0
|
|
29
|
+
scale.value = 1
|
|
30
|
+
x.value = 0
|
|
31
|
+
y.value = 0
|
|
42
32
|
}
|
|
43
33
|
|
|
44
34
|
function open(image: HTMLImageElement): void {
|
|
45
|
-
const candidates = [
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
images.value = candidates
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
: null;
|
|
61
|
-
previousOverflow = document.documentElement.style.overflow;
|
|
62
|
-
document.documentElement.style.overflow = "hidden";
|
|
63
|
-
visible.value = true;
|
|
64
|
-
void nextTick(() => overlay.value?.focus({ preventScroll: true }));
|
|
35
|
+
const candidates = [...document.querySelectorAll<HTMLImageElement>(props.selector)].filter(
|
|
36
|
+
eligible
|
|
37
|
+
)
|
|
38
|
+
images.value = candidates.map((item) => item.currentSrc || item.src).filter(Boolean)
|
|
39
|
+
const selected = image.currentSrc || image.src
|
|
40
|
+
const selectedIndex = images.value.indexOf(selected)
|
|
41
|
+
index.value = selectedIndex < 0 ? 0 : selectedIndex
|
|
42
|
+
if (selectedIndex < 0 && selected) images.value = [selected]
|
|
43
|
+
if (images.value.length === 0) return
|
|
44
|
+
reset()
|
|
45
|
+
previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null
|
|
46
|
+
previousOverflow = document.documentElement.style.overflow
|
|
47
|
+
document.documentElement.style.overflow = 'hidden'
|
|
48
|
+
visible.value = true
|
|
49
|
+
void nextTick(() => overlay.value?.focus({ preventScroll: true }))
|
|
65
50
|
}
|
|
66
51
|
|
|
67
52
|
function close(): void {
|
|
68
|
-
visible.value = false
|
|
69
|
-
document.documentElement.style.overflow = previousOverflow
|
|
70
|
-
previousFocus?.focus({ preventScroll: true })
|
|
53
|
+
visible.value = false
|
|
54
|
+
document.documentElement.style.overflow = previousOverflow
|
|
55
|
+
previousFocus?.focus({ preventScroll: true })
|
|
71
56
|
}
|
|
72
57
|
|
|
73
58
|
function move(step: number): void {
|
|
74
|
-
if (images.value.length < 2) return
|
|
75
|
-
index.value =
|
|
76
|
-
|
|
77
|
-
reset();
|
|
59
|
+
if (images.value.length < 2) return
|
|
60
|
+
index.value = (index.value + step + images.value.length) % images.value.length
|
|
61
|
+
reset()
|
|
78
62
|
}
|
|
79
63
|
|
|
80
64
|
function zoom(factor: number): void {
|
|
81
|
-
scale.value = Math.min(6, Math.max(0.2, scale.value * factor))
|
|
65
|
+
scale.value = Math.min(6, Math.max(0.2, scale.value * factor))
|
|
82
66
|
}
|
|
83
67
|
|
|
84
68
|
function resolvePreviewImage(target: EventTarget | null): HTMLImageElement | null {
|
|
85
|
-
if (target instanceof HTMLImageElement) return target
|
|
69
|
+
if (target instanceof HTMLImageElement) return target
|
|
86
70
|
if (target instanceof Element) {
|
|
87
|
-
return target.closest<HTMLImageElement>(
|
|
71
|
+
return target.closest<HTMLImageElement>('img')
|
|
88
72
|
}
|
|
89
|
-
return null
|
|
73
|
+
return null
|
|
90
74
|
}
|
|
91
75
|
|
|
92
76
|
function onDocumentClick(event: MouseEvent): void {
|
|
93
|
-
if (visible.value) return
|
|
94
|
-
const image = resolvePreviewImage(event.target)
|
|
95
|
-
if (!image || !image.matches(props.selector) || !eligible(image)) return
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return;
|
|
101
|
-
if (image.closest("a")) {
|
|
102
|
-
event.preventDefault();
|
|
103
|
-
event.stopPropagation();
|
|
77
|
+
if (visible.value) return
|
|
78
|
+
const image = resolvePreviewImage(event.target)
|
|
79
|
+
if (!image || !image.matches(props.selector) || !eligible(image)) return
|
|
80
|
+
if (image.closest('.swiper-container') && Math.abs(event.clientX - swiperDownX) > 5) return
|
|
81
|
+
if (image.closest('a')) {
|
|
82
|
+
event.preventDefault()
|
|
83
|
+
event.stopPropagation()
|
|
104
84
|
}
|
|
105
|
-
open(image)
|
|
85
|
+
open(image)
|
|
106
86
|
}
|
|
107
87
|
|
|
108
88
|
function onPreviewRequest(event: Event): void {
|
|
109
|
-
const detail = (event as CustomEvent<HTMLImageElement | { src?: string }>).detail
|
|
89
|
+
const detail = (event as CustomEvent<HTMLImageElement | { src?: string }>).detail
|
|
110
90
|
if (detail instanceof HTMLImageElement) {
|
|
111
|
-
open(detail)
|
|
112
|
-
return
|
|
91
|
+
open(detail)
|
|
92
|
+
return
|
|
113
93
|
}
|
|
114
|
-
const src = detail && typeof detail.src ===
|
|
115
|
-
if (!src) return
|
|
116
|
-
images.value = [src]
|
|
117
|
-
index.value = 0
|
|
118
|
-
reset()
|
|
119
|
-
previousFocus =
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
document.documentElement.style.overflow = "hidden";
|
|
125
|
-
visible.value = true;
|
|
126
|
-
void nextTick(() => overlay.value?.focus({ preventScroll: true }));
|
|
94
|
+
const src = detail && typeof detail.src === 'string' ? detail.src : ''
|
|
95
|
+
if (!src) return
|
|
96
|
+
images.value = [src]
|
|
97
|
+
index.value = 0
|
|
98
|
+
reset()
|
|
99
|
+
previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null
|
|
100
|
+
previousOverflow = document.documentElement.style.overflow
|
|
101
|
+
document.documentElement.style.overflow = 'hidden'
|
|
102
|
+
visible.value = true
|
|
103
|
+
void nextTick(() => overlay.value?.focus({ preventScroll: true }))
|
|
127
104
|
}
|
|
128
105
|
|
|
129
106
|
function onKeydown(event: KeyboardEvent): void {
|
|
130
|
-
if (!visible.value) return
|
|
131
|
-
if (event.key ===
|
|
132
|
-
else if (event.key ===
|
|
133
|
-
else if (event.key ===
|
|
134
|
-
else if (event.key ===
|
|
135
|
-
else if (event.key ===
|
|
136
|
-
else return
|
|
137
|
-
event.preventDefault()
|
|
107
|
+
if (!visible.value) return
|
|
108
|
+
if (event.key === 'Escape') close()
|
|
109
|
+
else if (event.key === 'ArrowLeft') move(-1)
|
|
110
|
+
else if (event.key === 'ArrowRight') move(1)
|
|
111
|
+
else if (event.key === 'ArrowUp' || event.key === '+') zoom(1.1)
|
|
112
|
+
else if (event.key === 'ArrowDown' || event.key === '-') zoom(1 / 1.1)
|
|
113
|
+
else return
|
|
114
|
+
event.preventDefault()
|
|
138
115
|
}
|
|
139
116
|
|
|
140
117
|
function onPointerDown(event: PointerEvent): void {
|
|
141
|
-
dragging.value = true
|
|
118
|
+
dragging.value = true
|
|
142
119
|
pointerStart = {
|
|
143
120
|
x: event.clientX,
|
|
144
121
|
y: event.clientY,
|
|
145
122
|
baseX: x.value,
|
|
146
|
-
baseY: y.value
|
|
147
|
-
}
|
|
148
|
-
window.addEventListener(
|
|
149
|
-
window.addEventListener(
|
|
123
|
+
baseY: y.value
|
|
124
|
+
}
|
|
125
|
+
window.addEventListener('pointermove', onPointerMove)
|
|
126
|
+
window.addEventListener('pointerup', onPointerUp, { once: true })
|
|
150
127
|
}
|
|
151
128
|
|
|
152
129
|
function onPointerMove(event: PointerEvent): void {
|
|
153
|
-
if (!dragging.value) return
|
|
154
|
-
x.value = pointerStart.baseX + event.clientX - pointerStart.x
|
|
155
|
-
y.value = pointerStart.baseY + event.clientY - pointerStart.y
|
|
130
|
+
if (!dragging.value) return
|
|
131
|
+
x.value = pointerStart.baseX + event.clientX - pointerStart.x
|
|
132
|
+
y.value = pointerStart.baseY + event.clientY - pointerStart.y
|
|
156
133
|
}
|
|
157
134
|
|
|
158
135
|
function onPointerUp(): void {
|
|
159
|
-
dragging.value = false
|
|
160
|
-
window.removeEventListener(
|
|
136
|
+
dragging.value = false
|
|
137
|
+
window.removeEventListener('pointermove', onPointerMove)
|
|
161
138
|
}
|
|
162
139
|
|
|
163
140
|
function onGlobalPointerDown(event: PointerEvent): void {
|
|
164
|
-
swiperDownX = event.clientX
|
|
141
|
+
swiperDownX = event.clientX
|
|
165
142
|
}
|
|
166
143
|
|
|
167
144
|
onMounted(() => {
|
|
168
|
-
document.addEventListener(
|
|
169
|
-
document.addEventListener(
|
|
170
|
-
document.addEventListener(
|
|
171
|
-
document.addEventListener(
|
|
172
|
-
})
|
|
145
|
+
document.addEventListener('click', onDocumentClick, true)
|
|
146
|
+
document.addEventListener('tn:preview-image', onPreviewRequest)
|
|
147
|
+
document.addEventListener('keydown', onKeydown)
|
|
148
|
+
document.addEventListener('pointerdown', onGlobalPointerDown, true)
|
|
149
|
+
})
|
|
173
150
|
|
|
174
151
|
onBeforeUnmount(() => {
|
|
175
|
-
document.removeEventListener(
|
|
176
|
-
document.removeEventListener(
|
|
177
|
-
document.removeEventListener(
|
|
178
|
-
document.removeEventListener(
|
|
179
|
-
window.removeEventListener(
|
|
180
|
-
window.removeEventListener(
|
|
181
|
-
if (visible.value) document.documentElement.style.overflow = previousOverflow
|
|
182
|
-
})
|
|
152
|
+
document.removeEventListener('click', onDocumentClick, true)
|
|
153
|
+
document.removeEventListener('tn:preview-image', onPreviewRequest)
|
|
154
|
+
document.removeEventListener('keydown', onKeydown)
|
|
155
|
+
document.removeEventListener('pointerdown', onGlobalPointerDown, true)
|
|
156
|
+
window.removeEventListener('pointermove', onPointerMove)
|
|
157
|
+
window.removeEventListener('pointerup', onPointerUp)
|
|
158
|
+
if (visible.value) document.documentElement.style.overflow = previousOverflow
|
|
159
|
+
})
|
|
183
160
|
</script>
|
|
184
161
|
|
|
185
162
|
<template>
|
|
@@ -214,9 +191,7 @@ onBeforeUnmount(() => {
|
|
|
214
191
|
›
|
|
215
192
|
</button>
|
|
216
193
|
<div class="tn-image-preview__tools">
|
|
217
|
-
<button type="button" aria-label="缩小" @click="zoom(1 / 1.1)">
|
|
218
|
-
−
|
|
219
|
-
</button>
|
|
194
|
+
<button type="button" aria-label="缩小" @click="zoom(1 / 1.1)">−</button>
|
|
220
195
|
<button type="button" aria-label="还原" @click="reset">↺</button>
|
|
221
196
|
<button type="button" aria-label="放大" @click="zoom(1.1)">+</button>
|
|
222
197
|
<button type="button" aria-label="关闭" @click="close">×</button>
|
|
@@ -19,12 +19,7 @@
|
|
|
19
19
|
:aria-pressed="centered ? 'true' : 'false'"
|
|
20
20
|
@click="toggleCenter"
|
|
21
21
|
>
|
|
22
|
-
<img
|
|
23
|
-
:src="centered ? iconCenterOn : iconCenterOff"
|
|
24
|
-
alt=""
|
|
25
|
-
width="16"
|
|
26
|
-
height="16"
|
|
27
|
-
/>
|
|
22
|
+
<img :src="centered ? iconCenterOn : iconCenterOff" alt="" width="16" height="16" />
|
|
28
23
|
</button>
|
|
29
24
|
<button
|
|
30
25
|
v-if="enableFullscreen"
|
|
@@ -65,7 +60,11 @@
|
|
|
65
60
|
ref="diagramRef"
|
|
66
61
|
class="tn-mermaid__diagram"
|
|
67
62
|
:class="{ 'is-centered': centered, 'is-obscured': empty || !!error }"
|
|
68
|
-
:style="
|
|
63
|
+
:style="
|
|
64
|
+
diagramTransform
|
|
65
|
+
? { transform: diagramTransform, transformOrigin: 'top left' }
|
|
66
|
+
: undefined
|
|
67
|
+
"
|
|
69
68
|
@wheel="handleWheel"
|
|
70
69
|
/>
|
|
71
70
|
<div v-if="loading" class="tn-mermaid__loading tn-mermaid__status">
|
|
@@ -152,7 +151,7 @@ const props = defineProps({
|
|
|
152
151
|
type: String,
|
|
153
152
|
default: ''
|
|
154
153
|
},
|
|
155
|
-
/** URI-encoded source from
|
|
154
|
+
/** URI-encoded source from SSG fence (`graph="…"`). */
|
|
156
155
|
graph: {
|
|
157
156
|
type: String,
|
|
158
157
|
default: ''
|
|
@@ -241,9 +240,7 @@ function detectDark() {
|
|
|
241
240
|
|
|
242
241
|
const COPY_RESET_DELAY = 1000
|
|
243
242
|
const copyState = ref('idle')
|
|
244
|
-
const copyIcon = computed(() =>
|
|
245
|
-
copyState.value === 'copied' ? iconCheck : iconClipboard
|
|
246
|
-
)
|
|
243
|
+
const copyIcon = computed(() => (copyState.value === 'copied' ? iconCheck : iconClipboard))
|
|
247
244
|
const copyTitle = computed(() => {
|
|
248
245
|
if (copyState.value === 'copied') return '已复制'
|
|
249
246
|
if (copyState.value === 'failed') return '复制失败'
|
|
@@ -369,7 +366,7 @@ async function toggleFullscreen() {
|
|
|
369
366
|
fullscreenMode = 'css'
|
|
370
367
|
isFullscreen.value = true
|
|
371
368
|
|
|
372
|
-
// Best-effort native fullscreen
|
|
369
|
+
// Best-effort native fullscreen (non-blocking).
|
|
373
370
|
if (!document.fullscreenEnabled || typeof root.requestFullscreen !== 'function') return
|
|
374
371
|
if (/Electron/i.test(navigator.userAgent)) return
|
|
375
372
|
try {
|
|
@@ -46,7 +46,7 @@ const openSiblings = computed(() => openNode.value?.parent?.children ?? [])
|
|
|
46
46
|
const menuStyle = computed(() => ({
|
|
47
47
|
left: `${menuPosition.value.left}px`,
|
|
48
48
|
top: `${menuPosition.value.top}px`,
|
|
49
|
-
width: `${MENU_WIDTH}px
|
|
49
|
+
width: `${MENU_WIDTH}px`
|
|
50
50
|
}))
|
|
51
51
|
|
|
52
52
|
function hasSiblingMenu(node: MindmapNode): boolean {
|
|
@@ -68,12 +68,16 @@ function updateMenuPosition() {
|
|
|
68
68
|
const rect = trigger.getBoundingClientRect()
|
|
69
69
|
const viewportWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0)
|
|
70
70
|
const viewportHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0)
|
|
71
|
-
const left = Math.max(
|
|
71
|
+
const left = Math.max(
|
|
72
|
+
VIEWPORT_GAP,
|
|
73
|
+
Math.min(rect.left, viewportWidth - MENU_WIDTH - VIEWPORT_GAP)
|
|
74
|
+
)
|
|
72
75
|
const estimatedHeight = Math.min(360, Math.max(44, openSiblings.value.length * 40 + 12))
|
|
73
76
|
const below = rect.bottom + 6
|
|
74
|
-
const top =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
const top =
|
|
78
|
+
below + estimatedHeight <= viewportHeight - VIEWPORT_GAP
|
|
79
|
+
? below
|
|
80
|
+
: Math.max(VIEWPORT_GAP, rect.top - estimatedHeight - 6)
|
|
77
81
|
menuPosition.value = { left, top }
|
|
78
82
|
}
|
|
79
83
|
|
|
@@ -85,11 +89,8 @@ function focusMenuItem(where: 'first' | 'last' | 'current') {
|
|
|
85
89
|
const items = menuItems()
|
|
86
90
|
if (items.length === 0) return
|
|
87
91
|
const currentIndex = items.findIndex((item) => item.getAttribute('aria-checked') === 'true')
|
|
88
|
-
const index =
|
|
89
|
-
? 0
|
|
90
|
-
: where === 'last'
|
|
91
|
-
? items.length - 1
|
|
92
|
-
: Math.max(0, currentIndex)
|
|
92
|
+
const index =
|
|
93
|
+
where === 'first' ? 0 : where === 'last' ? items.length - 1 : Math.max(0, currentIndex)
|
|
93
94
|
items[index]?.focus()
|
|
94
95
|
}
|
|
95
96
|
|
|
@@ -123,7 +124,7 @@ function requestKeyboardMenu(
|
|
|
123
124
|
node: MindmapNode,
|
|
124
125
|
depth: number,
|
|
125
126
|
event: KeyboardEvent,
|
|
126
|
-
where: 'first' | 'last' | 'current'
|
|
127
|
+
where: 'first' | 'last' | 'current'
|
|
127
128
|
) {
|
|
128
129
|
if (!hasSiblingMenu(node)) return
|
|
129
130
|
event.preventDefault()
|
|
@@ -189,7 +190,8 @@ function onMenuKeydown(event: KeyboardEvent) {
|
|
|
189
190
|
const current = items.indexOf(document.activeElement as HTMLButtonElement)
|
|
190
191
|
let next: number
|
|
191
192
|
if (event.key === 'ArrowDown') next = current < 0 ? 0 : (current + 1) % items.length
|
|
192
|
-
else if (event.key === 'ArrowUp')
|
|
193
|
+
else if (event.key === 'ArrowUp')
|
|
194
|
+
next = current < 0 ? items.length - 1 : (current - 1 + items.length) % items.length
|
|
193
195
|
else if (event.key === 'Home') next = 0
|
|
194
196
|
else if (event.key === 'End') next = items.length - 1
|
|
195
197
|
else if (event.key === 'Escape') {
|
|
@@ -223,13 +225,17 @@ function onViewportChange() {
|
|
|
223
225
|
if (openMenu.value) updateMenuPosition()
|
|
224
226
|
}
|
|
225
227
|
|
|
226
|
-
watch(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
228
|
+
watch(
|
|
229
|
+
focusPath,
|
|
230
|
+
() => {
|
|
231
|
+
closeMenu()
|
|
232
|
+
nextTick(() => {
|
|
233
|
+
const current = scroller.value?.querySelector<HTMLElement>('[aria-current="page"]')
|
|
234
|
+
current?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' })
|
|
235
|
+
})
|
|
236
|
+
},
|
|
237
|
+
{ immediate: true, flush: 'post' }
|
|
238
|
+
)
|
|
233
239
|
|
|
234
240
|
onMounted(() => {
|
|
235
241
|
document.addEventListener('pointerdown', onDocumentPointerDown)
|
|
@@ -249,12 +255,7 @@ onBeforeUnmount(() => {
|
|
|
249
255
|
</script>
|
|
250
256
|
|
|
251
257
|
<template>
|
|
252
|
-
<nav
|
|
253
|
-
v-if="focusPath.length > 0"
|
|
254
|
-
ref="root"
|
|
255
|
-
class="focus-breadcrumbs"
|
|
256
|
-
aria-label="主题导航"
|
|
257
|
-
>
|
|
258
|
+
<nav v-if="focusPath.length > 0" ref="root" class="focus-breadcrumbs" aria-label="主题导航">
|
|
258
259
|
<div ref="scroller" class="focus-breadcrumbs-scroller">
|
|
259
260
|
<div class="focus-breadcrumbs-track">
|
|
260
261
|
<button
|
|
@@ -276,7 +277,9 @@ onBeforeUnmount(() => {
|
|
|
276
277
|
:aria-current="depth === focusPath.length - 1 ? 'page' : undefined"
|
|
277
278
|
:aria-haspopup="hasSiblingMenu(node) ? 'menu' : undefined"
|
|
278
279
|
:aria-expanded="hasSiblingMenu(node) ? openMenu?.nodeId === node.id : undefined"
|
|
279
|
-
:aria-controls="
|
|
280
|
+
:aria-controls="
|
|
281
|
+
hasSiblingMenu(node) && openMenu?.nodeId === node.id ? menuId : undefined
|
|
282
|
+
"
|
|
280
283
|
:title="node.content.text"
|
|
281
284
|
@click="navigateAncestor(node, depth)"
|
|
282
285
|
@mouseenter="requestHoverMenu(node, depth, $event)"
|
|
@@ -364,7 +367,7 @@ onBeforeUnmount(() => {
|
|
|
364
367
|
|
|
365
368
|
.focus-crumb:hover,
|
|
366
369
|
.focus-crumb:focus-visible,
|
|
367
|
-
.focus-crumb[aria-expanded=
|
|
370
|
+
.focus-crumb[aria-expanded='true'] {
|
|
368
371
|
background: var(--tn-c-default-soft);
|
|
369
372
|
outline: none;
|
|
370
373
|
}
|
|
@@ -390,7 +393,7 @@ onBeforeUnmount(() => {
|
|
|
390
393
|
border: 1px solid var(--tn-c-divider);
|
|
391
394
|
border-radius: 10px;
|
|
392
395
|
background: var(--tn-c-bg-elv, var(--tn-c-bg));
|
|
393
|
-
box-shadow: var(--tn-shadow-2, 0 14px 38px rgb(0 0 0 / .24));
|
|
396
|
+
box-shadow: var(--tn-shadow-2, 0 14px 38px rgb(0 0 0 / 0.24));
|
|
394
397
|
color: var(--tn-c-text);
|
|
395
398
|
}
|
|
396
399
|
|
|
@@ -4,22 +4,31 @@ import { defineComponent, h } from 'vue'
|
|
|
4
4
|
export default defineComponent({
|
|
5
5
|
name: 'MindmapInlineRuns',
|
|
6
6
|
props: {
|
|
7
|
-
raw: { type: String, default: '' }
|
|
7
|
+
raw: { type: String, default: '' }
|
|
8
8
|
},
|
|
9
9
|
setup(props) {
|
|
10
|
-
return () =>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
10
|
+
return () =>
|
|
11
|
+
h(
|
|
12
|
+
'span',
|
|
13
|
+
{ class: 'mindmap-inline-runs' },
|
|
14
|
+
parseInlineSegments(props.raw).map((segment) => {
|
|
15
|
+
const classes = Object.entries(segment.marks)
|
|
16
|
+
.filter(([, enabled]) => enabled)
|
|
17
|
+
.map(([name]) => `is-${name}`)
|
|
18
|
+
if (segment.link) {
|
|
19
|
+
return h(
|
|
20
|
+
'a',
|
|
21
|
+
{
|
|
22
|
+
class: ['mindmap-inline-run', 'is-link', ...classes],
|
|
23
|
+
href: segment.link.url,
|
|
24
|
+
target: '_blank',
|
|
25
|
+
rel: 'noopener noreferrer'
|
|
26
|
+
},
|
|
27
|
+
segment.text
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
return h('span', { class: ['mindmap-inline-run', ...classes] }, segment.text)
|
|
31
|
+
})
|
|
32
|
+
)
|
|
33
|
+
}
|
|
25
34
|
})
|