npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dedit-react-editor

v0.2.5

Published

A flexible, unstyled React document editor with track changes and comments support

Readme

dedit-react-editor

A flexible, unstyled React component for document editing with track changes and comments support. Built on TipTap/ProseMirror.

Installation

npm install dedit-react-editor

Peer Dependencies

Ensure you have these installed in your project:

npm install react react-dom @tiptap/react @tiptap/core @tiptap/pm

Quick Start

import { DocumentEditor } from 'dedit-react-editor';

function MyApp() {
  const [content, setContent] = useState(null);

  return (
    <DocumentEditor
      initialContent={content}
      onChange={setContent}
      className="my-editor"
    />
  );
}

Basic Usage

Uncontrolled Component

Use initialContent for a simple uncontrolled editor:

import { DocumentEditor } from 'dedit-react-editor';

function Editor() {
  const handleChange = (json) => {
    console.log('Document changed:', json);
  };

  return (
    <DocumentEditor
      initialContent={{
        type: 'doc',
        content: [
          {
            type: 'paragraph',
            content: [{ type: 'text', text: 'Hello world!' }],
          },
        ],
      }}
      onChange={handleChange}
    />
  );
}

Controlled Component

Use content prop for controlled behavior:

import { DocumentEditor } from 'dedit-react-editor';

function Editor() {
  const [content, setContent] = useState(initialDocument);

  return (
    <DocumentEditor
      content={content}
      onChange={setContent}
    />
  );
}

Track Changes

Enable track changes to mark insertions and deletions instead of directly modifying text:

import { useRef, useState } from 'react';
import { DocumentEditor, EditorHandle } from 'dedit-react-editor';

function EditorWithTrackChanges() {
  const editorRef = useRef<EditorHandle>(null);
  const [content, setContent] = useState(initialDocument);
  const [trackChangesEnabled, setTrackChangesEnabled] = useState(true);

  return (
    <div>
      {/* Toggle track changes */}
      <button onClick={() => setTrackChangesEnabled(!trackChangesEnabled)}>
        Track Changes: {trackChangesEnabled ? 'ON' : 'OFF'}
      </button>

      {/* Accept/Reject controls */}
      <button onClick={() => editorRef.current?.acceptAllChanges()}>
        Accept All
      </button>
      <button onClick={() => editorRef.current?.rejectAllChanges()}>
        Reject All
      </button>

      <DocumentEditor
        editorRef={editorRef}
        content={content}
        onChange={setContent}
        trackChanges={{
          enabled: trackChangesEnabled,
          author: 'John Doe',
        }}
      />
    </div>
  );
}

Handling Individual Changes

Use the useTrackChanges hook for granular control:

import { useRef, useState } from 'react';
import { 
  DocumentEditor, 
  EditorHandle,
  useTrackChanges,
} from 'dedit-react-editor';

function EditorWithChangesList() {
  const editorRef = useRef<EditorHandle>(null);
  const [content, setContent] = useState(initialDocument);

  // Get the editor instance
  const editor = editorRef.current?.getEditor();

  // Use the track changes hook
  const { 
    changes, 
    acceptChange, 
    rejectChange,
    acceptAll,
    rejectAll,
  } = useTrackChanges(editor, {
    enabled: true,
    author: 'John Doe',
  });

  return (
    <div className="editor-layout">
      <DocumentEditor
        editorRef={editorRef}
        content={content}
        onChange={setContent}
        trackChanges={{
          enabled: true,
          author: 'John Doe',
        }}
      />

      {/* Changes sidebar */}
      <aside>
        <h3>Changes ({changes.length})</h3>
        <button onClick={acceptAll}>Accept All</button>
        <button onClick={rejectAll}>Reject All</button>
        
        <ul>
          {changes.map((change) => (
            <li key={change.id}>
              <span className={change.type}>
                {change.type === 'insertion' ? 'Added' : 'Deleted'}:
              </span>
              <span>"{change.text}"</span>
              <span>by {change.author}</span>
              <button onClick={() => acceptChange(change.id)}>Accept</button>
              <button onClick={() => rejectChange(change.id)}>Reject</button>
            </li>
          ))}
        </ul>
      </aside>
    </div>
  );
}

Comments

Add commenting functionality:

import { useState } from 'react';
import { DocumentEditor, CommentData } from 'dedit-react-editor';

