Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

tkeditor

datnt192133.2kMIT2.0.46TypeScript support: included

TKEditor is a powerful and extensible rich text editor built with React, Plate.js, and a suite of modern technologies. It offers a comprehensive editing experience with AI-powered features, seamless file uploads, and a highly customizable plugin-based arc

react, editor, rich-text-editor, platejs, react-plate, react-rich-text-editor, react-text-editor, react-text, react-text-editor, react-text-editor-component, react-text-editor-library, react-text-editor-plugin, react-text-editor-plugins, react-text-editor-ui, tkeditor, tkeditor-react, tkeditor-react-component, tkeditor-react-library, tkeditor-react-plugin, react editor, react editor component, react mordern editor, mordern, mordern editor, medium like editor

readme

TKEditor - Advanced Rich Text Editor

TKEditor is a powerful and extensible rich text editor built with Next.js, Plate.js, and a suite of modern technologies. It offers a comprehensive editing experience with AI-powered features, seamless file uploads, and a highly customizable plugin-based architecture.

TKEditor NPM Package Logo

Demo

TKEditor 📝✨

NPM Version License NPM Downloads

TKEditor is a powerful and scalable feature-rich text editor (rich text editor), built with React, Plate.js, and a modern tech stack. It provides a comprehensive editing experience with AI-powered features, seamless file uploads, and a highly customizable plugin-based architecture.

List of contents

Features 🚀

  • Powerful Plate.js Foundation: Built on Plate.js, providing a solid and high-performance foundation for text editing.
  • AI-Powered 🤖:
    • AI Copilot: Intelligent real-time text suggestions.
    • AI Chat & Commands: Interact with AI directly within the editor to generate, modify, or improve content.
    • Custom Prompts: Ability to define prompt templates to optimize interaction with AI.
  • Easy File Uploads ☁️: Integration with Uploadthing allows for smooth uploading of images, videos, audio, PDFs, and text files.
  • Flexible Plugin Architecture 🧩: Easily extend and customize functionality through a rich plugin system.
  • Comprehensive Markdown Support Ⓜ️: Compose content using Markdown syntax and preview directly.
  • Highly Customizable User Interface 🎨: Uses Tailwind CSS and Radix UI, allowing for flexible interface customization to fit your application.
  • Diverse Content Types 🧱: Supports various block and inline elements:
    • Headings (H1-H6)
    • Paragraph
    • Lists (ordered, unordered, task list)
    • Blockquote
    • Code Block with syntax highlighting
    • Table
    • Images, Videos, Audio
    • Link
    • Mention
    • Emoji
    • Date
    • Horizontal Rule
    • Table of Contents
    • Callout
    • And much more!
  • Collaboration Features 🤝:
    • Comments: Discuss content directly.
    • Suggestions: Propose changes and track editing history.
  • Optimized for Developers ✨:
    • Written in TypeScript.
    • Easy integration into React projects.

Install 📦

Install tkeditor and the necessary peer dependencies:

npm install tkeditor clsx react react-dom
# or
yarn add tkeditor clsx react react-dom

You also need to install Tailwind CSS in your project if you haven't already. TKEditor is designed to work best with Tailwind CSS.

Usage 🛠️

Here is a basic example of how to integrate TKEditor into your application:

"use client"; //Use it if your project is NextJS
import  'tkeditor/index.css'; //Maybe not but it will better when use it

import { CoreEditor, EditorProvider } from  'tkeditor';

function App() {
  return (
    <EditorProvider  >
       <CoreEditor />
    </EditorProvider>
  );
}

export default App;
/* use it when u use Tailwindcss v4 (same like `content: [...]` in tailwind.config.js/ts of tailwindcss v3)*/
@source  "../../node_modules/tkeditor";

Structure app (structure folder appply for NextJS)

#structure

~project

├── src
│ ├── app
│ │ ├── api
│ │ │ ├── ai
│ │ │ │ ├── command
│ │ │ │ │ └── route.ts
│ │ │ │ └── copilot
│ │ │ │   └── route.ts
│ │ │ └── uploadthing
│ │ │   └── route.ts
        ...

AI content routes

-  `/api/ai/command` - AI command API route.
-  `/api/ai/copilot` - AI copilot API route.
-  `/api/uploadthing` - Uploadthing API route to file uploads.
OPENAI_API_KEY=<your_open_api_key_here>
UPLOADTHING_TOKEN=<your_uploadthing_token_here>
WEBSITE_URL=<your_website_url>

