mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(editor): open the link that received the modifier-click (#35071)
* fix(editor): open the link that received the modifier-click * fix(editor): read clicked link href from its mark, not the sanitized DOM Tiptap blanks rendered hrefs for file:, Windows drive paths and custom schemes; resolve the clicked anchor's document position and read the link mark instead. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { Editor } from '@tiptap/react'
|
||||
import { Editor, type JSONContent } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { handleRichMarkdownEditorClick } from './rich-markdown-editor-click-routing'
|
||||
@@ -71,26 +71,40 @@ function clickLink({
|
||||
isMac = true,
|
||||
shiftKey = true,
|
||||
modKey = true,
|
||||
href = 'https://example.com/docs'
|
||||
href = 'https://example.com/docs',
|
||||
content,
|
||||
pos = 2,
|
||||
targetSelector = 'a'
|
||||
}: {
|
||||
sourceOwner?: HttpLinkSourceOwner
|
||||
isMac?: boolean
|
||||
shiftKey?: boolean
|
||||
modKey?: boolean
|
||||
href?: string
|
||||
content?: string | JSONContent
|
||||
pos?: number
|
||||
targetSelector?: string
|
||||
} = {}): boolean {
|
||||
editor = new Editor({
|
||||
extensions: [StarterKit],
|
||||
content: `<p><a href="${href}">example</a></p>`
|
||||
content: content ?? `<p><a href="${href}">example</a></p>`
|
||||
})
|
||||
const event = new MouseEvent('click', {
|
||||
metaKey: isMac && modKey,
|
||||
ctrlKey: !isMac && modKey,
|
||||
shiftKey,
|
||||
cancelable: true
|
||||
})
|
||||
event.preventDefault()
|
||||
const target = editor.view.dom.querySelector(targetSelector)
|
||||
if (!target) {
|
||||
throw new Error(`Missing click target: ${targetSelector}`)
|
||||
}
|
||||
target.dispatchEvent(event)
|
||||
return handleRichMarkdownEditorClick({
|
||||
activateMarkdownLink,
|
||||
editorRef: { current: editor },
|
||||
event: new MouseEvent('click', {
|
||||
metaKey: isMac && modKey,
|
||||
ctrlKey: !isMac && modKey,
|
||||
shiftKey
|
||||
}),
|
||||
event,
|
||||
filePath: '/repo/docs/README.md',
|
||||
isMac,
|
||||
htmlSuperscriptLinkContext: createRichMarkdownHtmlSuperscriptLinkContext({
|
||||
@@ -102,7 +116,7 @@ function clickLink({
|
||||
markdownCommentsRef: { current: [] },
|
||||
markdownSourceLineOffsetRef: { current: 0 },
|
||||
onOpenDocLinkRef: { current: undefined },
|
||||
pos: 2,
|
||||
pos,
|
||||
rootRef: { current: null },
|
||||
scrollRichMarkdownReviewNoteCardIntoView: vi.fn(),
|
||||
view: editor.view,
|
||||
@@ -223,3 +237,75 @@ describe('rich Markdown alternate browser click', () => {
|
||||
expect(openFileUri).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('rich Markdown link click targets', () => {
|
||||
it('opens the first character after plain text', () => {
|
||||
expect(
|
||||
clickLink({
|
||||
shiftKey: false,
|
||||
content: '<p>plain <a href="child.md">X</a> tail</p>',
|
||||
pos: 7
|
||||
})
|
||||
).toBe(true)
|
||||
expect(activateMarkdownLink).toHaveBeenCalledWith('child.md', expect.any(Object))
|
||||
})
|
||||
|
||||
it('opens the clicked adjacent link at their shared text position', () => {
|
||||
expect(
|
||||
clickLink({
|
||||
shiftKey: false,
|
||||
content: '<p><a href="first.md">A</a><a href="second.md">B</a></p>',
|
||||
targetSelector: 'a:last-child',
|
||||
pos: 2
|
||||
})
|
||||
).toBe(true)
|
||||
expect(activateMarkdownLink).toHaveBeenCalledWith('second.md', expect.any(Object))
|
||||
})
|
||||
|
||||
it('opens a link from nested formatting at its leading boundary', () => {
|
||||
expect(
|
||||
clickLink({
|
||||
shiftKey: false,
|
||||
content: '<p>plain <a href="child.md"><strong>X</strong></a></p>',
|
||||
targetSelector: 'strong',
|
||||
pos: 7
|
||||
})
|
||||
).toBe(true)
|
||||
expect(activateMarkdownLink).toHaveBeenCalledWith('child.md', expect.any(Object))
|
||||
})
|
||||
|
||||
it('keeps neighboring prose inert even when its caret position has a link mark', () => {
|
||||
expect(
|
||||
clickLink({
|
||||
shiftKey: false,
|
||||
content: '<p><a href="child.md">X</a><em>tail</em></p>',
|
||||
targetSelector: 'em',
|
||||
pos: 2
|
||||
})
|
||||
).toBe(false)
|
||||
expect(activateMarkdownLink).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each(['file:///repo/docs/child.md', 'C:\\repo\\docs\\child.md'])(
|
||||
'opens %s even though the rendered anchor drops its href',
|
||||
(href) => {
|
||||
expect(
|
||||
clickLink({
|
||||
shiftKey: false,
|
||||
content: {
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{ type: 'text', text: 'example', marks: [{ type: 'link', attrs: { href } }] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
).toBe(true)
|
||||
expect(activateMarkdownLink).toHaveBeenCalledWith(href, expect.any(Object))
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -114,7 +114,7 @@ export function handleRichMarkdownEditorClick({
|
||||
const href =
|
||||
clickedNode?.type.name === 'richMarkdownHtmlSuperscriptLink'
|
||||
? String(clickedNode.attrs.href ?? '')
|
||||
: getClickedLinkHref(view, pos)
|
||||
: getClickedLinkHref(view, event)
|
||||
if (
|
||||
clickedNode?.type.name === 'richMarkdownHtmlSuperscriptLink' &&
|
||||
!classifyHtmlSuperscriptLinkAction(href, sourceSnapshot)
|
||||
@@ -179,12 +179,26 @@ function activateMarkdownImageClick({
|
||||
return true
|
||||
}
|
||||
|
||||
function getClickedLinkHref(view: EditorView, pos: number): string {
|
||||
const linkMark = view.state.doc
|
||||
.resolve(pos)
|
||||
.marks()
|
||||
.find((mark) => mark.type.name === 'link')
|
||||
return linkMark ? (linkMark.attrs.href as string) || '' : ''
|
||||
function getClickedLinkHref(view: EditorView, event: MouseEvent): string {
|
||||
const target =
|
||||
event.target instanceof Element
|
||||
? event.target
|
||||
: event.target instanceof Node
|
||||
? event.target.parentElement
|
||||
: null
|
||||
const link = target?.closest('a')
|
||||
if (!link || !view.dom.contains(link)) {
|
||||
return ''
|
||||
}
|
||||
// The rendered href is blanked for file:, drive-path and custom schemes, so read the mark instead.
|
||||
let linkPos: number
|
||||
try {
|
||||
linkPos = view.posAtDOM(link, 0)
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
const linkMark = view.state.doc.nodeAt(linkPos)?.marks.find((mark) => mark.type.name === 'link')
|
||||
return linkMark ? String(linkMark.attrs.href ?? '') : ''
|
||||
}
|
||||
|
||||
function openShiftModifiedMarkdownLink({
|
||||
|
||||
Reference in New Issue
Block a user