erl-mathtextx-editor 0.2.5 → 0.3.1

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,542 @@
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
+ - 🖼️ **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, native/custom style
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
+ ### 8. Table Style Integration
190
+
191
+ ```tsx
192
+ import { MathTextXEditor } from 'erl-mathtextx-editor'
193
+ import 'erl-mathtextx-editor/styles'
194
+
195
+ // Custom styled table (default) — 6 themes available
196
+ function EditorWithCustomTable() {
197
+ return (
198
+ <MathTextXEditor
199
+ tableStyle="custom" // plain, light, dark, blue, striped, minimal
200
+ placeholder="Tulis soal dengan tabel..."
201
+ />
202
+ )
203
+ }
204
+
205
+ // Native TipTap table — plain, no custom CSS
206
+ function EditorWithNativeTable() {
207
+ return (
208
+ <MathTextXEditor
209
+ tableStyle="native"
210
+ placeholder="Tulis soal dengan tabel native..."
211
+ />
212
+ )
213
+ }
214
+ ```
215
+
216
+ ---
217
+
218
+ ## 🎛️ Toolbar Modes
219
+
220
+ Editor menyediakan 3 preset toolbar yang bisa dipilih via prop `toolbarMode`:
221
+
222
+ ### Basic
223
+ Toolbar standar dengan tombol format teks dasar, insert media, dan math formula.
224
+
225
+ ### Advanced
226
+ Toolbar lengkap dengan font family, text color, alignment, table operations, superscript/subscript, chemistry formula, dan formatting lanjutan.
227
+
228
+ ### Olimpiade
229
+ Toolbar minimal untuk kompetisi matematika — hanya tombol esensial:
230
+
231
+ | Grup | Tombol |
232
+ |---|---|
233
+ | **Undo/Redo** | Undo, Redo |
234
+ | **Format** | Bold, Italic, Underline |
235
+ | **Structure** | Paragraph, H1, H2 |
236
+ | **Lists** | Bullet List, Ordered List, Outdent, Indent |
237
+ | **Math** | Math Formula (inline), Block Math |
238
+ | **Actions** | Remove Format |
239
+
240
+ **Math Toolbar** di mode Olimpiade menampilkan section khusus: Basic, Relation, Set, Greek, Structure — tanpa section Calc yang kurang relevan untuk olimpiade.
241
+
242
+ ### Custom Mode
243
+ Jika preset tidak sesuai, Anda bisa atur sendiri via `internalToolbarMode` state atau kontrol toolbar secara manual menggunakan komponen terpisah (`MainToolbar`, `MathToolbar`, `MathTypeDialog`).
244
+
245
+ ---
246
+
247
+ ## 🧰 Image Upload
248
+
249
+ Editor mendukung upload gambar dari: **Insert dialog**, **drag-drop**, **paste dari clipboard**, dan **DOCX import**. Semuanya melalui satu callback `onImageUpload`.
250
+
251
+ ### Basic Upload
252
+
253
+ ```tsx
254
+ import { MathTextXEditor } from 'erl-mathtextx-editor'
255
+ import 'erl-mathtextx-editor/styles'
256
+
257
+ function EditorWithUpload() {
258
+ const handleImageUpload = async (file: File): Promise<string> => {
259
+ const formData = new FormData()
260
+ formData.append('image', file)
261
+ const res = await fetch('/api/upload', { method: 'POST', body: formData })
262
+ if (!res.ok) throw new Error('Upload failed: ' + res.statusText)
263
+ const data = await res.json()
264
+ return data.url // Expected: { "url": "https://cdn.example.com/img.jpg" }
265
+ }
266
+
267
+ return (
268
+ <MathTextXEditor
269
+ placeholder="Tulis soal..."
270
+ onImageUpload={handleImageUpload}
271
+ />
272
+ )
273
+ }
274
+ ```
275
+
276
+ ### Re-upload Gambar dari Paste (Google Docs / Website)
277
+
278
+ ```tsx
279
+ import { MathTextXEditor } from 'erl-mathtextx-editor'
280
+ import 'erl-mathtextx-editor/styles'
281
+
282
+ function EditorWithPasteReupload() {
283
+ const handleImageUpload = async (file: File): Promise<string> => {
284
+ const formData = new FormData()
285
+ formData.append('image', file)
286
+ const res = await fetch('/api/upload', { method: 'POST', body: formData })
287
+ return (await res.json()).url
288
+ }
289
+
290
+ const handleBeforePasteHTML = async (html: string): Promise<string> => {
291
+ // Download + re-upload external images from pasted HTML
292
+ const imgRegex = /<img\s+[^>]*src="([^"]+)"[^>]*>/gi
293
+ const replacements: Array<[string, string]> = []
294
+ let match
295
+
296
+ while ((match = imgRegex.exec(html)) !== null) {
297
+ const src = match[1]
298
+ if (src.startsWith('data:') || src.includes('cdn.example.com')) continue
299
+ try {
300
+ const blob = await (await fetch(src)).blob()
301
+ const file = new File([blob], 'image.' + (blob.type.split('/')[1] || 'jpg'))
302
+ replacements.push([src, await handleImageUpload(file)])
303
+ } catch { console.warn('Skip image:', src) }
304
+ }
305
+
306
+ let result = html
307
+ for (const [oldSrc, newSrc] of replacements) result = result.replaceAll(oldSrc, newSrc)
308
+ return result
309
+ }
310
+
311
+ return (
312
+ <MathTextXEditor
313
+ placeholder="Tulis soal..."
314
+ onImageUpload={handleImageUpload}
315
+ onBeforePasteHTML={handleBeforePasteHTML}
316
+ />
317
+ )
318
+ }
319
+ ```
320
+
321
+ ### Base64 Fallback (Tanpa Server)
322
+
323
+ ```tsx
324
+ const handleImageUpload = async (file: File): Promise<string> => {
325
+ return new Promise((resolve, reject) => {
326
+ const reader = new FileReader()
327
+ reader.onload = () => resolve(reader.result as string)
328
+ reader.onerror = reject
329
+ reader.readAsDataURL(file)
330
+ })
331
+ }
332
+ // ⚠️ Base64 hanya cocok untuk gambar < 100KB. Untuk produksi, gunakan upload ke server.
333
+ ```
334
+
335
+ ---
336
+
337
+ ## 📋 Props API
338
+
339
+ ### MathTextXEditor
340
+
341
+ | Prop | Type | Default | Description |
342
+ |------|------|---------|-------------|
343
+ | `content` | `string` | `''` | Initial HTML content |
344
+ | `onChange` | `(html: string) => void` | — | Callback on content change (debounced 150ms) |
345
+ | `onSave` | `(html: string) => void` | — | Callback on Ctrl+S |
346
+ | `onImageUpload` | `(file: File) => Promise<string>` | — | Custom image upload handler |
347
+ | `onBeforePasteHTML` | `(html: string) => Promise<string>` | — | Transform pasted HTML (re-upload images) |
348
+ | `placeholder` | `string` | `'Tulis soal...'` | Placeholder text |
349
+ | `minHeight` | `string` | `'200px'` | Minimum editor height |
350
+ | `maxHeight` | `string` | — | Maximum editor height |
351
+ | `autoFocus` | `boolean` | `false` | Auto-focus editor on mount |
352
+ | `toolbarMode` | `'basic' \| 'advanced' \| 'olimpiade'` | `'basic'` | Toolbar preset |
353
+ | `tableStyle` | `'custom' \| 'native'` | `'custom'` | Table styling: custom (mtx-table themes) or native (plain TipTap) |
354
+ | `onInsertBlockMath` | `() => void` | — | Callback to insert a block-level math node directly |
355
+ | `editable` | `boolean` | `true` | Set false for read-only mode |
356
+ | `className` | `string` | — | Additional CSS class |
357
+
358
+ ### ContentViewer
359
+
360
+ | Prop | Type | Default | Description |
361
+ |------|------|---------|-------------|
362
+ | `content` | `string` | — | HTML content to render (required) |
363
+ | `className` | `string` | — | Additional CSS class |
364
+
365
+ ---
366
+
367
+ ## 📦 Exports
368
+
369
+ ### Main Package (`erl-mathtextx-editor`)
370
+
371
+ ```tsx
372
+ import {
373
+ MathTextXEditor, // Main editor component
374
+ ContentViewer, // Read-only renderer
375
+ MathTypeDialog, // Standalone equation editor dialog
376
+ TemplatePanel, // Standalone formula template panel
377
+ MainToolbar, // Standalone text formatting toolbar
378
+ MathToolbar, // Standalone math symbols toolbar
379
+ SymbolPalette, // Standalone symbol picker
380
+ WordCount, // Status bar word/char counter
381
+ LinkDialog, // Standalone link insert/edit dialog
382
+ ImageEditDialog, // Standalone image edit dialog
383
+ InsertTableDialog, // Standalone table insert dialog
384
+ TableMenu, // Standalone table context menu
385
+ CellPropertiesDialog, // Standalone cell properties dialog
386
+ TablePropertiesDialog, // Standalone table properties dialog
387
+ TableTemplatesDialog, // Standalone table template picker
388
+ MathInlineNode, // TipTap inline math extension
389
+ MathBlockNode, // TipTap block math extension
390
+ getHTML, // Serialize editor HTML string
391
+ getJSON, // Serialize editor → JSON
392
+ sanitizeCKEditorHTML, // Clean CKEditor HTML for compatibility
393
+ toCompatibleHTML, // Convert to CKEditor-compatible format
394
+ createExtensions, // Create TipTap extensions programmatically
395
+ mathTemplates, // Template definitions
396
+ getTemplatesByLevel, // Filter templates by education level
397
+ getTemplatesByCategory,// Filter templates by category
398
+ getTemplateCategories, // Get all template categories
399
+ countWords, // Word count utility
400
+ countCharacters, // Character count utility
401
+ getTemplateStyles, // Table template CSS generator
402
+ } from 'erl-mathtextx-editor'
403
+
404
+ import 'erl-mathtextx-editor/styles'
405
+ ```
406
+
407
+ ### Viewer Only (`erl-mathtextx-editor/viewer`)
408
+
409
+ ```tsx
410
+ import { ContentViewer } from 'erl-mathtextx-editor/viewer'
411
+ import 'erl-mathtextx-editor/viewer/styles'
412
+ ```
413
+
414
+ ---
415
+
416
+ ## ⌨️ Keyboard Shortcuts
417
+
418
+ | Shortcut | Action |
419
+ |----------|--------|
420
+ | `Ctrl+B` | Bold |
421
+ | `Ctrl+I` | Italic |
422
+ | `Ctrl+U` | Underline |
423
+ | `Ctrl+K` | Insert/edit link |
424
+ | `Ctrl+M` | Insert inline math directly (without dialog) |
425
+ | `Ctrl+Shift+T` | Insert table |
426
+ | `Ctrl+S` | Save document |
427
+ | `Shift+Ctrl+V` | Paste as plain text |
428
+ | `Tab` | Navigate to next cell (table) |
429
+ | `Shift+Tab` | Navigate to previous cell (table) |
430
+ | `Esc` (equation editor dialog) | Close dialog |
431
+
432
+ ---
433
+
434
+ ## 🎨 Table Features
435
+
436
+ ### Table Style Mode
437
+
438
+ Pilih antara custom styled tables atau native TipTap tables:
439
+
440
+ ```tsx
441
+ // Custom styling (default) — 6 themes: plain, light, dark, blue, striped, minimal
442
+ <MathTextXEditor tableStyle="custom" />
443
+
444
+ // Native TipTap table — plain, no custom CSS
445
+ <MathTextXEditor tableStyle="native" />
446
+ ```
447
+
448
+ ### Table Context Menu
449
+
450
+ Right-click pada tabel untuk akses operasi:
451
+
452
+ | Group | Operations |
453
+ |-------|------------|
454
+ | **Rows** | Insert Row Above/Below, Delete Row |
455
+ | **Columns** | Insert Column Left/Right, Delete Column |
456
+ | **Cells** | Merge/Split (smart: merge if multi-select, split if single) |
457
+ | **Properties** | Cell Properties, Table Properties |
458
+ | **Headers** | Toggle Header Row/Column/Cell, Fix Table |
459
+ | **Danger Zone** | Delete Table |
460
+
461
+ ### Table Keyboard Shortcuts
462
+
463
+ | Shortcut | Action |
464
+ |----------|--------|
465
+ | `Tab` | Navigate to next cell |
466
+ | `Shift+Tab` | Navigate to previous cell |
467
+
468
+ ### Cell Properties
469
+
470
+ Klik kanan → Cell Properties untuk mengatur:
471
+ - **Width** — Lebar cell (px atau %)
472
+ - **Background Color** — Warna latar belakang
473
+ - **Horizontal Align** — Kiri, tengah, kanan
474
+ - **Vertical Align** — Atas, tengah, bawah
475
+
476
+ ### Table Properties
477
+
478
+ Klik kanan → Table Properties untuk mengatur:
479
+ - **Alignment** — Rata kiri, tengah, atau kanan
480
+
481
+ ### Table Templates
482
+
483
+ Klik kanan → Table Templates untuk memilih tema:
484
+ - **Polos** — Tabel sederhana tanpa gaya tambahan
485
+ - **Terang** — Kepala tabel abu-abu terang
486
+ - **Gelap** — Kepala tabel gelap dengan teks putih
487
+ - **Tema Biru** — Kepala tabel biru dengan baris belang
488
+ - **Belang** — Warna baris selang-seling
489
+ - **Minimalis** — Hanya border horizontal
490
+
491
+ ---
492
+
493
+ ## ⚠️ Troubleshooting
494
+
495
+ | Error | Solution |
496
+ |---|---|
497
+ | `Can't resolve 'erl-mathtextx-editor'` | `npm install erl-mathtextx-editor` |
498
+ | `Can't resolve 'erl-mathtextx-editor/styles'` | Version ≥ 0.1.3. Alternatif: `import 'erl-mathtextx-editor/dist/assets/erl-mathtextx-editor.css'` |
499
+ | `MathTextXEditor is not a function` | Pastikan React ≥ 18 (`npm ls react`) |
500
+ | `window is not defined` (Next.js) | Gunakan `dynamic()` dengan `{ ssr: false }` |
501
+ | `Unexpected token 'export'` (CRA) | Webpack config: `resolve.mainFields: ['main', 'module']` |
502
+ | MathLive fonts error | Set `(window as any).MATHLIVE_FONTS_PATH = '/fonts'` + copy font ke `public/fonts/` |
503
+
504
+ ---
505
+
506
+ ## ✅ Verified Import Paths
507
+
508
+ | Import | Resolves to |
509
+ |---|---|
510
+ | `erl-mathtextx-editor` | `dist/erl-mathtextx-editor.js` |
511
+ | `erl-mathtextx-editor/styles` | `dist/assets/erl-mathtextx-editor.css` |
512
+ | `erl-mathtextx-editor/viewer` | `dist/viewer.js` |
513
+ | `erl-mathtextx-editor/viewer/styles` | `dist/viewer-styles.js` |
514
+
515
+ ---
516
+
517
+ ## 🛠️ Tech Stack
518
+
519
+ - **UI Framework:** React 18+
520
+ - **Editor Engine:** TipTap / ProseMirror
521
+ - **Math Input:** MathLive (WYSIWYG math)
522
+ - **Math Rendering:** KaTeX
523
+ - **DOCX Import:** mammoth.js
524
+ - **XSS Protection:** DOMPurify
525
+ - **Graph Plotting:** Function Plot
526
+ - **Syntax Highlight:** lowlight (100+ languages)
527
+ - **Build:** Vite (Library Mode)
528
+
529
+ ---
530
+
531
+ ## 📄 License
532
+
533
+ [MIT](https://github.com/erlangga/richtext-editor-research/blob/main/LICENSE) © Erlangga Team
534
+
535
+ ---
536
+
537
+ ## 🔗 Links
538
+
539
+ - **NPM:** [erl-mathtextx-editor](https://www.npmjs.com/package/erl-mathtextx-editor)
540
+ - **Source:** [GitHub Repository](https://github.com/erlangga/richtext-editor-research)
541
+ - **Issues:** [Report Bug](https://github.com/erlangga/richtext-editor-research/issues)
542
+ - **📘 Embed Tutorial:** [docs/EMBED_TUTORIAL.md](https://github.com/erlangga/richtext-editor-research/blob/main/docs/EMBED_TUTORIAL.md)