if you are useing NextJS your_website_url is your web host url (cuz this is the host to use call AI Content)
if you are useing base React your_website_url may be your Backend host url to use call AI Content)

Configure AI Content (Optional for use AI Content)

/api/ai/command/route

import type { TextStreamPart, ToolSet } from 'ai';
import type { NextRequest } from 'next/server';

import { createOpenAI } from '@ai-sdk/openai';
import { InvalidArgumentError } from '@ai-sdk/provider';
import { delay as originalDelay } from '@ai-sdk/provider-utils';
import { convertToCoreMessages, streamText } from 'ai';
import { NextResponse } from 'next/server';

/**
 * Detects the first chunk in a buffer.
 *
 * @param buffer - The buffer to detect the first chunk in.
 * @returns The first detected chunk, or `undefined` if no chunk was detected.
 */
export type ChunkDetector = (buffer: string) => string | null | undefined;

type delayer = (buffer: string) => number;

/**
 * Smooths text streaming output.
 *
 * @param delayInMs - The delay in milliseconds between each chunk. Defaults to
 *   10ms. Can be set to `null` to skip the delay.
 * @param chunking - Controls how the text is chunked for streaming. Use "word"
 *   to stream word by word (default), "line" to stream line by line, or provide
 *   a custom RegExp pattern for custom chunking.
 * @returns A transform stream that smooths text streaming output.
 */
function smoothStream<TOOLS extends ToolSet>({
  _internal: { delay = originalDelay } = {},
  chunking = 'word',
  delayInMs = 10,
}: {
  /** Internal. For test use only. May change without notice. */
  _internal?: {
    delay?: (delayInMs: number | null) => Promise<void>;
  };
  chunking?: ChunkDetector | RegExp | 'line' | 'word';
  delayInMs?: delayer | number | null;
} = {}): (options: {
  tools: TOOLS;
}) => TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>> {
  let detectChunk: ChunkDetector;

  if (typeof chunking === 'function') {
    detectChunk = (buffer) => {
      const match = chunking(buffer);

      if (match == null) {
        return null;
      }

      if (match.length === 0) {
        throw new Error(`Chunking function must return a non-empty string.`);
      }

      if (!buffer.startsWith(match)) {
        throw new Error(
          `Chunking function must return a match that is a prefix of the buffer. Received: "${match}" expected to start with "${buffer}"`
        );
      }

      return match;
    };
  } else {
    const chunkingRegex =
      typeof chunking === 'string' ? CHUNKING_REGEXPS[chunking] : chunking;

    if (chunkingRegex == null) {
      throw new InvalidArgumentError({
        argument: 'chunking',
        message: `Chunking must be "word" or "line" or a RegExp. Received: ${chunking}`,
      });
    }

    detectChunk = (buffer) => {
      const match = chunkingRegex.exec(buffer);

      if (!match) {
        return null;
      }

      return buffer.slice(0, match.index) + match?.[0];
    };
  }

  return () => {
    let buffer = '';

    return new TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>({
      async transform(chunk, controller) {
        if (chunk.type !== 'text-delta') {
          console.log(buffer, 'finished');

          if (buffer.length > 0) {
            controller.enqueue({ textDelta: buffer, type: 'text-delta' });
            buffer = '';
          }

          controller.enqueue(chunk);
          return;
        }

        buffer += chunk.textDelta;

        let match;

        while ((match = detectChunk(buffer)) != null) {
          controller.enqueue({ textDelta: match, type: 'text-delta' });
          buffer = buffer.slice(match.length);

          const _delayInMs =
            typeof delayInMs === 'number'
              ? delayInMs
              : (delayInMs?.(buffer) ?? 10);

          await delay(_delayInMs);
        }
      },
    });
  };
}

const CHUNKING_REGEXPS = {
  line: /\n+/m,
  list: /.{8}/m,
  word: /\S+\s+/m,
};

