@tnotesjs/ui 0.1.4 → 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.
Files changed (67) hide show
  1. package/README.md +30 -97
  2. package/dist/code/highlight.js +6 -15
  3. package/dist/components/Footprints/parse.js +1 -3
  4. package/dist/components/Mindmap/markdown.d.ts +1 -7
  5. package/dist/components/Mindmap/markdown.js +1 -15
  6. package/package.json +28 -13
  7. package/src/browser/clipboard.ts +16 -19
  8. package/src/browser/theme.ts +10 -15
  9. package/src/code/highlight.test.ts +23 -6
  10. package/src/code/highlight.ts +89 -105
  11. package/src/components/Badge/Badge.vue +4 -4
  12. package/src/components/CodeBlock/CodeBlock.vue +43 -53
  13. package/src/components/CodeGroup/CodeGroup.vue +32 -35
  14. package/src/components/Footprints/Footprints.vue +2 -14
  15. package/src/components/Footprints/parse.ts +1 -3
  16. package/src/components/ImagePreview/ImagePreview.vue +101 -126
  17. package/src/components/Mermaid/Mermaid.vue +9 -12
  18. package/src/components/Mindmap/FocusBreadcrumbs.test.ts +40 -30
  19. package/src/components/Mindmap/FocusBreadcrumbs.vue +31 -28
  20. package/src/components/Mindmap/InlineRuns.ts +25 -16
  21. package/src/components/Mindmap/Mindmap.vue +147 -81
  22. package/src/components/Mindmap/MindmapOutlineNode.vue +4 -1
  23. package/src/components/Mindmap/editor/AppIcon.vue +32 -11
  24. package/src/components/Mindmap/editor/CanvasContextMenu.vue +126 -49
  25. package/src/components/Mindmap/editor/LinkPopover.vue +101 -23
  26. package/src/components/Mindmap/editor/MarkdownView.vue +7 -4
  27. package/src/components/Mindmap/editor/MindmapView.vue +11 -3
  28. package/src/components/Mindmap/editor/OutlineView.vue +144 -58
  29. package/src/components/Mindmap/editor/RichInlineEditor.vue +93 -44
  30. package/src/components/Mindmap/editor/SelectionToolbar.vue +133 -41
  31. package/src/components/Mindmap/editor/canvasClipboard.ts +1 -5
  32. package/src/components/Mindmap/editor/imagePaste.ts +1 -1
  33. package/src/components/Mindmap/editor/mindmapClipboard.ts +17 -5
  34. package/src/components/Mindmap/editor/outlineDrag.ts +1 -1
  35. package/src/components/Mindmap/markdown.ts +10 -29
  36. package/src/components/WordList/RightClickMenu.vue +10 -35
  37. package/src/components/WordList/WordList.vue +19 -42
  38. package/src/components/sharedMedia.test.ts +94 -112
  39. package/src/entries/bilibili-video.ts +1 -1
  40. package/src/entries/excalidraw-editor.ts +20 -0
  41. package/src/entries/excalidraw-view.ts +15 -0
  42. package/src/entries/footprints.ts +1 -1
  43. package/src/entries/mermaid.ts +1 -1
  44. package/src/entries/mindmap.ts +1 -1
  45. package/src/entries/notes-table.ts +2 -2
  46. package/src/entries/word-list.ts +1 -1
  47. package/src/excalidraw/ExcalidrawSvg.vue +125 -0
  48. package/src/excalidraw/editorHost.ts +110 -0
  49. package/src/excalidraw/editorSession.test.ts +205 -0
  50. package/src/excalidraw/editorSession.ts +224 -0
  51. package/src/excalidraw/exporter.ts +50 -0
  52. package/src/excalidraw/focus.test.ts +60 -0
  53. package/src/excalidraw/focus.ts +33 -0
  54. package/src/excalidraw/fonts.test.ts +66 -0
  55. package/src/excalidraw/fonts.ts +92 -0
  56. package/src/excalidraw/renderer.test.ts +148 -0
  57. package/src/excalidraw/renderer.ts +105 -0
  58. package/src/excalidraw/scene.ts +93 -0
  59. package/src/index.ts +25 -33
  60. package/src/markdown/image.test.ts +1 -3
  61. package/src/markdown/image.ts +4 -13
  62. package/src/styles/code.css +5 -5
  63. package/src/styles/prose.css +2 -2
  64. package/src/styles/swiper.css +1 -1
  65. package/src/styles/tokens.css +5 -6
  66. package/src/swiper/hydrate.test.ts +37 -41
  67. package/src/swiper/hydrate.ts +91 -102
@@ -1,185 +1,162 @@
1
1
  <script setup lang="ts">
2
- import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
2
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
3
3
 
