---
title: "Markdown editor"
type: plugin
version: "2.2.2"
doc_version: "2.7.3"
status: stable
date: 2026-07-05
library: Frutjam
stack: tailwind_css
compatibility: universal
framework_agnostic: true
runtime_requirement: none
description: "JavaScript markdown editor plugin for Tailwind CSS projects. WYSIWYG and plain Markdown modes, live preview, RTL support, dark mode. Works with Django, HTMX, Laravel, React."
url: https://frutjam.com/vi/plugins/markdown-editor
---

# The Native-First, High-Performance Markdown Plugin for JavaScript

## The Philosophy: "Native-First"

Most editors break the standard web workflow. **MarkdownEditor** embraces it.
Because it sits directly on top of a `<textarea>`, you don't need to learn a new way to handle data.

- **No Data Binding Needed:** Works with `<form method="POST">` out of the box
- **Standard Access:** Use `document.getElementById('editor').value` just like a normal input.
- **Backend Agnostic:** Works with any backend (Python, Node.js, PHP, etc.) just like a normal form field

## Key Features

### 🖼️ Advanced Image Upload (SEO Optimized)

Don't bloat your database with heavy Base64 strings. Configure our API to upload images directly to your server or S3 bucket. The editor receives the URL, keeping your Markdown files light and your site's SEO ranking high.

### 🔀 Hybrid & Plain Modes

Give your users the best of both worlds. Switch between a **Visual (WYSIWYG) Hybrid mode** for easy formatting and a **Plain Markdown mode** for distraction-free, raw syntax editing.

### 🌍 Global-Ready with RTL Support

Full native support for Right-to-Left (RTL) languages. Perfect for projects requiring **Arabic, Urdu, or Farsi** support with automatic text-direction alignment.

### ⚡ Performance at Scale

- **Lightweight**: A tiny ~116KB footprint that won't slow down your page load
- **Large Document Support**: Optimized to handle thousands of lines of text without input lag or browser freezing
- **Smart Rendering**: Debounced preview updates, cached style calculations, and conflict-free keyboard handling between list continuation and indentation — so Tab and Enter always do exactly one thing

### ♿ Accessible by Default

Full ARIA support is built in and requires no extra configuration. The toolbar is a proper `role="toolbar"` landmark, the preview pane is a labelled `role="region"`, all SVG icons are hidden from screen readers, the preview toggle exposes its on/off state via `aria-pressed`, and disabled toolbar buttons use both `disabled` and `aria-disabled` so assistive technology is never misled. Modals return focus to the triggering button when closed.

### 🛡️ Zero CSS Conflicts

All editor styles are fully scoped to the `.markdown-editor-wrapper` element. Tailwind's global preflight (element resets for `h1`–`h6`, `a`, `button`, etc.) is excluded, so the editor can live alongside Bootstrap, Tailwind, or any other CSS framework on the same page without breaking a single style.

### 🔒 XSS Safe Preview

