From 660403264365e8ab9500391d7baf398797b67263 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Tue, 24 Jun 2025 01:11:57 +0100 Subject: [PATCH] refactor(editor): consolidate editors into unified EnhancedComposer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create EnhancedComposer as the single unified editor component - Remove redundant editor components (Editor, EditorWithUpload, CaseStudyEditor, UniverseComposer) - Add editor-extensions.ts for centralized extension configuration - Enhance image placeholder with better UI and selection support - Update editor commands and slash command groups - Improve editor state management and content handling Simplifies the codebase by having one powerful editor component instead of multiple specialized ones. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../components/admin/CaseStudyEditor.svelte | 166 ---- src/lib/components/admin/Editor.svelte | 446 --------- ...hUpload.svelte => EnhancedComposer.svelte} | 717 +++++++++------ .../components/admin/UniverseComposer.svelte | 852 ------------------ src/lib/components/edra/commands/commands.ts | 100 +- src/lib/components/edra/editor-extensions.ts | 127 +++ .../edra/extensions/slash-command/groups.ts | 8 + .../EnhancedImagePlaceholder.svelte | 297 ++++++ .../components/edra/headless/editor.svelte | 54 +- 9 files changed, 902 insertions(+), 1865 deletions(-) delete mode 100644 src/lib/components/admin/CaseStudyEditor.svelte delete mode 100644 src/lib/components/admin/Editor.svelte rename src/lib/components/admin/{EditorWithUpload.svelte => EnhancedComposer.svelte} (63%) delete mode 100644 src/lib/components/admin/UniverseComposer.svelte create mode 100644 src/lib/components/edra/editor-extensions.ts create mode 100644 src/lib/components/edra/headless/components/EnhancedImagePlaceholder.svelte diff --git a/src/lib/components/admin/CaseStudyEditor.svelte b/src/lib/components/admin/CaseStudyEditor.svelte deleted file mode 100644 index 6f540b8..0000000 --- a/src/lib/components/admin/CaseStudyEditor.svelte +++ /dev/null @@ -1,166 +0,0 @@ - - -
- -
- - diff --git a/src/lib/components/admin/Editor.svelte b/src/lib/components/admin/Editor.svelte deleted file mode 100644 index 6278ca5..0000000 --- a/src/lib/components/admin/Editor.svelte +++ /dev/null @@ -1,446 +0,0 @@ - - -
-
- { - editor = e - }} - /> -
-
- - diff --git a/src/lib/components/admin/EditorWithUpload.svelte b/src/lib/components/admin/EnhancedComposer.svelte similarity index 63% rename from src/lib/components/admin/EditorWithUpload.svelte rename to src/lib/components/admin/EnhancedComposer.svelte index c6a0da5..17cb17d 100644 --- a/src/lib/components/admin/EditorWithUpload.svelte +++ b/src/lib/components/admin/EnhancedComposer.svelte @@ -1,52 +1,15 @@ -
+
{#if showToolbar && editor && !isLoading}
@@ -576,84 +678,89 @@ {/each} - -
- -
+ Insert + + + + +
- + + {/if} - { - 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() + {#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() + }} + /> + { + 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 false && showLinkBubbleMenu} - - {/if} - {#if showTableBubbleMenu} + + {#if features.tables} {/if} {/if} + {#if !editor}
Loading...
{/if} +
-{#if showMediaDropdown} +{#if showMediaDropdown && features.mediaLibrary}
{ - editor?.chain().focus().insertImagePlaceholder().run() + if (editor) { + // Get current position before inserting placeholder + const pos = editor.state.selection.anchor + + // Insert placeholder + editor.chain().focus().insertImagePlaceholder().run() + + // Store the position for later deletion + editor.storage.imageModal = { placeholderPos: pos } + + // Open the modal through the store + mediaSelectionStore.open({ + mode: 'single', + fileType: 'image', + albumId, + onSelect: handleGlobalMediaSelect, + onClose: handleGlobalMediaClose + }) + } showMediaDropdown = false }} > @@ -716,17 +842,28 @@ + {#if features.urlEmbed} + + {/if}
{/if} - + {#if showTextStyleDropdown} - -
- { - content = newContent - characterCount = getTextFromContent(newContent) - }} - placeholder="What's on your mind?" - minHeight={80} - autofocus={true} - mode="inline" - showToolbar={false} - /> - - {#if attachedPhotos.length > 0} -
- {#each attachedPhotos as photo} -
- - -
- {/each} -
- {/if} - - -
- - -{:else if mode === 'page'} - {#if postType === 'essay'} -
-
-

New Essay

-
- - -
-
- - - - - - -
- {#if essayTab === 0} - - {:else} -
- { - content = newContent - characterCount = getTextFromContent(newContent) - }} - placeholder="Start writing your essay..." - minHeight={500} - autofocus={true} - mode="default" - /> -
- {/if} -
-
- {:else} -
- {#if hasContent()} - - {/if} -
- { - content = newContent - characterCount = getTextFromContent(newContent) - }} - placeholder="What's on your mind?" - minHeight={120} - autofocus={true} - mode="inline" - showToolbar={false} - /> - - {#if attachedPhotos.length > 0} -
- {#each attachedPhotos as photo} -
- - -
- {/each} -
- {/if} - - -
-
- {/if} -{/if} - - - - - - - - -{#if selectedMedia} - -{/if} - - diff --git a/src/lib/components/edra/commands/commands.ts b/src/lib/components/edra/commands/commands.ts index 32f8f4d..1567f6b 100644 --- a/src/lib/components/edra/commands/commands.ts +++ b/src/lib/components/edra/commands/commands.ts @@ -255,6 +255,8 @@ export const commands: Record = { name: 'image-placeholder', label: 'Image', action: (editor) => { + // Set flag to auto-open modal and insert placeholder + editor.storage.imageModal = { autoOpen: true } editor.chain().focus().insertImagePlaceholder().run() } }, @@ -281,6 +283,14 @@ export const commands: Record = { action: (editor) => { editor.chain().focus().insertIFramePlaceholder().run() } + }, + { + iconName: 'MapPin', + name: 'geolocation-placeholder', + label: 'Location', + action: (editor) => { + editor.chain().focus().insertGeolocationPlaceholder().run() + } } ] }, @@ -349,95 +359,5 @@ export const commands: Record = { } } ] - }, - lists: { - name: 'Lists', - label: 'Lists', - commands: [ - { - iconName: 'List', - name: 'bulletList', - label: 'Bullet List', - shortCuts: [`${isMac ? 'Cmd' : 'Ctrl'}+Shift+8`], - action: (editor) => { - editor.chain().focus().toggleBulletList().run() - }, - isActive: (editor) => editor.isActive('bulletList') - }, - { - iconName: 'ListOrdered', - name: 'orderedList', - label: 'Ordered List', - shortCuts: [`${isMac ? 'Cmd' : 'Ctrl'}+Shift+7`], - action: (editor) => { - editor.chain().focus().toggleOrderedList().run() - }, - isActive: (editor) => editor.isActive('orderedList') - }, - { - iconName: 'ListTodo', - name: 'taskList', - label: 'Task List', - shortCuts: [`${isMac ? 'Cmd' : 'Ctrl'}+Shift+9`], - action: (editor) => { - editor.chain().focus().toggleTaskList().run() - }, - isActive: (editor) => editor.isActive('taskList') - } - ] - }, - media: { - name: 'Media', - label: 'Media', - commands: [ - { - iconName: 'Image', - name: 'image-placeholder', - label: 'Image', - action: (editor) => { - editor.chain().focus().insertImagePlaceholder().run() - } - }, - { - iconName: 'Images', - name: 'gallery-placeholder', - label: 'Gallery', - action: (editor) => { - editor.chain().focus().insertGalleryPlaceholder().run() - } - }, - { - iconName: 'Video', - name: 'video-placeholder', - label: 'Video', - action: (editor) => { - editor.chain().focus().insertVideoPlaceholder().run() - } - }, - { - iconName: 'Mic', - name: 'audio-placeholder', - label: 'Audio', - action: (editor) => { - editor.chain().focus().insertAudioPlaceholder().run() - } - }, - { - iconName: 'Code', - name: 'iframe-placeholder', - label: 'Iframe', - action: (editor) => { - editor.chain().focus().insertIframePlaceholder().run() - } - }, - { - iconName: 'Link', - name: 'url-embed-placeholder', - label: 'URL Embed', - action: (editor) => { - editor.chain().focus().insertUrlEmbedPlaceholder().run() - } - } - ] } } diff --git a/src/lib/components/edra/editor-extensions.ts b/src/lib/components/edra/editor-extensions.ts new file mode 100644 index 0000000..1ab2f4a --- /dev/null +++ b/src/lib/components/edra/editor-extensions.ts @@ -0,0 +1,127 @@ +import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight' +import { all, createLowlight } from 'lowlight' +import { SvelteNodeViewRenderer } from 'svelte-tiptap' +import type { Extensions } from '@tiptap/core' + +// Extension classes +import { AudioPlaceholder } from './extensions/audio/AudioPlaceholder.js' +import { ImagePlaceholder } from './extensions/image/ImagePlaceholder.js' +import { VideoPlaceholder } from './extensions/video/VideoPlaceholder.js' +import { AudioExtended } from './extensions/audio/AudiExtended.js' +import { ImageExtended } from './extensions/image/ImageExtended.js' +import { VideoExtended } from './extensions/video/VideoExtended.js' +import { GalleryPlaceholder } from './extensions/gallery/GalleryPlaceholder.js' +import { GalleryExtended } from './extensions/gallery/GalleryExtended.js' +import { IFramePlaceholder } from './extensions/iframe/IFramePlaceholder.js' +import { IFrameExtended } from './extensions/iframe/IFrameExtended.js' +import { UrlEmbed } from './extensions/url-embed/UrlEmbed.js' +import { UrlEmbedPlaceholder } from './extensions/url-embed/UrlEmbedPlaceholder.js' +import { UrlEmbedExtended } from './extensions/url-embed/UrlEmbedExtended.js' +import { LinkContextMenu } from './extensions/link-context-menu/LinkContextMenu.js' +import { GeolocationPlaceholder } from './extensions/geolocation/GeolocationPlaceholder.js' +import { GeolocationExtended } from './extensions/geolocation/GeolocationExtended.js' +import slashcommand from './extensions/slash-command/slashcommand.js' + +// Component imports +import CodeExtended from './headless/components/CodeExtended.svelte' +import AudioPlaceholderComponent from './headless/components/AudioPlaceholder.svelte' +import AudioExtendedComponent from './headless/components/AudioExtended.svelte' +import ImagePlaceholderComponent from './headless/components/ImagePlaceholder.svelte' +import ImageExtendedComponent from './headless/components/ImageExtended.svelte' +import VideoPlaceholderComponent from './headless/components/VideoPlaceholder.svelte' +import VideoExtendedComponent from './headless/components/VideoExtended.svelte' +import GalleryPlaceholderComponent from './headless/components/GalleryPlaceholder.svelte' +import GalleryExtendedComponent from './headless/components/GalleryExtended.svelte' +import IFramePlaceholderComponent from './headless/components/IFramePlaceholder.svelte' +import IFrameExtendedComponent from './headless/components/IFrameExtended.svelte' +import UrlEmbedPlaceholderComponent from './headless/components/UrlEmbedPlaceholder.svelte' +import UrlEmbedExtendedComponent from './headless/components/UrlEmbedExtended.svelte' +import GeolocationPlaceholderComponent from './headless/components/GeolocationPlaceholder.svelte' +import GeolocationExtendedComponent from './headless/components/GeolocationExtended.svelte' +import SlashCommandList from './headless/components/SlashCommandList.svelte' + +// Create lowlight instance +const lowlight = createLowlight(all) + +export interface EditorExtensionOptions { + showSlashCommands?: boolean + onShowUrlConvertDropdown?: (pos: number, url: string) => void + onShowLinkContextMenu?: (pos: number, url: string, coords: { x: number; y: number }) => void + imagePlaceholderComponent?: any // Allow custom image placeholder component +} + +export function getEditorExtensions(options: EditorExtensionOptions = {}): Extensions { + const { + showSlashCommands = true, + onShowUrlConvertDropdown, + onShowLinkContextMenu, + imagePlaceholderComponent = ImagePlaceholderComponent + } = options + + const extensions: Extensions = [ + CodeBlockLowlight.configure({ + lowlight + }).extend({ + addNodeView() { + return SvelteNodeViewRenderer(CodeExtended) + } + }), + AudioPlaceholder(AudioPlaceholderComponent), + ImagePlaceholder(imagePlaceholderComponent), + VideoPlaceholder(VideoPlaceholderComponent), + AudioExtended(AudioExtendedComponent), + ImageExtended(ImageExtendedComponent), + VideoExtended(VideoExtendedComponent), + GalleryPlaceholder(GalleryPlaceholderComponent), + GalleryExtended(GalleryExtendedComponent), + IFramePlaceholder(IFramePlaceholderComponent), + IFrameExtended(IFrameExtendedComponent), + GeolocationPlaceholder(GeolocationPlaceholderComponent), + GeolocationExtended(GeolocationExtendedComponent) + ] + + // Add URL embed extensions with callbacks if provided + if (onShowUrlConvertDropdown) { + extensions.push( + UrlEmbed.configure({ onShowDropdown: onShowUrlConvertDropdown }), + UrlEmbedPlaceholder(UrlEmbedPlaceholderComponent), + UrlEmbedExtended(UrlEmbedExtendedComponent) + ) + } else { + extensions.push( + UrlEmbed, + UrlEmbedPlaceholder(UrlEmbedPlaceholderComponent), + UrlEmbedExtended(UrlEmbedExtendedComponent) + ) + } + + // Add link context menu if callback provided + if (onShowLinkContextMenu) { + extensions.push(LinkContextMenu.configure({ onShowContextMenu: onShowLinkContextMenu })) + } else { + extensions.push(LinkContextMenu) + } + + // Add slash commands if enabled + if (showSlashCommands) { + extensions.push(slashcommand(SlashCommandList)) + } + + return extensions +} + +// Extension presets for different editor variants +export const EDITOR_PRESETS = { + full: { + showSlashCommands: true, + includeAllExtensions: true + }, + inline: { + showSlashCommands: true, + includeAllExtensions: true + }, + minimal: { + showSlashCommands: false, + includeAllExtensions: false + } +} diff --git a/src/lib/components/edra/extensions/slash-command/groups.ts b/src/lib/components/edra/extensions/slash-command/groups.ts index bea3631..8222026 100644 --- a/src/lib/components/edra/extensions/slash-command/groups.ts +++ b/src/lib/components/edra/extensions/slash-command/groups.ts @@ -40,6 +40,14 @@ export const GROUPS: Group[] = [ commands: [ ...commands.media.commands, ...commands.table.commands, + { + iconName: 'MapPin', + name: 'geolocation-placeholder', + label: 'Location', + action: (editor: Editor) => { + editor.chain().focus().insertGeolocationPlaceholder().run() + } + }, { iconName: 'Minus', name: 'horizontalRule', diff --git a/src/lib/components/edra/headless/components/EnhancedImagePlaceholder.svelte b/src/lib/components/edra/headless/components/EnhancedImagePlaceholder.svelte new file mode 100644 index 0000000..bd6bd2b --- /dev/null +++ b/src/lib/components/edra/headless/components/EnhancedImagePlaceholder.svelte @@ -0,0 +1,297 @@ + + + +
+ {#if isUploading} +
+
+ Uploading... +
+ {:else if !autoOpenModal} + + + + {/if} +
+ + + +
+ + diff --git a/src/lib/components/edra/headless/editor.svelte b/src/lib/components/edra/headless/editor.svelte index 4ca53ae..dde706b 100644 --- a/src/lib/components/edra/headless/editor.svelte +++ b/src/lib/components/edra/headless/editor.svelte @@ -3,45 +3,16 @@ import { onMount } from 'svelte' import { initiateEditor } from '../editor.js' + import { getEditorExtensions } from '../editor-extensions.js' import './style.css' import 'katex/dist/katex.min.css' - - // Lowlight - import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight' - import { all, createLowlight } from 'lowlight' import '../editor.css' import '../onedark.css' - import { SvelteNodeViewRenderer } from 'svelte-tiptap' - import CodeExtended from './components/CodeExtended.svelte' - import { AudioPlaceholder } from '../extensions/audio/AudioPlaceholder.js' - import AudioPlaceholderComponent from './components/AudioPlaceholder.svelte' - import AudioExtendedComponent from './components/AudioExtended.svelte' - import { ImagePlaceholder } from '../extensions/image/ImagePlaceholder.js' - import ImagePlaceholderComponent from './components/ImagePlaceholder.svelte' - import { VideoPlaceholder } from '../extensions/video/VideoPlaceholder.js' - import VideoPlaceholderComponent from './components/VideoPlaceholder.svelte' - import { ImageExtended } from '../extensions/image/ImageExtended.js' - import ImageExtendedComponent from './components/ImageExtended.svelte' - import VideoExtendedComponent from './components/VideoExtended.svelte' - import { VideoExtended } from '../extensions/video/VideoExtended.js' - import { AudioExtended } from '../extensions/audio/AudiExtended.js' - import { GalleryPlaceholder } from '../extensions/gallery/GalleryPlaceholder.js' - import GalleryPlaceholderComponent from './components/GalleryPlaceholder.svelte' - import { GalleryExtended } from '../extensions/gallery/GalleryExtended.js' - import GalleryExtendedComponent from './components/GalleryExtended.svelte' import LinkMenu from './menus/link-menu.svelte' import TableRowMenu from './menus/table/table-row-menu.svelte' import TableColMenu from './menus/table/table-col-menu.svelte' - import slashcommand from '../extensions/slash-command/slashcommand.js' - import SlashCommandList from './components/SlashCommandList.svelte' import LoaderCircle from 'lucide-svelte/icons/loader-circle' import { focusEditor, type EdraProps } from '../utils.js' - import IFramePlaceholderComponent from './components/IFramePlaceholder.svelte' - import { IFramePlaceholder } from '../extensions/iframe/IFramePlaceholder.js' - import { IFrameExtended } from '../extensions/iframe/IFrameExtended.js' - import IFrameExtendedComponent from './components/IFrameExtended.svelte' - - const lowlight = createLowlight(all) let { class: className = '', @@ -60,30 +31,13 @@ let element = $state() onMount(() => { + const extensions = getEditorExtensions({ showSlashCommands }) + editor = initiateEditor( element, content, limit, - [ - CodeBlockLowlight.configure({ - lowlight - }).extend({ - addNodeView() { - return SvelteNodeViewRenderer(CodeExtended) - } - }), - AudioPlaceholder(AudioPlaceholderComponent), - ImagePlaceholder(ImagePlaceholderComponent), - GalleryPlaceholder(GalleryPlaceholderComponent), - IFramePlaceholder(IFramePlaceholderComponent), - IFrameExtended(IFrameExtendedComponent), - VideoPlaceholder(VideoPlaceholderComponent), - AudioExtended(AudioExtendedComponent), - ImageExtended(ImageExtendedComponent), - GalleryExtended(GalleryExtendedComponent), - VideoExtended(VideoExtendedComponent), - ...(showSlashCommands ? [slashcommand(SlashCommandList)] : []) - ], + extensions, { editable, onUpdate,