erl-mathtextx-editor 0.1.10 → 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.
Files changed (29) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +448 -423
  3. package/dist/{CellPropertiesDialogImpl-Cl0pxbeQ.js → CellPropertiesDialogImpl-D-qjxrs5.js} +40 -40
  4. package/dist/{ImageInsertDialog-BVBl1y36.js → ImageInsertDialog-BUX79-_G.js} +1 -1
  5. package/dist/{InsertTableDialogImpl-Cx3ShX7u.js → InsertTableDialogImpl-DfGvKwP4.js} +25 -25
  6. package/dist/{LinkDialogImpl-gMjoZVma.js → LinkDialogImpl-BQEObyL2.js} +57 -57
  7. package/dist/MathTextXEditor.d.ts +1 -1
  8. package/dist/TablePropertiesDialogImpl-CDWcoc-Y.js +56 -0
  9. package/dist/TableTemplatesDialogImpl-BUSPvt83.js +64 -0
  10. package/dist/assets/erl-mathtextx-editor.css +1 -1
  11. package/dist/assets/viewer.css +1 -1
  12. package/dist/components/ImageEditDialog.d.ts +3 -0
  13. package/dist/components/useDraggable.d.ts +7 -0
  14. package/dist/core/serializer.d.ts +8 -0
  15. package/dist/erl-mathtextx-editor.js +1 -1
  16. package/dist/erl-mathtextx-editor.umd.cjs +178 -178
  17. package/dist/{index-CakccgVO.js → index-BHhpSW1p.js} +4208 -4478
  18. package/dist/{index-Djb9MY7m.js → index-D7JFZLgA.js} +1 -1
  19. package/dist/{index-QMz8TDH0.js → index-DBnDkAIP.js} +1 -1
  20. package/dist/toolbar/MainToolbar.d.ts +1 -0
  21. package/dist/toolbar/MathToolbar.d.ts +3 -1
  22. package/dist/toolbar/toolbar-config.d.ts +22 -0
  23. package/dist/toolbar/toolbar-icons.d.ts +53 -0
  24. package/dist/toolbar/useDialogManager.d.ts +45 -0
  25. package/dist/types/index.d.ts +1 -1
  26. package/dist/useDraggable-DEoLIB5k.js +34 -0
  27. package/package.json +1 -1
  28. package/dist/TablePropertiesDialogImpl-CrTTV3Zr.js +0 -56
  29. package/dist/TableTemplatesDialogImpl-DTcom8H5.js +0 -64
package/README.md CHANGED
@@ -1,423 +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 Keyboard** — Klik simbol, operator, template formula
15
- - 📝 **Rich Text Editor** — Bold, italic, tables, lists, links, images
16
- - 🧮 **Equation Editor** — Dialog MathType-style dengan tab, grid, KaTeX preview
17
- - 📋 **100+ Formula Templates** — Algebra, calculus, trigonometry, chemistry, matrix
18
- - 🖼️ **Free-form Image Drag** — Drag gambar ke posisi bebas (pixel-perfect)
19
- - 📄 **DOCX Import** — Import file .docx via mammoth.js (toolbar + drag-drop)
20
- - 🗂️ **Google Docs Paste** — Paste dari Google Docs (equations + document) auto-cleaned
21
- - 👁️ **Content Viewer** — Read-only renderer dengan KaTeX + DOMPurify
22
- - 🎨 **Table Editor** — 6 templates, column resize, cell merge/split
23
- - 🔒 **XSS Protection** — DOMPurify sanitization di paste + serializer
24
- - 🛡️ **Error Boundary** — Anti white-screen crash protection
25
-
26
- ---
27
-
28
- ## 📦 Installation
29
-
30
- ```bash
31
- npm install erl-mathtextx-editor
32
- ```
33
-
34
- ---
35
-
36
- ## 🚀 Quick Start
37
-
38
- ### 1. Basic Editor
39
-
40
- ```tsx
41
- import { MathTextXEditor } from 'erl-mathtextx-editor'
42
- import 'erl-mathtextx-editor/styles'
43
-
44
- function App() {
45
- return (
46
- <MathTextXEditor
47
- onChange={(html) => console.log(html)}
48
- placeholder="Tulis soal di sini..."
49
- />
50
- )
51
- }
52
- ```
53
-
54
- ### 2. Editor dengan Save Handler
55
-
56
- ```tsx
57
- import { useRef } from 'react'
58
- import { MathTextXEditor, getHTML } from 'erl-mathtextx-editor'
59
- import 'erl-mathtextx-editor/styles'
60
-
61
- function QuestionForm() {
62
- const editorRef = useRef<HTMLDivElement>(null)
63
-
64
- const handleSave = () => {
65
- if (!editorRef.current) return
66
- const html = getHTML(editorRef.current)
67
- fetch('/api/questions', {
68
- method: 'POST',
69
- headers: { 'Content-Type': 'application/json' },
70
- body: JSON.stringify({ content: html }),
71
- })
72
- }
73
-
74
- return (
75
- <div>
76
- <MathTextXEditor ref={editorRef} placeholder="Tulis pertanyaan..." onSave={handleSave} />
77
- <button onClick={handleSave}>Simpan</button>
78
- </div>
79
- )
80
- }
81
- ```
82
-
83
- ### 3. Edit Existing Content
84
-
85
- ```tsx
86
- import { MathTextXEditor } from 'erl-mathtextx-editor'
87
- import 'erl-mathtextx-editor/styles'
88
-
89
- function EditQuestion({ existingHtml }: { existingHtml: string }) {
90
- return (
91
- <MathTextXEditor
92
- content={existingHtml}
93
- onChange={(html) => console.log('Updated:', html)}
94
- />
95
- )
96
- }
97
- ```
98
-
99
- ### 4. Read-Only Mode (Viewer)
100
-
101
- ```tsx
102
- import { ContentViewer } from 'erl-mathtextx-editor/viewer'
103
- import 'erl-mathtextx-editor/viewer/styles'
104
-
105
- function ExamQuestion({ questionHtml }: { questionHtml: string }) {
106
- return <ContentViewer content={questionHtml} />
107
- }
108
- ```
109
-
110
- ### 5. Multi-Instance (Soal + Pilihan Jawaban)
111
-
112
- ```tsx
113
- import { useState } from 'react'
114
- import { MathTextXEditor } from 'erl-mathtextx-editor'
115
- import 'erl-mathtextx-editor/styles'
116
-
117
- function MultipleChoiceForm() {
118
- const [question, setQuestion] = useState('')
119
- const [options, setOptions] = useState(
120
- ['A', 'B', 'C', 'D'].map((id) => ({ id, content: '' }))
121
- )
122
-
123
- return (
124
- <div>
125
- <label>Pertanyaan:</label>
126
- <MathTextXEditor
127
- content={question}
128
- onChange={setQuestion}
129
- placeholder="Tulis pertanyaan..."
130
- minHeight="150px"
131
- />
132
- {options.map((opt) => (
133
- <div key={opt.id}>
134
- <label>Opsi {opt.id}:</label>
135
- <MathTextXEditor
136
- content={opt.content}
137
- onChange={(html) => setOptions((prev) => prev.map((o) => o.id === opt.id ? { ...o, content: html } : o))}
138
- placeholder={`Jawaban ${opt.id}...`}
139
- minHeight="60px"
140
- />
141
- </div>
142
- ))}
143
- </div>
144
- )
145
- }
146
- ```
147
-
148
- ### 6. Next.js (App Router)
149
-
150
- ```tsx
151
- 'use client'
152
- import dynamic from 'next/dynamic'
153
- import 'erl-mathtextx-editor/styles'
154
-
155
- const MathTextXEditor = dynamic(
156
- () => import('erl-mathtextx-editor').then((mod) => mod.MathTextXEditor),
157
- { ssr: false }
158
- )
159
-
160
- export default function EditorPage() {
161
- return (
162
- <MathTextXEditor
163
- placeholder="Tulis soal matematika..."
164
- onChange={(html) => console.log(html)}
165
- minHeight="300px"
166
- />
167
- )
168
- }
169
- ```
170
-
171
- ### 7. Vite + React
172
-
173
- ```tsx
174
- import { MathTextXEditor } from 'erl-mathtextx-editor'
175
- import 'erl-mathtextx-editor/styles'
176
-
177
- function App() {
178
- return (
179
- <MathTextXEditor
180
- placeholder="Tulis soal..."
181
- onChange={(html) => console.log(html)}
182
- />
183
- )
184
- }
185
- export default App
186
- ```
187
-
188
- ---
189
-
190
- ## 🧰 Image Upload
191
-
192
- Editor mendukung upload gambar dari: **Insert dialog**, **drag-drop**, **paste dari clipboard**, dan **DOCX import**. Semuanya melalui satu callback `onImageUpload`.
193
-
194
- ### Basic Upload
195
-
196
- ```tsx
197
- import { MathTextXEditor } from 'erl-mathtextx-editor'
198
- import 'erl-mathtextx-editor/styles'
199
-
200
- function EditorWithUpload() {
201
- const handleImageUpload = async (file: File): Promise<string> => {
202
- const formData = new FormData()
203
- formData.append('image', file)
204
- const res = await fetch('/api/upload', { method: 'POST', body: formData })
205
- if (!res.ok) throw new Error('Upload failed: ' + res.statusText)
206
- const data = await res.json()
207
- return data.url // Expected: { "url": "https://cdn.example.com/img.jpg" }
208
- }
209
-
210
- return (
211
- <MathTextXEditor
212
- placeholder="Tulis soal..."
213
- onImageUpload={handleImageUpload}
214
- />
215
- )
216
- }
217
- ```
218
-
219
- ### Re-upload Gambar dari Paste (Google Docs / Website)
220
-
221
- ```tsx
222
- import { MathTextXEditor } from 'erl-mathtextx-editor'
223
- import 'erl-mathtextx-editor/styles'
224
-
225
- function EditorWithPasteReupload() {
226
- const handleImageUpload = async (file: File): Promise<string> => {
227
- const formData = new FormData()
228
- formData.append('image', file)
229
- const res = await fetch('/api/upload', { method: 'POST', body: formData })
230
- return (await res.json()).url
231
- }
232
-
233
- const handleBeforePasteHTML = async (html: string): Promise<string> => {
234
- // Download + re-upload external images from pasted HTML
235
- const imgRegex = /<img\s+[^>]*src="([^"]+)"[^>]*>/gi
236
- const replacements: Array<[string, string]> = []
237
- let match
238
-
239
- while ((match = imgRegex.exec(html)) !== null) {
240
- const src = match[1]
241
- if (src.startsWith('data:') || src.includes('cdn.example.com')) continue
242
- try {
243
- const blob = await (await fetch(src)).blob()
244
- const file = new File([blob], 'image.' + (blob.type.split('/')[1] || 'jpg'))
245
- replacements.push([src, await handleImageUpload(file)])
246
- } catch { console.warn('Skip image:', src) }
247
- }
248
-
249
- let result = html
250
- for (const [oldSrc, newSrc] of replacements) result = result.replaceAll(oldSrc, newSrc)
251
- return result
252
- }
253
-
254
- return (
255
- <MathTextXEditor
256
- placeholder="Tulis soal..."
257
- onImageUpload={handleImageUpload}
258
- onBeforePasteHTML={handleBeforePasteHTML}
259
- />
260
- )
261
- }
262
- ```
263
-
264
- ### Base64 Fallback (Tanpa Server)
265
-
266
- ```tsx
267
- const handleImageUpload = async (file: File): Promise<string> => {
268
- return new Promise((resolve, reject) => {
269
- const reader = new FileReader()
270
- reader.onload = () => resolve(reader.result as string)
271
- reader.onerror = reject
272
- reader.readAsDataURL(file)
273
- })
274
- }
275
- // ⚠️ Base64 hanya cocok untuk gambar < 100KB. Untuk produksi, gunakan upload ke server.
276
- ```
277
-
278
- ---
279
-
280
- ## 📋 Props API
281
-
282
- ### MathTextXEditor
283
-
284
- | Prop | Type | Default | Description |
285
- |------|------|---------|-------------|
286
- | `content` | `string` | `''` | Initial HTML content |
287
- | `onChange` | `(html: string) => void` | — | Callback on content change (debounced 150ms) |
288
- | `onSave` | `(html: string) => void` | — | Callback on Ctrl+S |
289
- | `onImageUpload` | `(file: File) => Promise<string>` | — | Custom image upload handler |
290
- | `onBeforePasteHTML` | `(html: string) => Promise<string>` | — | Transform pasted HTML (re-upload images) |
291
- | `placeholder` | `string` | `'Tulis soal...'` | Placeholder text |
292
- | `minHeight` | `string` | `'200px'` | Minimum editor height |
293
- | `maxHeight` | `string` | — | Maximum editor height |
294
- | `autoFocus` | `boolean` | `false` | Auto-focus editor on mount |
295
- | `toolbarMode` | `'basic' \| 'advanced'` | `'basic'` | Toolbar preset |
296
- | `editable` | `boolean` | `true` | Set false for read-only mode |
297
- | `className` | `string` | — | Additional CSS class |
298
-
299
- ### ContentViewer
300
-
301
- | Prop | Type | Default | Description |
302
- |------|------|---------|-------------|
303
- | `content` | `string` | — | HTML content to render (required) |
304
- | `className` | `string` | | Additional CSS class |
305
-
306
- ---
307
-
308
- ## 📦 Exports
309
-
310
- ### Main Package (`erl-mathtextx-editor`)
311
-
312
- ```tsx
313
- import {
314
- MathTextXEditor, // Main editor component
315
- ContentViewer, // Read-only renderer
316
- MathTypeDialog, // Standalone equation editor dialog
317
- TemplatePanel, // Standalone formula template panel
318
- MainToolbar, // Standalone text formatting toolbar
319
- MathToolbar, // Standalone math symbols toolbar
320
- SymbolPalette, // Standalone symbol picker
321
- WordCount, // Status bar word/char counter
322
- LinkDialog, // Standalone link insert/edit dialog
323
- ImageEditDialog, // Standalone image edit dialog
324
- InsertTableDialog, // Standalone table insert dialog
325
- TableMenu, // Standalone table context menu
326
- CellPropertiesDialog, // Standalone cell properties dialog
327
- TablePropertiesDialog, // Standalone table properties dialog
328
- TableTemplatesDialog, // Standalone table template picker
329
- MathInlineNode, // TipTap inline math extension
330
- MathBlockNode, // TipTap block math extension
331
- getHTML, // Serialize editor HTML string
332
- getJSON, // Serialize editor JSON
333
- sanitizeCKEditorHTML, // Clean CKEditor HTML for compatibility
334
- toCompatibleHTML, // Convert to CKEditor-compatible format
335
- createExtensions, // Create TipTap extensions programmatically
336
- mathTemplates, // Template definitions
337
- getTemplatesByLevel, // Filter templates by education level
338
- getTemplatesByCategory,// Filter templates by category
339
- getTemplateCategories, // Get all template categories
340
- countWords, // Word count utility
341
- countCharacters, // Character count utility
342
- getTemplateStyles, // Table template CSS generator
343
- } from 'erl-mathtextx-editor'
344
-
345
- import 'erl-mathtextx-editor/styles'
346
- ```
347
-
348
- ### Viewer Only (`erl-mathtextx-editor/viewer`)
349
-
350
- ```tsx
351
- import { ContentViewer } from 'erl-mathtextx-editor/viewer'
352
- import 'erl-mathtextx-editor/viewer/styles'
353
- ```
354
-
355
- ---
356
-
357
- ## ⌨️ Keyboard Shortcuts
358
-
359
- | Shortcut | Action |
360
- |----------|--------|
361
- | `Ctrl+B` | Bold |
362
- | `Ctrl+I` | Italic |
363
- | `Ctrl+U` | Underline |
364
- | `Ctrl+K` | Insert/edit link |
365
- | `Ctrl+M` | Insert inline math |
366
- | `Ctrl+Shift+T` | Insert table |
367
- | `Ctrl+S` | Save document |
368
- | `Shift+Ctrl+V` | Paste as plain text |
369
- | `Enter` (equation editor) | Insert formula |
370
- | `Esc` (equation editor) | Close dialog |
371
-
372
- ---
373
-
374
- ## ⚠️ Troubleshooting
375
-
376
- | Error | Solution |
377
- |---|---|
378
- | `Can't resolve 'erl-mathtextx-editor'` | `npm install erl-mathtextx-editor` |
379
- | `Can't resolve 'erl-mathtextx-editor/styles'` | Version ≥ 0.1.3. Alternatif: `import 'erl-mathtextx-editor/dist/assets/erl-mathtextx-editor.css'` |
380
- | `MathTextXEditor is not a function` | Pastikan React ≥ 18 (`npm ls react`) |
381
- | `window is not defined` (Next.js) | Gunakan `dynamic()` dengan `{ ssr: false }` |
382
- | `Unexpected token 'export'` (CRA) | Webpack config: `resolve.mainFields: ['main', 'module']` |
383
- | MathLive fonts error | Set `(window as any).MATHLIVE_FONTS_PATH = '/fonts'` + copy font ke `public/fonts/` |
384
-
385
- ---
386
-
387
- ## ✅ Verified Import Paths
388
-
389
- | Import | Resolves to |
390
- |---|---|
391
- | `erl-mathtextx-editor` | `dist/erl-mathtextx-editor.js` |
392
- | `erl-mathtextx-editor/styles` | `dist/assets/erl-mathtextx-editor.css` |
393
- | `erl-mathtextx-editor/viewer` | `dist/viewer.js` |
394
- | `erl-mathtextx-editor/viewer/styles` | `dist/viewer-styles.js` |
395
-
396
- ---
397
-
398
- ## 🛠️ Tech Stack
399
-
400
- - **UI Framework:** React 18+
401
- - **Editor Engine:** TipTap / ProseMirror
402
- - **Math Input:** MathLive (WYSIWYG math)
403
- - **Math Rendering:** KaTeX
404
- - **DOCX Import:** mammoth.js
405
- - **XSS Protection:** DOMPurify
406
- - **Graph Plotting:** Function Plot
407
- - **Syntax Highlight:** lowlight (100+ languages)
408
- - **Build:** Vite (Library Mode)
409
-
410
- ---
411
-
412
- ## 📄 License
413
-
414
- [MIT](https://github.com/erlangga/richtext-editor-research/blob/main/LICENSE) © Erlangga Team
415
-
416
- ---
417
-
418
- ## 🔗 Links
419
-
420
- - **NPM:** [erl-mathtextx-editor](https://www.npmjs.com/package/erl-mathtextx-editor)
421
- - **Source:** [GitHub Repository](https://github.com/erlangga/richtext-editor-research)
422
- - **Issues:** [Report Bug](https://github.com/erlangga/richtext-editor-research/issues)
423
- - **📘 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