4
4
  const props = withDefaults(defineProps<{ selector?: string }>(), {
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;
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
- image.dataset.preview === "false" ||
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
- ...document.querySelectorAll<HTMLImageElement>(props.selector),
47
- ].filter(eligible);
48
- images.value = candidates
49
- .map((item) => item.currentSrc || item.src)
50
- .filter(Boolean);
51
- const selected = image.currentSrc || image.src;
52
- const selectedIndex = images.value.indexOf(selected);
53
- index.value = selectedIndex < 0 ? 0 : selectedIndex;
54
- if (selectedIndex < 0 && selected) images.value = [selected];
55
- if (images.value.length === 0) return;
56
- reset();
57
- previousFocus =
58
- document.activeElement instanceof HTMLElement
59
- ? document.activeElement
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
- (index.value + step + images.value.length) % images.value.length;
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>("img");
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
- image.closest(".swiper-container") &&
98
- Math.abs(event.clientX - swiperDownX) > 5
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 === "string" ? detail.src : "";
115
- if (!src) return;
116
- images.value = [src];
117
- index.value = 0;
118
- reset();
119
- previousFocus =
120
- document.activeElement instanceof HTMLElement
121
- ? document.activeElement
122
- : null;
123
- previousOverflow = document.documentElement.style.overflow;
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 === "Escape") close();
132
- else if (event.key === "ArrowLeft") move(-1);
133
- else if (event.key === "ArrowRight") move(1);
134
- else if (event.key === "ArrowUp" || event.key === "+") zoom(1.1);
135
- else if (event.key === "ArrowDown" || event.key === "-") zoom(1 / 1.1);
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("pointermove", onPointerMove);
149
- window.addEventListener("pointerup", onPointerUp, { once: true });
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("pointermove", onPointerMove);
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("click", onDocumentClick, true);
169
- document.addEventListener("tn:preview-image", onPreviewRequest);
170
- document.addEventListener("keydown", onKeydown);
171
- document.addEventListener("pointerdown", onGlobalPointerDown, true);
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("click", onDocumentClick, true);
176
- document.removeEventListener("tn:preview-image", onPreviewRequest);
177
- document.removeEventListener("keydown", onKeydown);
178
- document.removeEventListener("pointerdown", onGlobalPointerDown, true);
179
- window.removeEventListener("pointermove", onPointerMove);
180
- window.removeEventListener("pointerup", onPointerUp);
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="diagramTransform ? { transform: diagramTransform, transformOrigin: 'top left' } : undefined"
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 VitePress fence (`graph="…"`). */
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 for browser / VitePress (non-blocking).
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 {
@@ -49,7 +49,7 @@ function mountBreadcrumbs(session: MindmapSession) {
49
49
  const Host = defineComponent({
50
50
  setup() {
51
51
  return () => h(FocusBreadcrumbs, { session, version: version.value })
52
- },
52
+ }
53
53
  })
54
54
  const host = document.createElement('div')
55
55
  document.body.append(host)
@@ -105,17 +105,17 @@ describe('FocusBreadcrumbs', () => {
105
105
  expect(crumbs).toHaveLength(13)
106
106
  expect(crumbs.map((item) => item.textContent?.trim())).toEqual([
107
107
  '全部',
108
- ...current.map((node) => node.content.text),
108
+ ...current.map((node) => node.content.text)
109
109
  ])
110
110
  expect(crumbs.map((item) => item.dataset.nodeId)).toEqual([
111
111
  session.document.root.id,
112
- ...current.map((node) => node.id),
112
+ ...current.map((node) => node.id)
113
113
  ])
114
114
  expect(crumbs[crumbs.length - 1]?.getAttribute('aria-current')).toBe('page')
115
115
  expect(crumbs.slice(0, -1).every((item) => !item.hasAttribute('aria-current'))).toBe(true)
116
116
  expect(HTMLElement.prototype.scrollIntoView).toHaveBeenCalledWith({
117
117
  block: 'nearest',
118
- inline: 'nearest',
118
+ inline: 'nearest'
119
119
  })
120
120
  })
121
121
 
@@ -132,10 +132,11 @@ describe('FocusBreadcrumbs', () => {
132
132
  const items = [...menu!.querySelectorAll<HTMLButtonElement>('[role="menuitemradio"]')]
133
133
  expect(items.map((item) => item.dataset.nodeId)).toEqual(expected.map((node) => node.id))
134
134
  expect(items.map((item) => item.textContent?.replace('✓', '').trim())).toEqual(
135
- expected.map((node) => node.content.text),
135
+ expected.map((node) => node.content.text)
136
136
  )
137
- expect(items.find((item) => item.getAttribute('aria-checked') === 'true')?.dataset.nodeId)
138
- .toBe(current[depth].id)
137
+ expect(
138
+ items.find((item) => item.getAttribute('aria-checked') === 'true')?.dataset.nodeId
139
+ ).toBe(current[depth].id)
139
140
  document.body.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
140
141
  await settle()
141
142
  expect(document.body.querySelector('.focus-sibling-menu')).toBeNull()
@@ -149,8 +150,9 @@ describe('FocusBreadcrumbs', () => {
149
150
  await settle()
150
151
  const secondLevel = [...host.querySelectorAll<HTMLButtonElement>('.focus-crumb')][2]
151
152
  const menu = await hoverOpen(secondLevel)
152
- const duplicateItems = [...menu!.querySelectorAll<HTMLButtonElement>('[role="menuitemradio"]')]
153
- .filter((item) => item.textContent?.replace('✓', '').trim() === current[1].content.text)
153
+ const duplicateItems = [
154
+ ...menu!.querySelectorAll<HTMLButtonElement>('[role="menuitemradio"]')
155
+ ].filter((item) => item.textContent?.replace('✓', '').trim() === current[1].content.text)
154
156
 
155
157
  expect(duplicateItems).toHaveLength(2)
156
158
  menu!.querySelector<HTMLButtonElement>(`[data-node-id="${other[1].id}"]`)!.click()
@@ -164,7 +166,7 @@ describe('FocusBreadcrumbs', () => {
164
166
  expect(updated.map((item) => item.dataset.nodeId)).toEqual([
165
167
  session.document.root.id,
166
168
  current[0].id,
167
- other[1].id,
169
+ other[1].id
168
170
  ])
169
171
  })
170
172
 
@@ -227,11 +229,13 @@ describe('FocusBreadcrumbs', () => {
227
229
  await settle()
228
230
  const trigger = [...host.querySelectorAll<HTMLButtonElement>('.focus-crumb')][1]
229
231
  trigger.focus()
230
- trigger.dispatchEvent(new KeyboardEvent('keydown', {
231
- key: 'ArrowDown',
232
- bubbles: true,
233
- cancelable: true,
234
- }))
232
+ trigger.dispatchEvent(
233
+ new KeyboardEvent('keydown', {
234
+ key: 'ArrowDown',
235
+ bubbles: true,
236
+ cancelable: true
237
+ })
238
+ )
235
239
  await settle()
236
240
 
237
241
  const menu = document.body.querySelector<HTMLElement>('[role="menu"]')!
@@ -240,24 +244,30 @@ describe('FocusBreadcrumbs', () => {
240
244
  expect(trigger.getAttribute('aria-expanded')).toBe('true')
241
245
  expect(document.activeElement).toBe(items[0])
242
246
 
243
- items[0].dispatchEvent(new KeyboardEvent('keydown', {
244
- key: 'End',
245
- bubbles: true,
246
- cancelable: true,
247
- }))
247
+ items[0].dispatchEvent(
248
+ new KeyboardEvent('keydown', {
249
+ key: 'End',
250
+ bubbles: true,
251
+ cancelable: true
252
+ })
253
+ )
248
254
  expect(document.activeElement).toBe(items[items.length - 1])
249
- items[items.length - 1]!.dispatchEvent(new KeyboardEvent('keydown', {
250
- key: 'ArrowDown',
251
- bubbles: true,
252
- cancelable: true,
253
- }))
255
+ items[items.length - 1]!.dispatchEvent(
256
+ new KeyboardEvent('keydown', {
257
+ key: 'ArrowDown',
258
+ bubbles: true,
259
+ cancelable: true
260
+ })
261
+ )
254
262
  expect(document.activeElement).toBe(items[0])
255
263
 
256
- items[0].dispatchEvent(new KeyboardEvent('keydown', {
257
- key: 'Escape',
258
- bubbles: true,
259
- cancelable: true,
260
- }))
264
+ items[0].dispatchEvent(
265
+ new KeyboardEvent('keydown', {
266
+ key: 'Escape',
267
+ bubbles: true,
268
+ cancelable: true
269
+ })
270
+ )
261
271
  await settle()
262
272
  expect(document.body.querySelector('.focus-sibling-menu')).toBeNull()
263
273
  expect(document.activeElement).toBe(trigger)
@@ -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(VIEWPORT_GAP, Math.min(rect.left, viewportWidth - MENU_WIDTH - VIEWPORT_GAP))
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 = below + estimatedHeight <= viewportHeight - VIEWPORT_GAP
75
- ? below
76
- : Math.max(VIEWPORT_GAP, rect.top - estimatedHeight - 6)
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 = where === 'first'
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') next = current < 0 ? items.length - 1 : (current - 1 + items.length) % items.length
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(focusPath, () => {
227
- closeMenu()
228
- nextTick(() => {
229
- const current = scroller.value?.querySelector<HTMLElement>('[aria-current="page"]')
230
- current?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' })
231
- })
232
- }, { immediate: true, flush: 'post' })
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="hasSiblingMenu(node) && openMenu?.nodeId === node.id ? menuId : undefined"
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="true"] {
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 () => h('span', { class: 'mindmap-inline-runs' }, parseInlineSegments(props.raw).map((segment) => {
11
- const classes = Object.entries(segment.marks)
12
- .filter(([, enabled]) => enabled)
13
- .map(([name]) => `is-${name}`)
14
- if (segment.link) {
15
- return h('a', {
16
- class: ['mindmap-inline-run', 'is-link', ...classes],
17
- href: segment.link.url,
18
- target: '_blank',
19
- rel: 'noopener noreferrer',
20
- }, segment.text)
21
- }
22
- return h('span', { class: ['mindmap-inline-run', ...classes] }, segment.text)
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
  })