From e64788962edd070e20a4243b5124d95220ab01a2 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Thu, 26 Jun 2025 09:12:08 -0400 Subject: [PATCH] refactor: remove EnhancedComposer backward compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove EnhancedComposer shim and old backup file - Update all imports to use new Composer from ./composer - Fix editor command implementations for link operations - Fix dropdown hook usage with proper reactive patterns - All 5 components now directly import the modular implementation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/lib/components/admin/AlbumForm.svelte | 4 +- .../admin/EnhancedComposer.old.svelte | 1347 ----------------- .../components/admin/EnhancedComposer.svelte | 41 - .../admin/InlineComposerModal.svelte | 8 +- src/lib/components/admin/ProjectForm.svelte | 4 +- .../admin/composer/ComposerCore.svelte | 32 +- .../admin/composer/ComposerLinkManager.svelte | 16 +- src/lib/components/admin/composer/index.ts | 5 +- src/routes/admin/posts/[id]/edit/+page.svelte | 4 +- src/routes/admin/posts/new/+page.svelte | 4 +- 10 files changed, 47 insertions(+), 1418 deletions(-) delete mode 100644 src/lib/components/admin/EnhancedComposer.old.svelte delete mode 100644 src/lib/components/admin/EnhancedComposer.svelte diff --git a/src/lib/components/admin/AlbumForm.svelte b/src/lib/components/admin/AlbumForm.svelte index dd0d98e..1ea832b 100644 --- a/src/lib/components/admin/AlbumForm.svelte +++ b/src/lib/components/admin/AlbumForm.svelte @@ -8,7 +8,7 @@ import StatusDropdown from './StatusDropdown.svelte' import UnifiedMediaModal from './UnifiedMediaModal.svelte' import SmartImage from '../SmartImage.svelte' - import EnhancedComposer from './EnhancedComposer.svelte' + import Composer from './composer' import { authenticatedFetch } from '$lib/admin-auth' import { toast } from '$lib/stores/toast' import type { Album } from '@prisma/client' @@ -350,7 +350,7 @@
- - import { type Editor } from '@tiptap/core' - import { onMount, setContext } from 'svelte' - import { initiateEditor } from '$lib/components/edra/editor.ts' - import { getEditorExtensions, EDITOR_PRESETS } from '$lib/components/edra/editor-extensions.js' - import { EdraToolbar, EdraBubbleMenu } from '$lib/components/edra/headless/index.js' - import LoaderCircle from 'lucide-svelte/icons/loader-circle' - import { focusEditor, type EdraProps } from '$lib/components/edra/utils.js' - import EdraToolBarIcon from '$lib/components/edra/headless/components/EdraToolBarIcon.svelte' - import { commands } from '$lib/components/edra/commands/commands.js' - import EnhancedImagePlaceholder from '$lib/components/edra/headless/components/EnhancedImagePlaceholder.svelte' - import type { JSONContent } from '@tiptap/core' - - // Import Edra styles - import '$lib/components/edra/headless/style.css' - import 'katex/dist/katex.min.css' - import '$lib/components/edra/editor.css' - import '$lib/components/edra/onedark.css' - - // Import menus - import LinkMenu from '$lib/components/edra/headless/menus/link-menu.svelte' - import TableRowMenu from '$lib/components/edra/headless/menus/table/table-row-menu.svelte' - import TableColMenu from '$lib/components/edra/headless/menus/table/table-col-menu.svelte' - 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' - import UnifiedMediaModal from './UnifiedMediaModal.svelte' - import DragHandle from '$lib/components/edra/drag-handle.svelte' - import { mediaSelectionStore } from '$lib/stores/media-selection' - import type { Media } from '@prisma/client' - - // Component types - type ComposerVariant = 'full' | 'inline' | 'minimal' - - interface Props { - variant?: ComposerVariant - data?: JSONContent - onChange?: (content: JSONContent) => void - onCharacterCount?: (count: number) => void - placeholder?: string - minHeight?: number - autofocus?: boolean - editable?: boolean - class?: string - showToolbar?: boolean - showSlashCommands?: boolean - albumId?: number - features?: { - imageUpload?: boolean - mediaLibrary?: boolean - urlEmbed?: boolean - tables?: boolean - codeBlocks?: boolean - } - } - - let { - variant = 'full', - data = $bindable({ - type: 'doc', - content: [{ type: 'paragraph' }] - }), - onChange, - onCharacterCount, - placeholder = variant === 'inline' ? "What's on your mind?" : 'Type "/" for commands...', - minHeight = variant === 'inline' ? 80 : 400, - autofocus = false, - editable = true, - class: className = '', - showToolbar = variant === 'full', - showSlashCommands = variant !== 'minimal', - albumId, - features = { - imageUpload: true, - mediaLibrary: true, - urlEmbed: true, - tables: true, - codeBlocks: true - } - }: Props = $props() - - // Set editor context for child components - setContext('editorContext', { - albumId, - contentType: albumId ? 'album' : 'default', - isAlbumEditor: !!albumId - }) - - // State - let editor = $state() - let element = $state() - let isLoading = $state(true) - let initialized = false - const mediaSelectionState = $derived($mediaSelectionStore) - - // Dropdown states - let showTextStyleDropdown = $state(false) - let showMediaDropdown = $state(false) - let dropdownTriggerRef = $state() - let mediaDropdownTriggerRef = $state() - let dropdownPosition = $state({ top: 0, left: 0 }) - let mediaDropdownPosition = $state({ top: 0, left: 0 }) - - // URL convert dropdown state - let showUrlConvertDropdown = $state(false) - let urlConvertDropdownPosition = $state({ x: 0, y: 0 }) - let urlConvertPos = $state(null) - - // Link context menu state - let showLinkContextMenu = $state(false) - let linkContextMenuPosition = $state({ x: 0, y: 0 }) - let linkContextUrl = $state(null) - let linkContextPos = $state(null) - - // Link edit dialog state - let showLinkEditDialog = $state(false) - let linkEditDialogPosition = $state({ x: 0, y: 0 }) - let linkEditUrl = $state('') - let linkEditPos = $state(null) - - // Get filtered commands based on variant and features - const getFilteredCommands = () => { - const filtered = { ...commands } - - // Remove groups based on variant - if (variant === 'minimal') { - delete filtered['undo-redo'] - delete filtered['headings'] - delete filtered['lists'] - delete filtered['alignment'] - delete filtered['table'] - delete filtered['media'] - delete filtered['fonts'] - } else if (variant === 'inline') { - delete filtered['undo-redo'] - delete filtered['headings'] - delete filtered['lists'] - delete filtered['alignment'] - delete filtered['table'] - delete filtered['media'] - } else { - // Full variant - reorganize for toolbar - delete filtered['undo-redo'] - delete filtered['headings'] // In text style dropdown - delete filtered['lists'] // In text style dropdown - delete filtered['alignment'] - delete filtered['table'] - delete filtered['media'] // In media dropdown - } - - // Reorganize text formatting for toolbar - if (filtered['text-formatting']) { - const allCommands = filtered['text-formatting'].commands - const basicFormatting = [] - const advancedFormatting = [] - - // Group basic formatting first - const basicOrder = ['bold', 'italic', 'underline', 'strike'] - basicOrder.forEach((name) => { - const cmd = allCommands.find((c) => c.name === name) - if (cmd) basicFormatting.push(cmd) - }) - - // Then link and code - const advancedOrder = ['link', 'code'] - advancedOrder.forEach((name) => { - const cmd = allCommands.find((c) => c.name === name) - if (cmd) advancedFormatting.push(cmd) - }) - - // Create two groups - filtered['basic-formatting'] = { - name: 'Basic Formatting', - label: 'Basic Formatting', - commands: basicFormatting - } - - filtered['advanced-formatting'] = { - name: 'Advanced Formatting', - label: 'Advanced Formatting', - commands: advancedFormatting - } - - // Remove original text-formatting - delete filtered['text-formatting'] - } - - return filtered - } - - // Get media commands, but filter out based on features - const getMediaCommands = () => { - if (!commands.media) return [] - - let mediaCommands = [...commands.media.commands] - - // Filter based on features - if (!features.urlEmbed) { - mediaCommands = mediaCommands.filter((cmd) => cmd.name !== 'iframe-placeholder') - } - - return mediaCommands - } - - const filteredCommands = getFilteredCommands() - const colorCommands = commands.colors?.commands || [] - const excludedCommands = ['colors', 'fonts'] - - // Get current text style for dropdown - const getCurrentTextStyle = (editor: Editor) => { - if (editor.isActive('heading', { level: 1 })) return 'Heading 1' - if (editor.isActive('heading', { level: 2 })) return 'Heading 2' - if (editor.isActive('heading', { level: 3 })) return 'Heading 3' - if (editor.isActive('bulletList')) return 'Bullet List' - if (editor.isActive('orderedList')) return 'Ordered List' - if (editor.isActive('taskList')) return 'Task List' - if (editor.isActive('codeBlock')) return 'Code Block' - if (editor.isActive('blockquote')) return 'Blockquote' - return 'Paragraph' - } - - // Derived state for current text style - let currentTextStyle = $derived(editor ? getCurrentTextStyle(editor) : 'Paragraph') - - // Calculate dropdown position - const updateDropdownPosition = () => { - if (dropdownTriggerRef) { - const rect = dropdownTriggerRef.getBoundingClientRect() - dropdownPosition = { - top: rect.bottom + 4, - left: rect.left - } - } - } - - // Toggle dropdown with position update - const toggleDropdown = () => { - if (!showTextStyleDropdown) { - updateDropdownPosition() - } - showTextStyleDropdown = !showTextStyleDropdown - } - - // Update media dropdown position - const updateMediaDropdownPosition = () => { - if (mediaDropdownTriggerRef) { - const rect = mediaDropdownTriggerRef.getBoundingClientRect() - mediaDropdownPosition = { - top: rect.bottom + 4, - left: rect.left - } - } - } - - // Toggle media dropdown - const toggleMediaDropdown = () => { - if (!showMediaDropdown) { - updateMediaDropdownPosition() - } - showMediaDropdown = !showMediaDropdown - } - - // Close dropdown when clicking outside - const handleClickOutside = (event: MouseEvent) => { - const target = event.target as HTMLElement - if (!dropdownTriggerRef?.contains(target) && !target.closest('.dropdown-menu-portal')) { - showTextStyleDropdown = false - } - if (!mediaDropdownTriggerRef?.contains(target) && !target.closest('.media-dropdown-portal')) { - showMediaDropdown = false - } - if (!target.closest('.url-convert-dropdown')) { - showUrlConvertDropdown = false - } - if (!target.closest('.link-context-menu')) { - showLinkContextMenu = false - } - if (!target.closest('.link-edit-dialog')) { - showLinkEditDialog = false - } - } - - // URL convert handlers - const 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 - } - - const handleConvertToEmbed = () => { - if (!editor || urlConvertPos === null) return - editor.commands.convertLinkToEmbed(urlConvertPos) - showUrlConvertDropdown = false - urlConvertPos = null - } - - // Link context menu handlers - const handleShowLinkContextMenu = ( - pos: number, - url: string, - coords: { x: number; y: number } - ) => { - if (!editor) return - linkContextMenuPosition = { x: coords.x, y: coords.y + 5 } - linkContextUrl = url - linkContextPos = pos - showLinkContextMenu = true - } - - const handleConvertLinkToEmbed = () => { - if (!editor || linkContextPos === null) return - editor.commands.convertLinkToEmbed(linkContextPos) - showLinkContextMenu = false - linkContextPos = null - linkContextUrl = null - } - - const handleEditLink = () => { - if (!editor || !linkContextUrl) return - linkEditUrl = linkContextUrl - linkEditPos = linkContextPos - linkEditDialogPosition = { ...linkContextMenuPosition } - showLinkEditDialog = true - showLinkContextMenu = false - } - - const handleSaveLink = (newUrl: string) => { - if (!editor) return - editor.chain().focus().extendMarkRange('link').setLink({ href: newUrl }).run() - showLinkEditDialog = false - linkEditPos = null - linkEditUrl = '' - } - - const handleCopyLink = () => { - if (!linkContextUrl) return - navigator.clipboard.writeText(linkContextUrl) - showLinkContextMenu = false - linkContextPos = null - linkContextUrl = null - } - - const handleRemoveLink = () => { - if (!editor) return - editor.chain().focus().extendMarkRange('link').unsetLink().run() - showLinkContextMenu = false - linkContextPos = null - linkContextUrl = null - } - - const handleOpenLink = () => { - if (!linkContextUrl) return - window.open(linkContextUrl, '_blank', 'noopener,noreferrer') - showLinkContextMenu = false - linkContextPos = null - linkContextUrl = null - } - - // Handle media selection from the global store - function handleGlobalMediaSelect(media: Media | Media[]) { - if (!editor) return - - const selectedMedia = Array.isArray(media) ? media[0] : media - if (selectedMedia) { - // Set a reasonable default width (max 600px) - const displayWidth = - selectedMedia.width && selectedMedia.width > 600 ? 600 : selectedMedia.width - - editor - .chain() - .focus() - .insertContent({ - type: 'image', - attrs: { - src: selectedMedia.url, - alt: selectedMedia.altText || '', - title: selectedMedia.description || '', - width: displayWidth, - height: selectedMedia.height, - align: 'center', - mediaId: selectedMedia.id?.toString() - } - }) - .run() - } - - // Close the modal - mediaSelectionStore.close() - - // Remove the placeholder if it exists - if (editor.storage.imageModal?.placeholderPos !== undefined) { - const pos = editor.storage.imageModal.placeholderPos - editor - .chain() - .focus() - .deleteRange({ from: pos, to: pos + 1 }) - .run() - editor.storage.imageModal.placeholderPos = undefined - } - } - - function handleGlobalMediaClose() { - mediaSelectionStore.close() - - // Remove the placeholder if user cancelled - if (editor && editor.storage.imageModal?.placeholderPos !== undefined) { - const pos = editor.storage.imageModal.placeholderPos - editor - .chain() - .focus() - .deleteRange({ from: pos, to: pos + 1 }) - .run() - editor.storage.imageModal.placeholderPos = undefined - } - } - - // Handle paste for images - function handlePaste(view: any, event: ClipboardEvent) { - const clipboardData = event.clipboardData - if (!clipboardData) return false - - // Check for images first - const imageItem = Array.from(clipboardData.items).find( - (item) => item.type.indexOf('image') === 0 - ) - if (imageItem && features.imageUpload) { - const file = imageItem.getAsFile() - if (!file) return false - - // Check file size (2MB max) - const filesize = file.size / 1024 / 1024 - if (filesize > 2) { - alert(`Image too large! File size: ${filesize.toFixed(2)} MB (max 2MB)`) - return true - } - - // Upload to our media API - uploadImage(file) - return true // Prevent default paste behavior - } - - // Handle text paste - preserve links while stripping other formatting - const htmlData = clipboardData.getData('text/html') - const plainText = clipboardData.getData('text/plain') - - if (htmlData && plainText) { - event.preventDefault() - - // Use editor commands to insert HTML content - const editorInstance = (view as any).editor - if (editorInstance) { - editorInstance - .chain() - .focus() - .insertContent(htmlData, { parseOptions: { preserveWhitespace: false } }) - .run() - } else { - // Fallback to plain text - const { state, dispatch } = view - const { selection } = state - const transaction = state.tr.insertText(plainText, selection.from, selection.to) - dispatch(transaction) - } - - return true - } - - return false - } - - async function uploadImage(file: File) { - if (!editor || !features.imageUpload) return - - // Create a placeholder while uploading - const placeholderSrc = URL.createObjectURL(file) - editor.commands.insertContent({ - type: 'image', - attrs: { - src: placeholderSrc, - alt: '', - title: '', - mediaId: null - } - }) - - try { - const auth = localStorage.getItem('admin_auth') - if (!auth) { - throw new Error('Not authenticated') - } - - const formData = new FormData() - formData.append('file', file) - - // Add albumId if available - if (albumId) { - formData.append('albumId', albumId.toString()) - } - - const response = await fetch('/api/media/upload', { - method: 'POST', - headers: { - Authorization: `Basic ${auth}` - }, - body: formData - }) - - if (!response.ok) { - throw new Error('Upload failed') - } - - const media = await response.json() - - // Replace placeholder with actual URL - const displayWidth = media.width && media.width > 600 ? 600 : media.width - - editor.commands.insertContent({ - type: 'image', - attrs: { - src: media.url, - alt: media.filename || '', - title: media.description || '', - width: displayWidth, - height: media.height, - align: 'center', - mediaId: media.id?.toString() - } - }) - - // Clean up the object URL - URL.revokeObjectURL(placeholderSrc) - } catch (error) { - console.error('Image upload failed:', error) - alert('Failed to upload image. Please try again.') - // Remove the placeholder on error - editor.commands.undo() - } - } - - // Update content when editor changes - function handleUpdate({ editor: updatedEditor, transaction }: any) { - // Skip the first update to avoid circular updates - if (!initialized) { - initialized = true - return - } - - // Dismiss URL convert dropdown if user types - 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 - } - } - - const json = updatedEditor.getJSON() - data = json - onChange?.(json) - - // Calculate character count if callback provided - if (onCharacterCount) { - const text = updatedEditor.getText() - onCharacterCount(text.length) - } - } - - $effect(() => { - if ( - showTextStyleDropdown || - showMediaDropdown || - showUrlConvertDropdown || - showLinkContextMenu || - showLinkEditDialog - ) { - document.addEventListener('click', handleClickOutside) - return () => { - document.removeEventListener('click', handleClickOutside) - } - } - }) - - onMount(() => { - // Get extensions with custom options - const extensions = getEditorExtensions({ - showSlashCommands, - onShowUrlConvertDropdown: features.urlEmbed ? handleShowUrlConvertDropdown : undefined, - onShowLinkContextMenu: handleShowLinkContextMenu, - imagePlaceholderComponent: EnhancedImagePlaceholder - }) - - // Initialize editor storage for image modal - const newEditor = initiateEditor( - element, - data, - undefined, // no character limit by default - extensions, - { - editable, - onUpdate: handleUpdate, - editorProps: { - attributes: { - class: 'prose prose-sm max-w-none focus:outline-none' - }, - handlePaste: features.imageUpload ? handlePaste : undefined - } - }, - placeholder - ) - - // Initialize storage for image modal - newEditor.storage.imageModal = { autoOpen: false } - - editor = newEditor - - // Auto-focus if requested - if (autofocus) { - setTimeout(() => { - newEditor.commands.focus() - }, 100) - } - - isLoading = false - - return () => editor?.destroy() - }) - - // Public API - export function save(): JSONContent | null { - return editor?.getJSON() || null - } - - export function clear() { - editor?.commands.clearContent() - } - - export function focus() { - editor?.commands.focus() - } - - export function blur() { - editor?.commands.blur() - } - - export function getContent() { - return editor?.getJSON() - } - - export function getText() { - return editor?.getText() || '' - } - - -
- {#if showToolbar && editor && !isLoading} -
-
- -
- -
- - - - {#each Object.keys(filteredCommands).filter((key) => !excludedCommands.includes(key)) as keys} - {@const groups = filteredCommands[keys].commands} - {#each groups as command} - - {/each} - - {/each} - - {#if features.mediaLibrary} - -
- -
- - - {/if} - - {#if colorCommands.length > 0} - { - const color = editor.getAttributes('textStyle').color - const hasColor = editor.isActive('textStyle', { color }) - if (hasColor) { - editor.chain().focus().unsetColor().run() - } else { - const color = prompt('Enter the color of the text:') - if (color !== null) { - editor.chain().focus().setColor(color).run() - } - } - }} - /> - { - const hasHightlight = editor.isActive('highlight') - if (hasHightlight) { - editor.chain().focus().unsetHighlight().run() - } else { - const color = prompt('Enter the color of the highlight:') - if (color !== null) { - editor.chain().focus().setHighlight({ color }).run() - } - } - }} - /> - {/if} -
-
- {/if} - - {#if editor} - - {#if features.tables} - - - {/if} - {/if} - - {#if !editor} -
- Loading... -
- {/if} - -
focusEditor(editor, event)} - onkeydown={(event) => { - if (event.key === 'Enter' || event.key === ' ') { - focusEditor(editor, event) - } - }} - class="edra-editor" - class:with-toolbar={showToolbar} - style={`min-height: ${minHeight}px`} - >
- - {#if editor} - - {/if} -
- - -{#if showMediaDropdown && features.mediaLibrary} -
- -
-{/if} - - -{#if showTextStyleDropdown} - -{/if} - - -{#if showUrlConvertDropdown && features.urlEmbed} - { - showUrlConvertDropdown = false - urlConvertPos = null - }} - /> -{/if} - - -{#if showLinkContextMenu && linkContextUrl} - { - showLinkContextMenu = false - linkContextPos = null - linkContextUrl = null - }} - /> -{/if} - - -{#if showLinkEditDialog} - { - showLinkEditDialog = false - linkEditPos = null - linkEditUrl = '' - }} - /> -{/if} - - -{#if mediaSelectionState.isOpen} - -{/if} - - diff --git a/src/lib/components/admin/EnhancedComposer.svelte b/src/lib/components/admin/EnhancedComposer.svelte deleted file mode 100644 index 1e6261d..0000000 --- a/src/lib/components/admin/EnhancedComposer.svelte +++ /dev/null @@ -1,41 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/lib/components/admin/InlineComposerModal.svelte b/src/lib/components/admin/InlineComposerModal.svelte index e4ac69d..28e439a 100644 --- a/src/lib/components/admin/InlineComposerModal.svelte +++ b/src/lib/components/admin/InlineComposerModal.svelte @@ -2,7 +2,7 @@ import { createEventDispatcher } from 'svelte' import { goto } from '$app/navigation' import Modal from './Modal.svelte' - import EnhancedComposer from './EnhancedComposer.svelte' + import Composer from './composer' import AdminSegmentedControl from './AdminSegmentedControl.svelte' import FormField from './FormField.svelte' import Button from './Button.svelte' @@ -270,7 +270,7 @@
- { @@ -423,7 +423,7 @@
{:else}
- { @@ -471,7 +471,7 @@ {/if}
- { diff --git a/src/lib/components/admin/ProjectForm.svelte b/src/lib/components/admin/ProjectForm.svelte index fe449a4..e188813 100644 --- a/src/lib/components/admin/ProjectForm.svelte +++ b/src/lib/components/admin/ProjectForm.svelte @@ -4,7 +4,7 @@ import AdminPage from './AdminPage.svelte' import AdminSegmentedControl from './AdminSegmentedControl.svelte' import FormField from './FormField.svelte' - import EnhancedComposer from './EnhancedComposer.svelte' + import Composer from './composer' import ProjectMetadataForm from './ProjectMetadataForm.svelte' import ProjectBrandingForm from './ProjectBrandingForm.svelte' import ProjectImagesForm from './ProjectImagesForm.svelte' @@ -274,7 +274,7 @@
- (showTextStyleDropdown = false), - portalClass: 'dropdown-menu-portal' + const textStyleDropdown = $derived.by(() => { + return useDropdown({ + triggerRef: toolbarRef?.getDropdownRefs()?.textStyle, + isOpen: showTextStyleDropdown, + onClose: () => (showTextStyleDropdown = false), + portalClass: 'dropdown-menu-portal' + }) }) // Media dropdown - const mediaDropdown = useDropdown({ - triggerRef: toolbarRef?.getDropdownRefs().media, - isOpen: showMediaDropdown, - onClose: () => (showMediaDropdown = false), - portalClass: 'media-dropdown-portal' + const mediaDropdown = $derived.by(() => { + return useDropdown({ + triggerRef: toolbarRef?.getDropdownRefs()?.media, + isOpen: showMediaDropdown, + onClose: () => (showMediaDropdown = false), + portalClass: 'media-dropdown-portal' + }) }) // Event handlers @@ -242,11 +246,11 @@ showMediaLibrary={!!features.mediaLibrary} onTextStyleDropdownToggle={() => { showTextStyleDropdown = !showTextStyleDropdown - textStyleDropdown.toggle() + textStyleDropdown?.toggle() }} onMediaDropdownToggle={() => { showMediaDropdown = !showMediaDropdown - mediaDropdown.toggle() + mediaDropdown?.toggle() }} /> {/if} @@ -286,7 +290,7 @@ {#if showTextStyleDropdown && editor} (showTextStyleDropdown = false)} /> @@ -296,7 +300,7 @@ {#if showMediaDropdown && editor && features.mediaLibrary} (showMediaDropdown = false)} diff --git a/src/lib/components/admin/composer/ComposerLinkManager.svelte b/src/lib/components/admin/composer/ComposerLinkManager.svelte index 1a7b398..56a4c59 100644 --- a/src/lib/components/admin/composer/ComposerLinkManager.svelte +++ b/src/lib/components/admin/composer/ComposerLinkManager.svelte @@ -76,7 +76,18 @@ function handleSaveLink(newUrl: string) { if (!editor || linkEditPos === null) return - editor.commands.updateLinkUrl(linkEditPos, newUrl) + // 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 = '' @@ -92,7 +103,8 @@ function handleRemoveLink() { if (!editor || linkContextPos === null) return - editor.commands.removeLink(linkContextPos) + // Remove link by unset link command + editor.chain().focus().unsetLink().run() showLinkContextMenu = false linkContextPos = null linkContextUrl = null diff --git a/src/lib/components/admin/composer/index.ts b/src/lib/components/admin/composer/index.ts index fb88f8e..f59fc6d 100644 --- a/src/lib/components/admin/composer/index.ts +++ b/src/lib/components/admin/composer/index.ts @@ -1,5 +1,6 @@ -// Export ComposerCore as EnhancedComposer for backward compatibility -export { default as EnhancedComposer } from './ComposerCore.svelte' +// Export the main composer component +export { default } from './ComposerCore.svelte' +export { default as Composer } from './ComposerCore.svelte' // Export types export type { ComposerVariant, ComposerFeatures, ComposerProps } from './types' diff --git a/src/routes/admin/posts/[id]/edit/+page.svelte b/src/routes/admin/posts/[id]/edit/+page.svelte index 246d88f..22cf298 100644 --- a/src/routes/admin/posts/[id]/edit/+page.svelte +++ b/src/routes/admin/posts/[id]/edit/+page.svelte @@ -3,7 +3,7 @@ import { goto } from '$app/navigation' import { onMount } from 'svelte' import AdminPage from '$lib/components/admin/AdminPage.svelte' - import EnhancedComposer from '$lib/components/admin/EnhancedComposer.svelte' + import Composer from '$lib/components/admin/composer' import LoadingSpinner from '$lib/components/admin/LoadingSpinner.svelte' import PostMetadataPopover from '$lib/components/admin/PostMetadataPopover.svelte' import DeleteConfirmationModal from '$lib/components/admin/DeleteConfirmationModal.svelte' @@ -396,7 +396,7 @@ {#if config?.showContent && contentReady}
- +
{/if}
diff --git a/src/routes/admin/posts/new/+page.svelte b/src/routes/admin/posts/new/+page.svelte index 8682f9e..c435f8a 100644 --- a/src/routes/admin/posts/new/+page.svelte +++ b/src/routes/admin/posts/new/+page.svelte @@ -3,7 +3,7 @@ import { goto } from '$app/navigation' import { onMount } from 'svelte' import AdminPage from '$lib/components/admin/AdminPage.svelte' - import EnhancedComposer from '$lib/components/admin/EnhancedComposer.svelte' + import Composer from '$lib/components/admin/composer' import PostMetadataPopover from '$lib/components/admin/PostMetadataPopover.svelte' import Button from '$lib/components/admin/Button.svelte' import PublishDropdown from '$lib/components/admin/PublishDropdown.svelte' @@ -199,7 +199,7 @@ {#if config?.showContent}
- +
{/if}