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

@tnbt/quill-editor

v1.0.8

Published

A rich text editor component built on Quill that can be integrated into any React application

Readme

@tnbt/quill-editor

A powerful, feature-rich rich text editor component built on Quill that can be integrated into any React application. This package provides a flexible, callback-based API that works with any backend (REST, GraphQL, Firebase, MongoDB, etc.).

npm version GitHub

✨ Features

  • 🎨 Rich Text Editing: Full-featured WYSIWYG editor based on Quill
  • 🖼️ Image Support: Upload, resize, align (left, right, center), and manage images with custom callbacks
  • 📝 Multiple Modes: Create, edit, and readonly modes with preview toggle
  • 🔄 Auto-save: Built-in auto-save functionality with configurable intervals
  • 🎯 TypeScript: Full TypeScript support with exported types
  • 🔌 Flexible Backend: Works with any backend via callback-based architecture
  • 🎨 Customizable: Extensive customization options and styling
  • 📱 Responsive: Works on desktop and mobile devices
  • 😊 Emoji Picker: Built-in emoji picker with categorized emojis
  • 💬 Blockquote: Custom blockquote feature with styled rendering
  • 🎨 Image Layouts: Support for image-left-content, image-right-content, and centered image layouts
  • 📋 Code Blocks: Syntax highlighting and copy functionality for code blocks
  • Highlight Content: Custom highlight content feature
  • 🔄 Undo/Redo: Custom undo/redo functionality

📸 Screenshot

Quill Editor TNBT

🎥 Demo Video

📦 Installation

npm install @tnbt/quill-editor
# or
yarn add @tnbt/quill-editor
# or
pnpm add @tnbt/quill-editor

Requirements

  • React: >=18.0.0 (compatible with React 18, 19, and future versions)
  • React DOM: >=18.0.0

The package uses React as a peer dependency, so make sure you have React installed in your project:

npm install react react-dom

⚠️ React 19 Compatibility Note

If you're using React 19, you need to add a polyfill for findDOMNode (which was removed in React 19). Add this to your app's entry point before other imports:

// main.tsx or App.tsx
import ReactDOM from 'react-dom';

// Polyfill for React 19
if (!ReactDOM.findDOMNode) {
  (ReactDOM as any).findDOMNode = function(componentOrElement: any) {
    if (!componentOrElement) return null;
    if (componentOrElement.nodeType === 1 || componentOrElement.nodeType === 3) {
      return componentOrElement;
    }
    if (componentOrElement.current) return componentOrElement.current;
    if (componentOrElement.stateNode) return componentOrElement.stateNode;
    return null;
  };
}

// Now import your app
import App from './App';

🚀 Quick Start

Basic Usage

import React, { useState } from 'react';
import { QuillEditorTNBT_DefaultCss } from '@tnbt/quill-editor';
import '@tnbt/quill-editor/styles'; // Import styles

function MyEditor() {
  const [content, setContent] = useState('');

  const handleCreate = async (data) => {
    // Your save logic - works with ANY backend!
    const response = await fetch('/api/articles', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        content: data.content,
        title: data.metadata?.title,
      }),
    });
    
    const result = await response.json();
    return {
      success: true,
      articleId: result.id,
      message: 'Article created!',
    };
  };

  const handleImageUpload = async (file: File) => {
    const formData = new FormData();
    formData.append('image', file);
    
    const response = await fetch('/api/upload-image', {
      method: 'POST',
      body: formData,
    });
    
    const { url } = await response.json();
    return url;
  };

  return (
    <QuillEditorTNBT_DefaultCss
      value={content}
      onChange={setContent}
      onCreate={handleCreate}
      onImageUpload={handleImageUpload}
      articleMetadata={{
        title: 'My Article',
        tags: ['tech', 'tutorial'],
      }}
      defaultImageWidth={600}
    />
  );
}

📚 Components

QuillEditorTNBT_DefaultCss

The main editor component with default styling. This is the recommended component to use.

import { QuillEditorTNBT_DefaultCss } from '@tnbt/quill-editor';
import '@tnbt/quill-editor/styles';

QuillEditorTNBT

The base editor component without default CSS. Use this if you want to provide your own styling.

import { QuillEditorTNBT } from '@tnbt/quill-editor';

ConvertDocProperly

Utility component for converting editor content with custom syntax (blockquotes, image layouts, etc.) into proper HTML.

import { ConvertDocProperly } from '@tnbt/quill-editor';

function Preview({ content }) {
  return (
    <div dangerouslySetInnerHTML={{ 
      __html: ConvertDocProperly({ documentValue: content }) 
    }} />
  );
}

📖 Usage Examples

Create Article

import { QuillEditorTNBT_DefaultCss } from '@tnbt/quill-editor';
import '@tnbt/quill-editor/styles';

