Skip to content

markrahimi/mandoo-editor

Repository files navigation

MandooEditor

MandooEditor logo

A modern, lightweight WYSIWYG rich text editor for React & Next.js
Feature-flagged · Fully typed · Zero runtime dependencies · <300KB

npm version downloads TypeScript React Next.js license


Features

Feature Description
Visual editor contenteditable WYSIWYG — no iframe, no Flash
Block mode Drag-and-drop block editor with per-block type selector
Text / HTML mode Raw HTML editing with syntax highlighting
HTML & Markdown onChange fires in whichever format you choose
Feature flags Enable or disable every toolbar button individually
Media upload Wire any S3 / MinIO / custom API — just pass callbacks
Plugins Link checker, tables, image editor, history, YouTube embed, subscript/superscript
Fully typed End-to-end TypeScript with imperative ref handle
Zero deps No runtime dependencies beyond React

Installation

npm install mandoo-editor
# or
yarn add mandoo-editor
# or
pnpm add mandoo-editor

⚠️ Required — add this import wherever you use the editor:

import 'mandoo-editor/styles';

Add it in your layout, page, or component — wherever MandooEditor is rendered. Without it the editor has no styling.


Quick Start

"use client";

import MandooEditor from "mandoo-editor";

export default function MyPage() {
  return (
    <MandooEditor
      defaultValue="<p>Start writing...</p>"
      onChange={(html) => console.log(html)}
      height={400}
    />
  );
}

API Reference

###rops

Prop Type Default Description
value string Controlled HTML value
defaultValue string '' Uncontrolled initial HTML value
onChange (value: string) => void Fires on every change with current value
outputFormat 'html' | 'markdown' 'html' Format for onChange and getValue()
placeholder string 'Start writing…' Placeholder shown when empty
tabs TabId[] ['visual','text','block'] Which tabs to display
defaultTab TabId 'visual' Initially active tab
features Features all enabled Granular toolbar feature flags
plugins Plugins none Optional plugin flags
media MediaConfig File upload / library config
height number 400 Min height of editor content area (px)
className string Extra CSS class on root element
apiToken string Token for future paid pro features

###mperative Handle (ref)

import { useRef } from "react";
import MandooEditor, { MandooEditorHandle } from "mandoo-editor";

const ref = useRef<MandooEditorHandle>(null);

// Methods:
ref.current?.getValue(); // → string (respects outputFormat)
ref.current?.getHTML(); // → raw HTML string
ref.current?.getMarkdown(); // → Markdown string
ref.current?.setValue(html); // set content programmatically
ref.current?.focus(); // focus the editor
ref.current?.clear(); // clear content

Form Integration

MandooEditor outputs HTML or Markdown. There are two ways to use it in a form:

Option 1 — name prop (native forms, FormData, Server Actions)

Add a name prop and a hidden <input> is automatically rendered. Works with any form library or native HTML form submission.

// Native HTML form
<form action="/api/save" method="POST">
  <MandooEditor name="content" outputFormat="html" />
  <button type="submit">Save</button>
</form>

// Next.js Server Action
async function save(formData: FormData) {
  'use server';
  const content = formData.get('content'); // ← HTML or Markdown
}

<form action={save}>
  <MandooEditor name="content" outputFormat="markdown" />
  <button type="submit">Save</button>
</form>

Option 2 — onChange (controlled state, react-hook-form, Zustand…)

// useState
const [content, setContent] = useState('');
<MandooEditor onChange={setContent} outputFormat="html" />

// react-hook-form
const { setValue } = useForm();
<MandooEditor onChange={(v) => setValue('content', v)} outputFormat="markdown" />

// Zustand / Redux
<MandooEditor onChange={(v) => dispatch(setContent(v))} />

Feature Flags

Disable any toolbar button by setting its flag to false:

<MandooEditor
  features={{
    // Disable specific buttons
    strikethrough: false,
    align: false,
    charMap: false,
    help: false,
    // All others remain enabled
  }}
/>

Full list of flags: bold, italic, strikethrough, lists, blockquote, hr, align, link, fullscreen, kitchenSink, underline, justify, foreColor, pasteAsText, removeFormat, charMap, indent, undo, help, media, subscript, superscript


