@tnotesjs/ui 0.4.2 → 0.4.3
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/dist/markdown/lineBreaks.d.ts +47 -0
- package/dist/markdown/lineBreaks.js +78 -0
- package/dist/swiper/hydrate.d.ts +4 -1
- package/dist/swiper/hydrate.js +9 -4
- package/package.json +18 -11
- package/src/alerts/icons.ts +32 -0
- package/src/browser/fullscreenLayer.ts +16 -0
- package/src/components/ContributionHeatmap/ContributionHeatmap.vue +638 -0
- package/src/components/ImagePreview/ImagePreview.vue +29 -6
- package/src/components/Mermaid/Mermaid.vue +5 -1
- package/src/components/Mindmap/InlineRuns.ts +4 -1
- package/src/components/Mindmap/Mindmap.vue +27 -12
- package/src/components/Mindmap/editor/AppIcon.vue +0 -3
- package/src/components/Mindmap/editor/MindmapView.vue +16 -1
- package/src/components/Mindmap/editor/OutlineView.vue +44 -3
- package/src/components/Mindmap/editor/RichInlineEditor.vue +56 -22
- package/src/components/Mindmap/editor/SearchBar.vue +8 -2
- package/src/components/Mindmap/editor/SelectionToolbar.vue +2 -7
- package/src/components/Mindmap/editor/mindmapClipboard.ts +4 -4
- package/src/components/Mindmap/inlineOptions.ts +21 -0
- package/src/components/WordList/WordList.vue +29 -9
- package/src/components/WordList/wordListStorage.ts +21 -0
- package/src/index.ts +3 -0
- package/src/markdown/lineBreaks.ts +133 -0
- package/src/styles/prose.css +138 -30
- package/src/swiper/hydrate.ts +15 -4
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { parseInlineSegments } from '@tnotesjs/mindmap-core'
|
|
2
2
|
import { defineComponent, h } from 'vue'
|
|
3
3
|
|
|
4
|
+
import { useMindmapInlineOptions } from './inlineOptions'
|
|
5
|
+
|
|
4
6
|
export default defineComponent({
|
|
5
7
|
name: 'MindmapInlineRuns',
|
|
6
8
|
props: {
|
|
7
9
|
raw: { type: String, default: '' }
|
|
8
10
|
},
|
|
9
11
|
setup(props) {
|
|
12
|
+
const inlineOptions = useMindmapInlineOptions()
|
|
10
13
|
return () =>
|
|
11
14
|
h(
|
|
12
15
|
'span',
|
|
13
16
|
{ class: 'mindmap-inline-runs' },
|
|
14
|
-
parseInlineSegments(props.raw).map((segment) => {
|
|
17
|
+
parseInlineSegments(props.raw, inlineOptions()).map((segment) => {
|
|
15
18
|
const classes = Object.entries(segment.marks)
|
|
16
19
|
.filter(([, enabled]) => enabled)
|
|
17
20
|
.map(([name]) => `is-${name}`)
|
|
@@ -11,8 +11,10 @@ import {
|
|
|
11
11
|
resolvedExpandLevel,
|
|
12
12
|
stepExpandLevel
|
|
13
13
|
} from './expandLevel'
|
|
14
|
+
import { setFullscreenLayer } from '../../browser/fullscreenLayer'
|
|
14
15
|
import { FORCE_EXIT_FULLSCREEN_EVENT, reconcileMindmapFullscreen } from './fullscreenFlag'
|
|
15
16
|
import { normalizeMindmapMarkdown } from './markdown'
|
|
17
|
+
import { provideMindmapInlineOptions } from './inlineOptions'
|
|
16
18
|
import MindmapOutlineNode from './MindmapOutlineNode.vue'
|
|
17
19
|
import MindmapViewIcon from './MindmapViewIcon.vue'
|
|
18
20
|
import FocusBreadcrumbs from './FocusBreadcrumbs.vue'
|
|
@@ -96,6 +98,11 @@ const props = withDefaults(
|
|
|
96
98
|
* (e.g. `./assets/foo.png`). Required for editable paste-to-assets.
|
|
97
99
|
*/
|
|
98
100
|
writeAsset?: (blob: Blob) => Promise<{ relativePath: string; alt?: string }>
|
|
101
|
+
/**
|
|
102
|
+
* 链接引用定义(`[id]: url`,Desk 从围栏外的笔记正文收集)。
|
|
103
|
+
* 节点里的 `[文字][id]` / `[文字][]` / `[文字]` 据此显示为链接,写回时保留引用写法。
|
|
104
|
+
*/
|
|
105
|
+
linkDefinitions?: ReadonlyMap<string, string> | Record<string, string>
|
|
99
106
|
}>(),
|
|
100
107
|
{
|
|
101
108
|
content: '',
|
|
@@ -106,7 +113,8 @@ const props = withDefaults(
|
|
|
106
113
|
height: undefined,
|
|
107
114
|
isDark: undefined,
|
|
108
115
|
resolveImageSrc: undefined,
|
|
109
|
-
writeAsset: undefined
|
|
116
|
+
writeAsset: undefined,
|
|
117
|
+
linkDefinitions: undefined
|
|
110
118
|
}
|
|
111
119
|
)
|
|
112
120
|
|
|
@@ -220,7 +228,8 @@ function rebuildSession(): void {
|
|
|
220
228
|
suppressChangeEmit = true
|
|
221
229
|
const next = new MindmapSession({
|
|
222
230
|
markdown: normalizedContent.value,
|
|
223
|
-
fileName: 'mindmap-preview.tn-mindmap.md'
|
|
231
|
+
fileName: 'mindmap-preview.tn-mindmap.md',
|
|
232
|
+
definitions: props.linkDefinitions
|
|
224
233
|
})
|
|
225
234
|
applyInitialExpandLevel(next, expandLevel.value)
|
|
226
235
|
const invalidate = () => {
|
|
@@ -657,6 +666,19 @@ function handleDocumentKeydown(event: KeyboardEvent): void {
|
|
|
657
666
|
canvasHost.value?.blur()
|
|
658
667
|
}
|
|
659
668
|
|
|
669
|
+
provideMindmapInlineOptions(() => session.value?.inlineOptions ?? {})
|
|
670
|
+
|
|
671
|
+
/** 定义变了(笔记里改了 `[1]: url`)就按新定义重建会话;内容不变时不重建。 */
|
|
672
|
+
const definitionsKey = computed(() => {
|
|
673
|
+
const defs = props.linkDefinitions
|
|
674
|
+
if (!defs) return ''
|
|
675
|
+
const entries = defs instanceof Map ? [...defs.entries()] : Object.entries(defs)
|
|
676
|
+
return JSON.stringify(entries)
|
|
677
|
+
})
|
|
678
|
+
watch(definitionsKey, (value, previous) => {
|
|
679
|
+
if (value !== previous && session.value) rebuildSession()
|
|
680
|
+
})
|
|
681
|
+
|
|
660
682
|
watch(
|
|
661
683
|
normalizedContent,
|
|
662
684
|
(value) => {
|
|
@@ -722,9 +744,12 @@ onMounted(() => {
|
|
|
722
744
|
createViewer()
|
|
723
745
|
})
|
|
724
746
|
|
|
747
|
+
watch(isFullscreen, (on) => setFullscreenLayer(previewRoot.value, on), { flush: 'sync' })
|
|
748
|
+
|
|
725
749
|
onBeforeUnmount(() => {
|
|
726
750
|
mounted = false
|
|
727
751
|
exitFullscreenOverlay()
|
|
752
|
+
setFullscreenLayer(previewRoot.value, false)
|
|
728
753
|
unregisterFullscreenOwner?.()
|
|
729
754
|
unregisterFullscreenOwner = null
|
|
730
755
|
themeObserver?.disconnect()
|
|
@@ -1178,11 +1203,6 @@ onBeforeUnmount(() => {
|
|
|
1178
1203
|
.mindmap-canvas-host:deep(.mm-edit-input .inline-run.underline.strike) {
|
|
1179
1204
|
text-decoration-line: underline line-through;
|
|
1180
1205
|
}
|
|
1181
|
-
.mindmap-canvas-host:deep(.mm-edit-input .inline-run.highlight:not(.code)) {
|
|
1182
|
-
padding: 0 1px;
|
|
1183
|
-
border-radius: 2px;
|
|
1184
|
-
background: var(--tn-c-mark-bg, rgba(255, 221, 0, 0.45));
|
|
1185
|
-
}
|
|
1186
1206
|
.mindmap-canvas-host:deep(.mm-edit-input .inline-run.code) {
|
|
1187
1207
|
padding: 0 4px;
|
|
1188
1208
|
border: 1px solid var(--tn-c-inline-code-border);
|
|
@@ -1342,11 +1362,6 @@ onBeforeUnmount(() => {
|
|
|
1342
1362
|
.mindmap-outline :deep(.is-strike) {
|
|
1343
1363
|
text-decoration: line-through;
|
|
1344
1364
|
}
|
|
1345
|
-
.mindmap-outline :deep(.is-highlight) {
|
|
1346
|
-
padding: 0 2px;
|
|
1347
|
-
border-radius: 2px;
|
|
1348
|
-
background: var(--tn-c-mark-bg, rgba(255, 221, 0, 0.45));
|
|
1349
|
-
}
|
|
1350
1365
|
.mindmap-outline :deep(.is-code) {
|
|
1351
1366
|
padding: 0 4px;
|
|
1352
1367
|
border: 1px solid var(--tn-c-inline-code-border);
|
|
@@ -66,9 +66,6 @@ defineProps<{ name: string; size?: number }>()
|
|
|
66
66
|
<circle cx="9" cy="9" r="2" />
|
|
67
67
|
<path d="m4 17 5-5 3 3 2-2 6 6" />
|
|
68
68
|
</template>
|
|
69
|
-
<template v-else-if="name === 'highlight'">
|
|
70
|
-
<path d="m14.5 4.5 5 5-8.2 8.2-5.6.6.6-5.6zM12.5 6.5l5 5M4 21h16" />
|
|
71
|
-
</template>
|
|
72
69
|
<template v-else-if="name === 'link'">
|
|
73
70
|
<path
|
|
74
71
|
d="M10 13a5 5 0 0 0 7.5.5l2-2a5 5 0 0 0-7-7l-1.2 1.2M14 11a5 5 0 0 0-7.5-.5l-2 2a5 5 0 0 0 7 7l1.2-1.2"
|
|
@@ -48,7 +48,7 @@ const textFormats = computed<Partial<Record<InlineFormat, boolean>>>(() => {
|
|
|
48
48
|
const state = editState.value
|
|
49
49
|
const selection = textSelection.value
|
|
50
50
|
if (!state || !selection) return {}
|
|
51
|
-
const formats: InlineFormat[] = ['bold', 'italic', 'underline', 'strike', '
|
|
51
|
+
const formats: InlineFormat[] = ['bold', 'italic', 'underline', 'strike', 'code']
|
|
52
52
|
return Object.fromEntries(
|
|
53
53
|
formats.map((format) => [
|
|
54
54
|
format,
|
|
@@ -357,4 +357,19 @@ onBeforeUnmount(() => {
|
|
|
357
357
|
outline: none;
|
|
358
358
|
background: var(--mm-canvas-bg);
|
|
359
359
|
}
|
|
360
|
+
/* CanvasEditor 在宿主上切换这些状态类;光标规则原在已删除的独立 app 样式里,这里补回。 */
|
|
361
|
+
.mindmap-view-host.mm-editor {
|
|
362
|
+
cursor: default;
|
|
363
|
+
}
|
|
364
|
+
.mindmap-view-host.mm-editor.is-space-pan {
|
|
365
|
+
cursor: grab;
|
|
366
|
+
}
|
|
367
|
+
.mindmap-view-host.mm-editor.is-panning,
|
|
368
|
+
.mindmap-view-host.mm-editor.is-node-dragging {
|
|
369
|
+
cursor: grabbing;
|
|
370
|
+
}
|
|
371
|
+
.mindmap-view-host.mm-editor.is-link-hover,
|
|
372
|
+
.mindmap-view-host.mm-editor.is-control-hover {
|
|
373
|
+
cursor: pointer;
|
|
374
|
+
}
|
|
360
375
|
</style>
|
|
@@ -116,6 +116,15 @@ function measureTextBlockHeight(
|
|
|
116
116
|
return Math.max(minHeight, lines.length * lineHeight + 8)
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
function imageSizeLabel(node: MindmapNode): string {
|
|
120
|
+
if (!liveImageWidth.value.has(node.id) || !node.content.image) return ''
|
|
121
|
+
const width = displayImageWidth(node)
|
|
122
|
+
const aspect = imageAspects.value.get(node.content.image.src) ?? 1
|
|
123
|
+
const height = Math.min(Math.round(width / Math.max(aspect, 0.15)), IMG_MAX_H)
|
|
124
|
+
if (width < 88 || height < 32) return ''
|
|
125
|
+
return `${Math.round(width)} x ${height}`
|
|
126
|
+
}
|
|
127
|
+
|
|
119
128
|
function displayImageWidth(node: MindmapNode): number {
|
|
120
129
|
const live = liveImageWidth.value.get(node.id)
|
|
121
130
|
if (live != null) return live
|
|
@@ -376,7 +385,7 @@ const selectedTextFormats = computed<Partial<Record<InlineFormat, boolean>>>(()
|
|
|
376
385
|
if (props.version < 0) return {}
|
|
377
386
|
const selection = textSelection.value
|
|
378
387
|
if (!selection) return {}
|
|
379
|
-
const formats: InlineFormat[] = ['bold', 'italic', 'underline', 'strike', '
|
|
388
|
+
const formats: InlineFormat[] = ['bold', 'italic', 'underline', 'strike', 'code']
|
|
380
389
|
return Object.fromEntries(
|
|
381
390
|
formats.map((format) => [
|
|
382
391
|
format,
|
|
@@ -703,7 +712,6 @@ function inlineFormatShortcut(event: KeyboardEvent, hasSelection = true): Inline
|
|
|
703
712
|
if (key === 'u' && !event.shiftKey) return 'underline'
|
|
704
713
|
if (key === 'enter' && !event.shiftKey && hasSelection) return 'strike'
|
|
705
714
|
if ((key === 's' || key === 'x') && event.shiftKey) return 'strike'
|
|
706
|
-
if (key === 'h' && event.shiftKey) return 'highlight'
|
|
707
715
|
if (key === 'e' && !event.shiftKey) return 'code'
|
|
708
716
|
return null
|
|
709
717
|
}
|
|
@@ -1259,7 +1267,7 @@ function onEditorPasteMultiline(node: MindmapNode, text: string) {
|
|
|
1259
1267
|
return `${m[1]}- ${m[2]}`
|
|
1260
1268
|
})
|
|
1261
1269
|
.join('\n')
|
|
1262
|
-
const { doc: tmp } = parseMarkdown(`# _\n\n${fragment}\n
|
|
1270
|
+
const { doc: tmp } = parseMarkdown(`# _\n\n${fragment}\n`, '_', session.inlineOptions)
|
|
1263
1271
|
const nodes = tmp.root.children.map((c) => cloneSubtree(c))
|
|
1264
1272
|
if (nodes.length === 0) return
|
|
1265
1273
|
|
|
@@ -1506,7 +1514,19 @@ function onImageResizeUp() {
|
|
|
1506
1514
|
|
|
1507
1515
|
// ---------- 键盘导航(焦点在容器、非编辑态时) ----------
|
|
1508
1516
|
|
|
1517
|
+
function isNativeTextEntry(target: EventTarget | null): boolean {
|
|
1518
|
+
if (!(target instanceof HTMLElement)) return false
|
|
1519
|
+
return (
|
|
1520
|
+
target instanceof HTMLInputElement ||
|
|
1521
|
+
target instanceof HTMLTextAreaElement ||
|
|
1522
|
+
target.isContentEditable
|
|
1523
|
+
)
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1509
1526
|
function onKeydown(e: KeyboardEvent) {
|
|
1527
|
+
// 容器内的独立输入框(如 LinkPopover 的链接地址)冒泡上来的按键交给其原生行为,
|
|
1528
|
+
// 否则 ←/→ 会被当成折叠/展开、Backspace/Delete 会删节点。
|
|
1529
|
+
if (e.target !== e.currentTarget && isNativeTextEntry(e.target)) return
|
|
1510
1530
|
const session = props.session
|
|
1511
1531
|
const sel = session.selectedNode
|
|
1512
1532
|
const mod = e.metaKey || e.ctrlKey
|
|
@@ -2035,6 +2055,9 @@ function onDragUp() {
|
|
|
2035
2055
|
@load="onOutlineImageLoad(focusRoot.content.image.src, $event)"
|
|
2036
2056
|
@click="onImageClick(focusRoot, $event)"
|
|
2037
2057
|
/>
|
|
2058
|
+
<span v-if="imageSizeLabel(focusRoot)" class="row-image-size">{{
|
|
2059
|
+
imageSizeLabel(focusRoot)
|
|
2060
|
+
}}</span>
|
|
2038
2061
|
<span
|
|
2039
2062
|
class="row-image-handle"
|
|
2040
2063
|
title="拖动调节宽度"
|
|
@@ -2181,6 +2204,9 @@ function onDragUp() {
|
|
|
2181
2204
|
@load="onOutlineImageLoad(row.node.content.image.src, $event)"
|
|
2182
2205
|
@click="onImageClick(row.node, $event)"
|
|
2183
2206
|
/>
|
|
2207
|
+
<span v-if="imageSizeLabel(row.node)" class="row-image-size">{{
|
|
2208
|
+
imageSizeLabel(row.node)
|
|
2209
|
+
}}</span>
|
|
2184
2210
|
<span
|
|
2185
2211
|
class="row-image-handle"
|
|
2186
2212
|
title="拖动调节宽度"
|
|
@@ -2360,6 +2386,21 @@ function onDragUp() {
|
|
|
2360
2386
|
cursor: zoom-in;
|
|
2361
2387
|
user-select: none;
|
|
2362
2388
|
}
|
|
2389
|
+
.row-image-size {
|
|
2390
|
+
position: absolute;
|
|
2391
|
+
top: 50%;
|
|
2392
|
+
left: 50%;
|
|
2393
|
+
z-index: 2;
|
|
2394
|
+
transform: translate(-50%, -50%);
|
|
2395
|
+
padding: 4px 8px;
|
|
2396
|
+
border-radius: 6px;
|
|
2397
|
+
background: rgb(0 0 0 / 62%);
|
|
2398
|
+
color: #fff;
|
|
2399
|
+
font-size: 12px;
|
|
2400
|
+
line-height: 1.4;
|
|
2401
|
+
white-space: nowrap;
|
|
2402
|
+
pointer-events: none;
|
|
2403
|
+
}
|
|
2363
2404
|
.row-image-handle {
|
|
2364
2405
|
position: absolute;
|
|
2365
2406
|
right: 0;
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
} from '@tnotesjs/mindmap-core'
|
|
13
13
|
import type { InlineLink, RichInlineEditorElement } from '@tnotesjs/mindmap-core'
|
|
14
14
|
|
|
15
|
+
import { useMindmapInlineOptions } from '../inlineOptions'
|
|
16
|
+
|
|
15
17
|
const props = withDefaults(
|
|
16
18
|
defineProps<{
|
|
17
19
|
editorId: string
|
|
@@ -43,6 +45,7 @@ const emit = defineEmits<{
|
|
|
43
45
|
linkClick: [link: InlineLink, event: Event]
|
|
44
46
|
}>()
|
|
45
47
|
|
|
48
|
+
const inlineOptions = useMindmapInlineOptions()
|
|
46
49
|
const rootRef = ref<RichInlineEditorElement>()
|
|
47
50
|
const draftRaw = ref(props.raw)
|
|
48
51
|
let sourceRaw = props.raw
|
|
@@ -50,17 +53,17 @@ let dirty = false
|
|
|
50
53
|
let composing = false
|
|
51
54
|
let compositionBaseRaw = props.raw
|
|
52
55
|
|
|
53
|
-
const text = computed(() => stripInline(draftRaw.value))
|
|
56
|
+
const text = computed(() => stripInline(draftRaw.value, inlineOptions()))
|
|
54
57
|
|
|
55
58
|
function renderInlineDom() {
|
|
56
59
|
const root = rootRef.value
|
|
57
60
|
if (!root) return
|
|
58
61
|
const fragment = document.createDocumentFragment()
|
|
59
62
|
let plainOffset = 0
|
|
60
|
-
for (const segment of parseInlineSegments(draftRaw.value)) {
|
|
63
|
+
for (const segment of parseInlineSegments(draftRaw.value, inlineOptions())) {
|
|
61
64
|
const run = document.createElement('span')
|
|
62
65
|
run.className = 'inline-run'
|
|
63
|
-
for (const format of ['bold', 'italic', 'underline', 'strike', '
|
|
66
|
+
for (const format of ['bold', 'italic', 'underline', 'strike', 'code'] as const) {
|
|
64
67
|
if (segment.marks[format]) run.classList.add(format)
|
|
65
68
|
}
|
|
66
69
|
if (segment.link) {
|
|
@@ -89,7 +92,7 @@ function renderInlineDom() {
|
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
function payload(): DraftPayload {
|
|
92
|
-
return { raw: draftRaw.value, text: stripInline(draftRaw.value) }
|
|
95
|
+
return { raw: draftRaw.value, text: stripInline(draftRaw.value, inlineOptions()) }
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
function renderDraft(
|
|
@@ -120,7 +123,14 @@ function replaceSelection(textToInsert: string) {
|
|
|
120
123
|
focus: text.value.length,
|
|
121
124
|
direction: 'forward' as const
|
|
122
125
|
}
|
|
123
|
-
const nextRaw = replaceInlineRange(
|
|
126
|
+
const nextRaw = replaceInlineRange(
|
|
127
|
+
draftRaw.value,
|
|
128
|
+
selection.start,
|
|
129
|
+
selection.end,
|
|
130
|
+
textToInsert,
|
|
131
|
+
undefined,
|
|
132
|
+
inlineOptions()
|
|
133
|
+
)
|
|
124
134
|
const caret = selection.start + textToInsert.length
|
|
125
135
|
renderDraft(nextRaw, { start: caret })
|
|
126
136
|
}
|
|
@@ -148,7 +158,14 @@ function onBeforeInput(event: InputEvent) {
|
|
|
148
158
|
if (event.inputType === 'insertText' || event.inputType === 'insertReplacementText') {
|
|
149
159
|
if (event.data == null) return
|
|
150
160
|
event.preventDefault()
|
|
151
|
-
const nextRaw = replaceInlineRange(
|
|
161
|
+
const nextRaw = replaceInlineRange(
|
|
162
|
+
draftRaw.value,
|
|
163
|
+
selection.start,
|
|
164
|
+
selection.end,
|
|
165
|
+
event.data,
|
|
166
|
+
undefined,
|
|
167
|
+
inlineOptions()
|
|
168
|
+
)
|
|
152
169
|
renderDraft(nextRaw, { start: selection.start + event.data.length })
|
|
153
170
|
return
|
|
154
171
|
}
|
|
@@ -159,7 +176,10 @@ function onBeforeInput(event: InputEvent) {
|
|
|
159
176
|
selection.start === selection.end
|
|
160
177
|
? previousGraphemeOffset(text.value, selection.start)
|
|
161
178
|
: selection.start
|
|
162
|
-
renderDraft(
|
|
179
|
+
renderDraft(
|
|
180
|
+
replaceInlineRange(draftRaw.value, start, selection.end, '', undefined, inlineOptions()),
|
|
181
|
+
{ start }
|
|
182
|
+
)
|
|
163
183
|
return
|
|
164
184
|
}
|
|
165
185
|
|
|
@@ -169,17 +189,30 @@ function onBeforeInput(event: InputEvent) {
|
|
|
169
189
|
selection.start === selection.end
|
|
170
190
|
? nextGraphemeOffset(text.value, selection.end)
|
|
171
191
|
: selection.end
|
|
172
|
-
renderDraft(
|
|
173
|
-
|
|
174
|
-
|
|
192
|
+
renderDraft(
|
|
193
|
+
replaceInlineRange(draftRaw.value, selection.start, end, '', undefined, inlineOptions()),
|
|
194
|
+
{
|
|
195
|
+
start: selection.start
|
|
196
|
+
}
|
|
197
|
+
)
|
|
175
198
|
return
|
|
176
199
|
}
|
|
177
200
|
|
|
178
201
|
if (event.inputType === 'deleteByCut') {
|
|
179
202
|
event.preventDefault()
|
|
180
|
-
renderDraft(
|
|
181
|
-
|
|
182
|
-
|
|
203
|
+
renderDraft(
|
|
204
|
+
replaceInlineRange(
|
|
205
|
+
draftRaw.value,
|
|
206
|
+
selection.start,
|
|
207
|
+
selection.end,
|
|
208
|
+
'',
|
|
209
|
+
undefined,
|
|
210
|
+
inlineOptions()
|
|
211
|
+
),
|
|
212
|
+
{
|
|
213
|
+
start: selection.start
|
|
214
|
+
}
|
|
215
|
+
)
|
|
183
216
|
return
|
|
184
217
|
}
|
|
185
218
|
|
|
@@ -201,7 +234,10 @@ function onInput(event: InputEvent) {
|
|
|
201
234
|
const nextText = (root.textContent ?? '').replace(/[\r\n]/g, '')
|
|
202
235
|
if (nextText === text.value) return
|
|
203
236
|
const selection = richSelectionOffsets(root)
|
|
204
|
-
renderDraft(
|
|
237
|
+
renderDraft(
|
|
238
|
+
replaceInlineDisplayText(draftRaw.value, nextText, inlineOptions()),
|
|
239
|
+
selection ?? undefined
|
|
240
|
+
)
|
|
205
241
|
}
|
|
206
242
|
|
|
207
243
|
function onCompositionStart() {
|
|
@@ -216,7 +252,10 @@ function finishComposition() {
|
|
|
216
252
|
const nextText = (root.textContent ?? '').replace(/[\r\n]/g, '')
|
|
217
253
|
const selection = richSelectionOffsets(root)
|
|
218
254
|
composing = false
|
|
219
|
-
renderDraft(
|
|
255
|
+
renderDraft(
|
|
256
|
+
replaceInlineDisplayText(compositionBaseRaw, nextText, inlineOptions()),
|
|
257
|
+
selection ?? undefined
|
|
258
|
+
)
|
|
220
259
|
}
|
|
221
260
|
|
|
222
261
|
function onCompositionEnd() {
|
|
@@ -308,9 +347,9 @@ function installCompatibilityApi(root: RichInlineEditorElement) {
|
|
|
308
347
|
Object.defineProperties(root, {
|
|
309
348
|
value: {
|
|
310
349
|
configurable: true,
|
|
311
|
-
get: () => stripInline(draftRaw.value),
|
|
350
|
+
get: () => stripInline(draftRaw.value, inlineOptions()),
|
|
312
351
|
set: (next: string) => {
|
|
313
|
-
renderDraft(replaceInlineDisplayText(draftRaw.value, String(next)))
|
|
352
|
+
renderDraft(replaceInlineDisplayText(draftRaw.value, String(next), inlineOptions()))
|
|
314
353
|
}
|
|
315
354
|
},
|
|
316
355
|
rawValue: { configurable: true, get: () => draftRaw.value },
|
|
@@ -413,11 +452,6 @@ onBeforeUnmount(emitCommit)
|
|
|
413
452
|
.rich-inline-editor :deep(.inline-run.underline.strike) {
|
|
414
453
|
text-decoration-line: underline line-through;
|
|
415
454
|
}
|
|
416
|
-
.rich-inline-editor :deep(.inline-run.highlight:not(.code)) {
|
|
417
|
-
padding: 0 1px;
|
|
418
|
-
border-radius: 2px;
|
|
419
|
-
background: var(--tn-c-mark-bg, rgba(255, 221, 0, 0.45));
|
|
420
|
-
}
|
|
421
455
|
.rich-inline-editor :deep(.inline-run.code) {
|
|
422
456
|
padding: 0 4px;
|
|
423
457
|
border: 1px solid var(--tn-c-inline-code-border, #e7e9e8);
|
|
@@ -78,7 +78,6 @@ function inlineFormatShortcut(event: KeyboardEvent, hasSelection = true): Inline
|
|
|
78
78
|
if (key === 'u' && !event.shiftKey) return 'underline'
|
|
79
79
|
if (key === 'enter' && !event.shiftKey && hasSelection) return 'strike'
|
|
80
80
|
if ((key === 's' || key === 'x') && event.shiftKey) return 'strike'
|
|
81
|
-
if (key === 'h' && event.shiftKey) return 'highlight'
|
|
82
81
|
if (key === 'e' && !event.shiftKey) return 'code'
|
|
83
82
|
return null
|
|
84
83
|
}
|
|
@@ -182,7 +181,14 @@ function replacedNodeValue(
|
|
|
182
181
|
}
|
|
183
182
|
let raw = node.content.raw
|
|
184
183
|
for (const range of [...selectedRanges].reverse()) {
|
|
185
|
-
raw = replaceInlineRange(
|
|
184
|
+
raw = replaceInlineRange(
|
|
185
|
+
raw,
|
|
186
|
+
range.start,
|
|
187
|
+
range.end,
|
|
188
|
+
replacement.value,
|
|
189
|
+
undefined,
|
|
190
|
+
props.session.inlineOptions
|
|
191
|
+
)
|
|
186
192
|
}
|
|
187
193
|
return { raw, text: node.content.text, count: selectedRanges.length }
|
|
188
194
|
}
|
|
@@ -44,8 +44,7 @@ const formats: Array<{ id: InlineFormat; text: string; label: string }> = [
|
|
|
44
44
|
{ id: 'bold', text: 'B', label: `加粗 (${primaryShortcut('B')})` },
|
|
45
45
|
{ id: 'italic', text: 'I', label: `斜体 (${primaryShortcut('I')})` },
|
|
46
46
|
{ id: 'underline', text: 'U', label: `下划线 (${primaryShortcut('U')})` },
|
|
47
|
-
{ id: 'strike', text: 'S', label: `删除线 (${primaryShortcut('Enter')})` }
|
|
48
|
-
{ id: 'highlight', text: '▰', label: '高亮' }
|
|
47
|
+
{ id: 'strike', text: 'S', label: `删除线 (${primaryShortcut('Enter')})` }
|
|
49
48
|
]
|
|
50
49
|
|
|
51
50
|
const toolbarRef = ref<HTMLElement | null>(null)
|
|
@@ -113,8 +112,7 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
|
|
|
113
112
|
:aria-label="item.label"
|
|
114
113
|
@click="emit('format', item.id)"
|
|
115
114
|
>
|
|
116
|
-
|
|
117
|
-
<template v-else>{{ item.text }}</template>
|
|
115
|
+
{{ item.text }}
|
|
118
116
|
</button>
|
|
119
117
|
<span class="toolbar-divider" />
|
|
120
118
|
</template>
|
|
@@ -284,9 +282,6 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
|
|
|
284
282
|
.format-button.is-strike {
|
|
285
283
|
text-decoration: line-through;
|
|
286
284
|
}
|
|
287
|
-
.format-button.is-highlight {
|
|
288
|
-
color: #d3c900;
|
|
289
|
-
}
|
|
290
285
|
.tool-button.danger {
|
|
291
286
|
color: #e14f5b;
|
|
292
287
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { cloneSubtree, parseMarkdown } from '@tnotesjs/mindmap-core'
|
|
2
|
-
import type { MindmapNode, MindmapSession } from '@tnotesjs/mindmap-core'
|
|
2
|
+
import type { InlineParseOptions, MindmapNode, MindmapSession } from '@tnotesjs/mindmap-core'
|
|
3
3
|
|
|
4
4
|
const LIST_LINE_RE = /^\s*[-*+]\s+/
|
|
5
5
|
|
|
6
6
|
/** In-app fallback when Electron denies async Clipboard API (common in Desk). */
|
|
7
7
|
let mindmapClipboardBuffer = ''
|
|
8
8
|
|
|
9
|
-
function clipboardNodes(text: string): MindmapNode[] {
|
|
9
|
+
function clipboardNodes(text: string, options?: InlineParseOptions): MindmapNode[] {
|
|
10
10
|
const fragment = text
|
|
11
11
|
.split(/\r?\n/)
|
|
12
12
|
.filter((line) => line.trim() !== '')
|
|
@@ -17,7 +17,7 @@ function clipboardNodes(text: string): MindmapNode[] {
|
|
|
17
17
|
})
|
|
18
18
|
.join('\n')
|
|
19
19
|
if (!fragment) return []
|
|
20
|
-
const parsed = parseMarkdown(`# _\n\n${fragment}\n
|
|
20
|
+
const parsed = parseMarkdown(`# _\n\n${fragment}\n`, '_', options)
|
|
21
21
|
if (!parsed.valid) return []
|
|
22
22
|
return parsed.doc.root.children.map((node) => cloneSubtree(node))
|
|
23
23
|
}
|
|
@@ -74,7 +74,7 @@ export function pasteCanvasOutline(
|
|
|
74
74
|
anchorId: string,
|
|
75
75
|
text: string
|
|
76
76
|
): string[] {
|
|
77
|
-
const nodes = clipboardNodes(text)
|
|
77
|
+
const nodes = clipboardNodes(text, session.inlineOptions)
|
|
78
78
|
const anchor = session.document.find(anchorId)
|
|
79
79
|
if (!anchor || nodes.length === 0) return []
|
|
80
80
|
const insertedIds: string[] = []
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { inject, provide } from 'vue'
|
|
2
|
+
|
|
3
|
+
import type { InlineParseOptions } from '@tnotesjs/mindmap-core'
|
|
4
|
+
import type { InjectionKey } from 'vue'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 当前导图的行内解析选项(链接引用定义等)。由 Mindmap.vue 按会话提供,
|
|
8
|
+
* 大纲行、行内编辑器据此把 `[文字][id]` 渲染成链接、编辑后写回引用写法。
|
|
9
|
+
*/
|
|
10
|
+
const MINDMAP_INLINE_OPTIONS: InjectionKey<() => InlineParseOptions> =
|
|
11
|
+
Symbol('mindmapInlineOptions')
|
|
12
|
+
|
|
13
|
+
const NO_OPTIONS = (): InlineParseOptions => ({})
|
|
14
|
+
|
|
15
|
+
export function provideMindmapInlineOptions(get: () => InlineParseOptions): void {
|
|
16
|
+
provide(MINDMAP_INLINE_OPTIONS, get)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useMindmapInlineOptions(): () => InlineParseOptions {
|
|
20
|
+
return inject(MINDMAP_INLINE_OPTIONS, NO_OPTIONS)
|
|
21
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { marked } from 'marked'
|
|
3
|
-
import { computed, ref, onMounted, onUnmounted, nextTick } from 'vue'
|
|
3
|
+
import { computed, ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
|
|
4
4
|
|
|
5
5
|
import RightClickMenu from './RightClickMenu.vue'
|
|
6
6
|
import { WORD_LIST_FEATURES_FULL, resolveWordListFeatures } from './wordListFeatures'
|
|
7
|
+
import { wordListStorageKey } from './wordListStorage'
|
|
7
8
|
|
|
8
9
|
const DEFAULT_WORDS_BASE_URL = 'https://github.com/tnotesjs/en-words/blob/main/'
|
|
9
10
|
const DEFAULT_WORDS_RAW_BASE_URL =
|
|
@@ -30,6 +31,14 @@ const props = defineProps({
|
|
|
30
31
|
features: {
|
|
31
32
|
type: Object,
|
|
32
33
|
default: () => ({ ...WORD_LIST_FEATURES_FULL })
|
|
34
|
+
},
|
|
35
|
+
/**
|
|
36
|
+
* 勾选态 localStorage 键的作用域。不传时用 `window.location.pathname`(SSG 一页一路径);
|
|
37
|
+
* Desk 单页承载多篇笔记,需注入知识库 + 笔记维度,避免勾选态跨笔记串用。
|
|
38
|
+
*/
|
|
39
|
+
storageScope: {
|
|
40
|
+
type: String,
|
|
41
|
+
default: ''
|
|
33
42
|
}
|
|
34
43
|
})
|
|
35
44
|
|
|
@@ -51,8 +60,18 @@ const sortedWords = computed(() => {
|
|
|
51
60
|
const islandWords = computed(() => encodeURIComponent(JSON.stringify(sortedWords.value)))
|
|
52
61
|
const checkedStates = ref({})
|
|
53
62
|
|
|
63
|
+
const storageKey = (word) => wordListStorageKey(props.storageScope, pathname, word)
|
|
64
|
+
|
|
65
|
+
const loadCheckedStates = () => {
|
|
66
|
+
const next = {}
|
|
67
|
+
sortedWords.value.forEach((word) => {
|
|
68
|
+
next[word] = localStorage.getItem(storageKey(word)) === 'true'
|
|
69
|
+
})
|
|
70
|
+
checkedStates.value = next
|
|
71
|
+
}
|
|
72
|
+
|
|
54
73
|
const updateCheckedState = (word, isChecked) => {
|
|
55
|
-
const key =
|
|
74
|
+
const key = storageKey(word)
|
|
56
75
|
checkedStates.value[word] = isChecked
|
|
57
76
|
localStorage.setItem(key, isChecked)
|
|
58
77
|
}
|
|
@@ -66,8 +85,7 @@ const checkAll = () => {
|
|
|
66
85
|
|
|
67
86
|
const reset = () => {
|
|
68
87
|
sortedWords.value.forEach((word) => {
|
|
69
|
-
|
|
70
|
-
localStorage.removeItem(key)
|
|
88
|
+
localStorage.removeItem(storageKey(word))
|
|
71
89
|
checkedStates.value[word] = false
|
|
72
90
|
})
|
|
73
91
|
hideContextMenu()
|
|
@@ -448,11 +466,7 @@ const handlePronounce = (word, lang = 'en-GB') => {
|
|
|
448
466
|
// hooks ----------------------------------------------------------
|
|
449
467
|
|
|
450
468
|
onMounted(() => {
|
|
451
|
-
|
|
452
|
-
const key = `${pathname}-${word}`
|
|
453
|
-
const storedState = localStorage.getItem(key)
|
|
454
|
-
checkedStates.value[word] = storedState === 'true'
|
|
455
|
-
})
|
|
469
|
+
loadCheckedStates()
|
|
456
470
|
|
|
457
471
|
if (!isMobile.value && features.value.enableWordData) preloadWords()
|
|
458
472
|
|
|
@@ -462,6 +476,12 @@ onMounted(() => {
|
|
|
462
476
|
}
|
|
463
477
|
})
|
|
464
478
|
|
|
479
|
+
// 作用域或词表变了(同一实例换了笔记 / 词表)就按新键重新回读,不沿用旧勾选态
|
|
480
|
+
watch(
|
|
481
|
+
() => [props.storageScope, islandWords.value],
|
|
482
|
+
() => loadCheckedStates()
|
|
483
|
+
)
|
|
484
|
+
|
|
465
485
|
/**
|
|
466
486
|
* 销毁时清理定时器
|
|
467
487
|
*/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordList 勾选态的 localStorage 键。
|
|
3
|
+
*
|
|
4
|
+
* - SSG:一页一个 pathname,不传作用域,沿用 `${pathname}-${word}`(兼容已存的勾选记录)。
|
|
5
|
+
* - Desk:所有笔记共用同一个 index.html,pathname 固定,必须由宿主注入笔记维度的作用域
|
|
6
|
+
* (见 {@link deskWordListStorageScope}),否则勾选态会在笔记之间串用。
|
|
7
|
+
*/
|
|
8
|
+
export function wordListStorageKey(
|
|
9
|
+
storageScope: string | null | undefined,
|
|
10
|
+
pathname: string,
|
|
11
|
+
word: string
|
|
12
|
+
): string {
|
|
13
|
+
const scope = storageScope ? storageScope : pathname
|
|
14
|
+
return `${scope}-${word}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Desk 的作用域:知识库 + 笔记 uuid。缺任一项时返回空串,由组件退回 pathname。 */
|
|
18
|
+
export function deskWordListStorageScope(knowledgeBaseId: string, noteUuid: string): string {
|
|
19
|
+
if (!knowledgeBaseId || !noteUuid) return ''
|
|
20
|
+
return `tnotes-desk:word-list:${knowledgeBaseId}:${noteUuid}`
|
|
21
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ export {
|
|
|
11
11
|
resolveWordListFeatures
|
|
12
12
|
} from './components/WordList/wordListFeatures'
|
|
13
13
|
export type { WordListFeatures } from './components/WordList/wordListFeatures'
|
|
14
|
+
export { deskWordListStorageScope, wordListStorageKey } from './components/WordList/wordListStorage'
|
|
14
15
|
export {
|
|
15
16
|
clampMindmapHeight,
|
|
16
17
|
MINDMAP_MAX_HEIGHT,
|
|
@@ -31,6 +32,7 @@ export { default as CodeBlock } from './components/CodeBlock/CodeBlock.vue'
|
|
|
31
32
|
export { default as CodeGroup } from './components/CodeGroup/CodeGroup.vue'
|
|
32
33
|
export { default as ImagePreview } from './components/ImagePreview/ImagePreview.vue'
|
|
33
34
|
export * from './browser/clipboard'
|
|
35
|
+
export * from './browser/fullscreenLayer'
|
|
34
36
|
export * from './browser/theme'
|
|
35
37
|
export * from './code/highlight'
|
|
36
38
|
export {
|
|
@@ -39,3 +41,4 @@ export {
|
|
|
39
41
|
createSwiperTabNav,
|
|
40
42
|
wrapSlideIndex
|
|
41
43
|
} from './swiper/hydrate'
|
|
44
|
+
export { default as ContributionHeatmap } from './components/ContributionHeatmap/ContributionHeatmap.vue'
|