2026-04-08 19:06:21 +02:00
|
|
|
import { useEffect, type RefObject } from 'react'
|
2026-05-07 00:55:07 +02:00
|
|
|
import { openEditorAttachmentOrUrl } from './editorAttachmentActions'
|
2026-04-08 19:06:21 +02:00
|
|
|
|
|
|
|
|
const CODE_CONTEXT_SELECTOR = '[data-content-type="codeBlock"], pre, code'
|
|
|
|
|
|
|
|
|
|
function hasFollowModifier(event: KeyboardEvent | MouseEvent) {
|
|
|
|
|
return event.metaKey || event.ctrlKey
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isInsideCodeContext(target: HTMLElement) {
|
|
|
|
|
return !!target.closest(CODE_CONTEXT_SELECTOR)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-08 16:46:22 +02:00
|
|
|
function elementFromEventTarget(target: EventTarget | null) {
|
|
|
|
|
if (target instanceof HTMLElement) return target
|
|
|
|
|
if (target instanceof Text) return target.parentElement
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 19:06:21 +02:00
|
|
|
function resolveWikilinkTarget(target: HTMLElement) {
|
|
|
|
|
return target.closest<HTMLElement>('.wikilink[data-target]')?.dataset.target ?? null
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-26 02:16:20 +02:00
|
|
|
function resolveAnchorHref(target: HTMLElement) {
|
|
|
|
|
return target.closest<HTMLAnchorElement>('a[href]')?.getAttribute('href')?.trim() ?? null
|
2026-04-08 19:06:21 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-24 21:15:27 +02:00
|
|
|
function blurActiveEditable(container: HTMLElement) {
|
|
|
|
|
const active = document.activeElement
|
|
|
|
|
if (!(active instanceof HTMLElement) || !container.contains(active)) return
|
|
|
|
|
const editable = active.isContentEditable ? active : active.closest<HTMLElement>('[contenteditable="true"]')
|
|
|
|
|
editable?.blur()
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 19:06:21 +02:00
|
|
|
function setFollowLinksActive(container: HTMLElement, active: boolean) {
|
|
|
|
|
if (active) container.setAttribute('data-follow-links', '')
|
|
|
|
|
else container.removeAttribute('data-follow-links')
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 13:43:48 +02:00
|
|
|
function consumeEditorLinkEvent(event: MouseEvent) {
|
2026-04-26 02:16:20 +02:00
|
|
|
event.preventDefault()
|
|
|
|
|
event.stopPropagation()
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 13:43:48 +02:00
|
|
|
function scheduleAfterNativeClick(callback: () => void) {
|
|
|
|
|
if (typeof queueMicrotask === 'function') queueMicrotask(callback)
|
|
|
|
|
else window.setTimeout(callback, 0)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-26 02:16:20 +02:00
|
|
|
function activateWikilink(
|
|
|
|
|
event: MouseEvent,
|
|
|
|
|
container: HTMLElement,
|
|
|
|
|
target: string,
|
|
|
|
|
onNavigateWikilink: (target: string) => void,
|
|
|
|
|
) {
|
2026-05-05 13:43:48 +02:00
|
|
|
consumeEditorLinkEvent(event)
|
|
|
|
|
|
2026-04-26 02:16:20 +02:00
|
|
|
if (!hasFollowModifier(event)) return
|
|
|
|
|
|
|
|
|
|
blurActiveEditable(container)
|
2026-05-05 13:43:48 +02:00
|
|
|
scheduleAfterNativeClick(() => onNavigateWikilink(target))
|
2026-04-26 02:16:20 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-07 00:55:07 +02:00
|
|
|
function activateUrl(event: MouseEvent, href: string, vaultPath?: string) {
|
2026-05-05 13:43:48 +02:00
|
|
|
consumeEditorLinkEvent(event)
|
2026-04-26 02:16:20 +02:00
|
|
|
|
|
|
|
|
if (!hasFollowModifier(event)) return
|
|
|
|
|
|
2026-05-07 00:55:07 +02:00
|
|
|
openEditorAttachmentOrUrl({ url: href, vaultPath, source: 'link' })
|
2026-04-26 02:16:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleEditorLinkClick(
|
|
|
|
|
event: MouseEvent,
|
|
|
|
|
container: HTMLElement,
|
|
|
|
|
onNavigateWikilink: (target: string) => void,
|
2026-05-07 00:55:07 +02:00
|
|
|
vaultPath?: string,
|
2026-04-26 02:16:20 +02:00
|
|
|
) {
|
2026-05-08 16:46:22 +02:00
|
|
|
const target = elementFromEventTarget(event.target)
|
|
|
|
|
if (!target || isInsideCodeContext(target)) return
|
2026-04-26 02:16:20 +02:00
|
|
|
|
2026-05-08 16:46:22 +02:00
|
|
|
const wikilinkTarget = resolveWikilinkTarget(target)
|
2026-04-26 02:16:20 +02:00
|
|
|
if (wikilinkTarget) {
|
|
|
|
|
activateWikilink(event, container, wikilinkTarget, onNavigateWikilink)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-08 16:46:22 +02:00
|
|
|
const href = resolveAnchorHref(target)
|
2026-05-07 00:55:07 +02:00
|
|
|
if (href) activateUrl(event, href, vaultPath)
|
2026-04-26 02:16:20 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-08 16:46:22 +02:00
|
|
|
function handleEditorLinkMouseDown(event: MouseEvent, vaultPath?: string): string | null {
|
|
|
|
|
const target = elementFromEventTarget(event.target)
|
|
|
|
|
if (!target || isInsideCodeContext(target)) return null
|
|
|
|
|
|
|
|
|
|
if (resolveWikilinkTarget(target)) {
|
|
|
|
|
consumeEditorLinkEvent(event)
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const href = resolveAnchorHref(target)
|
|
|
|
|
if (hasFollowModifier(event) && href) {
|
|
|
|
|
activateUrl(event, href, vaultPath)
|
|
|
|
|
return href
|
|
|
|
|
}
|
2026-05-05 13:43:48 +02:00
|
|
|
|
2026-05-08 16:46:22 +02:00
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function followedAnchorHrefFromEvent(event: MouseEvent, fallback: HTMLElement) {
|
|
|
|
|
if (!hasFollowModifier(event)) return null
|
|
|
|
|
|
|
|
|
|
return resolveAnchorHref(elementFromEventTarget(event.target) ?? fallback)
|
2026-05-05 13:43:48 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 19:06:21 +02:00
|
|
|
export function useEditorLinkActivation(
|
|
|
|
|
containerRef: RefObject<HTMLDivElement | null>,
|
|
|
|
|
onNavigateWikilink: (target: string) => void,
|
2026-05-07 00:55:07 +02:00
|
|
|
vaultPath?: string,
|
2026-04-08 19:06:21 +02:00
|
|
|
) {
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const container = containerRef.current
|
|
|
|
|
if (!container) return
|
|
|
|
|
|
|
|
|
|
const resetModifierState = () => setFollowLinksActive(container, false)
|
|
|
|
|
const handleModifierChange = (event: KeyboardEvent) => {
|
|
|
|
|
setFollowLinksActive(container, hasFollowModifier(event))
|
|
|
|
|
}
|
|
|
|
|
const handleVisibilityChange = () => {
|
|
|
|
|
if (document.visibilityState !== 'visible') resetModifierState()
|
|
|
|
|
}
|
2026-05-08 16:46:22 +02:00
|
|
|
let handledMouseDownUrl: string | null = null
|
|
|
|
|
const rememberHandledMouseDownUrl = (href: string) => {
|
|
|
|
|
handledMouseDownUrl = href
|
|
|
|
|
window.setTimeout(() => {
|
|
|
|
|
if (handledMouseDownUrl === href) handledMouseDownUrl = null
|
|
|
|
|
}, 0)
|
|
|
|
|
}
|
|
|
|
|
const handleMouseDown = (event: MouseEvent) => {
|
|
|
|
|
const href = handleEditorLinkMouseDown(event, vaultPath)
|
|
|
|
|
if (href) rememberHandledMouseDownUrl(href)
|
|
|
|
|
}
|
2026-04-08 19:06:21 +02:00
|
|
|
const handleClick = (event: MouseEvent) => {
|
2026-05-08 16:46:22 +02:00
|
|
|
const followedHref = followedAnchorHrefFromEvent(event, container)
|
|
|
|
|
if (handledMouseDownUrl && followedHref === handledMouseDownUrl) {
|
|
|
|
|
handledMouseDownUrl = null
|
|
|
|
|
consumeEditorLinkEvent(event)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handledMouseDownUrl = null
|
2026-05-07 00:55:07 +02:00
|
|
|
handleEditorLinkClick(event, container, onNavigateWikilink, vaultPath)
|
2026-04-08 19:06:21 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-08 16:46:22 +02:00
|
|
|
container.addEventListener('mousedown', handleMouseDown, true)
|
2026-04-08 19:06:21 +02:00
|
|
|
container.addEventListener('click', handleClick, true)
|
|
|
|
|
window.addEventListener('keydown', handleModifierChange)
|
|
|
|
|
window.addEventListener('keyup', handleModifierChange)
|
|
|
|
|
window.addEventListener('blur', resetModifierState)
|
|
|
|
|
document.addEventListener('visibilitychange', handleVisibilityChange)
|
|
|
|
|
|
|
|
|
|
return () => {
|
2026-05-08 16:46:22 +02:00
|
|
|
container.removeEventListener('mousedown', handleMouseDown, true)
|
2026-04-08 19:06:21 +02:00
|
|
|
container.removeEventListener('click', handleClick, true)
|
|
|
|
|
window.removeEventListener('keydown', handleModifierChange)
|
|
|
|
|
window.removeEventListener('keyup', handleModifierChange)
|
|
|
|
|
window.removeEventListener('blur', resetModifierState)
|
|
|
|
|
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
|
|
|
|
resetModifierState()
|
|
|
|
|
}
|
2026-05-07 00:55:07 +02:00
|
|
|
}, [containerRef, onNavigateWikilink, vaultPath])
|
2026-04-08 19:06:21 +02:00
|
|
|
}
|