function EditorWithComments() {
  const [content, setContent] = useState(initialDocument);
  const [comments, setComments] = useState<CommentData[]>([]);

  const handleAddComment = (range, text) => {
    const newComment: CommentData = {
      id: `comment-${Date.now()}`,
      author: 'Current User',
      date: new Date().toISOString(),
      text,
    };
    setComments([...comments, newComment]);
  };

  const handleResolveComment = (commentId) => {
    setComments(comments.filter(c => c.id !== commentId));
  };

  return (
    <DocumentEditor
      content={content}
      onChange={setContent}
      comments={{
        data: comments,
        onAdd: handleAddComment,
        onResolve: handleResolveComment,
        onReply: (commentId, text) => {
          // Handle reply
        },
        onDelete: (commentId) => {
          setComments(comments.filter(c => c.id !== commentId));
        },
      }}
    />
  );
}

Exporting Documents

Create Export Payload

Use the imperative handle to create an export payload for your backend:

import { useRef } from 'react';
import { DocumentEditor, EditorHandle } from 'dedit-react-editor';

function EditorWithExport() {
  const editorRef = useRef<EditorHandle>(null);

  const handleExport = async () => {
    const payload = editorRef.current?.createExportPayload({
      filename: 'my-document.docx',
      includeComments: true,
      template: {
        type: 'none', // 'none' | 'original' | 'custom'
      },
    });

    // Send to your backend
    const response = await fetch('/api/export', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });

    // Download the file
    const blob = await response.blob();
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'my-document.docx';
    a.click();
  };

  return (
    <div>
      <button onClick={handleExport}>Export to Word</button>
      <DocumentEditor editorRef={editorRef} />
    </div>
  );
}

Using the Export Utility

For simpler export workflows:

import { exportToWord } from 'dedit-react-editor';

const handleExport = async () => {
  const content = editorRef.current?.getContent();
  
  await exportToWord('/api/export', content, comments, {
    filename: 'document.docx',
    template: { type: 'original', documentId: 'doc-123' },
  });
};

Template Options

// No template - plain document
template: { type: 'none' }

// Use original document as template (preserves styles)
template: { type: 'original', documentId: 'uploaded-doc-id' }

// Use custom template
template: { type: 'custom', templateId: 'template-id' }

Styling

The component is unstyled by default. Add your own styles:

Using className Props

<DocumentEditor
  className="editor-root"
  classNames={{
    root: 'editor-container',
    content: 'editor-content',
    insertion: 'track-insertion',
    deletion: 'track-deletion',
    comment: 'comment-highlight',
  }}
/>

Example CSS

/* Editor container */
.editor-root {
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 1rem;
}

/* Content area */
.editor-content {
  min-height: 300px;
  outline: none;
}

/* Track changes styling */
.editor-content ins.insertion {
  background-color: #d4edda;
  text-decoration: none;
}

.editor-content del.deletion {
  background-color: #f8d7da;
  text-decoration: line-through;
  color: #721c24;
}

/* Comment highlights */
.editor-content .comment-highlight {
  background-color: #fff3cd;
}

Imperative Handle API

Access editor functionality programmatically via editorRef:

const editorRef = useRef<EditorHandle>(null);

// Content
editorRef.current?.getContent();           // Get TipTap JSON
editorRef.current?.setContent(json);       // Set content

// Track changes
editorRef.current?.getChanges();           // Get all tracked changes
editorRef.current?.acceptChange(id);       // Accept specific change
editorRef.current?.rejectChange(id);       // Reject specific change
editorRef.current?.acceptAllChanges();     // Accept all changes
editorRef.current?.rejectAllChanges();     // Reject all changes
editorRef.current?.setTrackChangesEnabled(true);
editorRef.current?.setTrackChangesAuthor('Jane');

// Editor control
editorRef.current?.focus();                // Focus editor
editorRef.current?.blur();                 // Blur editor
editorRef.current?.getEditor();            // Get raw TipTap editor

// Export
editorRef.current?.createExportPayload(options);

Advanced: Custom Editor with Hooks

For complete control, use the hooks directly:

import { 
  useDocumentEditor, 
  useTrackChanges,
  useComments,
} from 'dedit-react-editor';
import { EditorContent } from '@tiptap/react';

function CustomEditor() {
  const { editor, content, setContent, isReady } = useDocumentEditor({
    initialContent: myDocument,
    onChange: handleChange,
    trackChangesEnabled: true,
    trackChangesAuthor: 'Custom User',
  });

  const trackChanges = useTrackChanges(editor, {
    enabled: true,
    author: 'Custom User',
  });

  const comments = useComments(editor, {
    data: myComments,
    onAdd: handleAddComment,
  });

  if (!isReady) return <div>Loading...</div>;

  return (
    <div>
      <MyCustomToolbar 
        onAcceptAll={trackChanges.acceptAll}
        changes={trackChanges.changes}
      />
      <EditorContent editor={editor} />
      <MyCustomCommentsSidebar 
        comments={comments.comments}
        onGoTo={comments.goToComment}
      />
    </div>
  );
}

Available Exports

import {
  // Main component
  DocumentEditor,

  // Hooks
  useDocumentEditor,
  useTrackChanges,
  useComments,

  // Utilities
  createExportPayload,
  exportToWord,
  downloadBlob,

  // TipTap extensions (for custom setups)
  Insertion,
  Deletion,
  Comment,
  TrackChangesMode,
  Section,
  TableWithId,

  // AI Components
  AIEditorProvider,
  useAIEditor,
  AIChatPanel,
  PromptInput,

  // Types
  type DocumentEditorProps,
  type EditorHandle,
  type TipTapDocument,
  type TrackedChange,
  type CommentData,
  type TrackChangesConfig,
  type CommentsConfig,
  type ExportOptions,
  type ExportPayload,
  type AIMode,
  type ModeContext,
  type ModeResult,
} from 'dedit-react-editor';

AI-Assisted Editing

The library includes AI components for document editing with support for custom slash commands:

import { AIEditorProvider, AIMode, ModeContext, ModeResult } from 'dedit-react-editor';

// Built-in modes: /edit and /review
// Add your own custom modes:
const customMode: AIMode = {
  name: "summarize",
  description: "Summarize the document",
  icon: <Sparkles size={14} />,
  handler: async (context: ModeContext): Promise<ModeResult> => {
    // Your AI logic here
    return { message: "Summary: ..." };
  },
};

<AIEditorProvider config={{ 
  aiAuthorName: "AI",
  modes: [customMode],           // Custom modes alongside /edit and /review
  onAIRequest: handleEdit,       // Backend proxy for /edit
  onAIReviewRequest: handleReview, // Backend proxy for /review
}}>
  <YourApp />
</AIEditorProvider>

See docs/USAGE.md for complete AI documentation.

Props Reference

DocumentEditorProps

| Prop | Type | Description | |------|------|-------------| | initialContent | TipTapDocument | Initial content (uncontrolled) | | content | TipTapDocument | Controlled content | | onChange | (content) => void | Called on content change | | editorRef | RefObject<EditorHandle> | Imperative handle ref | | readOnly | boolean | Make editor read-only | | placeholder | string | Placeholder text | | trackChanges | TrackChangesConfig | Track changes configuration | | comments | CommentsConfig | Comments configuration | | className | string | Root element class | | classNames | ClassNameConfig | Granular class names | | style | CSSProperties | Inline styles | | extensions | Extension[] | Additional TipTap extensions | | extensionConfig | ExtensionConfig | Configure built-in extensions |

TrackChangesConfig

| Prop | Type | Description | |------|------|-------------| | enabled | boolean | Enable track changes mode | | author | string | Author name for changes | | onAuthorChange | (author) => void | Called when author changes | | onAccept | (change) => void | Called when change accepted | | onReject | (change) => void | Called when change rejected |

CommentsConfig

| Prop | Type | Description | |------|------|-------------| | data | CommentData[] | Array of comments | | onAdd | (range, text) => void | Called when comment added | | onReply | (id, text) => void | Called on reply | | onResolve | (id) => void | Called when resolved | | onDelete | (id) => void | Called when deleted |

Backend API Contract

The export payload structure for your backend:

interface ExportPayload {
  tiptap: TipTapDocument;      // The document content
  comments: CommentData[];      // Comments to include
  template: 'none' | 'original' | 'custom';
  document_id?: string;         // For 'original' template
  template_id?: string;         // For 'custom' template
  filename: string;
}

Your backend should accept this JSON and return a Word document blob.

License

MIT