Skip to main content

Editor System

The template includes a rich text editor built on TipTap (ProseMirror) with a modular architecture of extensions, toolbar components, hooks, and utility functions. The editor supports headings, lists, task lists, images, code blocks, text formatting, and more.

Architecture Overview

Source Files

DirectoryContents
lib/editor/extensions/TipTap extension re-exports and configuration
lib/editor/components/UI components (toolbar buttons, popovers, icons)
lib/editor/hooks/React hooks for editor state management
lib/editor/providers/Editor context provider with extension setup
lib/editor/contents/Toolbar layout and editor content components
lib/editor/utils/Utility functions (shortcuts, validation, upload)

Extension Configuration

Extensions are registered in the EditorContextProvider. The StarterKit provides base functionality, with additional extensions layered on top:

const extensions = useMemo(() => [
StarterKit.configure({
horizontalRule: false,
link: { openOnClick: false, enableClickSelection: true },
}),
HorizontalRule,
TextAlign.configure({ types: ['heading', 'paragraph'] }),
ImageUploadNode.configure({
accept: 'image/*',
maxSize: MAX_FILE_SIZE, // 5MB
limit: 3,
upload: handleImageUpload,
onError: (error) => console.error('Upload failed:', error),
}),
TaskList,
TaskItem.configure({ nested: true }),
Highlight.configure({ multicolor: true }),
Image,
Typography,
Superscript,
Subscript,
Selection,
], []);

Extension Summary

ExtensionSourcePurpose
StarterKit@tiptap/starter-kitParagraphs, bold, italic, lists, code, blockquote
HorizontalRule@tiptap/extension-horizontal-ruleHorizontal dividers
TextAlign@tiptap/extension-text-alignLeft, center, right, justify alignment
TaskList / TaskItem@tiptap/extension-listInteractive checkbox lists
Highlight@tiptap/extension-highlightMulti-color text highlighting
Typography@tiptap/extension-typographySmart quotes, dashes, ellipsis
Superscript@tiptap/extension-superscriptSuperscript text
Subscript@tiptap/extension-subscriptSubscript text
Selection@tiptap/extensionsEnhanced selection handling
Image@tiptap/extension-imageStatic image display
ImageUploadNodeCustomDrag-and-drop image upload with progress

Editor Context Provider

The editor is provided via React Context for tree-wide access:

export const EditorContext = createContext<Editor | null>(null);

export function EditorContextProvider({ children }: { children: React.ReactNode }) {
const editor = useEditor({
immediatelyRender: false,
shouldRerenderOnTransaction: false,
editorProps: {
attributes: {
autocomplete: 'on',
autocorrect: 'on',
autocapitalize: 'off',
'aria-label': 'Main content area, start typing to enter text.',
class: cn('min-h-96'),
},
},
extensions,
});

return <EditorContext.Provider value={editor}>{children}</EditorContext.Provider>;
}

Key configuration choices:

  • immediatelyRender: false prevents SSR hydration mismatches
  • shouldRerenderOnTransaction: false optimizes performance by avoiding unnecessary re-renders

Toolbar Configuration

The ToolbarContent component defines the complete toolbar layout organized in groups:

GroupComponents
HistoryUndo, Redo
Block TypesHeading Dropdown (H1-H4), List Dropdown (bullet, ordered, task), Blockquote, Code Block
Inline MarksBold, Italic, Strikethrough, Code, Underline, Color Highlight, Link
ScriptSuperscript, Subscript
AlignmentLeft, Center, Right, Justify
MediaImage Upload

Groups are separated by ToolbarSeparator components with Spacer elements for positioning.

Editor Hooks

useTiptapEditor

Provides flexible access to the editor instance either from props or context:

export function useTiptapEditor(providedEditor?: Editor | null): {
editor: Editor | null;
editorState?: Editor["state"];
canCommand?: Editor["can"];
}

This hook merges a directly provided editor with the context editor, enabling components to work both standalone and within the provider tree.

Additional Hooks

HookPurpose
use-editor.tsCore editor state management
use-editor-sync.tsSynchronization between editor instances
use-cursor-visibility.tsCursor position and visibility tracking
use-element-rect.tsElement bounding rectangle tracking
use-scrolling.tsScroll position and behavior
use-throttled-callback.tsThrottled callback execution
use-window-size.tsResponsive window size tracking
use-unmount.tsCleanup on component unmount

Utility Functions

Shortcut Key Formatting

The system handles platform-specific keyboard shortcuts:

export const MAC_SYMBOLS: Record<string, string> = {
mod: "Command", command: "Command", meta: "Command",
ctrl: "Ctrl", alt: "Option", shift: "Shift",
// ... additional mappings
};

export const formatShortcutKey = (key: string, isMac: boolean, capitalize?: boolean) => {
// Returns Mac symbols or formatted key names
};

export const parseShortcutKeys = (props: {
shortcutKeys: string | undefined;
delimiter?: string;
capitalize?: boolean;
}) => string[];

Schema Validation

// Check if a mark type exists in the editor schema
export const isMarkInSchema = (markName: string, editor: Editor | null): boolean;

// Check if a node type exists in the editor schema
export const isNodeInSchema = (nodeName: string, editor: Editor | null): boolean;

// Check if extensions are registered
export function isExtensionAvailable(editor: Editor | null, extensionNames: string | string[]): boolean;

Node Navigation

// Find a node at a specific document position
export function findNodeAtPosition(editor: Editor, position: number): TiptapNode | null;

// Find a node by reference or position
export function findNodePosition(props: {
editor: Editor | null;
node?: TiptapNode | null;
nodePos?: number | null;
}): { pos: number; node: TiptapNode } | null;

// Move focus to the next node
export function focusNextNode(editor: Editor): boolean;

Image Upload

export const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB

export const handleImageUpload = async (
file: File,
onProgress?: (event: { progress: number }) => void,
abortSignal?: AbortSignal
): Promise<string>;

The upload handler validates file size, supports progress tracking, and handles cancellation via AbortSignal.

URL Sanitization

export function isAllowedUri(uri: string | undefined, protocols?: ProtocolConfig): boolean;
export function sanitizeUrl(inputUrl: string, baseUrl: string, protocols?: ProtocolConfig): string;

Ensures that only safe protocols (http, https, ftp, mailto, etc.) are allowed in links. Unsafe URLs are replaced with "#".