erl-mathtextx-editor
v0.2.5
Published
Visual math editor component for solutest.id — CKEditor replacement with zero-LaTeX approach
Downloads
969
Maintainers
Readme
erl-mathtextx-editor
Visual Math Editor Component — Zero LaTeX Required
Embeddable visual math editor widget untuk CMS dan platform edukasi. User tidak perlu tahu LaTeX — semua input matematika dilakukan secara visual.
✨ Fitur Utama
- 🎯 Visual Math Input — Insert math langsung inline tanpa dialog (Ctrl+M)
- 📝 Rich Text Editor — Bold, italic, tables, lists, links, images
- 🧮 Equation Editor — Dialog MathType-style dengan tab, grid, KaTeX preview (Edit existing math)
- 📋 100+ Formula Templates — Algebra, calculus, trigonometry, chemistry, matrix
- 🏆 Mode Olimpiade — Toolbar khusus untuk kompetisi matematika dengan simbol set, Greek letters, NT/Combo
- 🖼️ Free-form Image Drag — Drag gambar ke posisi bebas (pixel-perfect)
- 📄 DOCX Import — Import file .docx via mammoth.js (toolbar + drag-drop)
- 🗂️ Google Docs Paste — Paste dari Google Docs (equations + document) auto-cleaned
- 👁️ Content Viewer — Read-only renderer dengan KaTeX + DOMPurify
- 🎨 Table Editor — 6 templates, column resize, cell merge/split
- 🔒 XSS Protection — DOMPurify sanitization di paste + serializer
- 🛡️ Error Boundary — Anti white-screen crash protection
📦 Installation
npm install erl-mathtextx-editor🚀 Quick Start
1. Basic Editor
import { MathTextXEditor } from 'erl-mathtextx-editor'
import 'erl-mathtextx-editor/styles'
function App() {
return (
<MathTextXEditor
onChange={(html) => console.log(html)}
placeholder="Tulis soal di sini..."
/>
)
}2. Editor dengan Save Handler
import { useRef } from 'react'
import { MathTextXEditor, getHTML } from 'erl-mathtextx-editor'
import 'erl-mathtextx-editor/styles'
function QuestionForm() {
const editorRef = useRef<HTMLDivElement>(null)
const handleSave = () => {
if (!editorRef.current) return
const html = getHTML(editorRef.current)
fetch('/api/questions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: html }),
})
}
return (
<div>
<MathTextXEditor ref={editorRef} placeholder="Tulis pertanyaan..." onSave={handleSave} />
<button onClick={handleSave}>Simpan</button>
</div>
)
}3. Edit Existing Content
import { MathTextXEditor } from 'erl-mathtextx-editor'
import 'erl-mathtextx-editor/styles'
function EditQuestion({ existingHtml }: { existingHtml: string }) {
return (
<MathTextXEditor
content={existingHtml}
onChange={(html) => console.log('Updated:', html)}
/>
)
}4. Read-Only Mode (Viewer)
import { ContentViewer } from 'erl-mathtextx-editor/viewer'
import 'erl-mathtextx-editor/viewer/styles'
function ExamQuestion({ questionHtml }: { questionHtml: string }) {
return <ContentViewer content={questionHtml} />
}5. Multi-Instance (Soal + Pilihan Jawaban)
import { useState } from 'react'
import { MathTextXEditor } from 'erl-mathtextx-editor'
import 'erl-mathtextx-editor/styles'
function MultipleChoiceForm() {
const [question, setQuestion] = useState('')
const [options, setOptions] = useState(
['A', 'B', 'C', 'D'].map((id) => ({ id, content: '' }))
)
return (
<div>
<label>Pertanyaan:</label>
<MathTextXEditor
content={question}
onChange={setQuestion}
placeholder="Tulis pertanyaan..."
minHeight="150px"
/>
{options.map((opt) => (
<div key={opt.id}>
<label>Opsi {opt.id}:</label>
<MathTextXEditor
content={opt.content}
onChange={(html) => setOptions((prev) => prev.map((o) => o.id === opt.id ? { ...o, content: html } : o))}
placeholder={`Jawaban ${opt.id}...`}
minHeight="60px"
/>
</div>
))}
</div>
)
}6. Next.js (App Router)
'use client'
import dynamic from 'next/dynamic'
import 'erl-mathtextx-editor/styles'
const MathTextXEditor = dynamic(
() => import('erl-mathtextx-editor').then((mod) => mod.MathTextXEditor),
{ ssr: false }
)
export default function EditorPage() {
return (
<MathTextXEditor
placeholder="Tulis soal matematika..."
onChange={(html) => console.log(html)}
minHeight="300px"
/>
)
}7. Vite + React
import { MathTextXEditor } from 'erl-mathtextx-editor'
import 'erl-mathtextx-editor/styles'
function App() {
return (
<MathTextXEditor
placeholder="Tulis soal..."
onChange={(html) => console.log(html)}
/>
)
}
export default App🎛️ Toolbar Modes
Editor menyediakan 3 preset toolbar yang bisa dipilih via prop toolbarMode:
Basic
Toolbar standar dengan tombol format teks dasar, insert media, dan math formula.
Advanced
Toolbar lengkap dengan font family, text color, alignment, table operations, superscript/subscript, chemistry formula, dan formatting lanjutan.
Olimpiade
Toolbar minimal untuk kompetisi matematika — hanya tombol esensial:
| Grup | Tombol | |---|---| | Undo/Redo | Undo, Redo | | Format | Bold, Italic, Underline | | Structure | Paragraph, H1, H2 | | Lists | Bullet List, Ordered List, Outdent, Indent | | Math | Math Formula (inline), Block Math | | Actions | Remove Format |
Math Toolbar di mode Olimpiade menampilkan section khusus: Basic, Relation, Set, Greek, Structure — tanpa section Calc yang kurang relevan untuk olimpiade.
Custom Mode
Jika preset tidak sesuai, Anda bisa atur sendiri via internalToolbarMode state atau kontrol toolbar secara manual menggunakan komponen terpisah (MainToolbar, MathToolbar, MathTypeDialog).
🧰 Image Upload
Editor mendukung upload gambar dari: Insert dialog, drag-drop, paste dari clipboard, dan DOCX import. Semuanya melalui satu callback onImageUpload.
Basic Upload
import { MathTextXEditor } from 'erl-mathtextx-editor'
import 'erl-mathtextx-editor/styles'
function EditorWithUpload() {
const handleImageUpload = async (file: File): Promise<string> => {
const formData = new FormData()
formData.append('image', file)
const res = await fetch('/api/upload', { method: 'POST', body: formData })
if (!res.ok) throw new Error('Upload failed: ' + res.statusText)
const data = await res.json()
return data.url // Expected: { "url": "https://cdn.example.com/img.jpg" }
}
return (
<MathTextXEditor
placeholder="Tulis soal..."
onImageUpload={handleImageUpload}
/>
)
}Re-upload Gambar dari Paste (Google Docs / Website)
import { MathTextXEditor } from 'erl-mathtextx-editor'
import 'erl-mathtextx-editor/styles'
function EditorWithPasteReupload() {
const handleImageUpload = async (file: File): Promise<string> => {
const formData = new FormData()
formData.append('image', file)
const res = await fetch('/api/upload', { method: 'POST', body: formData })
return (await res.json()).url
}
const handleBeforePasteHTML = async (html: string): Promise<string> => {
// Download + re-upload external images from pasted HTML
const imgRegex = /<img\s+[^>]*src="([^"]+)"[^>]*>/gi
const replacements: Array<[string, string]> = []
let match
while ((match = imgRegex.exec(html)) !== null) {
const src = match[1]
if (src.startsWith('data:') || src.includes('cdn.example.com')) continue
try {
const blob = await (await fetch(src)).blob()
const file = new File([blob], 'image.' + (blob.type.split('/')[1] || 'jpg'))
replacements.push([src, await handleImageUpload(file)])
} catch { console.warn('Skip image:', src) }
}
let result = html
for (const [oldSrc, newSrc] of replacements) result = result.replaceAll(oldSrc, newSrc)
return result
}
return (
<MathTextXEditor
placeholder="Tulis soal..."
onImageUpload={handleImageUpload}
onBeforePasteHTML={handleBeforePasteHTML}
/>
)
}Base64 Fallback (Tanpa Server)
const handleImageUpload = async (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result as string)
reader.onerror = reject
reader.readAsDataURL(file)
})
}
// ⚠️ Base64 hanya cocok untuk gambar < 100KB. Untuk produksi, gunakan upload ke server.📋 Props API
MathTextXEditor
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| content | string | '' | Initial HTML content |
| onChange | (html: string) => void | — | Callback on content change (debounced 150ms) |
| onSave | (html: string) => void | — | Callback on Ctrl+S |
| onImageUpload | (file: File) => Promise<string> | — | Custom image upload handler |
| onBeforePasteHTML | (html: string) => Promise<string> | — | Transform pasted HTML (re-upload images) |
| placeholder | string | 'Tulis soal...' | Placeholder text |
| minHeight | string | '200px' | Minimum editor height |
| maxHeight | string | — | Maximum editor height |
| autoFocus | boolean | false | Auto-focus editor on mount |
| toolbarMode | 'basic' \| 'advanced' \| 'olimpiade' | 'basic' | Toolbar preset |
| onInsertBlockMath | () => void | — | Callback to insert a block-level math node directly |
| editable | boolean | true | Set false for read-only mode |
| className | string | — | Additional CSS class |
ContentViewer
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| content | string | — | HTML content to render (required) |
| className | string | — | Additional CSS class |
📦 Exports
Main Package (erl-mathtextx-editor)
import {
MathTextXEditor, // Main editor component
ContentViewer, // Read-only renderer
MathTypeDialog, // Standalone equation editor dialog
TemplatePanel, // Standalone formula template panel
MainToolbar, // Standalone text formatting toolbar
MathToolbar, // Standalone math symbols toolbar
SymbolPalette, // Standalone symbol picker
WordCount, // Status bar word/char counter
LinkDialog, // Standalone link insert/edit dialog
ImageEditDialog, // Standalone image edit dialog
InsertTableDialog, // Standalone table insert dialog
TableMenu, // Standalone table context menu
CellPropertiesDialog, // Standalone cell properties dialog
TablePropertiesDialog, // Standalone table properties dialog
TableTemplatesDialog, // Standalone table template picker
MathInlineNode, // TipTap inline math extension
MathBlockNode, // TipTap block math extension
getHTML, // Serialize editor → HTML string
getJSON, // Serialize editor → JSON
sanitizeCKEditorHTML, // Clean CKEditor HTML for compatibility
toCompatibleHTML, // Convert to CKEditor-compatible format
createExtensions, // Create TipTap extensions programmatically
mathTemplates, // Template definitions
getTemplatesByLevel, // Filter templates by education level
getTemplatesByCategory,// Filter templates by category
getTemplateCategories, // Get all template categories
countWords, // Word count utility
countCharacters, // Character count utility
getTemplateStyles, // Table template CSS generator
} from 'erl-mathtextx-editor'
import 'erl-mathtextx-editor/styles'Viewer Only (erl-mathtextx-editor/viewer)
import { ContentViewer } from 'erl-mathtextx-editor/viewer'
import 'erl-mathtextx-editor/viewer/styles'⌨️ Keyboard Shortcuts
| Shortcut | Action |
|----------|--------|
| Ctrl+B | Bold |
| Ctrl+I | Italic |
| Ctrl+U | Underline |
| Ctrl+K | Insert/edit link |
| Ctrl+M | Insert inline math directly (without dialog) |
| Ctrl+Shift+T | Insert table |
| Ctrl+S | Save document |
| Shift+Ctrl+V | Paste as plain text |
| Esc (equation editor dialog) | Close dialog |
⚠️ Troubleshooting
| Error | Solution |
|---|---|
| Can't resolve 'erl-mathtextx-editor' | npm install erl-mathtextx-editor |
| Can't resolve 'erl-mathtextx-editor/styles' | Version ≥ 0.1.3. Alternatif: import 'erl-mathtextx-editor/dist/assets/erl-mathtextx-editor.css' |
| MathTextXEditor is not a function | Pastikan React ≥ 18 (npm ls react) |
| window is not defined (Next.js) | Gunakan dynamic() dengan { ssr: false } |
| Unexpected token 'export' (CRA) | Webpack config: resolve.mainFields: ['main', 'module'] |
| MathLive fonts error | Set (window as any).MATHLIVE_FONTS_PATH = '/fonts' + copy font ke public/fonts/ |
✅ Verified Import Paths
| Import | Resolves to |
|---|---|
| erl-mathtextx-editor | dist/erl-mathtextx-editor.js |
| erl-mathtextx-editor/styles | dist/assets/erl-mathtextx-editor.css |
| erl-mathtextx-editor/viewer | dist/viewer.js |
| erl-mathtextx-editor/viewer/styles | dist/viewer-styles.js |
🛠️ Tech Stack
- UI Framework: React 18+
- Editor Engine: TipTap / ProseMirror
- Math Input: MathLive (WYSIWYG math)
- Math Rendering: KaTeX
- DOCX Import: mammoth.js
- XSS Protection: DOMPurify
- Graph Plotting: Function Plot
- Syntax Highlight: lowlight (100+ languages)
- Build: Vite (Library Mode)
📄 License
MIT © Erlangga Team
🔗 Links
- NPM: erl-mathtextx-editor
- Source: GitHub Repository
- Issues: Report Bug
- 📘 Embed Tutorial: docs/EMBED_TUTORIAL.md