export async function POST(req: NextRequest) {
  const { apiKey: key, messages, system } = await req.json();

  const apiKey = key || process.env.OPENAI_API_KEY;

  if (!apiKey) {
    return NextResponse.json(
      { error: 'Missing OpenAI API key.' },
      { status: 401 }
    );
  }

  const openai = createOpenAI({ apiKey });

  let isInCodeBlock = false;
  let isInTable = false;
  let isInList = false;
  let isInLink = false;
  try {
    const result = streamText({
      experimental_transform: smoothStream({
        chunking: (buffer) => {
          // Check for code block markers
          if (/```[^\s]+/.test(buffer)) {
            isInCodeBlock = true;
          } else if (isInCodeBlock && buffer.includes('```')) {
            isInCodeBlock = false;
          }
          // test case: should not deserialize link with markdown syntax
          if (buffer.includes('http')) {
            isInLink = true;
          } else if (buffer.includes('https')) {
            isInLink = true;
          } else if (buffer.includes('\n') && isInLink) {
            isInLink = false;
          }
          if (buffer.includes('*') || buffer.includes('-')) {
            isInList = true;
          } else if (buffer.includes('\n') && isInList) {
            isInList = false;
          }
          // Simple table detection: enter on |, exit on double newline
          if (!isInTable && buffer.includes('|')) {
            isInTable = true;
          } else if (isInTable && buffer.includes('\n\n')) {
            isInTable = false;
          }

          // Use line chunking for code blocks and tables, word chunking otherwise
          // Choose the appropriate chunking strategy based on content type
          let match;

          if (isInCodeBlock || isInTable || isInLink) {
            // Use line chunking for code blocks and tables
            match = CHUNKING_REGEXPS.line.exec(buffer);
          } else if (isInList) {
            // Use list chunking for lists
            match = CHUNKING_REGEXPS.list.exec(buffer);
          } else {
            // Use word chunking for regular text
            match = CHUNKING_REGEXPS.word.exec(buffer);
          }
          if (!match) {
            return null;
          }

          return buffer.slice(0, match.index) + match?.[0];
        },
        delayInMs: () => (isInCodeBlock || isInTable ? 100 : 30),
      }),
      maxTokens: 2048,
      messages: convertToCoreMessages(messages),
      model: openai('gpt-4o'),
      system: system,
    });

    return result.toDataStreamResponse();
  } catch {
    return NextResponse.json(
      { error: 'Failed to process AI request' },
      { status: 500 }
    );
  }
}

/api/ai/copilot/route

import type { NextRequest } from 'next/server';

import { createOpenAI } from '@ai-sdk/openai';
import { generateText } from 'ai';
import { NextResponse } from 'next/server';

export async function POST(req: NextRequest) {
  const {
    apiKey: key,
    model = 'gpt-4o-mini',
    prompt,
    system,
  } = await req.json();

  const apiKey = key || process.env.OPENAI_API_KEY;

  if (!apiKey) {
    return NextResponse.json(
      { error: 'Missing OpenAI API key.' },
      { status: 401 }
    );
  }

  const openai = createOpenAI({ apiKey });

  try {
    const result = await generateText({
      abortSignal: req.signal,
      maxTokens: 50,
      model: openai(model),
      prompt: prompt,
      system,
      temperature: 0.7,
    });

    return NextResponse.json(result);
  } catch (error) {
    if (error instanceof Error && error.name === 'AbortError') {
      return NextResponse.json(null, { status: 408 });
    }

    return NextResponse.json(
      { error: 'Failed to process AI request' },
      { status: 500 }
    );
  }
}

/api/uploadthing/route

import { createRouteHandler } from 'uploadthing/next';

import { ourFileRouter } from '@/lib/uploadthing';

export const { GET, POST } = createRouteHandler({ router: ourFileRouter });

API Context & Hooks

API Context & Hooks: A simple and reusable API context solution using React Context, Axios, and custom hooks for managing API interactions in your app.

Features:

  • Centralized API configuration using React Context

  • Custom hook to fetch data: useApi

  • Custom hook to mutate data (POST/PUT/DELETE): useApiMutation

Define Your API Provider
import { ApiProvider } from './apiProvider';

const apiConfig = {
  host: 'https://your-api-host.com',
  resources: {
    users: '/users',
    posts: '/posts',
    // add more resources here
  },
};

function App() {
  return (
    <ApiProvider host={apiConfig.host} resources={apiConfig.resources}>
      <YourAppComponents />
    </ApiProvider>
  );
}

📡 useApi – Fetch Data

Fetches data from the API based on the resource key and optional query parameters.

import { useApi } from './hooks/useApi';

