Novel Svelte

repository·main·Indexed 43 days ago

https://github.com/tglide/novel-svelte

A Svelte-based implementation of the Novel editor providing rich text editing capabilities. Built on Tiptap, it includes features such as slash menus, bubble menus, AI completions triggered by '++', local storage persistence, and image uploading. Version 0.1.7 provides a primary Editor component, a ColorSelector, and pre-configured defaultExtensions for Tailwind CSS styling.

Tokens
6K
Snippets
21
Records
24
Agent score
55%

What's inside Novel Svelte

  1. Trigger AI completions with '++'

    main

    The editor has built-in AI completion functionality. When a user types the characters ++ at the end of their text, the editor automatically:

    1. Deletes the ++ characters.
    2. Sends the preceding text (up to 5000 characters) to the completionApi endpoint.
    3. Streams the completion into the editor.

    Ensure your completionApi endpoint is correctly configured to handle the request and return the expected completion format.

  2. Implement the Suggestion UI lifecycle with `renderItems`

    main

    To render the slash command menu, you must implement a render function that returns a lifecycle object. This object manages a Svelte component (e.g., CommandList) and a floating UI element (typically using tippy.js).

    Lifecycle Hooks

    • onStart({ editor, clientRect }): Initializes the Svelte component and the floating popup. clientRect provides the position of the cursor/range.
    • onUpdate({ editor, clientRect }): Updates the Svelte component's props and repositions the popup as the user types or moves the cursor.
    • onKeyDown({ event }): Handles keyboard interactions, such as closing the menu when the Escape key is pressed.
    • onExit(): Cleans up the Svelte component and destroys the popup element.
    // The render function structure required by the SlashCommand extension
    const renderItems = () => {
      let component: any = null;
      let popup: any = null;
    
      return {
        onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
          // Initialize Svelte component and Tippy popup
        },
        onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => {
          // Update component props and popup position
        },
        onKeyDown: (props: { event: KeyboardEvent }) => {
          // Handle key events like 'Escape'
        },
        onExit: () => {
          // Cleanup component and popup
        }
      };
    };
  3. Use the Novel Editor component

    main

    The Editor component (exported from src/lib/ui/editor/index.svelte) is the primary entry point for the Novel Svelte editor. It is built on top of Tiptap and includes built-in support for AI completions (triggered by typing ++), local storage persistence, and image resizing.

    By default, it uses a local storage key to persist content and provides hooks for real-time and debounced updates.

    <script>
      import Editor from '$lib/ui/editor/index.svelte';
      
      function handleUpdate(editor) {
        console.log('Editor updated:', editor.getJSON());
      }
    </script>
    
    <Editor 
      defaultValue="<p>Hello World</p>" 
      onUpdate={handleUpdate} 
      class="my-custom-editor-class"
    />
  4. Implement image uploading in the editor

    main

    To enable image uploading with visual placeholders, you need to use the UploadImagesPlugin and the startImageUpload function.

    1. Register the plugin: Add UploadImagesPlugin to your Tiptap editor configuration. This plugin manages the lifecycle of image placeholders (showing a low-opacity version of the image while it uploads).
    2. Trigger the upload: Call startImageUpload(file, view, pos) when a user selects a file.

    Validation Rules:

    • Only files with an image/ MIME type are accepted.
    • Maximum file size is 20MB.
    • If validation fails, an error toast is displayed via addToast.

    Workflow:

    • startImageUpload immediately creates a local Data URL preview and inserts a placeholder widget into the editor at the specified pos.
    • It then calls handleImageUpload(file) to perform the actual upload.
    • Once the upload completes, the placeholder is removed and the actual image node is inserted into the document.
    import UploadImagesPlugin, { startImageUpload } from './path-to/upload-images';
    
    // In your editor setup
    const editor = new Editor({
      // ... other config
      extensions: [
        // ... other extensions
        UploadImagesPlugin(),
      ],
    });
    
    // When a file is dropped or selected
    const onFileSelect = (file: File) => {
      const { view } = editor;
      const { state } = view;
      const pos = state.selection.from;
      startImageUpload(file, view, pos);
    };
  5. Configure local storage persistence

    main

    The editor automatically persists its content to the browser's localStorage to prevent data loss. You can control this behavior using the following props:

    • storageKey: Set this to a custom string to change which key is used in localStorage (default: 'novel__content').
    • disableLocalStorage: Set this to true to completely disable the auto-save and auto-load functionality.
    <Editor 
      storageKey="my-app-editor-content" 
      disableLocalStorage={false} 
    />
  6. Use the default editor content for quickstart

    main

    The defaultEditorContent constant provides a pre-configured JSON object representing a document structure compatible with the editor. This is useful for initializing an editor with sample content (including headings, paragraphs, links, code blocks, and task lists) during development or for quickstart demonstrations.

    import { defaultEditorContent } from 'novel-svelte/default-content';
    
    // Use this object to initialize your editor instance
    const initialContent = defaultEditorContent;
  7. Use the content generation API endpoint

    main

    You can call the POST /api/generate endpoint by sending a JSON body containing a prompt. The server will return a stream of text generated by gpt-3.5-turbo. The AI is configured as a writing assistant that prioritizes recent context and limits responses to approximately 200 characters to ensure complete sentences.

    const response = await fetch('/api/generate', {
      method: 'POST',
      body: JSON.stringify({
        prompt: 'Once upon a time in a galaxy far away...'
      }),
    });
    
    // The response is a stream that can be read using the ReadableStream API
    const reader = response.body?.getReader();
    // ... handle stream
  8. Use startImageUpload to handle image uploads

    main

    The startImageUpload function manages the transition from a local file to an editor image node. It handles validation, placeholder insertion, and the final replacement of the placeholder with the uploaded image source.

    Parameters:

    • file: File: The image file to be uploaded.
    • view: EditorView: The current Tiptap/ProseMirror editor view.
    • pos: number: The position in the document where the image should be inserted.

    Returns: void (it dispatches transactions to the editor view directly).

    import { startImageUpload } from './path-to/upload-images';
    
    // Example usage within an event handler
    startImageUpload(myFile, editorView, currentPosition);
  9. Use the BubbleMenu component with Tiptap

    main

    The BubbleMenu component provides a floating UI toolbar that appears when text is selected in a Tiptap editor. It includes built-in support for common text formatting (Bold, Italic, Underline, Strike, Code) as well as specialized selectors for Nodes, Links, and Colors.

    To use it, pass an active Tiptap Editor instance to the component. You can customize its behavior using props like tippyOptions, shouldShow, and updateDelay.

    Props

    • editor: (Required) The Tiptap Editor instance.
    • tippyOptions: Configuration for the underlying Tippy.js positioning engine. Includes an onHidden callback to reset internal selector states.
    • pluginKey: A unique string identifier for the bubble menu plugin (defaults to 'SvelteTiptapBubbleMenu').
    • shouldShow: A function determining when the menu is visible. The default implementation hides the menu if an image is selected or if the selection size is 0.
    • updateDelay: The delay in milliseconds before the menu updates its position/visibility (defaults to 250).
    <script>
      import { Editor } from '@tiptap/core';
      import BubbleMenu from '$lib/ui/editor/bubble-menu';
    
      let editor = new Editor({ /* ... */ });
    </script>
    
    <BubbleMenu {editor} />
  10. Configure Novel Editor props

    main

    The following props can be passed to the Editor component to customize its behavior:

    PropTypeDefaultDescription
    completionApistring'/api/generate'The API route used for OpenAI completions.
    classstring(long Tailwind string)Additional CSS classes for the editor container.
    defaultValueJSONContent | stringdefaultEditorContentThe initial content of the editor.
    extensionsExtension[][]Additional Tiptap extensions to add to the default set.
    editorPropsEditorProps{}Props passed directly to the underlying Tiptap editor.
    onUpdate(editor?: Editor) => void | Promise<void>noopCallback triggered on every editor update.
    onDebouncedUpdate(editor?: Editor) => void | Promise<void>noopCallback triggered after debounceDuration has passed.
    debounceDurationnumber750Delay in milliseconds for onDebouncedUpdate.
    storageKeystring'novel__content'The localStorage key used to persist content.
    disableLocalStoragebooleanfalseIf true, prevents reading from or saving to localStorage.
    editorEditor | undefinedundefinedAllows passing an existing Tiptap editor instance.
  11. Use defaultExtensions for editor configuration

    main

    The defaultExtensions constant provides a pre-configured array of Tiptap extensions tailored for the Novel Svelte editor. It includes essential features like StarterKit, Markdown support, image uploading, slash commands, and specific styling for lists, blockquotes, and code blocks via Tailwind CSS classes.

    When initializing your editor, you should pass this array to the extensions configuration to ensure all built-in behaviors (like the / slash command and image handling) work as expected.

    import { defaultExtensions } from '$lib/ui/editor/extensions';
    
    // Example usage when initializing a Tiptap editor
    const editor = new Editor({
      extensions: defaultExtensions,
      // ... other config
    });
  12. Use the ColorSelector Svelte component

    main

    The ColorSelector component is a bubble menu UI element used to change text color and background highlight color in a Tiptap editor. It requires an instance of a Tiptap Editor and a boolean isOpen state to manage its visibility.

    Props

    • editor: The Tiptap Editor instance.
    • isOpen: A boolean controlling whether the color popover is currently visible.

    Behavior

    • Text Color: Selecting a color (other than 'Default') applies the color using editor.chain().focus().setColor(color).run(). Selecting 'Default' calls editor.commands.unsetColor().
    • Background Highlight: Selecting a color (other than 'Default') applies the highlight using editor.commands.setHighlight({ color }). Selecting 'Default' calls editor.commands.unsetHighlight().
    • State Sync: The component uses Melt UI's popover logic and synchronizes its internal open state with the isOpen prop via an onOpenChange callback.
    <script>
      import ColorSelector from './path/to/color-selector.svelte';
      import { editor } from './your-editor-setup';
      let isColorMenuOpen = false;
    </script>
    
    <ColorSelector {editor} bind:isOpen={isColorMenuOpen} />