erl-mathtextx-editor 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,453 +1,448 @@
1
- # erl-mathtextx-editor
2
-
3
- **Visual Math Editor Component — Zero LaTeX Required**
4
-
5
- [![npm version](https://badge.fury.io/js/erl-mathtextx-editor.svg)](https://www.npmjs.com/package/erl-mathtextx-editor)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
-
8
- Embeddable visual math editor widget untuk CMS dan platform edukasi. User tidak perlu tahu LaTeX — semua input matematika dilakukan secara visual.
9
-
10
- ---
11
-
12
- ## ✨ Fitur Utama
13
-
14
- - 🎯 **Visual Math Input** — Insert math langsung inline tanpa dialog (Ctrl+M)
15
- - 📝 **Rich Text Editor** — Bold, italic, tables, lists, links, images
16
- - 🧮 **Equation Editor** — Dialog MathType-style dengan tab, grid, KaTeX preview (Edit existing math)
17
- - 📋 **100+ Formula Templates** — Algebra, calculus, trigonometry, chemistry, matrix
18
- - 🏆 **Mode Olimpiade** — Toolbar khusus untuk kompetisi matematika dengan simbol set, Greek letters, NT/Combo
19
- - 🖼️ **Free-form Image Drag** — Drag gambar ke posisi bebas (pixel-perfect)
20
- - 📄 **DOCX Import** — Import file .docx via mammoth.js (toolbar + drag-drop)
21
- - 🗂️ **Google Docs Paste** — Paste dari Google Docs (equations + document) auto-cleaned
22
- - 👁️ **Content Viewer** — Read-only renderer dengan KaTeX + DOMPurify
23
- - 🎨 **Table Editor** — 6 templates, column resize, cell merge/split
24
- - 🔒 **XSS Protection** — DOMPurify sanitization di paste + serializer
25
- - 🛡️ **Error Boundary** — Anti white-screen crash protection
26
-
27
- ---
28
-
29
- ## 📦 Installation
30
-
31
- ```bash
32
- npm install erl-mathtextx-editor
33
- ```
34
-
35
- ---
36
-
37
- ## 🚀 Quick Start
38
-
39
- ### 1. Basic Editor
40
-
41
- ```tsx
42
- import { MathTextXEditor } from 'erl-mathtextx-editor'
43
- import 'erl-mathtextx-editor/styles'
44
-
45
- function App() {
46
- return (
47
- <MathTextXEditor
48
- onChange={(html) => console.log(html)}
49
- placeholder="Tulis soal di sini..."
50
- />
51
- )
52
- }
53
- ```
54
-
55
- ### 2. Editor dengan Save Handler
56
-
57
- ```tsx
58
- import { useRef } from 'react'
59
- import { MathTextXEditor, getHTML } from 'erl-mathtextx-editor'
60
- import 'erl-mathtextx-editor/styles'
61
-
62
- function QuestionForm() {
63
- const editorRef = useRef<HTMLDivElement>(null)
64
-
65
- const handleSave = () => {
66
- if (!editorRef.current) return
67
- const html = getHTML(editorRef.current)
68
- fetch('/api/questions', {
69
- method: 'POST',
70
- headers: { 'Content-Type': 'application/json' },
71
- body: JSON.stringify({ content: html }),
72
- })
73
- }
74
-
75
- return (
76
- <div>
77
- <MathTextXEditor ref={editorRef} placeholder="Tulis pertanyaan..." onSave={handleSave} />
78
- <button onClick={handleSave}>Simpan</button>
79
- </div>
80
- )
81
- }
82
- ```
83
-
84
- ### 3. Edit Existing Content
85
-
86
- ```tsx
87
- import { MathTextXEditor } from 'erl-mathtextx-editor'
88
- import 'erl-mathtextx-editor/styles'
89
-
90
- function EditQuestion({ existingHtml }: { existingHtml: string }) {
91
- return (
92
- <MathTextXEditor
93
- content={existingHtml}
94
- onChange={(html) => console.log('Updated:', html)}
95
- />
96
- )
97
- }
98
- ```
99
-
100
- ### 4. Read-Only Mode (Viewer)
101
-
102
- ```tsx
103
- import { ContentViewer } from 'erl-mathtextx-editor/viewer'
104
- import 'erl-mathtextx-editor/viewer/styles'
105
-
106
- function ExamQuestion({ questionHtml }: { questionHtml: string }) {
107
- return <ContentViewer content={questionHtml} />
108
- }
109
- ```
110
-
111
- ### 5. Multi-Instance (Soal + Pilihan Jawaban)
112
-
113
- ```tsx
114
- import { useState } from 'react'
115
- import { MathTextXEditor } from 'erl-mathtextx-editor'
116
- import 'erl-mathtextx-editor/styles'
117
-
118
- function MultipleChoiceForm() {
119
- const [question, setQuestion] = useState('')
120
- const [options, setOptions] = useState(
121
- ['A', 'B', 'C', 'D'].map((id) => ({ id, content: '' }))
122
- )
123
-
124
- return (
125
- <div>
126
- <label>Pertanyaan:</label>
127
- <MathTextXEditor
128
- content={question}
129
- onChange={setQuestion}
130
- placeholder="Tulis pertanyaan..."
131
- minHeight="150px"
132
- />
133
- {options.map((opt) => (
134
- <div key={opt.id}>
135
- <label>Opsi {opt.id}:</label>
136
- <MathTextXEditor
137
- content={opt.content}
138
- onChange={(html) => setOptions((prev) => prev.map((o) => o.id === opt.id ? { ...o, content: html } : o))}
139
- placeholder={`Jawaban ${opt.id}...`}
140
- minHeight="60px"
141
- />
142
- </div>
143
- ))}
144
- </div>
145
- )
146
- }
147
- ```
148
-
149
- ### 6. Next.js (App Router)
150
-
151
- ```tsx
152
- 'use client'
153
- import dynamic from 'next/dynamic'
154
- import 'erl-mathtextx-editor/styles'
155
-
156
- const MathTextXEditor = dynamic(
157
- () => import('erl-mathtextx-editor').then((mod) => mod.MathTextXEditor),
158
- { ssr: false }
159
- )
160
-
161
- export default function EditorPage() {
162
- return (
163
- <MathTextXEditor
164
- placeholder="Tulis soal matematika..."
165
- onChange={(html) => console.log(html)}
166
- minHeight="300px"
167
- />
168
- )
169
- }
170
- ```
171
-
172
- ### 7. Vite + React
173
-
174
- ```tsx
175
- import { MathTextXEditor } from 'erl-mathtextx-editor'
176
- import 'erl-mathtextx-editor/styles'
177
-
178
- function App() {
179
- return (
180
- <MathTextXEditor
181
- placeholder="Tulis soal..."
182
- onChange={(html) => console.log(html)}
183
- />
184
- )
185
- }
186
- export default App
187
- ```
188
-
189
- ---
190
-
191
- ## 🎛️ Toolbar Modes
192
-
193
- Editor menyediakan 3 preset toolbar yang bisa dipilih via prop `toolbarMode`:
194
-
195
- ### Basic
196
- Toolbar standar dengan tombol format teks dasar, insert media, dan math formula.
197
-
198
- ### Advanced
199
- Toolbar lengkap dengan font family, text color, alignment, table operations, superscript/subscript, chemistry formula, dan formatting lanjutan.
200
-
201
- ### Olimpiade
202
- Toolbar minimal untuk kompetisi matematika — hanya tombol esensial:
203
-
204
- | Grup | Tombol |
205
- |---|---|
206
- | **Undo/Redo** | Undo, Redo |
207
- | **Format** | Bold, Italic, Underline |
208
- | **Structure** | Paragraph, H1, H2 |
209
- | **Lists** | Bullet List, Ordered List, Outdent, Indent |
210
- | **Math** | Math Formula (inline), Block Math |
211
- | **Actions** | Remove Format |
212
-
213
- **Math Toolbar** di mode Olimpiade menampilkan section khusus: Basic, Relation, Set, Greek, Structure — tanpa section Calc yang kurang relevan untuk olimpiade.
214
-
215
- ### Custom Mode
216
- Jika preset tidak sesuai, Anda bisa atur sendiri via `internalToolbarMode` state atau kontrol toolbar secara manual menggunakan komponen terpisah (`MainToolbar`, `MathToolbar`, `MathTypeDialog`).
217
-
218
- ---
219
-
220
- ## 🧰 Image Upload
221
-
222
- Editor mendukung upload gambar dari: **Insert dialog**, **drag-drop**, **paste dari clipboard**, dan **DOCX import**. Semuanya melalui satu callback `onImageUpload`.
223
-
224
- ### Basic Upload
225
-
226
- ```tsx
227
- import { MathTextXEditor } from 'erl-mathtextx-editor'
228
- import 'erl-mathtextx-editor/styles'
229
-
230
- function EditorWithUpload() {
231
- const handleImageUpload = async (file: File): Promise<string> => {
232
- const formData = new FormData()
233
- formData.append('image', file)
234
- const res = await fetch('/api/upload', { method: 'POST', body: formData })
235
- if (!res.ok) throw new Error('Upload failed: ' + res.statusText)
236
- const data = await res.json()
237
- return data.url // Expected: { "url": "https://cdn.example.com/img.jpg" }
238
- }
239
-
240
- return (
241
- <MathTextXEditor
242
- placeholder="Tulis soal..."
243
- onImageUpload={handleImageUpload}
244
- />
245
- )
246
- }
247
- ```
248
-
249
- ### Re-upload Gambar dari Paste (Google Docs / Website)
250
-
251
- ```tsx
252
- import { MathTextXEditor } from 'erl-mathtextx-editor'
253
- import 'erl-mathtextx-editor/styles'
254
-
255
- function EditorWithPasteReupload() {
256
- const handleImageUpload = async (file: File): Promise<string> => {
257
- const formData = new FormData()
258
- formData.append('image', file)
259
- const res = await fetch('/api/upload', { method: 'POST', body: formData })
260
- return (await res.json()).url
261
- }
262
-
263
- const handleBeforePasteHTML = async (html: string): Promise<string> => {
264
- // Download + re-upload external images from pasted HTML
265
- const imgRegex = /<img\s+[^>]*src="([^"]+)"[^>]*>/gi
266
- const replacements: Array<[string, string]> = []
267
- let match
268
-
269
- while ((match = imgRegex.exec(html)) !== null) {
270
- const src = match[1]
271
- if (src.startsWith('data:') || src.includes('cdn.example.com')) continue
272
- try {
273
- const blob = await (await fetch(src)).blob()
274
- const file = new File([blob], 'image.' + (blob.type.split('/')[1] || 'jpg'))
275
- replacements.push([src, await handleImageUpload(file)])
276
- } catch { console.warn('Skip image:', src) }
277
- }
278
-
279
- let result = html
280
- for (const [oldSrc, newSrc] of replacements) result = result.replaceAll(oldSrc, newSrc)
281
- return result
282
- }
283
-
284
- return (
285
- <MathTextXEditor
286
- placeholder="Tulis soal..."
287
- onImageUpload={handleImageUpload}
288
- onBeforePasteHTML={handleBeforePasteHTML}
289
- />
290
- )
291
- }
292
- ```
293
-
294
- ### Base64 Fallback (Tanpa Server)
295
-
296
- ```tsx
297
- const handleImageUpload = async (file: File): Promise<string> => {
298
- return new Promise((resolve, reject) => {
299
- const reader = new FileReader()
300
- reader.onload = () => resolve(reader.result as string)
301
- reader.onerror = reject
302
- reader.readAsDataURL(file)
303
- })
304
- }
305
- // ⚠️ Base64 hanya cocok untuk gambar < 100KB. Untuk produksi, gunakan upload ke server.
306
- ```
307
-
308
- ---
309
-
310
- ## 📋 Props API
311
-
312
- ### MathTextXEditor
313
-
314
- | Prop | Type | Default | Description |
315
- |------|------|---------|-------------|
316
- | `content` | `string` | `''` | Initial HTML content |
317
- | `onChange` | `(html: string) => void` | — | Callback on content change (debounced 150ms) |
318
- | `onSave` | `(html: string) => void` | | Callback on Ctrl+S |
319
- | `onImageUpload` | `(file: File) => Promise<string>` | — | Custom image upload handler |
320
- | `onBeforePasteHTML` | `(html: string) => Promise<string>` | | Transform pasted HTML (re-upload images) |
321
- | `placeholder` | `string` | `'Tulis soal...'` | Placeholder text |
322
- | `minHeight` | `string` | `'200px'` | Minimum editor height |
323
- | `maxHeight` | `string` | — | Maximum editor height |
324
- | `autoFocus` | `boolean` | `false` | Auto-focus editor on mount |
325
- | `toolbarMode` | `'basic' \| 'advanced' \| 'olimpiade'` | `'basic'` | Toolbar preset |
326
- | `onInsertBlockMath` | `() => void` | | Callback to insert a block-level math node directly |
327
- | `editable` | `boolean` | `true` | Set false for read-only mode |
328
- | `className` | `string` | | Additional CSS class |
329
-
330
- ### ContentViewer
331
-
332
- | Prop | Type | Default | Description |
333
- |------|------|---------|-------------|
334
- | `content` | `string` | — | HTML content to render (required) |
335
- | `className` | `string` | — | Additional CSS class |
336
-
337
- ---
338
-
339
- ## 📦 Exports
340
-
341
- ### Main Package (`erl-mathtextx-editor`)
342
-
343
- ```tsx
344
- import {
345
- MathTextXEditor, // Main editor component
346
- ContentViewer, // Read-only renderer
347
- MathTypeDialog, // Standalone equation editor dialog
348
- TemplatePanel, // Standalone formula template panel
349
- MainToolbar, // Standalone text formatting toolbar
350
- MathToolbar, // Standalone math symbols toolbar
351
- SymbolPalette, // Standalone symbol picker
352
- WordCount, // Status bar word/char counter
353
- LinkDialog, // Standalone link insert/edit dialog
354
- ImageEditDialog, // Standalone image edit dialog
355
- InsertTableDialog, // Standalone table insert dialog
356
- TableMenu, // Standalone table context menu
357
- CellPropertiesDialog, // Standalone cell properties dialog
358
- TablePropertiesDialog, // Standalone table properties dialog
359
- TableTemplatesDialog, // Standalone table template picker
360
- MathInlineNode, // TipTap inline math extension
361
- MathBlockNode, // TipTap block math extension
362
- getHTML, // Serialize editor HTML string
363
- getJSON, // Serialize editor JSON
364
- sanitizeCKEditorHTML, // Clean CKEditor HTML for compatibility
365
- toCompatibleHTML, // Convert to CKEditor-compatible format
366
- createExtensions, // Create TipTap extensions programmatically
367
- mathTemplates, // Template definitions
368
- getTemplatesByLevel, // Filter templates by education level
369
- getTemplatesByCategory,// Filter templates by category
370
- getTemplateCategories, // Get all template categories
371
- countWords, // Word count utility
372
- countCharacters, // Character count utility
373
- getTemplateStyles, // Table template CSS generator
374
- } from 'erl-mathtextx-editor'
375
-
376
- import 'erl-mathtextx-editor/styles'
377
- ```
378
-
379
- ### Viewer Only (`erl-mathtextx-editor/viewer`)
380
-
381
- ```tsx
382
- import { ContentViewer } from 'erl-mathtextx-editor/viewer'
383
- import 'erl-mathtextx-editor/viewer/styles'
384
- ```
385
-
386
- ---
387
-
388
- ## ⌨️ Keyboard Shortcuts
389
-
390
- | Shortcut | Action |
391
- |----------|--------|
392
- | `Ctrl+B` | Bold |
393
- | `Ctrl+I` | Italic |
394
- | `Ctrl+U` | Underline |
395
- | `Ctrl+K` | Insert/edit link |
396
- | `Ctrl+M` | Insert inline math directly (without dialog) |
397
- | `Ctrl+Shift+T` | Insert table |
398
- | `Ctrl+S` | Save document |
399
- | `Shift+Ctrl+V` | Paste as plain text |
400
- | `Esc` (equation editor dialog) | Close dialog |
401
-
402
- ---
403
-
404
- ## ⚠️ Troubleshooting
405
-
406
- | Error | Solution |
407
- |---|---|
408
- | `Can't resolve 'erl-mathtextx-editor'` | `npm install erl-mathtextx-editor` |
409
- | `Can't resolve 'erl-mathtextx-editor/styles'` | Version ≥ 0.1.3. Alternatif: `import 'erl-mathtextx-editor/dist/assets/erl-mathtextx-editor.css'` |
410
- | `MathTextXEditor is not a function` | Pastikan React ≥ 18 (`npm ls react`) |
411
- | `window is not defined` (Next.js) | Gunakan `dynamic()` dengan `{ ssr: false }` |
412
- | `Unexpected token 'export'` (CRA) | Webpack config: `resolve.mainFields: ['main', 'module']` |
413
- | MathLive fonts error | Set `(window as any).MATHLIVE_FONTS_PATH = '/fonts'` + copy font ke `public/fonts/` |
414
-
415
- ---
416
-
417
- ## Verified Import Paths
418
-
419
- | Import | Resolves to |
420
- |---|---|
421
- | `erl-mathtextx-editor` | `dist/erl-mathtextx-editor.js` |
422
- | `erl-mathtextx-editor/styles` | `dist/assets/erl-mathtextx-editor.css` |
423
- | `erl-mathtextx-editor/viewer` | `dist/viewer.js` |
424
- | `erl-mathtextx-editor/viewer/styles` | `dist/viewer-styles.js` |
425
-
426
- ---
427
-
428
- ## 🛠️ Tech Stack
429
-
430
- - **UI Framework:** React 18+
431
- - **Editor Engine:** TipTap / ProseMirror
432
- - **Math Input:** MathLive (WYSIWYG math)
433
- - **Math Rendering:** KaTeX
434
- - **DOCX Import:** mammoth.js
435
- - **XSS Protection:** DOMPurify
436
- - **Graph Plotting:** Function Plot
437
- - **Syntax Highlight:** lowlight (100+ languages)
438
- - **Build:** Vite (Library Mode)
439
-
440
- ---
441
-
442
- ## 📄 License
443
-
444
- [MIT](https://github.com/erlangga/richtext-editor-research/blob/main/LICENSE) © Erlangga Team
445
-
446
- ---
447
-
448
- ## 🔗 Links
449
-
450
- - **NPM:** [erl-mathtextx-editor](https://www.npmjs.com/package/erl-mathtextx-editor)
451
- - **Source:** [GitHub Repository](https://github.com/erlangga/richtext-editor-research)
452
- - **Issues:** [Report Bug](https://github.com/erlangga/richtext-editor-research/issues)
453
- - **📘 Embed Tutorial:** [docs/EMBED_TUTORIAL.md](https://github.com/erlangga/richtext-editor-research/blob/main/docs/EMBED_TUTORIAL.md)
1
+ # erl-mathtextx-editor
2
+
3
+ **Visual Math Editor Component — Zero LaTeX Required**
4
+
5
+ [![npm version](https://badge.fury.io/js/erl-mathtextx-editor.svg)](https://www.npmjs.com/package/erl-mathtextx-editor)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ Embeddable visual math editor widget untuk CMS dan platform edukasi. User tidak perlu tahu LaTeX — semua input matematika dilakukan secara visual.
9
+
10
+ ---
11
+
12
+ ## ✨ Fitur Utama
13
+
14
+ - 🎯 **Visual Math Input** — Insert math langsung inline tanpa dialog (Ctrl+M)
15
+ - 📝 **Rich Text Editor** — Bold, italic, tables, lists, links, images
16
+ - 🧮 **Equation Editor** — Dialog MathType-style dengan tab, grid, KaTeX preview (Edit existing math)
17
+ - 📋 **100+ Formula Templates** — Algebra, calculus, trigonometry, chemistry, matrix
18
+ - 🏆 **Mode Olimpiade** — Toolbar khusus untuk kompetisi matematika dengan simbol set, Greek letters, NT/Combo
19
+ - 🖐️ **Draggable Dialogs** — Semua dialog (MathType, Link, Image, Table) bisa di-drag bebas
20
+ - 🌐 **Localized Table Dialogs** — Dialog insert/edit table dalam Bahasa Indonesia
21
+ - 🎨 **Enhanced Table CSS** — TipTap table styling yang lebih rapi dan responsif
22
+ - 🔣 **Enhanced Math Symbols** — Symbol palette diperluas dengan simbol tambahan
23
+ - 🖼️ **Free-form Image Drag** — Drag gambar ke posisi bebas (pixel-perfect)
24
+ - 📄 **DOCX Import** — Import file .docx via mammoth.js (toolbar + drag-drop)
25
+ - 🗂️ **Google Docs Paste** — Paste dari Google Docs (equations + document) auto-cleaned
26
+ - 👁️ **Content Viewer** — Read-only renderer dengan KaTeX + DOMPurify
27
+ - 📊 **Table Editor** — 6 templates, column resize, cell merge/split
28
+ - 🔒 **XSS Protection** — DOMPurify sanitization di paste + serializer
29
+ - 🛡️ **Error Boundary** — Anti white-screen crash protection
30
+
31
+ ---
32
+
33
+ ## 📦 Installation
34
+
35
+ ```bash
36
+ npm install erl-mathtextx-editor
37
+ ```
38
+
39
+ ---
40
+
41
+ ## 🚀 Quick Start
42
+
43
+ ### 1. Basic Editor
44
+
45
+ ```tsx
46
+ import { MathTextXEditor } from 'erl-mathtextx-editor'
47
+ import 'erl-mathtextx-editor/styles'
48
+
49
+ function App() {
50
+ return (
51
+ <MathTextXEditor
52
+ onChange={(html) => console.log(html)}
53
+ placeholder="Tulis soal di sini..."
54
+ />
55
+ )
56
+ }
57
+ ```
58
+
59
+ ### 2. Editor dengan Save Handler
60
+
61
+ ```tsx
62
+ import { useRef } from 'react'
63
+ import { MathTextXEditor, getHTML } from 'erl-mathtextx-editor'
64
+ import 'erl-mathtextx-editor/styles'
65
+
66
+ function QuestionForm() {
67
+ const editorRef = useRef<HTMLDivElement>(null)
68
+
69
+ const handleSave = () => {
70
+ if (!editorRef.current) return
71
+ const html = getHTML(editorRef.current)
72
+ fetch('/api/questions', {
73
+ method: 'POST',
74
+ headers: { 'Content-Type': 'application/json' },
75
+ body: JSON.stringify({ content: html }),
76
+ })
77
+ }
78
+
79
+ return (
80
+ <div>
81
+ <MathTextXEditor ref={editorRef} placeholder="Tulis pertanyaan..." onSave={handleSave} />
82
+ <button onClick={handleSave}>Simpan</button>
83
+ </div>
84
+ )
85
+ }
86
+ ```
87
+
88
+ ### 3. Edit Existing Content
89
+
90
+ ```tsx
91
+ import { MathTextXEditor } from 'erl-mathtextx-editor'
92
+ import 'erl-mathtextx-editor/styles'
93
+
94
+ function EditQuestion({ existingHtml }: { existingHtml: string }) {
95
+ return (
96
+ <MathTextXEditor
97
+ content={existingHtml}
98
+ onChange={(html) => console.log('Updated:', html)}
99
+ />
100
+ )
101
+ }
102
+ ```
103
+
104
+ ### 4. Read-Only Mode (Viewer)
105
+
106
+ ```tsx
107
+ import { ContentViewer } from 'erl-mathtextx-editor/viewer'
108
+ import 'erl-mathtextx-editor/viewer/styles'
109
+
110
+ function ExamQuestion({ questionHtml }: { questionHtml: string }) {
111
+ return <ContentViewer content={questionHtml} />
112
+ }
113
+ ```
114
+
115
+ ### 5. Multi-Instance (Soal + Pilihan Jawaban)
116
+
117
+ ```tsx
118
+ import { useState } from 'react'
119
+ import { MathTextXEditor } from 'erl-mathtextx-editor'
120
+ import 'erl-mathtextx-editor/styles'
121
+
122
+ function MultipleChoiceForm() {
123
+ const [question, setQuestion] = useState('')
124
+ const [options, setOptions] = useState(
125
+ ['A', 'B', 'C', 'D'].map((id) => ({ id, content: '' }))
126
+ )
127
+
128
+ return (
129
+ <div>
130
+ <label>Pertanyaan:</label>
131
+ <MathTextXEditor
132
+ content={question}
133
+ onChange={setQuestion}
134
+ placeholder="Tulis pertanyaan..."
135
+ minHeight="150px"
136
+ />
137
+ {options.map((opt) => (
138
+ <div key={opt.id}>
139
+ <label>Opsi {opt.id}:</label>
140
+ <MathTextXEditor
141
+ content={opt.content}
142
+ onChange={(html) => setOptions((prev) => prev.map((o) => o.id === opt.id ? { ...o, content: html } : o))}
143
+ placeholder={`Jawaban ${opt.id}...`}
144
+ minHeight="60px"
145
+ />
146
+ </div>
147
+ ))}
148
+ </div>
149
+ )
150
+ }
151
+ ```
152
+
153
+ ### 6. Next.js (App Router)
154
+
155
+ ```tsx
156
+ 'use client'
157
+ import dynamic from 'next/dynamic'
158
+ import 'erl-mathtextx-editor/styles'
159
+
160
+ const MathTextXEditor = dynamic(
161
+ () => import('erl-mathtextx-editor').then((mod) => mod.MathTextXEditor),
162
+ { ssr: false }
163
+ )
164
+
165
+ export default function EditorPage() {
166
+ return (
167
+ <MathTextXEditor
168
+ placeholder="Tulis soal matematika..."
169
+ onChange={(html) => console.log(html)}
170
+ minHeight="300px"
171
+ />
172
+ )
173
+ }
174
+ ```
175
+
176
+ ### 7. Vite + React
177
+
178
+ ```tsx
179
+ import { MathTextXEditor } from 'erl-mathtextx-editor'
180
+ import 'erl-mathtextx-editor/styles'
181
+
182
+ function App() {
183
+ return (
184
+ <MathTextXEditor
185
+ placeholder="Tulis soal..."
186
+ onChange={(html) => console.log(html)}
187
+ />
188
+ )
189
+ }
190
+ export default App
191
+ ```
192
+
193
+ ---
194
+
195
+ ## 🎛️ Toolbar Modes
196
+
197
+ Editor menyediakan 3 preset toolbar yang bisa dipilih via prop `toolbarMode`:
198
+
199
+ ### Basic
200
+ Toolbar standar dengan tombol format teks dasar, insert media, dan math formula.
201
+
202
+ ### Advanced
203
+ Toolbar lengkap dengan font family, text color, alignment, table operations, superscript/subscript, chemistry formula, dan formatting lanjutan.
204
+
205
+ ### Olimpiade
206
+ Toolbar minimal untuk kompetisi matematika — hanya tombol esensial:
207
+
208
+ | Grup | Tombol |
209
+ |---|---|
210
+ | **Undo/Redo** | Undo, Redo |
211
+ | **Format** | Bold, Italic, Underline |
212
+ | **Structure** | Paragraph, H1, H2 |
213
+ | **Lists** | Bullet List, Ordered List, Outdent, Indent |
214
+ | **Math** | Math Formula (inline), Block Math |
215
+ | **Actions** | Remove Format |
216
+
217
+ **Math Toolbar** di mode Olimpiade menampilkan section khusus: Basic, Relation, Set, Greek, Structure — tanpa section Calc yang kurang relevan untuk olimpiade.
218
+
219
+ ### Custom Mode
220
+ Jika preset tidak sesuai, Anda bisa atur sendiri via `internalToolbarMode` state atau kontrol toolbar secara manual menggunakan komponen terpisah (`MainToolbar`, `MathToolbar`, `MathTypeDialog`).
221
+
222
+ ---
223
+
224
+ ## 🧰 Image Upload
225
+
226
+ Editor mendukung upload gambar dari: **Insert dialog**, **drag-drop**, **paste dari clipboard**, dan **DOCX import**. Semuanya melalui satu callback `onImageUpload`.
227
+
228
+ ### Basic Upload
229
+
230
+ ```tsx
231
+ import { MathTextXEditor } from 'erl-mathtextx-editor'
232
+ import 'erl-mathtextx-editor/styles'
233
+
234
+ function EditorWithUpload() {
235
+ const handleImageUpload = async (file: File): Promise<string> => {
236
+ const formData = new FormData()
237
+ formData.append('image', file)
238
+ const res = await fetch('/api/upload', { method: 'POST', body: formData })
239
+ if (!res.ok) throw new Error('Upload failed: ' + res.statusText)
240
+ const data = await res.json()
241
+ return data.url // Expected: { "url": "https://cdn.example.com/img.jpg" }
242
+ }
243
+
244
+ return (
245
+ <MathTextXEditor
246
+ placeholder="Tulis soal..."
247
+ onImageUpload={handleImageUpload}
248
+ />
249
+ )
250
+ }
251
+ ```
252
+
253
+ ### Re-upload Gambar dari Paste (Google Docs / Website)
254
+
255
+ ```tsx
256
+ import { MathTextXEditor } from 'erl-mathtextx-editor'
257
+ import 'erl-mathtextx-editor/styles'
258
+
259
+ function EditorWithPasteReupload() {
260
+ const handleImageUpload = async (file: File): Promise<string> => {
261
+ const formData = new FormData()
262
+ formData.append('image', file)
263
+ const res = await fetch('/api/upload', { method: 'POST', body: formData })
264
+ return (await res.json()).url
265
+ }
266
+
267
+ const handleBeforePasteHTML = async (html: string): Promise<string> => {
268
+ // Download + re-upload external images from pasted HTML
269
+ const imgRegex = /<img\s+[^>]*src="([^"]+)"[^>]*>/gi
270
+ const replacements: Array<[string, string]> = []
271
+ let match
272
+
273
+ while ((match = imgRegex.exec(html)) !== null) {
274
+ const src = match[1]
275
+ if (src.startsWith('data:') || src.includes('cdn.example.com')) continue
276
+ try {
277
+ const blob = await (await fetch(src)).blob()
278
+ const file = new File([blob], 'image.' + (blob.type.split('/')[1] || 'jpg'))
279
+ replacements.push([src, await handleImageUpload(file)])
280
+ } catch { console.warn('Skip image:', src) }
281
+ }
282
+
283
+ let result = html
284
+ for (const [oldSrc, newSrc] of replacements) result = result.replaceAll(oldSrc, newSrc)
285
+ return result
286
+ }
287
+
288
+ return (
289
+ <MathTextXEditor
290
+ placeholder="Tulis soal..."
291
+ onImageUpload={handleImageUpload}
292
+ onBeforePasteHTML={handleBeforePasteHTML}
293
+ />
294
+ )
295
+ }
296
+ ```
297
+
298
+ ### Base64 Fallback (Tanpa Server)
299
+
300
+ ```tsx
301
+ const handleImageUpload = async (file: File): Promise<string> => {
302
+ return new Promise((resolve, reject) => {
303
+ const reader = new FileReader()
304
+ reader.onload = () => resolve(reader.result as string)
305
+ reader.onerror = reject
306
+ reader.readAsDataURL(file)
307
+ })
308
+ }
309
+ // ⚠️ Base64 hanya cocok untuk gambar < 100KB. Untuk produksi, gunakan upload ke server.
310
+ ```
311
+
312
+ ---
313
+
314
+ ## 📋 Props API
315
+
316
+ ### MathTextXEditor
317
+
318
+ | Prop | Type | Default | Description |
319
+ |------|------|---------|-------------|
320
+ | `content` | `string` | `''` | Initial HTML content |
321
+ | `onChange` | `(html: string) => void` | | Callback on content change (debounced 150ms) |
322
+ | `onSave` | `(html: string) => void` | | Callback on Ctrl+S |
323
+ | `onImageUpload` | `(file: File) => Promise<string>` | — | Custom image upload handler |
324
+ | `onBeforePasteHTML` | `(html: string) => Promise<string>` | | Transform pasted HTML (re-upload images) |
325
+ | `placeholder` | `string` | `'Tulis soal...'` | Placeholder text |
326
+ | `minHeight` | `string` | `'200px'` | Minimum editor height |
327
+ | `maxHeight` | `string` | | Maximum editor height |
328
+ | `autoFocus` | `boolean` | `false` | Auto-focus editor on mount |
329
+ | `toolbarMode` | `'basic' \| 'advanced' \| 'olimpiade'` | `'basic'` | Toolbar preset |
330
+ | `onInsertBlockMath` | `() => void` | — | Callback to insert a block-level math node directly |
331
+ | `editable` | `boolean` | `true` | Set false for read-only mode |
332
+ | `className` | `string` | | Additional CSS class |
333
+
334
+ ### ContentViewer
335
+
336
+ | Prop | Type | Default | Description |
337
+ |------|------|---------|-------------|
338
+ | `content` | `string` | — | HTML content to render (required) |
339
+ | `className` | `string` | — | Additional CSS class |
340
+
341
+ ---
342
+
343
+ ## 📦 Exports
344
+
345
+ ### Main Package (`erl-mathtextx-editor`)
346
+
347
+ ```tsx
348
+ import {
349
+ MathTextXEditor, // Main editor component
350
+ ContentViewer, // Read-only renderer
351
+ MathTypeDialog, // Standalone equation editor dialog
352
+ TemplatePanel, // Standalone formula template panel
353
+ MainToolbar, // Standalone text formatting toolbar
354
+ MathToolbar, // Standalone math symbols toolbar
355
+ SymbolPalette, // Standalone symbol picker
356
+ WordCount, // Status bar word/char counter
357
+ LinkDialog, // Standalone link insert/edit dialog
358
+ ImageEditDialog, // Standalone image edit dialog
359
+ InsertTableDialog, // Standalone table insert dialog
360
+ TableMenu, // Standalone table context menu
361
+ CellPropertiesDialog, // Standalone cell properties dialog
362
+ TablePropertiesDialog, // Standalone table properties dialog
363
+ TableTemplatesDialog, // Standalone table template picker
364
+ MathInlineNode, // TipTap inline math extension
365
+ MathBlockNode, // TipTap block math extension
366
+ getHTML, // Serialize editor HTML string
367
+ getJSON, // Serialize editor → JSON
368
+ sanitizeCKEditorHTML, // Clean CKEditor HTML for compatibility
369
+ toCompatibleHTML, // Convert to CKEditor-compatible format
370
+ createExtensions, // Create TipTap extensions programmatically
371
+ mathTemplates, // Template definitions
372
+ getTemplatesByLevel, // Filter templates by education level
373
+ getTemplatesByCategory,// Filter templates by category
374
+ getTemplateCategories, // Get all template categories
375
+ countWords, // Word count utility
376
+ countCharacters, // Character count utility
377
+ getTemplateStyles, // Table template CSS generator
378
+ } from 'erl-mathtextx-editor'
379
+
380
+ import 'erl-mathtextx-editor/styles'
381
+ ```
382
+
383
+ ### Viewer Only (`erl-mathtextx-editor/viewer`)
384
+
385
+ ```tsx
386
+ import { ContentViewer } from 'erl-mathtextx-editor/viewer'
387
+ import 'erl-mathtextx-editor/viewer/styles'
388
+ ```
389
+
390
+ ---
391
+
392
+ ## ⌨️ Keyboard Shortcuts
393
+
394
+ | Shortcut | Action |
395
+ |----------|--------|
396
+ | `Ctrl+B` | Bold |
397
+ | `Ctrl+I` | Italic |
398
+ | `Ctrl+U` | Underline |
399
+ | `Ctrl+K` | Insert/edit link |
400
+ | `Ctrl+M` | Insert inline math directly (without dialog) |
401
+ | `Ctrl+Shift+T` | Insert table |
402
+ | `Ctrl+S` | Save document |
403
+ | `Shift+Ctrl+V` | Paste as plain text |
404
+ | `Esc` (equation editor dialog) | Close dialog |
405
+
406
+ ---
407
+
408
+ ## ⚠️ Troubleshooting
409
+
410
+ | Error | Solution |
411
+ |---|---|
412
+ | `Can't resolve 'erl-mathtextx-editor'` | `npm install erl-mathtextx-editor` |
413
+ | `Can't resolve 'erl-mathtextx-editor/styles'` | Version 0.1.3. Alternatif: `import 'erl-mathtextx-editor/dist/assets/erl-mathtextx-editor.css'` |
414
+ | `MathTextXEditor is not a function` | Pastikan React ≥ 18 (`npm ls react`) |
415
+ | `window is not defined` (Next.js) | Gunakan `dynamic()` dengan `{ ssr: false }` |
416
+ | `Unexpected token 'export'` (CRA) | Webpack config: `resolve.mainFields: ['main', 'module']` |
417
+ | MathLive fonts error | Set `(window as any).MATHLIVE_FONTS_PATH = '/fonts'` + copy font ke `public/fonts/` |
418
+
419
+ ---
420
+
421
+ ## Verified Import Paths
422
+
423
+ | Import | Resolves to |
424
+ |---|---|
425
+ | `erl-mathtextx-editor` | `dist/erl-mathtextx-editor.js` |
426
+ | `erl-mathtextx-editor/styles` | `dist/assets/erl-mathtextx-editor.css` |
427
+ | `erl-mathtextx-editor/viewer` | `dist/viewer.js` |
428
+ | `erl-mathtextx-editor/viewer/styles` | `dist/viewer-styles.js` |
429
+
430
+ ---
431
+
432
+ ## 🛠️ Tech Stack
433
+
434
+ - **UI Framework:** React 18+
435
+ - **Editor Engine:** TipTap / ProseMirror
436
+ - **Math Input:** MathLive (WYSIWYG math)
437
+ - **Math Rendering:** KaTeX
438
+ - **DOCX Import:** mammoth.js
439
+ - **XSS Protection:** DOMPurify
440
+ - **Graph Plotting:** Function Plot
441
+ - **Syntax Highlight:** lowlight (100+ languages)
442
+ - **Build:** Vite (Library Mode)
443
+
444
+ ---
445
+
446
+ ## 📄 License
447
+
448
+ [MIT](https://github.com/erlangga/richtext-editor-research/blob/main/LICENSE) © Erlangga Team