function CreateArticle() {
  const handleCreate = async (data) => {
    // data.content - HTML content
    // data.plainText - Plain text version
    // data.images - Array of image data
    // data.wordCount - Word count
    // data.metadata - Article metadata
    
    const response = await fetch('/api/articles', {
      method: 'POST',
      body: JSON.stringify({ content: data.content }),
    });
    
    return { success: true, articleId: '123' };
  };

  return (
    <QuillEditorTNBT_DefaultCss
      onCreate={handleCreate}
      onImageUpload={async (file) => {
        // Upload image and return URL
        return 'https://example.com/image.jpg';
      }}
      defaultImageWidth={600}
    />
  );
}

Edit Article

import { QuillEditorTNBT_DefaultCss } from '@tnbt/quill-editor';
import '@tnbt/quill-editor/styles';

function EditArticle({ articleId, initialContent }) {
  const handleUpdate = async (data, id) => {
    const response = await fetch(`/api/articles/${id}`, {
      method: 'PUT',
      body: JSON.stringify({ content: data.content }),
    });
    
    return { success: true };
  };

  return (
    <QuillEditorTNBT_DefaultCss
      mode="edit"
      value={initialContent}
      onUpdate={handleUpdate}
      onImageUpload={async (file) => {
        return 'https://example.com/image.jpg';
      }}
    />
  );
}

Read-Only Mode with Preview

import { QuillEditorTNBT_DefaultCss, ConvertDocProperly } from '@tnbt/quill-editor';
import '@tnbt/quill-editor/styles';

function ViewArticle({ content }) {
  return (
    <QuillEditorTNBT_DefaultCss
      mode="readonly"
      value={content}
      showPreview={true}
    />
  );
}

// Or render converted content directly
function ArticlePreview({ content }) {
  const convertedContent = ConvertDocProperly({ documentValue: content });
  return <div dangerouslySetInnerHTML={{ __html: convertedContent }} />;
}

Using Ref Methods

import { useRef } from 'react';
import { QuillEditorTNBT_DefaultCss, type QuillEditorRef } from '@tnbt/quill-editor';

function EditorWithRef() {
  const editorRef = useRef<QuillEditorRef>(null);

  const handleSave = async () => {
    // Get content
    const content = editorRef.current?.getContent();
    const wordCount = editorRef.current?.getWordCount();
    
    // Save/Create/Update
    await editorRef.current?.create();
    // or
    await editorRef.current?.update('article-id');
    // or
    await editorRef.current?.save();
    
    // Utility methods
    const validation = editorRef.current?.validate();
    editorRef.current?.focus();
    editorRef.current?.clear();
  };

  return (
    <>
      <button onClick={handleSave}>Save</button>
      <QuillEditorTNBT_DefaultCss ref={editorRef} />
    </>
  );
}

🔧 API Reference

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | value | string | - | Controlled content value | | defaultValue | string | - | Uncontrolled default content | | onChange | (html: string) => void | - | Called on every content change | | onContentChange | (html: string) => void | - | Debounced version of onChange (optional) | | mode | 'create' \| 'edit' \| 'readonly' | 'create' | Editor mode | | showPreview | boolean | true | Show preview mode toggle | | showToolbar | boolean | true | Show/hide toolbar | | onCreate | (data: EditorData) => Promise<CreateResult> | - | Create article callback | | onUpdate | (data: EditorData, id: string) => Promise<UpdateResult> | - | Update article callback | | onSave | (data: EditorData) => Promise<SaveResult> | - | Generic save callback | | onImageUpload | (file: File) => Promise<string> | - | Image upload callback (must return URL) | | defaultImageWidth | number | 500 | Default width for inserted images | | articleMetadata | ArticleMetadata | - | Article metadata (title, tags, etc.) | | autoSave | boolean | false | Enable auto-save | | autoSaveInterval | number | 30000 | Auto-save interval (ms) | | onAutoSave | (data: EditorData) => Promise<void> | - | Auto-save callback | | placeholder | string | "Write something awesome..." | Editor placeholder | | onSuccess | (message: string) => void | - | Success callback | | onError | (error: EditorError) => void | - | Error callback | | onModeChange | (mode: 'write' \| 'preview') => void | - | Mode change callback | | className | string | - | Custom CSS class |

Ref Methods (QuillEditorRef)

interface QuillEditorRef {
  // Content Methods
  getContent: () => string;
  getPlainText: () => string;
  setContent: (html: string) => void;
  clear: () => void;
  
  // Editor Methods
  focus: () => void;
  blur: () => void;
  getSelection: () => any;
  getEditor: () => any; // Get Quill instance
  
  // Save Methods
  save: () => Promise<SaveResult>;
  create: () => Promise<CreateResult>;
  update: (articleId: string) => Promise<UpdateResult>;
  
  // Preview Methods
  switchToPreview: () => void;
  switchToWrite: () => void;
  
  // Image Methods
  insertImage: (url: string, width?: number) => void;
  extractImages: () => ImageData[];
  
  // Utility Methods
  getWordCount: () => number;
  getCharacterCount: () => number;
  validate: () => ValidationResult;
}

Types

import type {
  QuillEditorTNBTProps,
  QuillEditorRef,
  EditorData,
  ImageData,
  ArticleMetadata,
  SaveResult,
  CreateResult,
  UpdateResult,
  EditorError,
  ValidationResult,
  ToolbarConfig,
} from '@tnbt/quill-editor';

