- Added menuNode state to capture the node position when menu opens - Updated all action functions to use menuNode instead of currentNode - This ensures drag handle actions (Turn into, Delete, etc.) always affect the block where the handle was clicked, not where the mouse currently hovers - Also formatted code with prettier 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
209 lines
5.7 KiB
Svelte
209 lines
5.7 KiB
Svelte
<script lang="ts">
|
|
import type { Editor } from '@tiptap/core'
|
|
import UrlConvertDropdown from '$lib/components/edra/headless/components/UrlConvertDropdown.svelte'
|
|
import LinkContextMenuComponent from '$lib/components/edra/headless/components/LinkContextMenu.svelte'
|
|
import LinkEditDialog from '$lib/components/edra/headless/components/LinkEditDialog.svelte'
|
|
|
|
interface Props {
|
|
editor: Editor
|
|
features: {
|
|
urlEmbed?: boolean
|
|
}
|
|
}
|
|
|
|
let { editor, features }: Props = $props()
|
|
|
|
// URL convert dropdown state
|
|
let showUrlConvertDropdown = $state(false)
|
|
let urlConvertDropdownPosition = $state({ x: 0, y: 0 })
|
|
let urlConvertPos = $state<number | null>(null)
|
|
|
|
// Link context menu state
|
|
let showLinkContextMenu = $state(false)
|
|
let linkContextMenuPosition = $state({ x: 0, y: 0 })
|
|
let linkContextUrl = $state<string | null>(null)
|
|
let linkContextPos = $state<number | null>(null)
|
|
|
|
// Link edit dialog state
|
|
let showLinkEditDialog = $state(false)
|
|
let linkEditDialogPosition = $state({ x: 0, y: 0 })
|
|
let linkEditUrl = $state<string>('')
|
|
let linkEditPos = $state<number | null>(null)
|
|
|
|
// URL convert handlers
|
|
export function handleShowUrlConvertDropdown(pos: number, url: string) {
|
|
if (!editor) return
|
|
const coords = editor.view.coordsAtPos(pos)
|
|
urlConvertDropdownPosition = { x: coords.left, y: coords.bottom + 5 }
|
|
urlConvertPos = pos
|
|
showUrlConvertDropdown = true
|
|
}
|
|
|
|
function handleConvertToEmbed() {
|
|
if (!editor || urlConvertPos === null) return
|
|
editor.commands.convertLinkToEmbed(urlConvertPos)
|
|
showUrlConvertDropdown = false
|
|
urlConvertPos = null
|
|
}
|
|
|
|
// Link context menu handlers
|
|
export function handleShowLinkContextMenu(pos: number, url: string) {
|
|
if (!editor) return
|
|
const coords = editor.view.coordsAtPos(pos)
|
|
linkContextMenuPosition = { x: coords.left, y: coords.bottom + 5 }
|
|
linkContextUrl = url
|
|
linkContextPos = pos
|
|
showLinkContextMenu = true
|
|
}
|
|
|
|
function handleConvertLinkToEmbed() {
|
|
if (!editor || linkContextPos === null) return
|
|
editor.commands.convertLinkToEmbed(linkContextPos)
|
|
showLinkContextMenu = false
|
|
linkContextPos = null
|
|
linkContextUrl = null
|
|
}
|
|
|
|
function handleEditLink() {
|
|
if (!editor || linkContextPos === null || !linkContextUrl) return
|
|
const coords = editor.view.coordsAtPos(linkContextPos)
|
|
linkEditDialogPosition = { x: coords.left, y: coords.bottom + 5 }
|
|
linkEditUrl = linkContextUrl
|
|
linkEditPos = linkContextPos
|
|
showLinkEditDialog = true
|
|
showLinkContextMenu = false
|
|
}
|
|
|
|
function handleSaveLink(newUrl: string) {
|
|
if (!editor || linkEditPos === null) return
|
|
// Update link by setting selection and re-applying link mark
|
|
const { state } = editor
|
|
const { doc } = state
|
|
const node = doc.nodeAt(linkEditPos)
|
|
if (node) {
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.setTextSelection({ from: linkEditPos, to: linkEditPos + node.nodeSize })
|
|
.setLink({ href: newUrl })
|
|
.run()
|
|
}
|
|
showLinkEditDialog = false
|
|
linkEditPos = null
|
|
linkEditUrl = ''
|
|
}
|
|
|
|
function handleCopyLink() {
|
|
if (!linkContextUrl) return
|
|
navigator.clipboard.writeText(linkContextUrl)
|
|
showLinkContextMenu = false
|
|
linkContextPos = null
|
|
linkContextUrl = null
|
|
}
|
|
|
|
function handleRemoveLink() {
|
|
if (!editor || linkContextPos === null) return
|
|
// Remove link by unset link command
|
|
editor.chain().focus().unsetLink().run()
|
|
showLinkContextMenu = false
|
|
linkContextPos = null
|
|
linkContextUrl = null
|
|
}
|
|
|
|
function handleOpenLink() {
|
|
if (!linkContextUrl) return
|
|
window.open(linkContextUrl, '_blank')
|
|
showLinkContextMenu = false
|
|
linkContextPos = null
|
|
linkContextUrl = null
|
|
}
|
|
|
|
// Handle click outside
|
|
function handleClickOutside(event: MouseEvent) {
|
|
const target = event.target as HTMLElement
|
|
if (!target.closest('.url-convert-dropdown')) {
|
|
showUrlConvertDropdown = false
|
|
}
|
|
if (!target.closest('.link-context-menu')) {
|
|
showLinkContextMenu = false
|
|
}
|
|
if (!target.closest('.link-edit-dialog')) {
|
|
showLinkEditDialog = false
|
|
}
|
|
}
|
|
|
|
// Dismiss dropdowns on typing
|
|
export function dismissOnTyping(transaction: any) {
|
|
if (showUrlConvertDropdown && transaction.docChanged) {
|
|
const hasTextChange = transaction.steps.some(
|
|
(step: any) =>
|
|
step.toJSON().stepType === 'replace' || step.toJSON().stepType === 'replaceAround'
|
|
)
|
|
if (hasTextChange) {
|
|
showUrlConvertDropdown = false
|
|
urlConvertPos = null
|
|
}
|
|
}
|
|
}
|
|
|
|
$effect(() => {
|
|
if (showUrlConvertDropdown || showLinkContextMenu || showLinkEditDialog) {
|
|
document.addEventListener('click', handleClickOutside)
|
|
return () => {
|
|
document.removeEventListener('click', handleClickOutside)
|
|
}
|
|
}
|
|
})
|
|
|
|
// Export state for parent to check if any menus are open
|
|
export function hasOpenMenus() {
|
|
return showUrlConvertDropdown || showLinkContextMenu || showLinkEditDialog
|
|
}
|
|
</script>
|
|
|
|
<!-- URL Convert Dropdown -->
|
|
{#if showUrlConvertDropdown && features.urlEmbed}
|
|
<UrlConvertDropdown
|
|
x={urlConvertDropdownPosition.x}
|
|
y={urlConvertDropdownPosition.y}
|
|
onConvert={handleConvertToEmbed}
|
|
onDismiss={() => {
|
|
showUrlConvertDropdown = false
|
|
urlConvertPos = null
|
|
}}
|
|
/>
|
|
{/if}
|
|
|
|
<!-- Link Context Menu -->
|
|
{#if showLinkContextMenu && linkContextUrl}
|
|
<LinkContextMenuComponent
|
|
x={linkContextMenuPosition.x}
|
|
y={linkContextMenuPosition.y}
|
|
url={linkContextUrl}
|
|
onConvertToCard={features.urlEmbed ? handleConvertLinkToEmbed : undefined}
|
|
onEditLink={handleEditLink}
|
|
onCopyLink={handleCopyLink}
|
|
onRemoveLink={handleRemoveLink}
|
|
onOpenLink={handleOpenLink}
|
|
onDismiss={() => {
|
|
showLinkContextMenu = false
|
|
linkContextPos = null
|
|
linkContextUrl = null
|
|
}}
|
|
/>
|
|
{/if}
|
|
|
|
<!-- Link Edit Dialog -->
|
|
{#if showLinkEditDialog}
|
|
<LinkEditDialog
|
|
x={linkEditDialogPosition.x}
|
|
y={linkEditDialogPosition.y}
|
|
currentUrl={linkEditUrl}
|
|
onSave={handleSaveLink}
|
|
onCancel={() => {
|
|
showLinkEditDialog = false
|
|
linkEditPos = null
|
|
linkEditUrl = ''
|
|
}}
|
|
/>
|
|
{/if}
|