jedmund-svelte/src/lib/components/admin/composer/ComposerLinkManager.svelte
Justin Edmund 1c38dc87e3 fix: drag handle actions now affect the correct block
- 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>
2025-06-26 10:33:27 -04:00

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}