Skip to main content

Introduction

Noteverse’s editor is built on top of Novel.sh, which provides a powerful React wrapper around TipTap. The editor supports real-time collaboration, rich text formatting, media embeds, and custom extensions.

Architecture

The editor is composed of several key components:
  • EditorRoot: Container that manages editor state
  • EditorContent: The main editing surface
  • EditorBubble: Floating toolbar for text formatting
  • EditorCommand: Slash command menu for quick actions
  • Custom Extensions: Collaborative features and enhanced functionality

Component Props

content
JSONContent
Initial editor content in JSON format from TipTap
onChange
(value: JSONContent) => void
required
Callback fired when editor content changes
className
string
Additional CSS classes for the editor container
placeholder
string
Placeholder text shown when editor is empty
connectedUsers
any[]
Array of connected users for collaborative editing
userData
any
required
Current user data for collaboration features
notesId
number
required
Unique identifier for the note being edited
canEdit
boolean
required
Whether the user has permission to edit
authToken
string
required
Authentication token for API requests

Basic Usage

import Editor from '@/components/editor/editor'
import { useState } from 'react'
import { JSONContent } from 'novel'

export default function MyEditor() {
  const [content, setContent] = useState<JSONContent>()

  return (
    <Editor
      content={content}
      onChange={setContent}
      userData={{ id: 1, name: 'User' }}
      notesId={123}
      canEdit={true}
      authToken="your-token"
      placeholder="Start writing..."
    />
  )
}

Real-Time Collaboration

The editor integrates with Socket.IO for real-time collaboration features:

Cursor Position Tracking

When content updates, the editor broadcasts cursor position:
socket.emit(
  'updateUser',
  {
    userId: userData.id,
    notesId,
    updates: { position: cursorPosition },
  },
  (response) => {
    if (response.success) {
      setLiveUsers(response.users)
    }
  },
)

Multiple Carets

The editor displays colored carets for all connected users using the MultipleCarets extension:
MultipleCarets.configure({
  carets: connectedUsers.map((u) => ({
    position: u.position,
    name: u.userName,
    color: u.color,
    isActive: u.isActive || false,
  })),
})

Editor Configuration

Editor Props

The editor is configured with several TipTap properties:
editorProps={{
  handleDOMEvents: {
    keydown: (_view, event) => handleCommandNavigation(event),
  },
}}
Handles keyboard navigation for command menu.

Bubble Menu

The floating toolbar appears when text is selected:
<EditorBubble
  tippyOptions={{
    placement: 'top',
    zIndex: 10,
  }}
  className="flex w-fit max-w-[90vw] overflow-hidden rounded-md border"
>
  <NodeSelector open={openNode} onOpenChange={setOpenNode} />
  <LinkSelector open={openLink} onOpenChange={setOpenLink} />
  <TextButtons />
  <ColorSelector open={openColor} onOpenChange={setOpenColor} />
</EditorBubble>
See the Selectors documentation for detailed information.

Command Menu

Press / to open the command menu for quick formatting:
<EditorCommand className="z-20 h-auto max-h-[330px] overflow-y-auto">
  <EditorCommandEmpty>No results</EditorCommandEmpty>
  <EditorCommandList>
    {suggestionItems.map((item) => (
      <EditorCommandItem
        value={item.title}
        onCommand={(val) => item.command?.(val)}
        key={item.title}
      >
        <div className="flex h-10 w-10 items-center justify-center">
          {item.icon}
        </div>
        <div>
          <p className="font-medium">{item.title}</p>
          <p className="text-xs text-muted-foreground">
            {item.description}
          </p>
        </div>
      </EditorCommandItem>
    ))}
  </EditorCommandList>
</EditorCommand>
See the Slash Commands documentation for available commands.

Image Upload

The editor includes image upload functionality with the ImageResizer component:
<EditorContent
  // ... other props
  slotAfter={<ImageResizer />}
/>
Images can be:
  • Pasted from clipboard
  • Dragged and dropped
  • Uploaded via slash command
You need to implement the uploadFn function in image-upload.ts to handle your image storage solution.

Extensions

The editor uses a combination of built-in and custom extensions:
extensions={[
  ...defaultExtensions,
  slashCommand,
  TextSearch,
  MultipleCarets.configure({
    carets: connectedUsers.map((u) => ({
      position: u.position,
      name: u.userName,
      color: u.color,
      isActive: u.isActive || false,
    })),
  }),
]}
See the Extensions documentation for the complete list.

Accessing the Editor Instance

The editor instance is managed through context:
import { useEditorContext } from '@/context/editorContext'

function MyComponent() {
  const { editor, setEditor } = useEditorContext()
  
  // Use editor instance
  const getContent = () => {
    return editor.getJSON()
  }
  
  const clearContent = () => {
    editor.commands.clearContent()
  }
}

TypeScript Interface

interface EditorProp {
  content?: JSONContent
  onChange: (value: JSONContent) => void
  className?: string
  placeholder?: string
  connectedUsers?: any
  userData: any
  notesId: number
  canEdit: boolean
  authToken: string
}

Styling

The editor includes custom ProseMirror styles:
import './style/prosemirror.css'
The editor container can be styled with Tailwind classes:
<EditorContent
  className={cn('border rounded-xl w-full pb-10', className)}
/>

Next Steps

Extensions

Learn about TipTap extensions and configuration

Slash Commands

Explore available slash commands

Selectors

Understand the bubble menu selectors

Build docs developers (and LLMs) love