const UserList = () => {
  const { data, loading, error } = useApi('users', { page: 1 });

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error loading users</p>;

  return (
    <ul>
      {data?.map((user: any) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
};
Parameters:
  • resourceKey – key defined in resources (e.g., 'users')

  • params – query parameters (optional)

  • options.skip – set true to skip fetching initially


✏️ useApiMutation – Mutate Data (POST/PUT/DELETE)

Handles data mutation (e.g., submitting a form or deleting a record).

import { useApiMutation } from './hooks/useApiMutation';

const CreateUser = () => {
  const { mutate, loading, error, data } = useApiMutation('users', {
    method: 'POST',
    onSuccess: () => alert('User created!'),
    onError: (err) => console.error(err),
  });

  const handleSubmit = () => {
    mutate({ name: 'John Doe', email: 'john@example.com' });
  };

  return (
    <div>
      <button onClick={handleSubmit} disabled={loading}>
        Create User
      </button>
      {error && <p>Error creating user</p>}
    </div>
  );
};
Parameters:
  • resourceKey – key defined in resources (e.g., 'users')

  • options.method – HTTP method (default is 'POST')

  • options.onSuccess – callback on successful response

  • options.onError – callback on error


🧠 Accessing Context Directly

If needed, access the context manually with:

import { useApiContext } from './apiProvider';

const Component = () => {
  const { host, resources } = useApiContext();
  console.log(host, resources);
};

⚠️ Error Handling

Both useApi and useApiMutation expose error which you can use to display or log errors.

~project/
  ├─ src
    ├─ api
      ├─ apiProvider.ts
      ├─ useApi.ts
      └─ useApiMutation.ts

Props of TKEditor

CoreEditor Props

Prop Type Required Description
editorRef React.Ref<HTMLDivElement> ✖️ Reference to the editor container DOM node.
variant `"default" "select" "none"
focused boolean ✖️ Indicates if the editor is currently focused.
disabled boolean ✖️ Disables editing capabilities when set to true.
className string ✖️ Custom CSS class for the editor container.
...PlateContentProps PlateContentProps ✖️ Props passed to the Plate content component.

EditorProvider Props

Prop Type Required Description
children React.ReactNode Editor children components.
isToolbar boolean ✖️ Show or hide the main toolbar.
isFloating boolean ✖️ Enable floating toolbar behavior.
allowPlugins PLUGIN_KEY[] ✖️ List of allowed plugins (see below).
toolbarClasses { className?: string; groupClass?: string } ✖️ Classes for styling the toolbar and groups.

Available Plugin Keys (PLUGIN_KEY)

"undo" | "redo" | "ai" | "export" | "import" | "insert" | "turn_into" | "font_size" | "bold" | "italic" | "underline" | "strikethrough" | "code" | "font_color" | "background_color" | "align" | "indent_list" | "bulleted_list" | "indent_todo" | "toggle" | "link" | "table" | "emoji" | "image" | "video" | "audio" | "file" | "line_height" | "outdent" | "indent" | "more" | "highlight" | "comment" | "mode" | "equation" | "suggestion"

useCreateEditor Options

Prop Type Required Description
isFixedToolbar boolean ✖️ Enables a fixed (non-scrolling) toolbar.
isFloatingToolbar boolean ✖️ Enables a floating toolbar on text selection.
allowPlugins PLUGIN_KEY[] ✖️ Limit editor features by allowed plugin keys.
toolbarClasses { className?: string; groupClass?: string } ✖️ Customize classes for fixed toolbar.
floatingClasses { className?: string; groupClass?: string } ✖️ Customize classes for floating toolbar.
components Record<string, any> ✖️ Override or extend default UI components.
placeholders boolean ✖️ Enable placeholder behavior.
readOnly boolean ✖️ Set editor to read-only mode.
...options CreatePlateEditorOptions ✖️ Additional editor configuration.

Contributing 🤝

We welcome all contributions! If you would like to contribute to TKEditor, please:

  • Fork this repository.
  • Create a new branch for your feature or bug fix git checkout -b feature/amazing-feature.
  • Commit your changes git commit -m 'Add some amazing feature'.
  • Push to your branch git push origin feature/amazing-feature.
  • Open a Pull Request.
  • Please ensure that your code adheres to the standards and includes necessary tests. You can report bugs or request new features on GitHub Issues.

License 📄

TKEditor is released under the MIT License.

Thank you for using TKEditor! We hope it helps you build great editing experiences. 🎉