@h-ai/ui 0.1.0-alpha.12 → 0.1.0-alpha.15
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 +20 -4
- package/dist/components/compounds/BottomNav.svelte +21 -1
- package/dist/components/compounds/BottomNav.svelte.d.ts +5 -0
- package/dist/components/primitives/Switch.svelte +5 -0
- package/dist/components/scenes/ai/AiDocumentDownloadMenu.svelte +1 -0
- package/dist/components/scenes/ai/AiDocumentEditor.svelte +1210 -145
- package/dist/components/scenes/ai/MarkdownRenderer.svelte +0 -4
- package/dist/components/scenes/ai/document-types.d.ts +28 -2
- package/dist/components/scenes/ai/editor-markdown-extensions.js +19 -4
- package/dist/components/scenes/crud/CrudFilterBar.svelte +1 -1
- package/dist/components/scenes/crud/CrudPage.svelte +27 -22
- package/dist/components/scenes/crud/CrudPage.svelte.d.ts +22 -15
- package/dist/components/scenes/crud/index.d.ts +2 -0
- package/dist/components/scenes/crud/index.js +1 -0
- package/dist/components/scenes/crud/nav-adapter.d.ts +36 -0
- package/dist/components/scenes/crud/nav-adapter.js +31 -0
- package/dist/components/scenes/crypto/EncryptedInput.svelte +2 -4
- package/dist/components/scenes/crypto/EncryptedInput.svelte.d.ts +1 -1
- package/dist/components/scenes/crypto/HashDisplay.svelte +1 -2
- package/dist/components/scenes/crypto/SignatureDisplay.svelte +1 -3
- package/dist/components/scenes/iam/UserProfile.svelte +7 -1
- package/dist/components/scenes/types.d.ts +3 -3
- package/dist/messages/en-US.json +5 -0
- package/dist/messages/zh-CN.json +5 -0
- package/dist/styles/mobile.css +28 -0
- package/package.json +3 -3
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
MarkdownColorFormatRequest,
|
|
22
22
|
MarkdownInlineFormatKind,
|
|
23
23
|
MarkdownRewriteAction,
|
|
24
|
+
MarkdownSelectionSnapshot,
|
|
24
25
|
MarkdownTextAlignKind,
|
|
25
26
|
} from './document-types.js'
|
|
27
|
+
import { tick } from 'svelte'
|
|
26
28
|
import { uiM } from '../../../messages.js'
|
|
27
29
|
import { cn } from '../../../utils.js'
|
|
28
30
|
import AiDocumentDownloadMenu from './AiDocumentDownloadMenu.svelte'
|
|
@@ -33,7 +35,7 @@
|
|
|
33
35
|
interface SelectionToolbarPosition {
|
|
34
36
|
/** 选区工具条相对滚动容器的 top 坐标。 */
|
|
35
37
|
top: number
|
|
36
|
-
/**
|
|
38
|
+
/** 选区工具条左侧边缘相对滚动容器的 left 坐标。 */
|
|
37
39
|
left: number
|
|
38
40
|
/** 选区工具条应该出现在选区上方还是下方,避免遮挡正文。 */
|
|
39
41
|
placement: 'top' | 'bottom'
|
|
@@ -84,6 +86,28 @@
|
|
|
84
86
|
text: string
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
interface ColorSwatchOption {
|
|
90
|
+
/** 颜色值;为空表示恢复默认或清除当前维度。 */
|
|
91
|
+
value: string | null
|
|
92
|
+
/** 展示标题,用于 hover / a11y。 */
|
|
93
|
+
titleKey: 'markdown_background_none'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface HoveredLinkState {
|
|
97
|
+
/** 当前悬停的链接元素。 */
|
|
98
|
+
element: HTMLAnchorElement
|
|
99
|
+
/** 浏览器可直接打开的绝对链接地址。 */
|
|
100
|
+
href: string
|
|
101
|
+
/** 编辑时优先回填原始 href 属性,避免相对地址被强制绝对化。 */
|
|
102
|
+
rawHref: string
|
|
103
|
+
/** 悬停浮层相对滚动容器的 top 坐标。 */
|
|
104
|
+
top: number
|
|
105
|
+
/** 悬停浮层左侧边缘相对滚动容器的 left 坐标。 */
|
|
106
|
+
left: number
|
|
107
|
+
/** 浮层出现在链接上方还是下方。 */
|
|
108
|
+
placement: 'top' | 'bottom'
|
|
109
|
+
}
|
|
110
|
+
|
|
87
111
|
type SelectionMenuKind = 'rewrite' | 'block' | 'align' | 'link' | 'color'
|
|
88
112
|
|
|
89
113
|
const BLOCK_FORMAT_OPTIONS: Array<{
|
|
@@ -94,6 +118,8 @@
|
|
|
94
118
|
| 'markdown_format_heading_2'
|
|
95
119
|
| 'markdown_format_heading_3'
|
|
96
120
|
| 'markdown_format_heading_4'
|
|
121
|
+
| 'markdown_format_ordered_list'
|
|
122
|
+
| 'markdown_format_bullet_list'
|
|
97
123
|
shortLabel: string
|
|
98
124
|
}> = [
|
|
99
125
|
{
|
|
@@ -116,6 +142,16 @@
|
|
|
116
142
|
labelKey: 'markdown_format_heading_3',
|
|
117
143
|
shortLabel: 'H3',
|
|
118
144
|
},
|
|
145
|
+
{
|
|
146
|
+
value: 'orderedList',
|
|
147
|
+
labelKey: 'markdown_format_ordered_list',
|
|
148
|
+
shortLabel: '1.',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
value: 'bulletList',
|
|
152
|
+
labelKey: 'markdown_format_bullet_list',
|
|
153
|
+
shortLabel: '•',
|
|
154
|
+
},
|
|
119
155
|
{
|
|
120
156
|
value: 'heading4',
|
|
121
157
|
labelKey: 'markdown_format_heading_4',
|
|
@@ -158,6 +194,13 @@
|
|
|
158
194
|
'#f3f4f6',
|
|
159
195
|
'#e2e8f0',
|
|
160
196
|
] as const
|
|
197
|
+
const BACKGROUND_COLOR_OPTIONS: readonly (typeof BACKGROUND_COLOR_PRESETS[number] | ColorSwatchOption)[] = [
|
|
198
|
+
{ value: null, titleKey: 'markdown_background_none' },
|
|
199
|
+
...BACKGROUND_COLOR_PRESETS,
|
|
200
|
+
] as const
|
|
201
|
+
const FALLBACK_BLOCK_FORMAT_OPTION = BLOCK_FORMAT_OPTIONS.find(
|
|
202
|
+
option => option.value === 'paragraph',
|
|
203
|
+
) ?? BLOCK_FORMAT_OPTIONS[0]
|
|
161
204
|
|
|
162
205
|
const DEFAULT_SELECTION_FORMAT_STATE: SelectionFormatState = {
|
|
163
206
|
blockFormat: 'paragraph',
|
|
@@ -206,6 +249,8 @@
|
|
|
206
249
|
breaks = true,
|
|
207
250
|
// 是否显示左侧目录。
|
|
208
251
|
showOutline = true,
|
|
252
|
+
// 是否显示目录项的序号前缀。
|
|
253
|
+
showOutlineNumbering = false,
|
|
209
254
|
// 是否显示顶部工具栏。
|
|
210
255
|
showToolbar = true,
|
|
211
256
|
// 目录面板初始是否折叠。
|
|
@@ -289,6 +334,10 @@
|
|
|
289
334
|
placement: 'top',
|
|
290
335
|
alignment: 'center',
|
|
291
336
|
})
|
|
337
|
+
// 窄容器下目录会改成悬浮覆盖模式,保存 DOM 引用后才能在定位工具条时主动避让。
|
|
338
|
+
let outlinePanelEl = $state<HTMLElement | null>(null)
|
|
339
|
+
// chrome 节点同时包住主工具条和下拉面板,方便判断焦点是否仍留在浮层内部。
|
|
340
|
+
let selectionChromeEl = $state<HTMLDivElement | null>(null)
|
|
292
341
|
// 选区工具条自身 DOM,用于根据真实尺寸修正贴边位置。
|
|
293
342
|
let selectionToolbarEl = $state<HTMLDivElement | null>(null)
|
|
294
343
|
// 当前展开菜单对应的触发按钮,用来把面板锚定到具体按钮下方/上方。
|
|
@@ -309,13 +358,28 @@
|
|
|
309
358
|
})
|
|
310
359
|
// linkDraft 只在链接弹层里暂存输入值,避免每次打开都丢掉正在编辑的链接。
|
|
311
360
|
let linkDraft = $state('')
|
|
361
|
+
// 链接输入框允许接管焦点,但格式动作仍需要恢复到正文的最后一个有效选区。
|
|
362
|
+
let linkInputEl = $state<HTMLInputElement | null>(null)
|
|
363
|
+
// 链接按钮 DOM 用来在“悬停编辑链接”场景下复用现有链接弹层定位。
|
|
364
|
+
let selectionLinkButtonEl = $state<HTMLButtonElement | null>(null)
|
|
365
|
+
// 原始 DOM Range 在结构没变时恢复最快,也能保留浏览器原生的精确边界。
|
|
366
|
+
let savedSelectionRange = $state<Range | null>(null)
|
|
367
|
+
// 纯文本快照用于 DOM 被局部重建后的兜底恢复,避免只靠 Range 引用失效。
|
|
368
|
+
let savedSelectionSnapshot = $state<MarkdownSelectionSnapshot | null>(null)
|
|
312
369
|
// 真实尺寸修正只需要排队一帧,避免反复触发 handleSelectionChange 形成抖动。
|
|
313
370
|
let selectionToolbarMeasurePending = false
|
|
314
371
|
// 选区刷新统一合并到同一帧,避免 selectionchange / mouseup / blur 连续触发时抖动。
|
|
315
372
|
let selectionRefreshFrame: number | undefined
|
|
373
|
+
// 同一帧内任一来源要求重新量尺时,都要把这次刷新升级成 remeasure 版本。
|
|
316
374
|
let selectionRefreshNeedsRemeasure = false
|
|
317
375
|
// 浏览器在滚动到底部或切换焦点时会短暂给出空选区,这里延迟一拍再关闭工具条。
|
|
318
376
|
let selectionToolbarCloseTimer: number | undefined
|
|
377
|
+
// 悬停到已配置链接时显示快捷浮层,便于直接打开或编辑。
|
|
378
|
+
let hoveredLinkState = $state<HoveredLinkState | null>(null)
|
|
379
|
+
// 悬停浮层需要拿到真实尺寸后再二次校正位置。
|
|
380
|
+
let hoveredLinkPopoverEl = $state<HTMLDivElement | null>(null)
|
|
381
|
+
// 鼠标从正文移动到悬停浮层时给一小段缓冲,避免浮层闪烁。
|
|
382
|
+
let hoveredLinkHideTimer: number | undefined
|
|
319
383
|
|
|
320
384
|
// code 类型产物通常只有裸代码文本,这里统一包成 fenced block 进入同一条渲染链路。
|
|
321
385
|
const documentContent = $derived(
|
|
@@ -325,6 +389,7 @@
|
|
|
325
389
|
const codePreviewToggleEnabled = $derived(
|
|
326
390
|
showCodePreviewToggle && sourceKind === 'code',
|
|
327
391
|
)
|
|
392
|
+
// hint 文案统一做 trim,避免只传空格时仍把占位区域撑开。
|
|
328
393
|
const normalizedCodePreviewHint = $derived(codePreviewHint.trim())
|
|
329
394
|
const renderResult = $derived(
|
|
330
395
|
renderMarkdownDocument(documentContent, {
|
|
@@ -348,6 +413,7 @@
|
|
|
348
413
|
const resolvedRewriteActions = $derived(
|
|
349
414
|
resolveRewriteActions(rewriteActions, onrewrite),
|
|
350
415
|
)
|
|
416
|
+
// 只有一个改写动作时直接渲染成按钮,多动作才保留菜单,减少一次点击。
|
|
351
417
|
const directRewriteAction = $derived(
|
|
352
418
|
resolvedRewriteActions.length === 1 ? resolvedRewriteActions[0] : null,
|
|
353
419
|
)
|
|
@@ -378,7 +444,7 @@
|
|
|
378
444
|
const activeBlockFormatOption = $derived(
|
|
379
445
|
BLOCK_FORMAT_OPTIONS.find(
|
|
380
446
|
option => option.value === selectionFormatState.blockFormat,
|
|
381
|
-
) ??
|
|
447
|
+
) ?? FALLBACK_BLOCK_FORMAT_OPTION,
|
|
382
448
|
)
|
|
383
449
|
// readerDocumentClass 只负责正文文章区域,不和外层容器类名混用。
|
|
384
450
|
const readerDocumentClass = $derived(
|
|
@@ -432,7 +498,8 @@
|
|
|
432
498
|
$effect(() => {
|
|
433
499
|
void documentContent
|
|
434
500
|
codePreviews = {}
|
|
435
|
-
|
|
501
|
+
hoveredLinkState = null
|
|
502
|
+
queueSelectionRefresh(true)
|
|
436
503
|
})
|
|
437
504
|
|
|
438
505
|
$effect(() => {
|
|
@@ -457,6 +524,10 @@
|
|
|
457
524
|
if (selectionToolbarCloseTimer) {
|
|
458
525
|
window.clearTimeout(selectionToolbarCloseTimer)
|
|
459
526
|
}
|
|
527
|
+
|
|
528
|
+
if (hoveredLinkHideTimer) {
|
|
529
|
+
window.clearTimeout(hoveredLinkHideTimer)
|
|
530
|
+
}
|
|
460
531
|
}
|
|
461
532
|
})
|
|
462
533
|
|
|
@@ -479,6 +550,64 @@
|
|
|
479
550
|
}
|
|
480
551
|
})
|
|
481
552
|
|
|
553
|
+
$effect(() => {
|
|
554
|
+
if (
|
|
555
|
+
typeof document === 'undefined'
|
|
556
|
+
|| !selectionToolbarVisible
|
|
557
|
+
) {
|
|
558
|
+
return
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const handleDocumentPointerDown = (event: MouseEvent): void => {
|
|
562
|
+
const target = event.target
|
|
563
|
+
if (!(target instanceof Node)) {
|
|
564
|
+
return
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (selectionChromeEl?.contains(target)) {
|
|
568
|
+
return
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
closeSelectionToolbar()
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
document.addEventListener('mousedown', handleDocumentPointerDown, true)
|
|
575
|
+
|
|
576
|
+
return () => {
|
|
577
|
+
document.removeEventListener('mousedown', handleDocumentPointerDown, true)
|
|
578
|
+
}
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
$effect(() => {
|
|
582
|
+
if (typeof window === 'undefined' || activeSelectionMenu !== 'link' || !linkInputEl) {
|
|
583
|
+
return
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const nextLinkInput = linkInputEl
|
|
587
|
+
const focusFrame = window.requestAnimationFrame(() => {
|
|
588
|
+
nextLinkInput.focus()
|
|
589
|
+
nextLinkInput.select()
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
return () => {
|
|
593
|
+
window.cancelAnimationFrame(focusFrame)
|
|
594
|
+
}
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
$effect(() => {
|
|
598
|
+
if (typeof window === 'undefined' || !hoveredLinkState || !hoveredLinkPopoverEl) {
|
|
599
|
+
return
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const measureFrame = window.requestAnimationFrame(() => {
|
|
603
|
+
syncHoveredLinkPosition()
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
return () => {
|
|
607
|
+
window.cancelAnimationFrame(measureFrame)
|
|
608
|
+
}
|
|
609
|
+
})
|
|
610
|
+
|
|
482
611
|
function resolveRewriteActions(
|
|
483
612
|
actions: MarkdownRewriteAction[],
|
|
484
613
|
handler?: AiDocumentEditorProps['onrewrite'],
|
|
@@ -786,6 +915,10 @@ ${safeCode}
|
|
|
786
915
|
}
|
|
787
916
|
}
|
|
788
917
|
|
|
918
|
+
/**
|
|
919
|
+
* 统一承接正文里通过 `{@html}` 注入的交互节点点击。
|
|
920
|
+
* 这些按钮和链接不受 Svelte 逐个绑定管理,因此在容器级别做代理更稳妥。
|
|
921
|
+
*/
|
|
789
922
|
async function handleClick(event: MouseEvent): Promise<void> {
|
|
790
923
|
const target = event.target as HTMLElement
|
|
791
924
|
const copyButton = target.closest(
|
|
@@ -822,6 +955,22 @@ ${safeCode}
|
|
|
822
955
|
const codeBlockId = runButton?.dataset.codeBlockId
|
|
823
956
|
if (runButton && codeBlockId) {
|
|
824
957
|
await runCodeBlock(codeBlockId)
|
|
958
|
+
return
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const link = target.closest('a[href]') as HTMLAnchorElement | null
|
|
962
|
+
if (editable && link && previewHost?.contains(link)) {
|
|
963
|
+
const href = link.href.trim()
|
|
964
|
+
if (!href) {
|
|
965
|
+
return
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
event.preventDefault()
|
|
969
|
+
window.open(
|
|
970
|
+
href,
|
|
971
|
+
link.target || '_blank',
|
|
972
|
+
'noopener,noreferrer',
|
|
973
|
+
)
|
|
825
974
|
}
|
|
826
975
|
}
|
|
827
976
|
|
|
@@ -963,7 +1112,18 @@ ${safeCode}
|
|
|
963
1112
|
}
|
|
964
1113
|
|
|
965
1114
|
function readBlockFormat(element: HTMLElement | null): MarkdownBlockStyleKind {
|
|
966
|
-
const
|
|
1115
|
+
const block = getClosestBlockElement(element)
|
|
1116
|
+
const tagName = block?.tagName.toLowerCase()
|
|
1117
|
+
|
|
1118
|
+
if (tagName === 'li') {
|
|
1119
|
+
const listTag = block?.parentElement?.tagName.toLowerCase()
|
|
1120
|
+
if (listTag === 'ol') {
|
|
1121
|
+
return 'orderedList'
|
|
1122
|
+
}
|
|
1123
|
+
if (listTag === 'ul') {
|
|
1124
|
+
return 'bulletList'
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
967
1127
|
|
|
968
1128
|
if (tagName === 'h1') {
|
|
969
1129
|
return 'heading1'
|
|
@@ -1090,6 +1250,370 @@ ${safeCode}
|
|
|
1090
1250
|
}
|
|
1091
1251
|
}
|
|
1092
1252
|
|
|
1253
|
+
/**
|
|
1254
|
+
* 下面这组 helper 在 contenteditable DOM 与“正文纯文本偏移”之间来回映射。
|
|
1255
|
+
* 选区工具条一旦失焦或内容局部重渲染,就靠它们把原选区重新定位回来。
|
|
1256
|
+
*/
|
|
1257
|
+
function getNodeTextLength(node: Node | null | undefined): number {
|
|
1258
|
+
if (!node) {
|
|
1259
|
+
return 0
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1263
|
+
return node.textContent?.length ?? 0
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
let length = 0
|
|
1267
|
+
for (const childNode of Array.from(node.childNodes)) {
|
|
1268
|
+
length += getNodeTextLength(childNode)
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
return length
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function getTextOffsetWithinNode(node: Node, offset: number): number {
|
|
1275
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1276
|
+
return Math.max(0, Math.min(offset, node.textContent?.length ?? 0))
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
const childNodes = Array.from(node.childNodes)
|
|
1280
|
+
const nextOffset = Math.max(0, Math.min(offset, childNodes.length))
|
|
1281
|
+
let length = 0
|
|
1282
|
+
|
|
1283
|
+
for (const childNode of childNodes.slice(0, nextOffset)) {
|
|
1284
|
+
length += getNodeTextLength(childNode)
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
return length
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function getTextOffsetBeforeNode(root: Node, target: Node): number | null {
|
|
1291
|
+
let length = 0
|
|
1292
|
+
|
|
1293
|
+
const visit = (node: Node): boolean => {
|
|
1294
|
+
if (node === target) {
|
|
1295
|
+
return true
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1299
|
+
length += node.textContent?.length ?? 0
|
|
1300
|
+
return false
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
for (const childNode of Array.from(node.childNodes)) {
|
|
1304
|
+
if (visit(childNode)) {
|
|
1305
|
+
return true
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
return false
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
return visit(root) ? length : null
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function getTextOffsetInPreview(node: Node, offset: number): number | null {
|
|
1316
|
+
if (!previewHost || (node !== previewHost && !previewHost.contains(node))) {
|
|
1317
|
+
return null
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
const prefixLength = getTextOffsetBeforeNode(previewHost, node)
|
|
1321
|
+
if (prefixLength === null) {
|
|
1322
|
+
return null
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
return prefixLength + getTextOffsetWithinNode(node, offset)
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function resolveTextPositionInPreview(
|
|
1329
|
+
offset: number,
|
|
1330
|
+
): { node: Node, offset: number } | null {
|
|
1331
|
+
if (!previewHost) {
|
|
1332
|
+
return null
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
const targetOffset = Math.max(0, offset)
|
|
1336
|
+
const walker = document.createTreeWalker(previewHost, NodeFilter.SHOW_TEXT)
|
|
1337
|
+
let current = walker.nextNode()
|
|
1338
|
+
let accumulatedLength = 0
|
|
1339
|
+
let lastTextNode: Node | null = null
|
|
1340
|
+
|
|
1341
|
+
while (current) {
|
|
1342
|
+
const textLength = current.textContent?.length ?? 0
|
|
1343
|
+
if (targetOffset <= accumulatedLength + textLength) {
|
|
1344
|
+
return {
|
|
1345
|
+
node: current,
|
|
1346
|
+
offset: Math.max(0, targetOffset - accumulatedLength),
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
accumulatedLength += textLength
|
|
1351
|
+
lastTextNode = current
|
|
1352
|
+
current = walker.nextNode()
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
if (lastTextNode) {
|
|
1356
|
+
return {
|
|
1357
|
+
node: lastTextNode,
|
|
1358
|
+
offset: lastTextNode.textContent?.length ?? 0,
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
return {
|
|
1363
|
+
node: previewHost,
|
|
1364
|
+
offset: Math.min(targetOffset, previewHost.childNodes.length),
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/**
|
|
1369
|
+
* 把当前 Range 压缩成纯文本偏移快照。
|
|
1370
|
+
* 只记录非空选区,避免工具条动作把折叠光标也当成可恢复选区。
|
|
1371
|
+
*/
|
|
1372
|
+
function capturePreviewSelectionSnapshot(
|
|
1373
|
+
range: Range,
|
|
1374
|
+
): MarkdownSelectionSnapshot | null {
|
|
1375
|
+
if (!previewHost) {
|
|
1376
|
+
return null
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
const start = getTextOffsetInPreview(range.startContainer, range.startOffset)
|
|
1380
|
+
const end = getTextOffsetInPreview(range.endContainer, range.endOffset)
|
|
1381
|
+
const text = range.toString()
|
|
1382
|
+
|
|
1383
|
+
if (start === null || end === null || end <= start || !text.trim()) {
|
|
1384
|
+
return null
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
return { start, end, text }
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/**
|
|
1391
|
+
* 用快照重新读取一遍当前正文中的文本,用来判断 DOM 变化后原 offsets 是否仍然可信。
|
|
1392
|
+
*/
|
|
1393
|
+
function readTextFromSnapshot(
|
|
1394
|
+
snapshot: MarkdownSelectionSnapshot,
|
|
1395
|
+
): string | null {
|
|
1396
|
+
const startPosition = resolveTextPositionInPreview(snapshot.start)
|
|
1397
|
+
const endPosition = resolveTextPositionInPreview(snapshot.end)
|
|
1398
|
+
if (!startPosition || !endPosition) {
|
|
1399
|
+
return null
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
try {
|
|
1403
|
+
const range = document.createRange()
|
|
1404
|
+
range.setStart(startPosition.node, startPosition.offset)
|
|
1405
|
+
range.setEnd(endPosition.node, endPosition.offset)
|
|
1406
|
+
return range.toString()
|
|
1407
|
+
}
|
|
1408
|
+
catch {
|
|
1409
|
+
return null
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
/**
|
|
1414
|
+
* 当原 offsets 对应的文本已经变了时,按“离旧起点最近”的同文本片段回退。
|
|
1415
|
+
* 这样比只取第一个命中更接近用户刚才真实操作的位置。
|
|
1416
|
+
*/
|
|
1417
|
+
function findClosestSelectionOffsets(
|
|
1418
|
+
snapshot: MarkdownSelectionSnapshot,
|
|
1419
|
+
): { start: number, end: number } | null {
|
|
1420
|
+
if (!previewHost || !snapshot.text) {
|
|
1421
|
+
return null
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
const textContent = previewHost.textContent ?? ''
|
|
1425
|
+
let bestMatchStart = -1
|
|
1426
|
+
let smallestDistance = Number.POSITIVE_INFINITY
|
|
1427
|
+
let searchIndex = textContent.indexOf(snapshot.text)
|
|
1428
|
+
|
|
1429
|
+
while (searchIndex >= 0) {
|
|
1430
|
+
const distance = Math.abs(searchIndex - snapshot.start)
|
|
1431
|
+
if (distance < smallestDistance) {
|
|
1432
|
+
smallestDistance = distance
|
|
1433
|
+
bestMatchStart = searchIndex
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
searchIndex = textContent.indexOf(snapshot.text, searchIndex + 1)
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
if (bestMatchStart < 0) {
|
|
1440
|
+
return null
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
return {
|
|
1444
|
+
start: bestMatchStart,
|
|
1445
|
+
end: bestMatchStart + snapshot.text.length,
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/**
|
|
1450
|
+
* 优先按纯文本偏移恢复选区;如果 DOM 结构变动导致 offsets 漂移,再退回最近文本片段。
|
|
1451
|
+
* 这一步会主动把焦点拉回正文,保证后续格式化命令仍作用于内容区。
|
|
1452
|
+
*/
|
|
1453
|
+
function restoreSelectionFromSnapshot(
|
|
1454
|
+
snapshot: MarkdownSelectionSnapshot,
|
|
1455
|
+
): boolean {
|
|
1456
|
+
if (typeof window === 'undefined' || !previewHost) {
|
|
1457
|
+
return false
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
const selection = window.getSelection()
|
|
1461
|
+
if (!selection || snapshot.end <= snapshot.start) {
|
|
1462
|
+
return false
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
let nextOffsets = {
|
|
1466
|
+
start: snapshot.start,
|
|
1467
|
+
end: snapshot.end,
|
|
1468
|
+
}
|
|
1469
|
+
const currentText = readTextFromSnapshot(snapshot)
|
|
1470
|
+
if (snapshot.text && currentText !== snapshot.text) {
|
|
1471
|
+
const matchedOffsets = findClosestSelectionOffsets(snapshot)
|
|
1472
|
+
if (!matchedOffsets) {
|
|
1473
|
+
return false
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
nextOffsets = matchedOffsets
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
const startPosition = resolveTextPositionInPreview(nextOffsets.start)
|
|
1480
|
+
const endPosition = resolveTextPositionInPreview(nextOffsets.end)
|
|
1481
|
+
if (!startPosition || !endPosition) {
|
|
1482
|
+
return false
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
try {
|
|
1486
|
+
const range = document.createRange()
|
|
1487
|
+
range.setStart(startPosition.node, startPosition.offset)
|
|
1488
|
+
range.setEnd(endPosition.node, endPosition.offset)
|
|
1489
|
+
previewHost.focus()
|
|
1490
|
+
selection.removeAllRanges()
|
|
1491
|
+
selection.addRange(range)
|
|
1492
|
+
return true
|
|
1493
|
+
}
|
|
1494
|
+
catch {
|
|
1495
|
+
return false
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
/**
|
|
1500
|
+
* 纯文本匹配是最后一道兜底,只在 Range 和偏移快照都失效时使用。
|
|
1501
|
+
* 它无法区分完全相同的多个片段,所以只适合作为尽力恢复方案。
|
|
1502
|
+
*/
|
|
1503
|
+
function restoreSelectionByTextMatch(text: string): boolean {
|
|
1504
|
+
if (typeof window === 'undefined' || !previewHost || !text.trim()) {
|
|
1505
|
+
return false
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
const selection = window.getSelection()
|
|
1509
|
+
if (!selection) {
|
|
1510
|
+
return false
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
const walker = document.createTreeWalker(previewHost, NodeFilter.SHOW_TEXT)
|
|
1514
|
+
let current = walker.nextNode()
|
|
1515
|
+
|
|
1516
|
+
while (current) {
|
|
1517
|
+
const content = current.textContent ?? ''
|
|
1518
|
+
const start = content.indexOf(text)
|
|
1519
|
+
if (start >= 0) {
|
|
1520
|
+
try {
|
|
1521
|
+
const range = document.createRange()
|
|
1522
|
+
range.setStart(current, start)
|
|
1523
|
+
range.setEnd(current, start + text.length)
|
|
1524
|
+
previewHost.focus()
|
|
1525
|
+
selection.removeAllRanges()
|
|
1526
|
+
selection.addRange(range)
|
|
1527
|
+
return true
|
|
1528
|
+
}
|
|
1529
|
+
catch {
|
|
1530
|
+
return false
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
current = walker.nextNode()
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
return false
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
/**
|
|
1541
|
+
* 工具条包含输入框和下拉面板,焦点落在内部时不应把“正文失焦”误判成关闭信号。
|
|
1542
|
+
*/
|
|
1543
|
+
function isSelectionChromeFocused(): boolean {
|
|
1544
|
+
if (typeof document === 'undefined' || !selectionChromeEl) {
|
|
1545
|
+
return false
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
const activeElement = document.activeElement
|
|
1549
|
+
return activeElement instanceof HTMLElement && selectionChromeEl.contains(activeElement)
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
/**
|
|
1553
|
+
* 同时保存浏览器原生 Range 和纯文本快照。
|
|
1554
|
+
* 前者恢复精度更高,后者能跨 DOM 重建,两者组合能覆盖大多数编辑场景。
|
|
1555
|
+
*/
|
|
1556
|
+
function rememberPreviewSelection(range: Range): void {
|
|
1557
|
+
savedSelectionRange = range.cloneRange()
|
|
1558
|
+
savedSelectionSnapshot = capturePreviewSelectionSnapshot(range)
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
/**
|
|
1562
|
+
* 选区恢复按“快照 -> 原始 Range -> 文本兜底”三层顺序尝试。
|
|
1563
|
+
* 这样既优先命中最精确的位置,也能在内容轻微重排后保留工具条操作连续性。
|
|
1564
|
+
*/
|
|
1565
|
+
function restoreSavedPreviewSelection(): boolean {
|
|
1566
|
+
if (savedSelectionSnapshot && restoreSelectionFromSnapshot(savedSelectionSnapshot)) {
|
|
1567
|
+
return true
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
if (typeof window === 'undefined' || !previewHost || !savedSelectionRange) {
|
|
1571
|
+
const fallbackText = savedSelectionSnapshot?.text || selectedText
|
|
1572
|
+
return restoreSelectionByTextMatch(fallbackText)
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
const selection = window.getSelection()
|
|
1576
|
+
if (!selection) {
|
|
1577
|
+
return false
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
try {
|
|
1581
|
+
const restoredRange = savedSelectionRange.cloneRange()
|
|
1582
|
+
if (
|
|
1583
|
+
!previewHost.contains(restoredRange.startContainer)
|
|
1584
|
+
|| !previewHost.contains(restoredRange.endContainer)
|
|
1585
|
+
) {
|
|
1586
|
+
return false
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
previewHost.focus()
|
|
1590
|
+
selection.removeAllRanges()
|
|
1591
|
+
selection.addRange(restoredRange)
|
|
1592
|
+
return true
|
|
1593
|
+
}
|
|
1594
|
+
catch {
|
|
1595
|
+
const fallbackText = savedSelectionSnapshot?.text || selectedText
|
|
1596
|
+
return restoreSelectionByTextMatch(fallbackText)
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
/**
|
|
1601
|
+
* 工具条动作触发前先同步一次真实选区,避免点击按钮后浏览器把正文选区清掉。
|
|
1602
|
+
*/
|
|
1603
|
+
function syncSelectionForToolbarAction(): boolean {
|
|
1604
|
+
const previewSelection = readPreviewSelectionState()
|
|
1605
|
+
if (previewSelection) {
|
|
1606
|
+
rememberPreviewSelection(previewSelection.range)
|
|
1607
|
+
selectedText = previewSelection.text
|
|
1608
|
+
return true
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
return restoreSavedPreviewSelection()
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
/**
|
|
1615
|
+
* 所有关闭动作都走同一个 timer,方便菜单 hover、输入框 focus 等场景随时撤销关闭。
|
|
1616
|
+
*/
|
|
1093
1617
|
function clearPendingSelectionToolbarClose(): void {
|
|
1094
1618
|
if (typeof window === 'undefined' || !selectionToolbarCloseTimer) {
|
|
1095
1619
|
return
|
|
@@ -1099,135 +1623,406 @@ ${safeCode}
|
|
|
1099
1623
|
selectionToolbarCloseTimer = undefined
|
|
1100
1624
|
}
|
|
1101
1625
|
|
|
1626
|
+
/**
|
|
1627
|
+
* 关闭工具条刻意延后一小拍,给菜单 hover、输入框 focus 和选区回流留出时间。
|
|
1628
|
+
* 只要子菜单还开着,或焦点仍在 chrome 里,就继续保留当前工具条。
|
|
1629
|
+
*/
|
|
1102
1630
|
function scheduleSelectionToolbarClose(): void {
|
|
1103
1631
|
if (typeof window === 'undefined') {
|
|
1104
1632
|
closeSelectionToolbar()
|
|
1105
1633
|
return
|
|
1106
1634
|
}
|
|
1107
1635
|
|
|
1108
|
-
if (!selectionToolbarVisible) {
|
|
1109
|
-
closeSelectionToolbar()
|
|
1636
|
+
if (!selectionToolbarVisible) {
|
|
1637
|
+
closeSelectionToolbar()
|
|
1638
|
+
return
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
if (activeSelectionMenu) {
|
|
1642
|
+
return
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
clearPendingSelectionToolbarClose()
|
|
1646
|
+
selectionToolbarCloseTimer = window.setTimeout(() => {
|
|
1647
|
+
selectionToolbarCloseTimer = undefined
|
|
1648
|
+
if (activeSelectionMenu || isSelectionChromeFocused()) {
|
|
1649
|
+
return
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
if (!readPreviewSelectionState()) {
|
|
1653
|
+
closeSelectionToolbar()
|
|
1654
|
+
}
|
|
1655
|
+
}, 600)
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
/**
|
|
1659
|
+
* 选区变化可能在一帧内收到多次事件,这里统一合并,避免定位和状态读取来回抖动。
|
|
1660
|
+
*/
|
|
1661
|
+
function queueSelectionRefresh(remeasured = false): void {
|
|
1662
|
+
if (typeof window === 'undefined') {
|
|
1663
|
+
return
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
selectionRefreshNeedsRemeasure ||= remeasured
|
|
1667
|
+
if (selectionRefreshFrame) {
|
|
1668
|
+
return
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
selectionRefreshFrame = window.requestAnimationFrame(() => {
|
|
1672
|
+
const nextRemeasured = selectionRefreshNeedsRemeasure
|
|
1673
|
+
selectionRefreshFrame = undefined
|
|
1674
|
+
selectionRefreshNeedsRemeasure = false
|
|
1675
|
+
handleSelectionChange(nextRemeasured)
|
|
1676
|
+
})
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/**
|
|
1680
|
+
* 子菜单首次打开前拿不到真实宽度,先给一个稳定的经验值参与贴边计算。
|
|
1681
|
+
*/
|
|
1682
|
+
function estimateSelectionMenuWidth(menu: SelectionMenuKind): number {
|
|
1683
|
+
if (menu === 'color' || menu === 'link') {
|
|
1684
|
+
return 256
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
if (menu === 'rewrite') {
|
|
1688
|
+
return 320
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
return 288
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
/**
|
|
1695
|
+
* 下拉面板以触发按钮为锚点,但最终位置要限制在工具条宽度内,避免菜单越界裁切。
|
|
1696
|
+
*/
|
|
1697
|
+
function updateSelectionMenuAnchor(menu: SelectionMenuKind): void {
|
|
1698
|
+
if (!selectionToolbarEl || !activeSelectionMenuTrigger) {
|
|
1699
|
+
selectionMenuLeft = 0
|
|
1700
|
+
selectionMenuAlignment = 'center'
|
|
1701
|
+
return
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
const toolbarWidth = Math.max(
|
|
1705
|
+
selectionToolbarEl.offsetWidth,
|
|
1706
|
+
selectionToolbarEl.clientWidth,
|
|
1707
|
+
)
|
|
1708
|
+
const triggerCenter
|
|
1709
|
+
= activeSelectionMenuTrigger.offsetLeft
|
|
1710
|
+
+ activeSelectionMenuTrigger.offsetWidth / 2
|
|
1711
|
+
const estimatedHalfWidth = Math.min(
|
|
1712
|
+
estimateSelectionMenuWidth(menu) / 2,
|
|
1713
|
+
Math.max(112, (toolbarWidth - 16) / 2),
|
|
1714
|
+
)
|
|
1715
|
+
const minCenter = 8 + estimatedHalfWidth
|
|
1716
|
+
const maxCenter = Math.max(minCenter, toolbarWidth - 8 - estimatedHalfWidth)
|
|
1717
|
+
|
|
1718
|
+
if (triggerCenter <= minCenter) {
|
|
1719
|
+
selectionMenuLeft = 0
|
|
1720
|
+
selectionMenuAlignment = 'left'
|
|
1721
|
+
return
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
if (triggerCenter >= maxCenter) {
|
|
1725
|
+
selectionMenuLeft = toolbarWidth
|
|
1726
|
+
selectionMenuAlignment = 'right'
|
|
1727
|
+
return
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
selectionMenuLeft = triggerCenter
|
|
1731
|
+
selectionMenuAlignment = 'center'
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
/**
|
|
1735
|
+
* 工具条初次出现时尺寸还没稳定,下一帧再量一次可以避免首帧贴边位置偏差。
|
|
1736
|
+
*/
|
|
1737
|
+
function scheduleSelectionToolbarMeasurement(): void {
|
|
1738
|
+
if (
|
|
1739
|
+
typeof window === 'undefined'
|
|
1740
|
+
|| selectionToolbarMeasurePending
|
|
1741
|
+
|| !selectionToolbarVisible
|
|
1742
|
+
) {
|
|
1743
|
+
return
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
selectionToolbarMeasurePending = true
|
|
1747
|
+
window.requestAnimationFrame(() => {
|
|
1748
|
+
selectionToolbarMeasurePending = false
|
|
1749
|
+
if (selectionToolbarVisible) {
|
|
1750
|
+
handleSelectionChange(true)
|
|
1751
|
+
}
|
|
1752
|
+
})
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/**
|
|
1756
|
+
* 子菜单统一从这里开关,顺手记录触发按钮和链接初始值,保证定位与默认值一致。
|
|
1757
|
+
*/
|
|
1758
|
+
function toggleSelectionMenu(menu: SelectionMenuKind, event?: MouseEvent): void {
|
|
1759
|
+
clearPendingSelectionToolbarClose()
|
|
1760
|
+
const opening = activeSelectionMenu !== menu
|
|
1761
|
+
activeSelectionMenu = opening ? menu : null
|
|
1762
|
+
activeSelectionMenuTrigger = opening && event?.currentTarget instanceof HTMLElement
|
|
1763
|
+
? event.currentTarget
|
|
1764
|
+
: null
|
|
1765
|
+
if (opening) {
|
|
1766
|
+
updateSelectionMenuAnchor(menu)
|
|
1767
|
+
}
|
|
1768
|
+
if (opening && menu === 'link') {
|
|
1769
|
+
linkDraft = selectionFormatState.linkHref || 'https://'
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
/**
|
|
1774
|
+
* 关闭时把选区相关缓存一并清干净,避免下一次打开还误用上一次的链接和格式状态。
|
|
1775
|
+
*/
|
|
1776
|
+
function closeSelectionToolbar(): void {
|
|
1777
|
+
clearPendingSelectionToolbarClose()
|
|
1778
|
+
selectionToolbarVisible = false
|
|
1779
|
+
activeSelectionMenu = null
|
|
1780
|
+
activeSelectionMenuTrigger = null
|
|
1781
|
+
selectionMenuLeft = 0
|
|
1782
|
+
selectionMenuAlignment = 'center'
|
|
1783
|
+
selectedText = ''
|
|
1784
|
+
linkDraft = ''
|
|
1785
|
+
savedSelectionRange = null
|
|
1786
|
+
savedSelectionSnapshot = null
|
|
1787
|
+
selectionFormatState = { ...DEFAULT_SELECTION_FORMAT_STATE }
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
/**
|
|
1791
|
+
* 面板上的按钮点击不应让浏览器先把正文选区清掉。
|
|
1792
|
+
* 但输入框要保留原生 focus 行为,方便直接编辑链接。
|
|
1793
|
+
*/
|
|
1794
|
+
function handleSelectionPanelMouseDown(event: MouseEvent): void {
|
|
1795
|
+
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
|
1796
|
+
return
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
event.preventDefault()
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
function clearPendingHoveredLinkHide(): void {
|
|
1803
|
+
if (typeof window === 'undefined' || !hoveredLinkHideTimer) {
|
|
1804
|
+
return
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
window.clearTimeout(hoveredLinkHideTimer)
|
|
1808
|
+
hoveredLinkHideTimer = undefined
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
function closeHoveredLink(): void {
|
|
1812
|
+
clearPendingHoveredLinkHide()
|
|
1813
|
+
hoveredLinkState = null
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
function scheduleHoveredLinkHide(): void {
|
|
1817
|
+
if (typeof window === 'undefined') {
|
|
1818
|
+
closeHoveredLink()
|
|
1819
|
+
return
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
clearPendingHoveredLinkHide()
|
|
1823
|
+
hoveredLinkHideTimer = window.setTimeout(() => {
|
|
1824
|
+
hoveredLinkHideTimer = undefined
|
|
1825
|
+
hoveredLinkState = null
|
|
1826
|
+
}, 140)
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
function resolveHoveredLinkTargets(
|
|
1830
|
+
link: HTMLAnchorElement,
|
|
1831
|
+
): { href: string, rawHref: string } | null {
|
|
1832
|
+
const rawHref = link.getAttribute('href')?.trim() ?? ''
|
|
1833
|
+
const href = link.href.trim() || rawHref
|
|
1834
|
+
if (!href) {
|
|
1835
|
+
return null
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
return {
|
|
1839
|
+
href,
|
|
1840
|
+
rawHref: rawHref || href,
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
function measureHoveredLinkPopover(): { width: number, height: number } {
|
|
1845
|
+
const width = hoveredLinkPopoverEl
|
|
1846
|
+
? Math.max(
|
|
1847
|
+
hoveredLinkPopoverEl.offsetWidth,
|
|
1848
|
+
hoveredLinkPopoverEl.clientWidth,
|
|
1849
|
+
)
|
|
1850
|
+
: 92
|
|
1851
|
+
const height = hoveredLinkPopoverEl
|
|
1852
|
+
? Math.max(
|
|
1853
|
+
hoveredLinkPopoverEl.offsetHeight,
|
|
1854
|
+
hoveredLinkPopoverEl.clientHeight,
|
|
1855
|
+
)
|
|
1856
|
+
: 40
|
|
1857
|
+
|
|
1858
|
+
return { width, height }
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
function buildHoveredLinkState(
|
|
1862
|
+
link: HTMLAnchorElement,
|
|
1863
|
+
): HoveredLinkState | null {
|
|
1864
|
+
if (!editorScrollHost || !previewHost || !previewHost.contains(link)) {
|
|
1865
|
+
return null
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
const targets = resolveHoveredLinkTargets(link)
|
|
1869
|
+
if (!targets) {
|
|
1870
|
+
return null
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
const hostRect = editorScrollHost.getBoundingClientRect()
|
|
1874
|
+
const linkRect = link.getBoundingClientRect()
|
|
1875
|
+
const { width, height } = measureHoveredLinkPopover()
|
|
1876
|
+
const offset = 8
|
|
1877
|
+
const horizontalPadding = 16
|
|
1878
|
+
const topSpace = linkRect.top - hostRect.top
|
|
1879
|
+
const bottomSpace = hostRect.bottom - linkRect.bottom
|
|
1880
|
+
const placement: HoveredLinkState['placement']
|
|
1881
|
+
= topSpace >= height + offset || bottomSpace < height + offset
|
|
1882
|
+
? 'top'
|
|
1883
|
+
: 'bottom'
|
|
1884
|
+
const center = linkRect.left - hostRect.left + linkRect.width / 2
|
|
1885
|
+
const maxLeft = Math.max(
|
|
1886
|
+
horizontalPadding,
|
|
1887
|
+
editorScrollHost.clientWidth - horizontalPadding - width,
|
|
1888
|
+
)
|
|
1889
|
+
const left = Math.min(
|
|
1890
|
+
Math.max(center - width / 2, horizontalPadding),
|
|
1891
|
+
maxLeft,
|
|
1892
|
+
)
|
|
1893
|
+
const top = placement === 'top'
|
|
1894
|
+
? editorScrollHost.scrollTop + linkRect.top - hostRect.top - height - offset
|
|
1895
|
+
: editorScrollHost.scrollTop + linkRect.bottom - hostRect.top + offset
|
|
1896
|
+
|
|
1897
|
+
return {
|
|
1898
|
+
element: link,
|
|
1899
|
+
href: targets.href,
|
|
1900
|
+
rawHref: targets.rawHref,
|
|
1901
|
+
top: Math.max(8, top),
|
|
1902
|
+
left,
|
|
1903
|
+
placement,
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
function syncHoveredLinkPosition(): void {
|
|
1908
|
+
if (!hoveredLinkState) {
|
|
1110
1909
|
return
|
|
1111
1910
|
}
|
|
1112
1911
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
selectionToolbarCloseTimer = undefined
|
|
1116
|
-
if (!readPreviewSelectionState()) {
|
|
1117
|
-
closeSelectionToolbar()
|
|
1118
|
-
}
|
|
1119
|
-
}, 120)
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
function queueSelectionRefresh(remeasured = false): void {
|
|
1123
|
-
if (typeof window === 'undefined') {
|
|
1912
|
+
if (!hoveredLinkState.element.isConnected) {
|
|
1913
|
+
closeHoveredLink()
|
|
1124
1914
|
return
|
|
1125
1915
|
}
|
|
1126
1916
|
|
|
1127
|
-
|
|
1128
|
-
if (
|
|
1917
|
+
const nextState = buildHoveredLinkState(hoveredLinkState.element)
|
|
1918
|
+
if (!nextState) {
|
|
1919
|
+
closeHoveredLink()
|
|
1129
1920
|
return
|
|
1130
1921
|
}
|
|
1131
1922
|
|
|
1132
|
-
|
|
1133
|
-
const nextRemeasured = selectionRefreshNeedsRemeasure
|
|
1134
|
-
selectionRefreshFrame = undefined
|
|
1135
|
-
selectionRefreshNeedsRemeasure = false
|
|
1136
|
-
handleSelectionChange(nextRemeasured)
|
|
1137
|
-
})
|
|
1923
|
+
hoveredLinkState = nextState
|
|
1138
1924
|
}
|
|
1139
1925
|
|
|
1140
|
-
function
|
|
1141
|
-
|
|
1142
|
-
|
|
1926
|
+
function resolveColorOptionValue(colorOption: string | ColorSwatchOption): string | null {
|
|
1927
|
+
return typeof colorOption === 'string' ? colorOption : colorOption.value
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
function updateHoveredLink(link: HTMLAnchorElement | null): void {
|
|
1931
|
+
if (!editable || !onapplylink || selectionToolbarVisible) {
|
|
1932
|
+
closeHoveredLink()
|
|
1933
|
+
return
|
|
1143
1934
|
}
|
|
1144
1935
|
|
|
1145
|
-
if (
|
|
1146
|
-
|
|
1936
|
+
if (!link) {
|
|
1937
|
+
scheduleHoveredLinkHide()
|
|
1938
|
+
return
|
|
1147
1939
|
}
|
|
1148
1940
|
|
|
1149
|
-
|
|
1941
|
+
const nextState = buildHoveredLinkState(link)
|
|
1942
|
+
if (!nextState) {
|
|
1943
|
+
scheduleHoveredLinkHide()
|
|
1944
|
+
return
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
clearPendingHoveredLinkHide()
|
|
1948
|
+
hoveredLinkState = nextState
|
|
1150
1949
|
}
|
|
1151
1950
|
|
|
1152
|
-
function
|
|
1153
|
-
if (
|
|
1154
|
-
selectionMenuLeft = 0
|
|
1155
|
-
selectionMenuAlignment = 'center'
|
|
1951
|
+
function openHoveredLink(): void {
|
|
1952
|
+
if (typeof window === 'undefined' || !hoveredLinkState?.href) {
|
|
1156
1953
|
return
|
|
1157
1954
|
}
|
|
1158
1955
|
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
const triggerCenter
|
|
1164
|
-
= activeSelectionMenuTrigger.offsetLeft
|
|
1165
|
-
+ activeSelectionMenuTrigger.offsetWidth / 2
|
|
1166
|
-
const estimatedHalfWidth = Math.min(
|
|
1167
|
-
estimateSelectionMenuWidth(menu) / 2,
|
|
1168
|
-
Math.max(112, (toolbarWidth - 16) / 2),
|
|
1956
|
+
window.open(
|
|
1957
|
+
hoveredLinkState.href,
|
|
1958
|
+
hoveredLinkState.element.target || '_blank',
|
|
1959
|
+
'noopener,noreferrer',
|
|
1169
1960
|
)
|
|
1170
|
-
|
|
1171
|
-
const maxCenter = Math.max(minCenter, toolbarWidth - 8 - estimatedHalfWidth)
|
|
1961
|
+
}
|
|
1172
1962
|
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1963
|
+
function createRangeForLinkText(link: HTMLAnchorElement): Range {
|
|
1964
|
+
const range = document.createRange()
|
|
1965
|
+
const walker = document.createTreeWalker(link, NodeFilter.SHOW_TEXT)
|
|
1966
|
+
let current = walker.nextNode()
|
|
1967
|
+
let firstTextNode: Node | null = null
|
|
1968
|
+
let lastTextNode: Node | null = null
|
|
1969
|
+
|
|
1970
|
+
while (current) {
|
|
1971
|
+
if ((current.textContent ?? '').length > 0) {
|
|
1972
|
+
firstTextNode ??= current
|
|
1973
|
+
lastTextNode = current
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
current = walker.nextNode()
|
|
1177
1977
|
}
|
|
1178
1978
|
|
|
1179
|
-
if (
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
return
|
|
1979
|
+
if (!firstTextNode || !lastTextNode) {
|
|
1980
|
+
range.selectNodeContents(link)
|
|
1981
|
+
return range
|
|
1183
1982
|
}
|
|
1184
1983
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1984
|
+
range.setStart(firstTextNode, 0)
|
|
1985
|
+
range.setEnd(lastTextNode, lastTextNode.textContent?.length ?? 0)
|
|
1986
|
+
return range
|
|
1187
1987
|
}
|
|
1188
1988
|
|
|
1189
|
-
function
|
|
1989
|
+
async function editHoveredLink(): Promise<void> {
|
|
1190
1990
|
if (
|
|
1191
1991
|
typeof window === 'undefined'
|
|
1192
|
-
||
|
|
1193
|
-
|| !
|
|
1992
|
+
|| !previewHost
|
|
1993
|
+
|| !onapplylink
|
|
1994
|
+
|| !hoveredLinkState
|
|
1194
1995
|
) {
|
|
1195
1996
|
return
|
|
1196
1997
|
}
|
|
1197
1998
|
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
}
|
|
1204
|
-
})
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
function toggleSelectionMenu(menu: SelectionMenuKind, event?: MouseEvent): void {
|
|
1208
|
-
const opening = activeSelectionMenu !== menu
|
|
1209
|
-
activeSelectionMenu = opening ? menu : null
|
|
1210
|
-
activeSelectionMenuTrigger = opening && event?.currentTarget instanceof HTMLElement
|
|
1211
|
-
? event.currentTarget
|
|
1212
|
-
: null
|
|
1213
|
-
if (opening) {
|
|
1214
|
-
updateSelectionMenuAnchor(menu)
|
|
1999
|
+
const link = hoveredLinkState.element
|
|
2000
|
+
const linkHref = hoveredLinkState.rawHref
|
|
2001
|
+
if (!previewHost.contains(link)) {
|
|
2002
|
+
closeHoveredLink()
|
|
2003
|
+
return
|
|
1215
2004
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
2005
|
+
|
|
2006
|
+
const selection = window.getSelection()
|
|
2007
|
+
if (!selection) {
|
|
2008
|
+
return
|
|
1218
2009
|
}
|
|
1219
|
-
}
|
|
1220
2010
|
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
2011
|
+
const range = createRangeForLinkText(link)
|
|
2012
|
+
previewHost.focus()
|
|
2013
|
+
selection.removeAllRanges()
|
|
2014
|
+
selection.addRange(range)
|
|
2015
|
+
rememberPreviewSelection(range)
|
|
2016
|
+
selectedText = range.toString().trim()
|
|
2017
|
+
closeHoveredLink()
|
|
2018
|
+
handleSelectionChange(true)
|
|
2019
|
+
await tick()
|
|
2020
|
+
linkDraft = linkHref || 'https://'
|
|
2021
|
+
activeSelectionMenu = 'link'
|
|
2022
|
+
activeSelectionMenuTrigger = selectionLinkButtonEl
|
|
2023
|
+
if (selectionLinkButtonEl) {
|
|
2024
|
+
updateSelectionMenuAnchor('link')
|
|
2025
|
+
}
|
|
1231
2026
|
}
|
|
1232
2027
|
|
|
1233
2028
|
/**
|
|
@@ -1293,6 +2088,7 @@ ${safeCode}
|
|
|
1293
2088
|
function handleDocumentScroll(event: Event): void {
|
|
1294
2089
|
syncActiveHeadingFromScroll()
|
|
1295
2090
|
syncPinnedTitleVisibility()
|
|
2091
|
+
syncHoveredLinkPosition()
|
|
1296
2092
|
if (selectionToolbarVisible) {
|
|
1297
2093
|
queueSelectionRefresh(true)
|
|
1298
2094
|
}
|
|
@@ -1332,11 +2128,16 @@ ${safeCode}
|
|
|
1332
2128
|
|
|
1333
2129
|
const previewSelection = readPreviewSelectionState()
|
|
1334
2130
|
if (!previewSelection) {
|
|
2131
|
+
if (savedSelectionRange && isSelectionChromeFocused()) {
|
|
2132
|
+
return
|
|
2133
|
+
}
|
|
2134
|
+
|
|
1335
2135
|
scheduleSelectionToolbarClose()
|
|
1336
2136
|
return
|
|
1337
2137
|
}
|
|
1338
2138
|
|
|
1339
2139
|
const { selection, range, text } = previewSelection
|
|
2140
|
+
hoveredLinkState = null
|
|
1340
2141
|
const rect = range.getBoundingClientRect()
|
|
1341
2142
|
const hostRect = editorScrollHost.getBoundingClientRect()
|
|
1342
2143
|
// 浮层尺寸在首次渲染前拿不到精确高度,这里用稳定的经验值决定上下避让方向,避免直接盖住选中文本。
|
|
@@ -1351,31 +2152,47 @@ ${safeCode}
|
|
|
1351
2152
|
: 'bottom'
|
|
1352
2153
|
const center = rect.left - hostRect.left + rect.width / 2
|
|
1353
2154
|
const horizontalPadding = 16
|
|
1354
|
-
const
|
|
1355
|
-
?
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
2155
|
+
const outlineOverlap = outlinePanelEl
|
|
2156
|
+
? Math.max(
|
|
2157
|
+
0,
|
|
2158
|
+
Math.min(
|
|
2159
|
+
outlinePanelEl.getBoundingClientRect().right,
|
|
2160
|
+
hostRect.right,
|
|
2161
|
+
) - hostRect.left,
|
|
2162
|
+
)
|
|
2163
|
+
: 0
|
|
2164
|
+
const minLeft = Math.max(
|
|
2165
|
+
horizontalPadding,
|
|
2166
|
+
outlineOverlap > 24 ? outlineOverlap + 12 : horizontalPadding,
|
|
1360
2167
|
)
|
|
1361
|
-
const
|
|
1362
|
-
|
|
2168
|
+
const measuredToolbarWidth = selectionChromeEl
|
|
2169
|
+
? Math.max(selectionChromeEl.offsetWidth, selectionChromeEl.clientWidth)
|
|
2170
|
+
: selectionToolbarEl
|
|
2171
|
+
? Math.max(selectionToolbarEl.offsetWidth, selectionToolbarEl.clientWidth)
|
|
2172
|
+
: 360
|
|
2173
|
+
const maxToolbarWidth = Math.max(
|
|
2174
|
+
220,
|
|
2175
|
+
editorScrollHost.clientWidth - minLeft - horizontalPadding,
|
|
2176
|
+
)
|
|
2177
|
+
const toolbarWidth = Math.min(measuredToolbarWidth, maxToolbarWidth)
|
|
2178
|
+
const maxLeft = Math.max(
|
|
2179
|
+
minLeft,
|
|
2180
|
+
editorScrollHost.clientWidth - horizontalPadding - toolbarWidth,
|
|
2181
|
+
)
|
|
2182
|
+
const unclampedLeft = center - toolbarWidth / 2
|
|
2183
|
+
const toolbarLeft = Math.min(Math.max(unclampedLeft, minLeft), maxLeft)
|
|
1363
2184
|
const alignment: SelectionToolbarPosition['alignment']
|
|
1364
|
-
=
|
|
2185
|
+
= toolbarLeft <= minLeft + 0.5
|
|
1365
2186
|
? 'left'
|
|
1366
|
-
:
|
|
2187
|
+
: toolbarLeft >= maxLeft - 0.5
|
|
1367
2188
|
? 'right'
|
|
1368
2189
|
: 'center'
|
|
1369
|
-
const toolbarLeft = alignment === 'left'
|
|
1370
|
-
? horizontalPadding
|
|
1371
|
-
: alignment === 'right'
|
|
1372
|
-
? editorScrollHost.clientWidth - horizontalPadding
|
|
1373
|
-
: center
|
|
1374
2190
|
const toolbarTop = placement === 'top'
|
|
1375
2191
|
? editorScrollHost.scrollTop + rect.top - hostRect.top - estimatedToolbarHeight - offset
|
|
1376
2192
|
: editorScrollHost.scrollTop + rect.bottom - hostRect.top + offset
|
|
1377
2193
|
|
|
1378
2194
|
selectedText = text
|
|
2195
|
+
rememberPreviewSelection(range)
|
|
1379
2196
|
selectionToolbarVisible = true
|
|
1380
2197
|
selectionFormatState = readSelectionFormatState(selection)
|
|
1381
2198
|
toolbarPosition = {
|
|
@@ -1410,6 +2227,10 @@ ${safeCode}
|
|
|
1410
2227
|
}
|
|
1411
2228
|
|
|
1412
2229
|
function applyBlockFormat(kind: MarkdownBlockFormatKind): void {
|
|
2230
|
+
if (!syncSelectionForToolbarAction()) {
|
|
2231
|
+
return
|
|
2232
|
+
}
|
|
2233
|
+
|
|
1413
2234
|
onapplyblockformat?.(kind)
|
|
1414
2235
|
activeSelectionMenu = null
|
|
1415
2236
|
activeSelectionMenuTrigger = null
|
|
@@ -1417,66 +2238,160 @@ ${safeCode}
|
|
|
1417
2238
|
}
|
|
1418
2239
|
|
|
1419
2240
|
function applyBlockStyle(kind: MarkdownBlockStyleKind): void {
|
|
2241
|
+
if (!syncSelectionForToolbarAction()) {
|
|
2242
|
+
return
|
|
2243
|
+
}
|
|
2244
|
+
|
|
1420
2245
|
onapplyblockstyle?.(kind)
|
|
1421
2246
|
activeSelectionMenu = null
|
|
1422
2247
|
activeSelectionMenuTrigger = null
|
|
1423
2248
|
queueSelectionRefresh()
|
|
1424
2249
|
}
|
|
1425
2250
|
|
|
2251
|
+
/**
|
|
2252
|
+
* 行内样式按钮允许连续点按,因此这里不主动关掉工具条,只刷新状态。
|
|
2253
|
+
*/
|
|
1426
2254
|
function applyInlineFormat(kind: MarkdownInlineFormatKind): void {
|
|
2255
|
+
if (!syncSelectionForToolbarAction()) {
|
|
2256
|
+
return
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
clearPendingSelectionToolbarClose()
|
|
1427
2260
|
onapplyinlineformat?.(kind)
|
|
1428
2261
|
queueSelectionRefresh()
|
|
1429
2262
|
}
|
|
1430
2263
|
|
|
1431
2264
|
function applyAlignment(kind: MarkdownTextAlignKind): void {
|
|
2265
|
+
if (!syncSelectionForToolbarAction()) {
|
|
2266
|
+
return
|
|
2267
|
+
}
|
|
2268
|
+
|
|
1432
2269
|
onapplyalignment?.(kind)
|
|
1433
2270
|
activeSelectionMenu = null
|
|
1434
2271
|
activeSelectionMenuTrigger = null
|
|
1435
2272
|
queueSelectionRefresh()
|
|
1436
2273
|
}
|
|
1437
2274
|
|
|
2275
|
+
/**
|
|
2276
|
+
* 链接菜单提交时总是以恢复后的正文选区为准,避免输入框焦点把目标文本丢掉。
|
|
2277
|
+
*/
|
|
1438
2278
|
function applyLink(): void {
|
|
1439
|
-
|
|
2279
|
+
if (!syncSelectionForToolbarAction()) {
|
|
2280
|
+
return
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
onapplylink?.(
|
|
2284
|
+
linkDraft.trim() || null,
|
|
2285
|
+
savedSelectionSnapshot
|
|
2286
|
+
? {
|
|
2287
|
+
...savedSelectionSnapshot,
|
|
2288
|
+
}
|
|
2289
|
+
: undefined,
|
|
2290
|
+
selectedText.trim() || undefined,
|
|
2291
|
+
)
|
|
1440
2292
|
activeSelectionMenu = null
|
|
1441
2293
|
activeSelectionMenuTrigger = null
|
|
1442
2294
|
queueSelectionRefresh()
|
|
1443
2295
|
}
|
|
1444
2296
|
|
|
1445
2297
|
function removeLink(): void {
|
|
1446
|
-
|
|
2298
|
+
if (!syncSelectionForToolbarAction()) {
|
|
2299
|
+
return
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
onapplylink?.(
|
|
2303
|
+
null,
|
|
2304
|
+
savedSelectionSnapshot
|
|
2305
|
+
? {
|
|
2306
|
+
...savedSelectionSnapshot,
|
|
2307
|
+
}
|
|
2308
|
+
: undefined,
|
|
2309
|
+
selectedText.trim() || undefined,
|
|
2310
|
+
)
|
|
1447
2311
|
activeSelectionMenu = null
|
|
1448
2312
|
activeSelectionMenuTrigger = null
|
|
1449
2313
|
queueSelectionRefresh()
|
|
1450
2314
|
}
|
|
1451
2315
|
|
|
2316
|
+
/**
|
|
2317
|
+
* 颜色动作除了颜色值,还会把当前选中文本一起带给外层,供宿主侧做兜底选区恢复。
|
|
2318
|
+
*/
|
|
1452
2319
|
function applyColor(request: MarkdownColorFormatRequest): void {
|
|
1453
|
-
|
|
2320
|
+
if (!syncSelectionForToolbarAction()) {
|
|
2321
|
+
return
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
onapplycolor?.({
|
|
2325
|
+
...request,
|
|
2326
|
+
selectedText: selectedText.trim() || undefined,
|
|
2327
|
+
selectionSnapshot: savedSelectionSnapshot
|
|
2328
|
+
? {
|
|
2329
|
+
...savedSelectionSnapshot,
|
|
2330
|
+
}
|
|
2331
|
+
: undefined,
|
|
2332
|
+
})
|
|
1454
2333
|
queueSelectionRefresh()
|
|
1455
2334
|
}
|
|
1456
2335
|
|
|
1457
2336
|
async function copySelection(): Promise<void> {
|
|
2337
|
+
if (!syncSelectionForToolbarAction()) {
|
|
2338
|
+
return
|
|
2339
|
+
}
|
|
2340
|
+
|
|
1458
2341
|
await oncopyselection?.()
|
|
1459
2342
|
activeSelectionMenu = null
|
|
1460
2343
|
activeSelectionMenuTrigger = null
|
|
1461
2344
|
}
|
|
1462
2345
|
|
|
1463
2346
|
async function annotateSelection(): Promise<void> {
|
|
2347
|
+
if (!syncSelectionForToolbarAction()) {
|
|
2348
|
+
return
|
|
2349
|
+
}
|
|
2350
|
+
|
|
1464
2351
|
await onannotation?.()
|
|
1465
2352
|
activeSelectionMenu = null
|
|
1466
2353
|
activeSelectionMenuTrigger = null
|
|
1467
2354
|
}
|
|
1468
2355
|
|
|
2356
|
+
function handlePreviewMouseMove(event: MouseEvent): void {
|
|
2357
|
+
const target = event.target
|
|
2358
|
+
const link = target instanceof Element
|
|
2359
|
+
? target.closest<HTMLAnchorElement>('a[href]')
|
|
2360
|
+
: null
|
|
2361
|
+
|
|
2362
|
+
updateHoveredLink(link && previewHost?.contains(link) ? link : null)
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
function handlePreviewMouseLeave(): void {
|
|
2366
|
+
scheduleHoveredLinkHide()
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
/**
|
|
2370
|
+
* mouseup 比 selectionchange 更接近用户“完成一次选中”的时机,适合触发工具条刷新。
|
|
2371
|
+
*/
|
|
1469
2372
|
function handlePreviewMouseUp(): void {
|
|
1470
2373
|
queueSelectionRefresh()
|
|
1471
2374
|
onpreviewmouseup?.()
|
|
1472
2375
|
}
|
|
1473
2376
|
|
|
2377
|
+
/**
|
|
2378
|
+
* 输入事件本身只透传给宿主;选区状态交给 selectionchange/mouseup 统一管理。
|
|
2379
|
+
*/
|
|
1474
2380
|
function handlePreviewInput(event: Event): void {
|
|
1475
2381
|
onpreviewinput?.(event)
|
|
1476
2382
|
}
|
|
1477
2383
|
|
|
1478
|
-
|
|
2384
|
+
/**
|
|
2385
|
+
* 正文失焦时先观察焦点是否转移到工具条内部,避免点击链接输入框时误触发提交或收起。
|
|
2386
|
+
*/
|
|
2387
|
+
function handlePreviewBlur(event: FocusEvent): void {
|
|
1479
2388
|
queueSelectionRefresh(true)
|
|
2389
|
+
|
|
2390
|
+
const nextTarget = event.relatedTarget
|
|
2391
|
+
if (nextTarget instanceof Node && selectionChromeEl?.contains(nextTarget)) {
|
|
2392
|
+
return
|
|
2393
|
+
}
|
|
2394
|
+
|
|
1480
2395
|
onpreviewblur?.()
|
|
1481
2396
|
}
|
|
1482
2397
|
</script>
|
|
@@ -1625,7 +2540,7 @@ ${safeCode}
|
|
|
1625
2540
|
class='hai-ai-doc-layout'
|
|
1626
2541
|
>
|
|
1627
2542
|
{#if showOutline && !outlineCollapsed}
|
|
1628
|
-
<aside class='hai-ai-doc-outline'>
|
|
2543
|
+
<aside bind:this={outlinePanelEl} class='hai-ai-doc-outline'>
|
|
1629
2544
|
<div class='hai-ai-doc-outline-head'>
|
|
1630
2545
|
<strong>{uiM('markdown_outline')}</strong>
|
|
1631
2546
|
|
|
@@ -1654,7 +2569,7 @@ ${safeCode}
|
|
|
1654
2569
|
style={`padding-left:${0.65 + (item.level - 1) * 0.56}rem`}
|
|
1655
2570
|
onclick={() => scrollToHeading(item.id)}
|
|
1656
2571
|
>
|
|
1657
|
-
{item.numberedTitle}
|
|
2572
|
+
{showOutlineNumbering ? item.numberedTitle : item.text}
|
|
1658
2573
|
</button>
|
|
1659
2574
|
{/each}
|
|
1660
2575
|
</nav>
|
|
@@ -1681,7 +2596,10 @@ ${safeCode}
|
|
|
1681
2596
|
style={`top:${toolbarPosition.top}px; left:${toolbarPosition.left}px;`}
|
|
1682
2597
|
>
|
|
1683
2598
|
<div
|
|
2599
|
+
bind:this={selectionChromeEl}
|
|
1684
2600
|
class='hai-ai-doc-selection-chrome'
|
|
2601
|
+
onmouseenter={clearPendingSelectionToolbarClose}
|
|
2602
|
+
onmouseleave={() => scheduleSelectionToolbarClose()}
|
|
1685
2603
|
>
|
|
1686
2604
|
<div
|
|
1687
2605
|
bind:this={selectionToolbarEl}
|
|
@@ -1845,7 +2763,7 @@ ${safeCode}
|
|
|
1845
2763
|
title={uiM('markdown_format_strike')}
|
|
1846
2764
|
onclick={() => applyInlineFormat('strike')}
|
|
1847
2765
|
>
|
|
1848
|
-
S
|
|
2766
|
+
<s>S</s>
|
|
1849
2767
|
</button>
|
|
1850
2768
|
<button
|
|
1851
2769
|
type='button'
|
|
@@ -1871,12 +2789,13 @@ ${safeCode}
|
|
|
1871
2789
|
title={uiM('markdown_format_underline')}
|
|
1872
2790
|
onclick={() => applyInlineFormat('underline')}
|
|
1873
2791
|
>
|
|
1874
|
-
U
|
|
2792
|
+
<u>U</u>
|
|
1875
2793
|
</button>
|
|
1876
2794
|
{/if}
|
|
1877
2795
|
|
|
1878
2796
|
{#if onapplylink || onapplyinlineformat}
|
|
1879
2797
|
<button
|
|
2798
|
+
bind:this={selectionLinkButtonEl}
|
|
1880
2799
|
type='button'
|
|
1881
2800
|
class={cn(
|
|
1882
2801
|
'hai-ai-doc-selection-btn',
|
|
@@ -1893,10 +2812,18 @@ ${safeCode}
|
|
|
1893
2812
|
? toggleSelectionMenu('link', event)
|
|
1894
2813
|
: applyInlineFormat('link')}
|
|
1895
2814
|
>
|
|
1896
|
-
<svg
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
2815
|
+
<svg
|
|
2816
|
+
viewBox='0 0 24 24'
|
|
2817
|
+
aria-hidden='true'
|
|
2818
|
+
fill='none'
|
|
2819
|
+
stroke='currentColor'
|
|
2820
|
+
stroke-width='2'
|
|
2821
|
+
stroke-linecap='round'
|
|
2822
|
+
stroke-linejoin='round'
|
|
2823
|
+
>
|
|
2824
|
+
<path d='M9.5 14.5 14.5 9.5'></path>
|
|
2825
|
+
<path d='M7.6 16.4 6.3 17.7a3 3 0 1 1-4.24-4.24l3-3a3 3 0 0 1 4.24 0'></path>
|
|
2826
|
+
<path d='M16.4 7.6 17.7 6.3a3 3 0 1 1 4.24 4.24l-3 3a3 3 0 0 1-4.24 0'></path>
|
|
1900
2827
|
</svg>
|
|
1901
2828
|
</button>
|
|
1902
2829
|
{/if}
|
|
@@ -1915,19 +2842,6 @@ ${safeCode}
|
|
|
1915
2842
|
>
|
|
1916
2843
|
</>
|
|
1917
2844
|
</button>
|
|
1918
|
-
<button
|
|
1919
|
-
type='button'
|
|
1920
|
-
class={cn(
|
|
1921
|
-
'hai-ai-doc-selection-btn',
|
|
1922
|
-
selectionFormatState.highlight
|
|
1923
|
-
? 'hai-ai-doc-selection-btn--active'
|
|
1924
|
-
: '',
|
|
1925
|
-
)}
|
|
1926
|
-
title={uiM('markdown_format_highlight')}
|
|
1927
|
-
onclick={() => applyInlineFormat('highlight')}
|
|
1928
|
-
>
|
|
1929
|
-
<span class='hai-ai-doc-selection-highlight-icon'>A</span>
|
|
1930
|
-
</button>
|
|
1931
2845
|
{/if}
|
|
1932
2846
|
|
|
1933
2847
|
{#if onapplycolor}
|
|
@@ -2001,7 +2915,7 @@ ${safeCode}
|
|
|
2001
2915
|
role='menu'
|
|
2002
2916
|
tabindex='-1'
|
|
2003
2917
|
style={`--hai-ai-doc-selection-menu-left:${selectionMenuLeft}px;`}
|
|
2004
|
-
onmousedown={
|
|
2918
|
+
onmousedown={handleSelectionPanelMouseDown}
|
|
2005
2919
|
>
|
|
2006
2920
|
{#each resolvedRewriteActions as action}
|
|
2007
2921
|
<button
|
|
@@ -2021,7 +2935,7 @@ ${safeCode}
|
|
|
2021
2935
|
role='menu'
|
|
2022
2936
|
tabindex='-1'
|
|
2023
2937
|
style={`--hai-ai-doc-selection-menu-left:${selectionMenuLeft}px;`}
|
|
2024
|
-
onmousedown={
|
|
2938
|
+
onmousedown={handleSelectionPanelMouseDown}
|
|
2025
2939
|
>
|
|
2026
2940
|
{#each BLOCK_FORMAT_OPTIONS as option}
|
|
2027
2941
|
<button
|
|
@@ -2053,7 +2967,7 @@ ${safeCode}
|
|
|
2053
2967
|
role='menu'
|
|
2054
2968
|
tabindex='-1'
|
|
2055
2969
|
style={`--hai-ai-doc-selection-menu-left:${selectionMenuLeft}px;`}
|
|
2056
|
-
onmousedown={
|
|
2970
|
+
onmousedown={handleSelectionPanelMouseDown}
|
|
2057
2971
|
>
|
|
2058
2972
|
{#each ALIGN_OPTIONS as option}
|
|
2059
2973
|
<button
|
|
@@ -2081,9 +2995,10 @@ ${safeCode}
|
|
|
2081
2995
|
aria-label={uiM('markdown_link_dialog_label')}
|
|
2082
2996
|
tabindex='-1'
|
|
2083
2997
|
style={`--hai-ai-doc-selection-menu-left:${selectionMenuLeft}px;`}
|
|
2084
|
-
onmousedown={
|
|
2998
|
+
onmousedown={handleSelectionPanelMouseDown}
|
|
2085
2999
|
>
|
|
2086
3000
|
<input
|
|
3001
|
+
bind:this={linkInputEl}
|
|
2087
3002
|
class='hai-ai-doc-selection-input'
|
|
2088
3003
|
bind:value={linkDraft}
|
|
2089
3004
|
aria-label={uiM('markdown_link_input_label')}
|
|
@@ -2121,7 +3036,7 @@ ${safeCode}
|
|
|
2121
3036
|
aria-label={uiM('markdown_color_dialog_label')}
|
|
2122
3037
|
tabindex='-1'
|
|
2123
3038
|
style={`--hai-ai-doc-selection-menu-left:${selectionMenuLeft}px;`}
|
|
2124
|
-
onmousedown={
|
|
3039
|
+
onmousedown={handleSelectionPanelMouseDown}
|
|
2125
3040
|
>
|
|
2126
3041
|
<div class='hai-ai-doc-color-group'>
|
|
2127
3042
|
<div class='hai-ai-doc-color-group-title'>
|
|
@@ -2151,23 +3066,34 @@ ${safeCode}
|
|
|
2151
3066
|
{uiM('markdown_background_color')}
|
|
2152
3067
|
</div>
|
|
2153
3068
|
<div class='hai-ai-doc-color-grid'>
|
|
2154
|
-
{#each
|
|
3069
|
+
{#each BACKGROUND_COLOR_OPTIONS as colorOption}
|
|
2155
3070
|
<button
|
|
2156
3071
|
type='button'
|
|
2157
3072
|
class={cn(
|
|
2158
3073
|
'hai-ai-doc-color-swatch',
|
|
2159
|
-
selectionFormatState.backgroundColor ===
|
|
3074
|
+
selectionFormatState.backgroundColor === resolveColorOptionValue(colorOption)
|
|
2160
3075
|
? 'hai-ai-doc-color-swatch--active'
|
|
2161
3076
|
: '',
|
|
2162
3077
|
)}
|
|
2163
|
-
|
|
3078
|
+
data-empty={typeof colorOption === 'string'
|
|
3079
|
+
? undefined
|
|
3080
|
+
: 'true'}
|
|
3081
|
+
title={typeof colorOption === 'string'
|
|
3082
|
+
? colorOption
|
|
3083
|
+
: uiM(colorOption.titleKey)}
|
|
2164
3084
|
onclick={() =>
|
|
2165
3085
|
applyColor({
|
|
2166
3086
|
target: 'background',
|
|
2167
|
-
value:
|
|
3087
|
+
value: typeof colorOption === 'string'
|
|
3088
|
+
? colorOption
|
|
3089
|
+
: colorOption.value,
|
|
2168
3090
|
})}
|
|
2169
3091
|
>
|
|
2170
|
-
<span
|
|
3092
|
+
<span
|
|
3093
|
+
style={typeof colorOption === 'string'
|
|
3094
|
+
? `background:${colorOption};`
|
|
3095
|
+
: undefined}
|
|
3096
|
+
></span>
|
|
2171
3097
|
</button>
|
|
2172
3098
|
{/each}
|
|
2173
3099
|
</div>
|
|
@@ -2188,11 +3114,87 @@ ${safeCode}
|
|
|
2188
3114
|
</div>
|
|
2189
3115
|
{/if}
|
|
2190
3116
|
|
|
3117
|
+
{#if hoveredLinkState}
|
|
3118
|
+
<div
|
|
3119
|
+
bind:this={hoveredLinkPopoverEl}
|
|
3120
|
+
class='hai-ai-doc-link-hover'
|
|
3121
|
+
data-placement={hoveredLinkState.placement}
|
|
3122
|
+
style={`top:${hoveredLinkState.top}px; left:${hoveredLinkState.left}px;`}
|
|
3123
|
+
onmouseenter={clearPendingHoveredLinkHide}
|
|
3124
|
+
onmouseleave={handlePreviewMouseLeave}
|
|
3125
|
+
>
|
|
3126
|
+
<button
|
|
3127
|
+
type='button'
|
|
3128
|
+
class='hai-ai-doc-link-hover-btn'
|
|
3129
|
+
title={uiM('markdown_link_open')}
|
|
3130
|
+
aria-label={uiM('markdown_link_open')}
|
|
3131
|
+
onmousedown={event => event.preventDefault()}
|
|
3132
|
+
onclick={openHoveredLink}
|
|
3133
|
+
>
|
|
3134
|
+
<svg viewBox='0 0 24 24' aria-hidden='true'>
|
|
3135
|
+
<path
|
|
3136
|
+
d='M13.75 5.75h4.5v4.5'
|
|
3137
|
+
fill='none'
|
|
3138
|
+
stroke='currentColor'
|
|
3139
|
+
stroke-width='1.9'
|
|
3140
|
+
stroke-linecap='round'
|
|
3141
|
+
stroke-linejoin='round'
|
|
3142
|
+
></path>
|
|
3143
|
+
<path
|
|
3144
|
+
d='M18 6 11 13'
|
|
3145
|
+
fill='none'
|
|
3146
|
+
stroke='currentColor'
|
|
3147
|
+
stroke-width='1.9'
|
|
3148
|
+
stroke-linecap='round'
|
|
3149
|
+
stroke-linejoin='round'
|
|
3150
|
+
></path>
|
|
3151
|
+
<path
|
|
3152
|
+
d='M10.25 7.75h-2.5A2.5 2.5 0 0 0 5.25 10.25v6A2.5 2.5 0 0 0 7.75 18.75h6a2.5 2.5 0 0 0 2.5-2.5v-2.5'
|
|
3153
|
+
fill='none'
|
|
3154
|
+
stroke='currentColor'
|
|
3155
|
+
stroke-width='1.9'
|
|
3156
|
+
stroke-linecap='round'
|
|
3157
|
+
stroke-linejoin='round'
|
|
3158
|
+
></path>
|
|
3159
|
+
</svg>
|
|
3160
|
+
</button>
|
|
3161
|
+
<button
|
|
3162
|
+
type='button'
|
|
3163
|
+
class='hai-ai-doc-link-hover-btn'
|
|
3164
|
+
title={uiM('markdown_link_edit')}
|
|
3165
|
+
aria-label={uiM('markdown_link_edit')}
|
|
3166
|
+
onmousedown={event => event.preventDefault()}
|
|
3167
|
+
onclick={() => void editHoveredLink()}
|
|
3168
|
+
>
|
|
3169
|
+
<svg viewBox='0 0 24 24' aria-hidden='true'>
|
|
3170
|
+
<path
|
|
3171
|
+
d='M4.75 16.88V19.25h2.37l9.96-9.96-2.37-2.37-9.96 9.96Z'
|
|
3172
|
+
fill='none'
|
|
3173
|
+
stroke='currentColor'
|
|
3174
|
+
stroke-width='1.9'
|
|
3175
|
+
stroke-linecap='round'
|
|
3176
|
+
stroke-linejoin='round'
|
|
3177
|
+
></path>
|
|
3178
|
+
<path
|
|
3179
|
+
d='M13.96 7.92 16.33 5.55a1.68 1.68 0 0 1 2.37 2.37l-2.37 2.37'
|
|
3180
|
+
fill='none'
|
|
3181
|
+
stroke='currentColor'
|
|
3182
|
+
stroke-width='1.9'
|
|
3183
|
+
stroke-linecap='round'
|
|
3184
|
+
stroke-linejoin='round'
|
|
3185
|
+
></path>
|
|
3186
|
+
</svg>
|
|
3187
|
+
</button>
|
|
3188
|
+
</div>
|
|
3189
|
+
{/if}
|
|
3190
|
+
|
|
2191
3191
|
<article
|
|
2192
3192
|
bind:this={previewHost}
|
|
2193
3193
|
class={readerDocumentClass}
|
|
2194
3194
|
contenteditable={editable}
|
|
2195
3195
|
role='document'
|
|
3196
|
+
onmousemove={handlePreviewMouseMove}
|
|
3197
|
+
onmouseleave={handlePreviewMouseLeave}
|
|
2196
3198
|
onmouseup={handlePreviewMouseUp}
|
|
2197
3199
|
oninput={handlePreviewInput}
|
|
2198
3200
|
onblur={handlePreviewBlur}
|
|
@@ -2576,21 +3578,17 @@ ${safeCode}
|
|
|
2576
3578
|
display: block;
|
|
2577
3579
|
width: fit-content;
|
|
2578
3580
|
max-width: calc(100% - 2rem);
|
|
2579
|
-
transform: translateX(-50%);
|
|
2580
3581
|
transition:
|
|
2581
3582
|
top 0.14s cubic-bezier(0.2, 0.85, 0.24, 1),
|
|
2582
|
-
left 0.14s cubic-bezier(0.2, 0.85, 0.24, 1)
|
|
2583
|
-
transform 0.14s cubic-bezier(0.2, 0.85, 0.24, 1);
|
|
3583
|
+
left 0.14s cubic-bezier(0.2, 0.85, 0.24, 1);
|
|
2584
3584
|
pointer-events: none;
|
|
2585
3585
|
}
|
|
2586
3586
|
|
|
2587
3587
|
.hai-ai-doc-selection-layer[data-alignment='left'] {
|
|
2588
|
-
transform: translateX(0);
|
|
2589
3588
|
justify-items: start;
|
|
2590
3589
|
}
|
|
2591
3590
|
|
|
2592
3591
|
.hai-ai-doc-selection-layer[data-alignment='right'] {
|
|
2593
|
-
transform: translateX(-100%);
|
|
2594
3592
|
justify-items: end;
|
|
2595
3593
|
}
|
|
2596
3594
|
|
|
@@ -2605,6 +3603,55 @@ ${safeCode}
|
|
|
2605
3603
|
max-width: min(100%, calc(100vw - 2rem));
|
|
2606
3604
|
}
|
|
2607
3605
|
|
|
3606
|
+
.hai-ai-doc-link-hover {
|
|
3607
|
+
position: absolute;
|
|
3608
|
+
z-index: 58;
|
|
3609
|
+
display: inline-flex;
|
|
3610
|
+
align-items: center;
|
|
3611
|
+
gap: 0.18rem;
|
|
3612
|
+
padding: 0.22rem;
|
|
3613
|
+
border-radius: 9999px;
|
|
3614
|
+
background: color-mix(in srgb, oklch(var(--b1, 1 0 0)) 94%, white 6%);
|
|
3615
|
+
border: 1px solid color-mix(in srgb, oklch(var(--bc, 0.22 0 0)) 12%, white 88%);
|
|
3616
|
+
box-shadow:
|
|
3617
|
+
0 20px 36px -26px rgb(15 23 42 / 0.26),
|
|
3618
|
+
0 10px 18px -16px rgb(15 23 42 / 0.18);
|
|
3619
|
+
pointer-events: auto;
|
|
3620
|
+
transition:
|
|
3621
|
+
top 0.14s cubic-bezier(0.2, 0.85, 0.24, 1),
|
|
3622
|
+
left 0.14s cubic-bezier(0.2, 0.85, 0.24, 1);
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
.hai-ai-doc-link-hover-btn {
|
|
3626
|
+
width: 2rem;
|
|
3627
|
+
height: 2rem;
|
|
3628
|
+
display: inline-flex;
|
|
3629
|
+
align-items: center;
|
|
3630
|
+
justify-content: center;
|
|
3631
|
+
border: none;
|
|
3632
|
+
border-radius: 9999px;
|
|
3633
|
+
background: transparent;
|
|
3634
|
+
color: oklch(var(--bc) / 0.76);
|
|
3635
|
+
cursor: pointer;
|
|
3636
|
+
transition:
|
|
3637
|
+
background-color 0.14s ease,
|
|
3638
|
+
color 0.14s ease,
|
|
3639
|
+
transform 0.14s ease;
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3642
|
+
.hai-ai-doc-link-hover-btn:hover {
|
|
3643
|
+
background: oklch(var(--bc) / 0.06);
|
|
3644
|
+
color: oklch(var(--bc));
|
|
3645
|
+
transform: translateY(-1px);
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
.hai-ai-doc-link-hover-btn svg {
|
|
3649
|
+
width: 1rem;
|
|
3650
|
+
height: 1rem;
|
|
3651
|
+
fill: none;
|
|
3652
|
+
stroke: currentColor;
|
|
3653
|
+
}
|
|
3654
|
+
|
|
2608
3655
|
.hai-ai-doc-selection-toolbar {
|
|
2609
3656
|
display: flex;
|
|
2610
3657
|
align-items: center;
|
|
@@ -2973,6 +4020,20 @@ ${safeCode}
|
|
|
2973
4020
|
border: 1px solid rgb(15 23 42 / 0.08);
|
|
2974
4021
|
}
|
|
2975
4022
|
|
|
4023
|
+
.hai-ai-doc-color-swatch[data-empty='true'] span {
|
|
4024
|
+
position: relative;
|
|
4025
|
+
border-color: oklch(var(--bc) / 0.18);
|
|
4026
|
+
background:
|
|
4027
|
+
linear-gradient(
|
|
4028
|
+
135deg,
|
|
4029
|
+
transparent calc(50% - 0.75px),
|
|
4030
|
+
oklch(var(--bc) / 0.34) calc(50% - 0.75px),
|
|
4031
|
+
oklch(var(--bc) / 0.34) calc(50% + 0.75px),
|
|
4032
|
+
transparent calc(50% + 0.75px)
|
|
4033
|
+
),
|
|
4034
|
+
color-mix(in srgb, oklch(var(--b1)) 96%, white 4%);
|
|
4035
|
+
}
|
|
4036
|
+
|
|
2976
4037
|
.hai-ai-doc-rewrite-menu-btn {
|
|
2977
4038
|
justify-content: center;
|
|
2978
4039
|
}
|
|
@@ -3082,6 +4143,10 @@ ${safeCode}
|
|
|
3082
4143
|
font-weight: 500;
|
|
3083
4144
|
}
|
|
3084
4145
|
|
|
4146
|
+
.hai-markdown-editable :global(a[href]) {
|
|
4147
|
+
cursor: pointer;
|
|
4148
|
+
}
|
|
4149
|
+
|
|
3085
4150
|
.hai-markdown :global(a:hover) {
|
|
3086
4151
|
text-decoration: underline;
|
|
3087
4152
|
}
|