The rendered preview is sanitized via [DOMPurify](https://github.com/cure53/DOMPurify) before being written to the DOM. Script tags, inline event handlers, and malicious URLs in crafted markdown input are stripped automatically — no configuration required.

## Markdown Editor Demo

# Markdown Editor

## Quick Implementation

### 1. Installation

You can install it using NPM, import the JavaScript file directly, or use a CDN for rapid deployment.

#### Install via NPM

```bash
npm install markdown-text-editor
```

OR

#### Using a CDN

Alternatively, include the following CDN links in your HTML

```html
<script src="https://cdn.jsdelivr.net/npm/markdown-text-editor"></script>
```

### 2. The HTML Structure

Simply place a `<textarea>` inside your standard form. No special wrappers required.

```html
<form action="/api/save" method="POST">
  <textarea id="markdown-editor" name="content"># Hello World</textarea>
  <button type="submit">Save Content</button>
</form>
```

### 3. Import JS and CSS and initialise the plugin on your textarea element

```javascript
import MarkdownEditor from "markdown-text-editor";

const editor = new MarkdownEditor('#markdown-editor', {
  placeholder: 'Write your markdown...',
  toolbar: ['heading', 'bold', 'italic', 'strikethrough', 'ul', 'ol', 'checklist', 'blockquote', 'link', 'preview'],
});
```

### 4. Configuration & Customization

You can fully customize the editor's behavior and interface by passing an `options` object. If you omit an option, the **default** value is used.

| Property      | Kiểu             | Mặc định     | Mục đích                                                                                                                                                                                                 |
| ------------- | ---------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mode`        | `string`         | `'plain'`    | Sets the initial view. Use `'hybrid'` for a WYSIWYG experience or `'plain'` for raw syntax.                                                                                                              |
| `placeholder` | `string`         | `'Write...'` | Text shown when the editor is empty.                                                                                                                                                                     |
| `toolbar`     | `array`          | `[...]`      | Defines which tools appear and in what order.                                                                                                                                                            |
| `footer`      | `false | object` | all visible  | Controls the status bar shown below the editor. Set to `false` to hide it entirely, or pass an object to toggle individual stats.                                                                        |
| `theme`       | `string`         | inherited    | Explicitly sets the editor theme (`'light'`, `'dark'`, `'snowberry'`, `'darkberry'`). If omitted, the editor inherits `data-theme` from the nearest ancestor element or the `<textarea>` itself.         |
| `minHeight`   | `number`         | `200`        | Minimum height in pixels the editor will shrink to when content is short. Pairs with `maxHeight` to set the auto-grow range.                                                                             |
| `maxHeight`   | `number`         | `500`        | Maximum height in pixels the editor can grow to in non-fullscreen mode. Once content exceeds this height a scrollbar appears inside the editor. The editor also has a drag handle so users can manually resize it beyond this limit. |
| `onChange`    | `function`       | `undefined`  | Callback fired on every content change — typing, toolbar actions, undo/redo, and list continuation. Receives the current markdown string as its only argument.                                           |

#### 🛠 Toolbar Customization

The toolbar is modular. You can create a minimal experience or a full-featured power suite by modifying the array.

##### Available Tools

| Category   | Tool Keys                                                  |
| ---------- | ---------------------------------------------------------- |
| Typography | `heading`, `bold`, `italic`, `strikethrough`, `blockquote` |
| Lists      | `ul` (bullet), `ol` (numbered), `checklist`                |
| Code       | `code` (inline), `codeblock` (fenced block)                |
| Inserts    | `hr` (horizontal rule), `table` (table template)           |
| Media      | `link`, `image`                                            |
| Editing    | `undo`, `redo`, `indent`, `outdent`                        |
| View       | `preview`                                                  |

###### 💡 Implementation Tips:

- **Reordering**: The buttons appear in the exact order you list them in the array
- **Removing**: Simply omit any key (like `image`) from the array to disable that feature entirely for the user
- **Native Fallback**: If you don't provide a `placeholder` in JS, the plugin will automatically use the `placeholder` attribute from your HTML `<textarea>`

#### 📊 Footer (Status Bar)

The footer sits below the editor and shows the cursor's **line**, **column**, the document's **character count**, and optionally the **word count** — all updated in real time. It is visible by default and each stat can be toggled independently.

| Key     | Kiểu      | Mặc định | Sự miêu tả                                                           |
| ------- | --------- | -------- | -------------------------------------------------------------------- |
| `line`  | `boolean` | `true`   | Show the current line number.                                        |
| `col`   | `boolean` | `true`   | Show the current column number.                                      |
| `chars` | `boolean` | `true`   | Show the total character count.                                      |
| `words` | `boolean` | `false`  | Show the total word count. Off by default — set to `true` to enable. |

##### Usage Examples

```javascript
// Default — line, col, and chars visible
new MarkdownEditor('#editor');

// Disable the footer entirely
new MarkdownEditor('#editor', { footer: false });

// Hide only character count
new MarkdownEditor('#editor', { footer: { chars: false } });

// Hide line and column, keep character count
new MarkdownEditor('#editor', { footer: { line: false, col: false } });

// Show only line number
new MarkdownEditor('#editor', { footer: { col: false, chars: false } });

// Enable word count alongside the defaults
new MarkdownEditor('#editor', { footer: { words: true } });

// Show word count only
new MarkdownEditor('#editor', { footer: { line: false, col: false, chars: false, words: true } });
```

### 5. Getting, Setting, and Submitting Content

One of the core strengths of **MarkdownEditor** is that it keeps the underlying `<textarea>` perfectly synchronized. Whether you are using a modern JavaScript framework or a traditional backend like **Django, PHP, or Laravel**, the workflow remains simple and native.

#### 1. The Native Way (Recommended)

Because the editor enhances a standard textarea, you can use familiar DOM methods. This is the fastest way to interact with your data without learning a new API.

```javascript
// Retrieve content via ID
const markdown = document.getElementById('markdown-editor').value;

// Set content via ID (The editor UI updates automatically)
document.getElementById('markdown-editor').value = "# New Heading Content";
```

#### 2. Using a Variable Reference

If you have a reference to the textarea element, you can use it directly — no library-specific API needed.

```javascript
const textarea = document.getElementById('markdown-editor');

// Retrieve content
const markdown = textarea.value;

// Set content (the editor UI reflects this immediately)
textarea.value = "## Updated via JS";
```

#### 3. Setting and reading content

The recommended way to set initial content is directly in the `<textarea>` HTML — this works naturally with every backend framework (Django, Laravel, Rails, PHP, etc.) and the editor renders it automatically on init.

```html
<!-- Recommended: set content server-side -->
<textarea id="markdown-editor"># Hello World</textarea>
```

To read or update content at runtime, use the native `textarea` value. Call `editor.render()` after an update to refresh the preview and hybrid layer.

```javascript
const textarea = document.getElementById('markdown-editor');

// Read
const markdown = textarea.value;

// Update at runtime
textarea.value = '# New content';
editor.render();
```

#### 4. Tearing down the editor — `destroy()`

Call `editor.destroy()` to remove the editor DOM wrapper and restore the original `<textarea>` to its position in the document. Useful in single-page applications when unmounting a view.

```javascript
const editor = new MarkdownEditor('#markdown-editor');

// Remove the editor and restore the plain textarea
editor.destroy();
```

##### Reacting to changes with `onChange`

Pass an `onChange` callback to be notified on every content change. Receives the current markdown string.

```javascript
const editor = new MarkdownEditor('#markdown-editor', {
    onChange(value) {
        console.log('Content changed:', value.length, 'characters');
    }
});
```

###### Draft auto-save with `localStorage`

Use `onChange` to save a draft on every keystroke. Restore it by pre-filling the textarea before initialising the editor.

```javascript
const DRAFT_KEY = 'my-page-draft';

// Restore saved draft before init (only if textarea starts empty)
const textarea = document.getElementById('markdown-editor');
const saved = localStorage.getItem(DRAFT_KEY);
if (saved && !textarea.value) textarea.value = saved;

// Save on every change
const editor = new MarkdownEditor('#markdown-editor', {
    onChange(value) {
        localStorage.setItem(DRAFT_KEY, value);
    }
});

// Clear draft after successful form submission
document.querySelector('form').addEventListener('submit', () => {
    localStorage.removeItem(DRAFT_KEY);
});
```

#### 4. Effortless Automatic Form Submission

Because **MarkdownEditor** is built directly on the native `<textarea>`, it is compatible with every backend framework (Django, Laravel, PHP, Ruby on Rails, etc.) right out of the box.

This is where the "Native-First" philosophy shines. You don't need to manually sync data before submitting a form. The browser treats the editor exactly like a standard input field.

```html
<form method="POST" action="/api/submit">
  <textarea id="markdown-editor" name="content" class="h-48" rows="5">
    # Initial Content
  </textarea>

  <button type="submit">Submit to Server</button>
</form>

<script>
  // Just initialize it. That's it.
  new MarkdownEditor('#markdown-editor');
</script>
```

**Note:** MarkdownEditor plugin initialization mandatory

Just use a standard HTML `<form>`. The `name` attribute on the textarea is what your server will use to identify the content.

##### 🚀 Why it's a Game-Changer for Backends

Since the editor preserves the native `<textarea>` behavior, your server handles the data as a standard string. There is zero extra logic required—no `preventDefault()` and no manual `FormData` construction.

##### 💡 Why this is a "Killer Feature":

Most editors (like Quill, Editor.js, simpleMDE, easyMDE) save data in complex JSON structures. If a developer uses those, they have to rewrite their database schema and their rendering logic.

With **MarkdownEditor**, a developer can take an old website, replace a plain `<textarea>` with your editor, and the **backend doesn't even know it changed**. It just receives the same raw text it always did, but the user gets a 10x better experience.

| Framework / Language | How to access the Markdown content |
| -------------------- | ---------------------------------- |
| PHP                  | $\_POST['content']                 |
| Django               | request.POST.get('content')        |
| Node.js (Express)    | req.body.content                   |
| Laravel              | $request->input('content')         |
| Ruby on Rails        | params[:content]                   |

## Configuration Options

### 🖼️ Advanced image upload

Handling image uploads natively—rather than relying on slow, memory-heavy Base64 strings—is a significant win for both performance and SEO.

#### Configuration Options

The image tool supports a `fileInput` configuration to handle direct server uploads.

- `accept`: Define an array of allowed image formats (e.g., 'webp', 'avif')
- `uploadUrl`: Specify the backend endpoint where the `File` object will be sent via `POST`
- `params`: Optional object to send additional data (like CSRF tokens, user IDs, or folder names) alongside the image file

#### Usage example (Full Config)

```javascript
const options = {
  placeholder: 'Start writing...',
  toolbar: [
    'link',
    {
      image: {
        fileInput: {
            accept: ['webp', 'avif'], // restrict the image upload format
            uploadUrl: '/api/upload', // Your upload endpoint
            params: {
                _token: 'your_csrf_token_here', // Essential for Laravel/Django
                folder: 'blog_posts'
            }
        },
        // Supports boolean: true/false OR object: { required: true }
        altInput: { required: true }
      }
    },
    'preview'
  ],
}
const editor = new MarkdownEditor('#markdown-editor', options);
```

#### 📡 Server Integration Details

##### 1. The Request

The editor sends a `POST` request as `multipart/form-data`. By default, it includes:

- `image_file`: The actual file object
- `image_alt`: The alt text entered by the user
- ...plus any custom data defined in the `params` object

##### 2. The Required Response

To confirm a successful upload and insert the image into the editor, your server **must** return the following JSON structure:

```json
{
  "success": true,
  "image_path": "https://cdn.yourdomain.com/uploads/image.webp"
}
```

**Note**: Ensure you use the key `image_path` for the URL of the uploaded image.

#### Xác thực văn bản thay thế hình ảnh (`altInput`)

Để đảm bảo nội dung của bạn vẫn có thể truy cập được và thân thiện với SEO, `MarkdownEditor` thực thi xác thực văn bản thay thế theo mặc định. Bạn có thể định cấu hình hành vi này bằng cách sử dụng tốc ký boolean hoặc đối tượng chi tiết.

- **Hành vi mặc định**: Nếu `altInput` không được xác định, nó sẽ mặc định là `{ bắt buộc: true `
- **Thực thi khả năng truy cập**: Người dùng sẽ bị ngăn chèn hình ảnh cho đến khi mô tả thay thế được cung cấp

##### Ví dụ về cấu hình:

###### 1. Mặc định (Không cần cấu hình)

```javascript
// Alt text is REQUIRED by default
image: {
  fileInput: { uploadUrl: '/api/upload' }
}
```

###### 2. Viết tắt (Tắt xác thực)

Nếu bạn muốn cho phép hình ảnh không có mô tả, chỉ cần đặt boolean thành `false`.

```javascript
image: {
  altInput: false // Users can now skip the alt text field
}
```

###### 3. Dựa trên đối tượng (Rõ ràng)

```javascript
image: {
    altInput: {
        required: false // Disables alt text validation — users can skip the alt field
    }
}
```

#### Cách sử dụng hình ảnh tiêu chuẩn (Không có `fileInput`):

Nếu `fileInput` không được định cấu hình, trình chỉnh sửa sẽ mặc định sử dụng phương thức dựa trên URL đơn giản. Điều này lý tưởng nếu người dùng của bạn chủ yếu liên kết đến các máy chủ hình ảnh bên ngoài.

```javascript
const options = {
  toolbar: [
    'link',
    'image',
    'preview'
  ],
}
const editor = new MarkdownEditor('#markdown-editor', options);
```

##### 💡 Tại sao nên sử dụng thông số?

Trong các framework như Laravel hoặc Django, bạn không thể tải tệp lên mà không có mã thông báo CSRF. Bằng cách thêm `_token` vào đối tượng `params`, yêu cầu của bạn sẽ chuyển qua phần mềm trung gian bảo mật của chương trình phụ trợ một cách liền mạch, duy trì triết lý "Zero Logic" cho bộ điều khiển phía máy chủ của bạn.

### 🔀 Chế độ chỉnh sửa

**MarkdownEditor** cung cấp hai cách riêng biệt để viết và định dạng nội dung của bạn. Bạn có thể chuyển đổi giữa chế độ xem tập trung vào cú pháp truyền thống hoặc trải nghiệm hiện đại, ưu tiên hình ảnh.

#### Cấu hình

Bạn có thể xác định chế độ bắt đầu bằng thuộc tính `mode` trong quá trình khởi tạo.

- `plain` (**Mặc định**): Môi trường Markdown rõ ràng, hiệu suất cao trong đó hiển thị cú pháp (như `**bold**` hoặc `# tiêu đề`). Lý tưởng cho các nhà phát triển và những người theo chủ nghĩa thuần túy Markdown
- `kết hợp`: Trải nghiệm lấy cảm hứng từ WYSIWYG giúp hiển thị định dạng (Đậm, Nghiêng, Tiêu đề) trong thời gian thực khi bạn nhập, trong khi vẫn duy trì cấu trúc Markdown cơ bản.

##### Thực hiện:

```javascript
// Default initialization (Plain Mode)
new MarkdownEditor('#markdown-editor');

// Explicit Plain Mode
new MarkdownEditor('#markdown-editor', {
    mode: 'plain'
});

// Hybrid (Visual) Mode
new MarkdownEditor('#markdown-editor', {
    mode: 'hybrid'
});
```

##### Xem trước chế độ kết hợp và đơn giản:

###### Chế độ kết hợp

Định dạng trực quan được hiển thị theo thời gian thực trong khi bạn nhập.

# Hybrid mode markdown editor
This will render visual styles immediately.

###### Chế độ đơn giản (Mặc định)

Tập trung vào cú pháp Markdown thô để có trải nghiệm nhẹ nhàng.

# Plain mode markdown editor
This shows the raw **syntax**.

| Cấu hình                                                                                                                                                                                                 | Thuộc tính/Công cụ                                                                                                                                                                        | Sự miêu tả                                                                                                                                                                                               |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Đối tượng tùy chọn                                                                                                                                                                                       | `placeholder`                                                                                                                                                                             | Đặt văn bản giữ chỗ cho vùng văn bản (tùy chọn, vì bạn cũng có thể sử dụng thuộc tính vùng văn bản HTML tiêu chuẩn)                                                                                      |
|                                                                                                                                                                                                          | `mode: 'hybrid'`                                                                                                                                                                          | Cho phép trải nghiệm lấy cảm hứng từ WYSIWYG hiển thị định dạng (in đậm, in nghiêng, tiêu đề) trong thời gian thực khi bạn nhập                                                                          |
| `thanh công cụ`: Xác định công cụ nào    xuất hiện trên thanh công cụ và thứ tự của chúng.                                                                                                               | `heading`                                                                                                                                                                                 | Mở menu thả xuống để chọn cấp tiêu đề H1–H6                                                                                                                                                              |
|                                                                                                                                                                                                          | `bold`                                                                                                                                                                                    | Cho phép định dạng văn bản in đậm.                                                                                                                                                                       |
|                                                                                                                                                                                                          | `italic`                                                                                                                                                                                  | Cho phép định dạng văn bản in nghiêng.                                                                                                                                                                   |
|                                                                                                                                                                                                          | `strikethrough`                                                                                                                                                                           | Cho phép gạch ngang văn bản.                                                                                                                                                                             |
|                                                                                                                                                                                                          | `ol`                                                                                                                                                                                      | (Danh sách có thứ tự): Chuyển văn bản sang dạng danh sách đánh số.                                                                                                                                       |
|                                                                                                                                                                                                          | `ul`                                                                                                                                                                                      | (Danh sách không có thứ tự): Chuyển văn bản thành danh sách dấu đầu dòng.                                                                                                                                |
|                                                                                                                                                                                                          | `checklist`                                                                                                                                                                               | Thêm các hộp kiểm vào văn bản của bạn, giúp nó trở nên tuyệt vời cho các nhiệm vụ, danh sách việc cần làm hoặc theo dõi trạng thái hoàn thành.                                                           |
|                                                                                                                                                                                                          | `blockquote`                                                                                                                                                                              | Đánh dấu văn bản được trích dẫn hoặc nhấn mạnh.                                                                                                                                                          |
|                                                                                                                                                                                                          | `code`                                                                                                                                                                                    | Bao văn bản đã chọn bằng dấu backticks đơn cho mã nội tuyến. Nhấp lại sẽ loại bỏ dấu tích ngược.                                                                                                         |
|                                                                                                                                                                                                          | `codeblock`                                                                                                                                                                               | Bao bọc văn bản đã chọn trong khối mã có hàng rào ba dấu ngược. Nhấp chuột một lần nữa sẽ loại bỏ hàng rào.                                                                                              |
|                                                                                                                                                                                                          | `hr`                                                                                                                                                                                      | Chèn một quy tắc ngang `---` vào vị trí con trỏ trên dòng riêng của nó.                                                                                                                                  |
|                                                                                                                                                                                                          | `table`                                                                                                                                                                                   | Chèn mẫu bảng đánh dấu 2x3 ban đầu vào vị trí con trỏ.                                                                                                                                                   |
|                                                                                                                                                                                                          | `image`                                                                                                                                                                                   | Cho phép bạn chèn hình ảnh thông qua cú pháp đánh dấu.                                                                                                                                                   |
|                                                                                                                                                                                                          | `link`                                                                                                                                                                                    | Cho phép bạn thêm siêu liên kết vào văn bản của mình.                                                                                                                                                    |
|                                                                                                                                                                                                          | `undo`                                                                                                                                                                                    | Để đảo ngược những thay đổi cuối cùng.                                                                                                                                                                   |
| `redo`                                                                                                                                                                                                   | Để áp dụng lại những thay đổi được hoàn tác gần đây nhất.                                                                                                                                 |
| `indent`                                                                                                                                                                                                 | Để tăng mức độ thụt lề.                                                                                                                                                                   |
| `outdent`                                                                                                                                                                                                | Để giảm mức độ thụt lề.                                                                                                                                                                   |
| `preview`                                                                                                                                                                                                | Chuyển đổi chế độ xem trước toàn màn hình cạnh nhau. Các hộp kiểm trong khung xem trước có thể nhấp vào được và cập nhật nguồn đánh dấu ngay lập tức. Nhấn Escape để thoát toàn màn hình. |
| [**Tính năng tải hình ảnh nâng cao lên**](#advanced-image-upload):    Cho phép định cấu hình tải hình ảnh lên     máy chủ của riêng bạn và đặt đường dẫn hình ảnh     thông qua API. Điều này cải thiện hiệu suất và SEO. | `fileInput`                                                                                                                                                                               | `chấp nhận`: Mảng loại tệp hình ảnh được phép (ví dụ: `'webp'`, `'avif'`).   `uploadUrl`: Điểm cuối phụ trợ nơi tệp được gửi qua `POST`.   `params`: Đối tượng tùy chọn cho dữ liệu bổ sung (mã thông báo CSRF, ID người dùng, tên thư mục) |
|                                                                                                                                                                                                          | `altInput`                                                                                                                                                                                | `bắt buộc: false`: Tắt xác thực nhập văn bản thay thế (mặc định là đúng)                                                                                                                                 |

### ⌨️ Phím tắt

Các hành động định dạng phổ biến có thể được kích hoạt trực tiếp từ bàn phím mà không cần chạm vào thanh công cụ. Mỗi phím tắt cũng được hiển thị trong chú giải công cụ của nút thanh công cụ tương ứng.

| Phím tắt                     | Hoạt động                                              |
| ---------------------------- | ------------------------------------------------------ |
| `Ctrl + B` / `⌘ B`           | Chuyển đổi **Đậm**                                     |
| `Ctrl + I` / `⌘ I`           | Chuyển đổi *nghiêng*                                   |
| `Ctrl + K` / `⌘ K`           | Chèn liên kết                                          |
| `` Ctrl + ` `` / `` ⌘ ` ``   | Chuyển đổi `Mã` nội tuyến                              |
| `Ctrl + Shift + S` / `⌘ ⇧ S` | Chuyển đổi ~~Gạch ngang~~                              |
| `Ctrl + Z` / `⌘ Z`           | Hoàn tác                                               |
| `Ctrl + Shift + Z` / `⌘ ⇧ Z` | Làm lại                                                |
| `Tab`                        | Thụt lề các dòng đã chọn                               |
| `Shift + Tab`                | Nhô ra các dòng đã chọn                                |
| `Ctrl + F` / `⌘ F`           | Mở bảng Tìm kiếm                                       |
| `Ctrl + H` / `⌘ H`           | Mở bảng Tìm & Thay thế                                 |
| `Escape`                     | Đóng Tìm bảng điều khiển/Thoát xem trước toàn màn hình |

### 🔍 Tìm & amp; Thay thế

Bảng điều khiển tìm và thay thế tích hợp có sẵn bên trong trình chỉnh sửa — không cần tiện ích mở rộng trình duyệt hoặc công cụ riêng biệt.

- Nhấn `Ctrl + F` (hoặc `⌘ F`) để mở bảng **Tìm**
- Nhấn `Ctrl + H` (hoặc `⌘ H`) để mở **Tìm & Thay thế bảng điều khiển**
- Tìm kiếm **không phân biệt chữ hoa chữ thường** và hiển thị bộ đếm kết quả khớp trực tiếp (ví dụ: *3 trên 12*)
- Điều hướng các kết quả khớp bằng các nút **▲ / ▼** hoặc `Enter` / `Shift + Enter`
- **Thay thế** thay thế kết quả được đánh dấu hiện tại; **Thay thế tất cả** thay thế mọi lần xuất hiện cùng một lúc
- Nhấn `Escape` để đóng bảng và trả tiêu điểm về trình chỉnh sửa

Bảng điều khiển nổi ở góc trên bên phải của khu vực nội dung trình soạn thảo và không làm gián đoạn quá trình viết.

### 🌙 Chủ đề

**MarkdownEditor** tự động kế thừa chủ đề của nó từ trang xung quanh — không cần cấu hình. Trình chỉnh sửa đọc `data-theme` từ tổ tiên gần nhất khi khởi tạo, để nó luôn đồng bộ với chủ đề trang web của bạn ngay từ đầu.

#### Cách giải quyết chủ đề (thứ tự ưu tiên)

1. **`tùy chọn chủ đề`** — ghi đè rõ ràng được truyền trong đối tượng tùy chọn
2. **`data-theme` trên `<textarea>`** — được đặt trực tiếp trên phần tử
3. **`data-theme` trên bất kỳ tổ tiên nào** — ví dụ: `<html>`, `<body>` hoặc trình bao bọc `<div>`

#### Chủ đề có sẵn

`'light'` (mặc định), `'dark'`, `'snowberry'`, `'darkberry'`

##### Tùy chọn 1 — kế thừa từ `<html>` hoặc bất kỳ tổ tiên nào (cấu hình không)

```html
<html data-theme="dark">
  ...
  <textarea id="markdown-editor"></textarea>
  <script>
    new MarkdownEditor('#markdown-editor'); // picks up dark automatically
  </script>
```

##### Tùy chọn 2 — đặt `data-theme` trực tiếp trên `<textarea>`

```html
<textarea id="markdown-editor" data-theme="dark"></textarea>
<script>
  new MarkdownEditor('#markdown-editor');
</script>
```

##### Tùy chọn 3 — tùy chọn `theme` rõ ràng (ghi đè mọi thứ)

```javascript
new MarkdownEditor('#markdown-editor', {
    theme: 'dark'
});
```

#### 🎨 Chủ đề tùy chỉnh thông qua các biến CSS

Bạn hoàn toàn có thể tùy chỉnh giao diện của trình chỉnh sửa bằng cách ghi đè các biến CSS của nó trên phần tử `.markdown-editor-wrapper` hoặc bất kỳ bộ chọn `[data-theme]` nào. Tất cả các màu đều sử dụng [không gian màu OKLCH](https://oklch.com) để mang lại kết quả đồng nhất về mặt nhận thức.

| Biến                 | Mục đích                                             | Ánh sáng mặc định       | Mặc định tối            |
| -------------------- | ---------------------------------------------------- | ----------------------- | ----------------------- |
| `--color-base`       | Nền soạn thảo                                        | `oklch(100% 0 0)`       | `oklch(10.9% 0 0)`      |
| `--color-on-base`    | Màu văn bản chính                                    | `oklch(22% 0 0)`        | `oklch(98% 0 0)`        |
| `--color-primary`    | Giọng chính (thanh công cụ đang hoạt động, liên kết) | `oklch(51.1% .262 277)` | `oklch(66.4% .184 286)` |
| `--color-on-primary` | Văn bản trên bề mặt màu cơ bản                       | `oklch(96.2% .018 272)` | `oklch(10% .01 270)`    |
| `--color-secondary`  | Giọng phụ                                            | `oklch(59.1% .293 323)` | `oklch(65% .18 220)`    |
| `--color-accent`     | Đánh dấu trọng âm (mã nội tuyến, in nghiêng)         | `oklch(54.1% .281 293)` | `oklch(75% .18 50)`     |
| `--color-neutral`    | Bề mặt trung tính (viền, vách ngăn)                  | `oklch(15% 0 0)`        | `oklch(85% 0 0)`        |
| `--color-error`      | Màu trạng thái lỗi                                   | `oklch(57.7% .245 27)`  | `oklch(60% .22 30)`     |
| `--border-radius`    | Làm tròn góc của khung soạn thảo                     | `0.25rem`               | |

##### Ví dụ về chủ đề tùy chỉnh

Ghi đè bất kỳ biến nào trên `.markdown-editor-wrapper` sau khi trình soạn thảo khởi tạo hoặc xác định khối `[data-theme]` tùy chỉnh trong biểu định kiểu của bạn:

```css
/* Override individual variables */
.markdown-editor-wrapper {
    --color-primary: oklch(60% 0.2 30);   /* orange accent */
    --border-radius: 0.5rem;
}

/* Or define a full custom theme */
[data-theme="brand"] .markdown-editor-wrapper,
.markdown-editor-wrapper[data-theme="brand"] {
    --color-base:       oklch(15% 0.01 250);
    --color-on-base:    oklch(95% 0 0);
    --color-primary:    oklch(65% 0.22 145);   /* green */
    --color-on-primary: oklch(10% 0 0);
    --color-accent:     oklch(75% 0.18 60);
    --color-neutral:    oklch(80% 0 0);
    --border-radius:    0.75rem;
}
```

```javascript
new MarkdownEditor('#markdown-editor', { theme: 'brand' });
```

- **Xem trước theo thời gian thực:** Xem phần đánh dấu của bạn được hiển thị ngay lập tức khi bạn nhập.
- **Đánh dấu cú pháp:** Khả năng đọc nâng cao với mã rõ ràng và định dạng đánh dấu.
- **Tích hợp dễ dàng**: Tích hợp liền mạch vào bất kỳ dự án web nào với thiết lập tối thiểu.
- **Thanh công cụ có thể tùy chỉnh:** Tự động định cấu hình và sắp xếp lại các tùy chọn thanh công cụ như in đậm, in nghiêng, v.v.

## Đặc trưng

## 🔌 Tích hợp biểu mẫu gốc

Hoạt động chính xác như `<textarea>` tiêu chuẩn. Không có API phức tạp—chỉ cần sử dụng thuộc tính `value` hoặc `name`. Nó "chỉ hoạt động" với việc gửi biểu mẫu HTML tiêu chuẩn bằng PHP, Django hoặc Node.js.

## 🖼️ Tải lên hình ảnh nâng cao

Định cấu hình tải lên máy chủ gốc thông qua API. Tránh các chuỗi Base64 nặng để đảm bảo tải trang nhanh hơn và SEO vượt trội bằng cách lưu trữ hình ảnh trên CDN của riêng bạn.

## 🔀 Hybrid & Plain Modes

Chuyển đổi giữa trải nghiệm **Kết hợp (WYSIWYG)** để chỉnh sửa trực quan hoặc chế độ **Đánh dấu đơn giản** để có cảm giác viết mã truyền thống.

## 🚀 Hiệu suất cao

Gói **~116KB** nhỏ được tối ưu hóa cho "Nội dung nặng". Xử lý các tài liệu lớn và các tệp lớn mà không có bất kỳ độ trễ đầu vào hoặc giảm hiệu suất nào.

## 🌍 Hỗ trợ RTL tích hợp

Hỗ trợ gốc cho các ngôn ngữ từ phải sang trái như tiếng Ả Rập, tiếng Urdu và tiếng Farsi. Hoàn hảo để xây dựng các ứng dụng có thể truy cập toàn cầu.

## 🌙 Chủ đề thích ứng

Bao gồm hỗ trợ Chế độ tối tự động. Nó đồng bộ hóa với cài đặt hệ thống của bạn hoặc thư viện **Giao diện người dùng Frutjam** để có trải nghiệm hình ảnh liền mạch.

## 📝 Chỉnh sửa thông minh

Tính năng tiếp tục danh sách tự động theo kiểu GitHub dành cho danh sách có thứ tự, danh sách không có thứ tự và danh sách kiểm tra — nhấn `Enter` và trình chỉnh sửa sẽ tiếp tục mẫu. Các hộp kiểm trong khung xem trước có thể nhấp vào được và đồng bộ hóa trở lại nguồn đánh dấu ngay lập tức.

## 📱 Hoàn toàn đáp ứng

Giao diện người dùng linh hoạt, ưu tiên thiết bị di động, thích ứng hoàn hảo với máy tính để bàn, máy tính bảng và điện thoại thông minh để chỉnh sửa khi đang di chuyển.

## 📦 Hỗ trợ toàn cầu

Tương thích với **ESM, UMD, CommonJS và IIFE**. Hoạt động ngay lập tức thông qua CDN (`<script src>`), npm hoặc bất kỳ gói nào (Vite, webpack, Rollup) — không cần cấu hình bổ sung.

## ♿ Accessible by Default

Hỗ trợ ARIA đầy đủ được tích hợp sẵn — mốc thanh công cụ, vùng xem trước được gắn nhãn, các nút thân thiện với trình đọc màn hình, `aria-pressed` trên nút chuyển đổi xem trước, `đã tắt` và `aria-disabled` trên các công cụ không hoạt động và khôi phục tiêu điểm chính xác khi các chế độ đóng.

## 🛡️ Zero CSS Conflicts

Các kiểu trình soạn thảo hoàn toàn nằm trong phạm vi `.markdown-editor-wrapper`. Tính năng kiểm tra trước toàn cầu của Tailwind bị loại trừ để trình soạn thảo hoạt động an toàn cùng với Bootstrap, Tailwind hoặc bất kỳ khung nào khác mà không phá vỡ phong cách của chúng.

## ⌨️ Phím tắt

`Ctrl+B`, `Ctrl+I`, `Ctrl+K`, `` Ctrl+` ``, `Ctrl+Shift+S` — các thao tác định dạng phổ biến mà không cần chạm vào chuột. Mỗi phím tắt được hiển thị trong chú giải công cụ của nút thanh công cụ.

## 🔍 Tìm & amp; Thay thế

Nhấn `Ctrl+F` để tìm hoặc `Ctrl+H` để mở find & thay thế. Tìm kiếm không phân biệt chữ hoa chữ thường với bộ đếm so khớp trực tiếp, điều hướng tiếp theo/trước, thay thế một lần và thay thế tất cả — mà không cần rời khỏi trình chỉnh sửa.

## Ví dụ về cấu hình đầy đủ

Sử dụng ví dụ toàn diện này để khởi tạo **MarkdownEditor** với tất cả các tính năng chính, bao gồm sắp xếp thanh công cụ tùy chỉnh và xử lý tải lên hình ảnh nâng cao.

```javascript
const editor = new MarkdownEditor('#markdown-editor', {
    mode: 'hybrid',
    placeholder: 'Start writing...',
    footer: {
        line: true,
        col: true,
        chars: true,
        words: true,
    },
    onChange(value) {
        console.log('Content updated:', value.length, 'characters');
    },
    toolbar: [
        'heading', 'bold', 'italic', 'strikethrough', 'blockquote',
        'ul', 'ol', 'checklist',
        'code', 'codeblock', 'hr', 'table',
        {
            image: {
                fileInput: {
                    accept: ['webp', 'avif', 'png'],
                    uploadUrl: '/api/upload'
                }
            }
        },
        'link', 'undo', 'redo', 'indent', 'outdent', 'preview'
    ],
});

// Read content natively
const markdown = document.getElementById('markdown-editor').value;

// Update content programmatically (dispatch input to keep preview in sync)
// const ta = document.getElementById('markdown-editor');
// ta.value = '# New content';
// ta.dispatchEvent(new Event('input', { bubbles: true }));

// destroy() when the view unmounts (SPAs)
// editor.destroy();
```