interface EditorData {
  content: string;           // HTML content
  plainText: string;          // Plain text version
  images: ImageData[];        // Extracted images info
  wordCount: number;
  characterCount: number;
  metadata?: ArticleMetadata;
}

interface ImageData {
  src: string;
  alt?: string;
  width?: number;
  height?: number;
  file?: File;
}

interface ArticleMetadata {
  title?: string;
  tags?: string[];
  coverImage?: File | string;
  category?: string;
  [key: string]: any;
}

Utility Functions

import {
  processImagesInContent,
  extractImageUrls,
  hasBase64Images,
} from '@tnbt/quill-editor';

// Process images in HTML content (extract base64, upload, replace)
const processedContent = await processImagesInContent(
  htmlContent,
  uploadCallback
);

// Extract image URLs from HTML
const imageUrls = extractImageUrls(htmlContent);

// Check if content has base64 images
const hasBase64 = hasBase64Images(htmlContent);

🎨 Styling

Import Default Styles

import '@tnbt/quill-editor/styles';

Custom CSS (Optional)

If the style does not show correctly, you add code below to index.css file. If you need additional styling, you can add custom CSS:

/* fix quill-editor-tnbt/styles.css */
.top-and-bottom{
  margin-top: 0px !important;
  top: 100px !important;
  margin-left: -20px !important;
}

/* Quill Editor List Styles for Preview Mode */
.ql-editor ol,
.ql-editor ul,
.ql-preview ol,
.ql-preview ul,
[class*="ql-"] ol,
[class*="ql-"] ul {
  padding-left: 1.5em !important;
  margin: 1em 0 !important;
  list-style-position: outside !important;
}

.ql-editor ol li,
.ql-editor ul li,
.ql-preview ol li,
.ql-preview ul li,
[class*="ql-"] ol li,
[class*="ql-"] ul li {
  margin: 0.5em 0 !important;
  line-height: 1.6 !important;
  display: list-item !important;
}

.ql-editor ol {
  list-style-type: decimal !important;
}

.ql-editor ul {
  list-style-type: disc !important;
}

.ql-preview ol {
  list-style-type: decimal !important;
}

.ql-preview ul {
  list-style-type: disc !important;
}

/* Ensure list styles are preserved in preview/content areas */
.article-content ol,
.article-content ul {
  padding-left: 1.5em !important;
  margin: 1em 0 !important;
  list-style-position: outside !important;
}

.article-content ol li,
.article-content ul li {
  margin: 0.5em 0 !important;
  line-height: 1.6 !important;
  display: list-item !important;
}

.article-content ol {
  list-style-type: decimal !important;
}

.article-content ul {
  list-style-type: disc !important;
}

/* Quill Editor Blockquote Styles for Preview Mode */
.ql-editor blockquote,
.ql-preview blockquote,
[class*="ql-"] blockquote {
  border-left: 4px solid #d1d5db !important; /* Gray vertical line */
  padding-left: 1.5em !important;
  margin: 1em 0 !important;
  color: #374151 !important; /* Gray text */
  font-style: normal !important;
  background-color: transparent !important;
}

.dark .ql-editor blockquote,
.dark .ql-preview blockquote,
.dark [class*="ql-"] blockquote {
  border-left-color: #4b5563 !important; /* Darker gray for dark mode */
  color: #d1d5db !important; /* Lighter gray text for dark mode */
}

/* Article content blockquote styles */
blockquote {
  border-left: 4px solid #d1d5db !important; /* Gray vertical line */
  padding-left: 1.5em !important;
  margin: 1em 0 !important;
  color: #374151 !important; /* Gray text */
  font-style: normal !important;
  background-color: transparent !important;
}

.dark .article-content blockquote {
  border-left-color: #4b5563 !important; /* Darker gray for dark mode */
  color: #d1d5db !important; /* Lighter gray text for dark mode */
}

🎯 Features in Detail

Image Alignment

The editor supports three image alignment options:

  • Left: Image on the left, content on the right
  • Right: Image on the right, content on the left
  • Center: Image centered horizontally

These are available via toolbar buttons and render properly in preview mode.

Emoji Picker

Access a categorized emoji picker from the toolbar. Click the emoji button to open a dropdown with emojis organized by category.

Blockquote

Insert styled blockquotes using the blockquote button. Blockquotes render with a gray vertical line, background color, and proper spacing.

Code Blocks

Code blocks support syntax highlighting and include a copy button for easy code sharing.

Highlight Content

Use the highlight feature to emphasize important content with custom styling.

🔗 TypeScript Support

Full TypeScript support with exported types:

import { 
  QuillEditorTNBT_DefaultCss,
  QuillEditorTNBT,
  ConvertDocProperly,
  QuillEditorRef,
  EditorData,
  ImageData,
  ArticleMetadata,
  processImagesInContent,
  extractImageUrls,
  hasBase64Images,
} from '@tnbt/quill-editor';

📝 License

MIT

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

📧 Support

For issues and questions, please open an issue on GitHub.

🔗 Links


Made with ❤️ using Quill and React