Plugins

<MandooEditor
  plugins={{
    linkChecker: true, // Validate URLs when inserting links
    spellChecker: true, // Browser-native spell check
    tables: true, // Insert & edit tables
    imageEditor: true, // Crop/resize images before upload
    history: true, // Edit history with restore
    youtube: true, // Embed YouTube videos by URL
  }}
/>

Media Upload

Wire any storage backend — S3, MinIO, Cloudflare R2, or your own API:

<MandooEditor
  media={{
    accept: "image/*,video/*",
    maxSize: 10 * 1024 * 1024, // 10 MB

    async onUpload(file) {
      const fd = new FormData();
      fd.append("file", file);
      const res = await fetch("/api/upload", { method: "POST", body: fd });
      return res.json(); // { url: string, name?: string, alt?: string }
    },

    async onListFiles() {
      const res = await fetch("/api/media");
      return res.json(); // MediaFile[]
    },
  }}
/>

###inIO / S3 Server Route (Next.js App Router)

// app/api/upload/route.ts
import { Client } from "minio"; // npm install minio
import { NextRequest, NextResponse } from "next/server";

const minio = new Client({
  endPoint: process.env.MINIO_ENDPOINT!,
  useSSL: true,
  accessKey: process.env.MINIO_ACCESS_KEY!,
  secretKey: process.env.MINIO_SECRET_KEY!,
});

export async function POST(req: NextRequest) {
  const form = await req.formData();
  const file = form.get("file") as File;
  const buf = Buffer.from(await file.arrayBuffer());
  const name = `uploads/${Date.now()}-${file.name}`;
  await minio.putObject(process.env.MINIO_BUCKET!, name, buf, buf.length, {
    "Content-Type": file.type,
  });
  const url = await minio.presignedGetObject(
    process.env.MINIO_BUCKET!,
    name,
    604800
  );
  return NextResponse.json({ url, name: file.name });
}

Output Formats

// HTML output (default)
<MandooEditor
  outputFormat="html"
  onChange={(html) => {
    // "<p>Hello <strong>world</strong></p>"
    console.log(html);
  }}
/>

// Markdown output
<MandooEditor
  outputFormat="markdown"
  onChange={(md) => {
    // "Hello **world**"
    console.log(md);
  }}
/>

Tabs Configuration

// Only show Visual and Text tabs (no Block editor)
<MandooEditor tabs={['visual', 'text']} />

// Start on Block tab
<MandooEditor defaultTab="block" />

// Only Block editor
<MandooEditor tabs={['block']} />

Pro Features (Coming Soon)

The following features require an apiToken and will be available in a future paid tier:

  • Export to PDF — one-click export via Mandoo cloud API
  • Word Import/Export — read and write .docx files
  • AI Assistant — chat with AI to rewrite, summarise, or extend content
// Reserve your token now — setting it has no effect until pro plugins are released
<MandooEditor apiToken="mk_live_..." />

Token Infrastructure

import { mandooFetch, validateToken } from "mandoo-editor";

// Validate a token format
const valid = validateToken("mk_live_abc123...");

// Call Mandoo API (for pro features)
const result = await mandooFetch(
  "/export/pdf",
  { method: "POST", body: fd },
  {
    token: "mk_live_...",
    baseUrl: "https://api.mandooeditor.com/v1", // optional override
  }
);

TypeScript Types

import type {
  MandooEditorProps,
  MandooEditorHandle,
  Features,
  Plugins,
  MediaConfig,
  MediaFile,
  MediaUploadResult,
  TabId,
  OutputFormat,
  TokenConfig,
} from "mandoo-editor";

Links

🌍 Website mandooeditor.markrahimi.com
📦 npm npmjs.com/package/mandoo-editor
🐙 GitHub github.com/markrahimi/mandoo-editor
🐛 Issues github.com/markrahimi/mandoo-editor/issues
Support ko-fi.com/markrahimi
👤 Author markrahimi.com

License

MIT © Mohammad Ali Rahimi

About

A modern WYSIWYG editor for React & Next.js

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors