erl-mathtextx-editor 0.1.9 → 0.1.10
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 +262 -107
- package/dist/{CellPropertiesDialogImpl-DBgs-7H9.js → CellPropertiesDialogImpl-Cl0pxbeQ.js} +1 -1
- package/dist/{ContentViewer-CsFSAN_B.js → ContentViewer-RijJ5nlJ.js} +15 -14
- package/dist/{ImageInsertDialog-B24KHrgt.js → ImageInsertDialog-BVBl1y36.js} +27 -25
- package/dist/{InsertTableDialogImpl-B6_PRu5m.js → InsertTableDialogImpl-Cx3ShX7u.js} +1 -1
- package/dist/{LinkDialogImpl-BTA8u_qQ.js → LinkDialogImpl-gMjoZVma.js} +1 -1
- package/dist/MathTextXEditor.d.ts +1 -1
- package/dist/{TablePropertiesDialogImpl-CuRRWS4H.js → TablePropertiesDialogImpl-CrTTV3Zr.js} +1 -1
- package/dist/{TableTemplatesDialogImpl-CU8seEdV.js → TableTemplatesDialogImpl-DTcom8H5.js} +2 -2
- package/dist/assets/erl-mathtextx-editor.css +1 -1
- package/dist/assets/viewer.css +1 -1
- package/dist/components/ErrorBoundary.d.ts +18 -0
- package/dist/components/ImageEditDialog.d.ts +0 -1
- package/dist/components/TableMenu.d.ts +4 -1
- package/dist/erl-mathtextx-editor.js +2 -2
- package/dist/erl-mathtextx-editor.umd.cjs +345 -115
- package/dist/{index-UCSefQk0.js → index-CakccgVO.js} +3801 -3201
- package/dist/{index-CB1g0gXh.js → index-Djb9MY7m.js} +1 -1
- package/dist/index-QMz8TDH0.js +16549 -0
- package/dist/toolbar/MainToolbar.d.ts +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/utils/docxImporter.d.ts +31 -0
- package/dist/utils/pasteHandler.d.ts +14 -3
- package/dist/{viewer-deps-CjbAqdti.js → viewer-deps-BDYoL2Ts.js} +5794 -3489
- package/dist/viewer.js +1 -1
- package/package.json +2 -1
- package/dist/extensions/TableAlignPlugin.d.ts +0 -7
package/README.md
CHANGED
|
@@ -13,16 +13,15 @@ Embeddable visual math editor widget untuk CMS dan platform edukasi. User tidak
|
|
|
13
13
|
|
|
14
14
|
- 🎯 **Visual Math Keyboard** — Klik simbol, operator, template formula
|
|
15
15
|
- 📝 **Rich Text Editor** — Bold, italic, tables, lists, links, images
|
|
16
|
-
- 🧮 **
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
- 🔒 **XSS Protection** — DOMPurify sanitization
|
|
24
|
-
-
|
|
25
|
-
- 📋 **Collapsible Sections** — Details/summary for hidden content
|
|
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
|
|
26
25
|
|
|
27
26
|
---
|
|
28
27
|
|
|
@@ -36,7 +35,7 @@ npm install erl-mathtextx-editor
|
|
|
36
35
|
|
|
37
36
|
## 🚀 Quick Start
|
|
38
37
|
|
|
39
|
-
### Basic
|
|
38
|
+
### 1. Basic Editor
|
|
40
39
|
|
|
41
40
|
```tsx
|
|
42
41
|
import { MathTextXEditor } from 'erl-mathtextx-editor'
|
|
@@ -52,14 +51,19 @@ function App() {
|
|
|
52
51
|
}
|
|
53
52
|
```
|
|
54
53
|
|
|
55
|
-
###
|
|
54
|
+
### 2. Editor dengan Save Handler
|
|
56
55
|
|
|
57
56
|
```tsx
|
|
58
|
-
import {
|
|
57
|
+
import { useRef } from 'react'
|
|
58
|
+
import { MathTextXEditor, getHTML } from 'erl-mathtextx-editor'
|
|
59
59
|
import 'erl-mathtextx-editor/styles'
|
|
60
60
|
|
|
61
61
|
function QuestionForm() {
|
|
62
|
-
const
|
|
62
|
+
const editorRef = useRef<HTMLDivElement>(null)
|
|
63
|
+
|
|
64
|
+
const handleSave = () => {
|
|
65
|
+
if (!editorRef.current) return
|
|
66
|
+
const html = getHTML(editorRef.current)
|
|
63
67
|
fetch('/api/questions', {
|
|
64
68
|
method: 'POST',
|
|
65
69
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -67,32 +71,210 @@ function QuestionForm() {
|
|
|
67
71
|
})
|
|
68
72
|
}
|
|
69
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 }) {
|
|
70
90
|
return (
|
|
71
91
|
<MathTextXEditor
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
placeholder="Tulis pertanyaan..."
|
|
92
|
+
content={existingHtml}
|
|
93
|
+
onChange={(html) => console.log('Updated:', html)}
|
|
75
94
|
/>
|
|
76
95
|
)
|
|
77
96
|
}
|
|
78
97
|
```
|
|
79
98
|
|
|
80
|
-
###
|
|
99
|
+
### 4. Read-Only Mode (Viewer)
|
|
81
100
|
|
|
82
101
|
```tsx
|
|
83
102
|
import { ContentViewer } from 'erl-mathtextx-editor/viewer'
|
|
84
103
|
import 'erl-mathtextx-editor/viewer/styles'
|
|
85
104
|
|
|
86
|
-
function
|
|
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
|
+
|
|
87
123
|
return (
|
|
88
|
-
<div
|
|
89
|
-
<
|
|
90
|
-
<
|
|
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
|
+
))}
|
|
91
143
|
</div>
|
|
92
144
|
)
|
|
93
145
|
}
|
|
94
146
|
```
|
|
95
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
|
+
|
|
96
278
|
---
|
|
97
279
|
|
|
98
280
|
## 📋 Props API
|
|
@@ -102,15 +284,17 @@ function QuestionCard({ questionHtml }) {
|
|
|
102
284
|
| Prop | Type | Default | Description |
|
|
103
285
|
|------|------|---------|-------------|
|
|
104
286
|
| `content` | `string` | `''` | Initial HTML content |
|
|
105
|
-
| `onChange` | `(html: string) => void` | — | Callback on content change |
|
|
287
|
+
| `onChange` | `(html: string) => void` | — | Callback on content change (debounced 150ms) |
|
|
106
288
|
| `onSave` | `(html: string) => void` | — | Callback on Ctrl+S |
|
|
107
289
|
| `onImageUpload` | `(file: File) => Promise<string>` | — | Custom image upload handler |
|
|
290
|
+
| `onBeforePasteHTML` | `(html: string) => Promise<string>` | — | Transform pasted HTML (re-upload images) |
|
|
108
291
|
| `placeholder` | `string` | `'Tulis soal...'` | Placeholder text |
|
|
109
292
|
| `minHeight` | `string` | `'200px'` | Minimum editor height |
|
|
110
293
|
| `maxHeight` | `string` | — | Maximum editor height |
|
|
111
294
|
| `autoFocus` | `boolean` | `false` | Auto-focus editor on mount |
|
|
112
295
|
| `toolbarMode` | `'basic' \| 'advanced'` | `'basic'` | Toolbar preset |
|
|
113
|
-
| `
|
|
296
|
+
| `editable` | `boolean` | `true` | Set false for read-only mode |
|
|
297
|
+
| `className` | `string` | — | Additional CSS class |
|
|
114
298
|
|
|
115
299
|
### ContentViewer
|
|
116
300
|
|
|
@@ -127,17 +311,35 @@ function QuestionCard({ questionHtml }) {
|
|
|
127
311
|
|
|
128
312
|
```tsx
|
|
129
313
|
import {
|
|
130
|
-
MathTextXEditor,
|
|
131
|
-
ContentViewer,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
141
343
|
} from 'erl-mathtextx-editor'
|
|
142
344
|
|
|
143
345
|
import 'erl-mathtextx-editor/styles'
|
|
@@ -160,87 +362,36 @@ import 'erl-mathtextx-editor/viewer/styles'
|
|
|
160
362
|
| `Ctrl+I` | Italic |
|
|
161
363
|
| `Ctrl+U` | Underline |
|
|
162
364
|
| `Ctrl+K` | Insert/edit link |
|
|
365
|
+
| `Ctrl+M` | Insert inline math |
|
|
163
366
|
| `Ctrl+Shift+T` | Insert table |
|
|
164
367
|
| `Ctrl+S` | Save document |
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
368
|
+
| `Shift+Ctrl+V` | Paste as plain text |
|
|
369
|
+
| `Enter` (equation editor) | Insert formula |
|
|
370
|
+
| `Esc` (equation editor) | Close dialog |
|
|
168
371
|
|
|
169
372
|
---
|
|
170
373
|
|
|
171
|
-
##
|
|
374
|
+
## ⚠️ Troubleshooting
|
|
172
375
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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/` |
|
|
177
384
|
|
|
178
|
-
|
|
179
|
-
const [question, setQuestion] = useState('')
|
|
180
|
-
const [options, setOptions] = useState([
|
|
181
|
-
{ id: 'A', content: '', isCorrect: false },
|
|
182
|
-
{ id: 'B', content: '', isCorrect: false },
|
|
183
|
-
{ id: 'C', content: '', isCorrect: false },
|
|
184
|
-
{ id: 'D', content: '', isCorrect: false },
|
|
185
|
-
])
|
|
186
|
-
|
|
187
|
-
const handleSubmit = async () => {
|
|
188
|
-
await fetch('/api/questions', {
|
|
189
|
-
method: 'POST',
|
|
190
|
-
headers: { 'Content-Type': 'application/json' },
|
|
191
|
-
body: JSON.stringify({ question, options }),
|
|
192
|
-
})
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return (
|
|
196
|
-
<div>
|
|
197
|
-
<h2>Buat Soal Pilihan Ganda</h2>
|
|
198
|
-
|
|
199
|
-
<MathTextXEditor
|
|
200
|
-
content={question}
|
|
201
|
-
onChange={setQuestion}
|
|
202
|
-
placeholder="Tulis pertanyaan..."
|
|
203
|
-
minHeight="150px"
|
|
204
|
-
/>
|
|
385
|
+
---
|
|
205
386
|
|
|
206
|
-
|
|
207
|
-
<div key={option.id}>
|
|
208
|
-
<label>
|
|
209
|
-
<input
|
|
210
|
-
type="radio"
|
|
211
|
-
name="correct"
|
|
212
|
-
checked={option.isCorrect}
|
|
213
|
-
onChange={() => {
|
|
214
|
-
setOptions(prev =>
|
|
215
|
-
prev.map(o => ({
|
|
216
|
-
...o,
|
|
217
|
-
isCorrect: o.id === option.id
|
|
218
|
-
}))
|
|
219
|
-
)
|
|
220
|
-
}}
|
|
221
|
-
/>
|
|
222
|
-
Opsi {option.id}
|
|
223
|
-
</label>
|
|
224
|
-
<MathTextXEditor
|
|
225
|
-
content={option.content}
|
|
226
|
-
onChange={(html) => {
|
|
227
|
-
setOptions(prev =>
|
|
228
|
-
prev.map(o =>
|
|
229
|
-
o.id === option.id ? { ...o, content: html } : o
|
|
230
|
-
)
|
|
231
|
-
)
|
|
232
|
-
}}
|
|
233
|
-
placeholder={`Jawaban ${option.id}...`}
|
|
234
|
-
minHeight="80px"
|
|
235
|
-
/>
|
|
236
|
-
</div>
|
|
237
|
-
))}
|
|
387
|
+
## ✅ Verified Import Paths
|
|
238
388
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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` |
|
|
244
395
|
|
|
245
396
|
---
|
|
246
397
|
|
|
@@ -250,8 +401,11 @@ export default function QuestionForm() {
|
|
|
250
401
|
- **Editor Engine:** TipTap / ProseMirror
|
|
251
402
|
- **Math Input:** MathLive (WYSIWYG math)
|
|
252
403
|
- **Math Rendering:** KaTeX
|
|
404
|
+
- **DOCX Import:** mammoth.js
|
|
253
405
|
- **XSS Protection:** DOMPurify
|
|
254
406
|
- **Graph Plotting:** Function Plot
|
|
407
|
+
- **Syntax Highlight:** lowlight (100+ languages)
|
|
408
|
+
- **Build:** Vite (Library Mode)
|
|
255
409
|
|
|
256
410
|
---
|
|
257
411
|
|
|
@@ -266,3 +420,4 @@ export default function QuestionForm() {
|
|
|
266
420
|
- **NPM:** [erl-mathtextx-editor](https://www.npmjs.com/package/erl-mathtextx-editor)
|
|
267
421
|
- **Source:** [GitHub Repository](https://github.com/erlangga/richtext-editor-research)
|
|
268
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,6 +1,6 @@
|
|
|
1
1
|
import { jsx as t, jsxs as l } from "react/jsx-runtime";
|
|
2
2
|
import z, { useId as H, useRef as A, useCallback as b, useState as d, useEffect as V } from "react";
|
|
3
|
-
import { u as B } from "./index-
|
|
3
|
+
import { u as B } from "./index-CakccgVO.js";
|
|
4
4
|
const M = z.memo(({
|
|
5
5
|
isOpen: s,
|
|
6
6
|
initialData: i = {},
|
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import { jsx as
|
|
2
|
-
import { useRef as
|
|
3
|
-
import { p as s, k as l } from "./viewer-deps-
|
|
1
|
+
import { jsx as a } from "react/jsx-runtime";
|
|
2
|
+
import { useRef as o, useMemo as i, useEffect as m } from "react";
|
|
3
|
+
import { p as s, k as l } from "./viewer-deps-BDYoL2Ts.js";
|
|
4
4
|
function c(t) {
|
|
5
|
+
if (t.querySelector(".katex")) return;
|
|
5
6
|
const e = t.getAttribute("data-latex") || t.getAttribute("latex");
|
|
6
7
|
if (!e) return;
|
|
7
|
-
const
|
|
8
|
+
const r = t.classList.contains("mtx-math-block");
|
|
8
9
|
try {
|
|
9
10
|
l.render(e, t, {
|
|
10
11
|
throwOnError: !1,
|
|
11
|
-
displayMode:
|
|
12
|
+
displayMode: r,
|
|
12
13
|
output: "htmlAndMathml"
|
|
13
14
|
});
|
|
14
15
|
} catch {
|
|
15
|
-
t.textContent =
|
|
16
|
+
t.textContent = r ? `$$${e}$$` : `$${e}$`;
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
|
-
function
|
|
19
|
-
const
|
|
19
|
+
function x({ content: t, className: e }) {
|
|
20
|
+
const r = o(null), n = i(
|
|
20
21
|
() => s.sanitize(t, {
|
|
21
22
|
ADD_TAGS: ["math-field", "span", "div"],
|
|
22
23
|
ADD_ATTR: ["data-latex", "latex", "data-type", "data-mathml"],
|
|
@@ -26,19 +27,19 @@ function p({ content: t, className: e }) {
|
|
|
26
27
|
[t]
|
|
27
28
|
);
|
|
28
29
|
return m(() => {
|
|
29
|
-
if (!
|
|
30
|
-
|
|
30
|
+
if (!r.current) return;
|
|
31
|
+
r.current.querySelectorAll(
|
|
31
32
|
'.mtx-math-inline, .mtx-math-block, [data-type="math-inline"], [data-type="math-block"]'
|
|
32
33
|
).forEach(c);
|
|
33
|
-
}, [
|
|
34
|
+
}, [n]), /* @__PURE__ */ a(
|
|
34
35
|
"div",
|
|
35
36
|
{
|
|
36
|
-
ref:
|
|
37
|
+
ref: r,
|
|
37
38
|
className: `mtx-content-viewer${e ? ` ${e}` : ""}`,
|
|
38
|
-
dangerouslySetInnerHTML: { __html:
|
|
39
|
+
dangerouslySetInnerHTML: { __html: n }
|
|
39
40
|
}
|
|
40
41
|
);
|
|
41
42
|
}
|
|
42
43
|
export {
|
|
43
|
-
|
|
44
|
+
x as C
|
|
44
45
|
};
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { jsxs as i, Fragment as J, jsx as e } from "react/jsx-runtime";
|
|
2
2
|
import { useId as V, useRef as y, useState as d, useCallback as s } from "react";
|
|
3
|
-
import { u as q } from "./index-
|
|
3
|
+
import { u as q } from "./index-CakccgVO.js";
|
|
4
4
|
function Z({
|
|
5
5
|
isOpen: U,
|
|
6
|
-
onClose:
|
|
6
|
+
onClose: w,
|
|
7
7
|
onInsert: u,
|
|
8
8
|
onImageUpload: v
|
|
9
9
|
}) {
|
|
10
|
-
const
|
|
11
|
-
m.current && (URL.revokeObjectURL(m.current), m.current = null), D(""), C(""),
|
|
10
|
+
const N = V(), R = y(null), [p, L] = d(v ? "upload" : "url"), [r, D] = d(""), [g, C] = d(""), [B, h] = d(!1), [T, k] = d(!1), [f, c] = d(null), [P, l] = d(""), F = y(null), m = y(null), j = s(() => {
|
|
11
|
+
m.current && (URL.revokeObjectURL(m.current), m.current = null), D(""), C(""), c(null), l(""), k(!1), h(!1);
|
|
12
12
|
}, []), t = s(() => {
|
|
13
|
-
j(),
|
|
14
|
-
}, [
|
|
13
|
+
j(), w();
|
|
14
|
+
}, [w, j]);
|
|
15
15
|
q({ isOpen: U, dialogRef: R, onClose: t });
|
|
16
16
|
const A = async (a) => {
|
|
17
17
|
if (!["image/png", "image/jpeg", "image/jpg", "image/gif", "image/webp"].includes(a.type))
|
|
@@ -26,7 +26,7 @@ function Z({
|
|
|
26
26
|
"image/webp": [[82, 73, 70, 70]]
|
|
27
27
|
};
|
|
28
28
|
try {
|
|
29
|
-
const
|
|
29
|
+
const o = await a.slice(0, 8).arrayBuffer(), x = new Uint8Array(o), S = _[a.type];
|
|
30
30
|
if (!S)
|
|
31
31
|
return { valid: !1, error: "Format gambar tidak dikenali" };
|
|
32
32
|
if (!S.some(
|
|
@@ -46,22 +46,24 @@ function Z({
|
|
|
46
46
|
}
|
|
47
47
|
l(""), m.current && URL.revokeObjectURL(m.current);
|
|
48
48
|
const _ = URL.createObjectURL(a);
|
|
49
|
-
if (m.current = _,
|
|
49
|
+
if (m.current = _, c(_), v) {
|
|
50
50
|
k(!0);
|
|
51
51
|
try {
|
|
52
|
-
const
|
|
53
|
-
|
|
52
|
+
const o = await v(a);
|
|
53
|
+
if (!o)
|
|
54
|
+
throw new Error("Upload gagal: URL tidak valid");
|
|
55
|
+
u(o, g || a.name), t();
|
|
54
56
|
} catch {
|
|
55
|
-
l("Upload gagal. Silakan coba lagi."),
|
|
57
|
+
l("Upload gagal. Silakan coba lagi."), c(null);
|
|
56
58
|
} finally {
|
|
57
59
|
k(!1);
|
|
58
60
|
}
|
|
59
61
|
} else {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
const x =
|
|
62
|
+
const o = new FileReader();
|
|
63
|
+
o.onload = () => {
|
|
64
|
+
const x = o.result;
|
|
63
65
|
u(x, g || a.name), t();
|
|
64
|
-
},
|
|
66
|
+
}, o.readAsDataURL(a);
|
|
65
67
|
}
|
|
66
68
|
},
|
|
67
69
|
[v, u, g, t]
|
|
@@ -73,9 +75,9 @@ function Z({
|
|
|
73
75
|
[b]
|
|
74
76
|
), I = s((a) => {
|
|
75
77
|
a.preventDefault(), a.stopPropagation(), h(!0);
|
|
76
|
-
}, []),
|
|
78
|
+
}, []), E = s((a) => {
|
|
77
79
|
a.preventDefault(), a.stopPropagation(), h(!1);
|
|
78
|
-
}, []),
|
|
80
|
+
}, []), O = s(
|
|
79
81
|
(a) => {
|
|
80
82
|
a.preventDefault(), a.stopPropagation(), h(!1);
|
|
81
83
|
const n = a.dataTransfer.files?.[0];
|
|
@@ -97,16 +99,16 @@ function Z({
|
|
|
97
99
|
}, [r, g, u, t]), K = s(() => {
|
|
98
100
|
if (r.trim())
|
|
99
101
|
try {
|
|
100
|
-
new URL(r),
|
|
102
|
+
new URL(r), c(r.trim()), l("");
|
|
101
103
|
} catch {
|
|
102
104
|
l("URL tidak valid");
|
|
103
105
|
}
|
|
104
106
|
}, [r]);
|
|
105
107
|
return U ? /* @__PURE__ */ i(J, { children: [
|
|
106
108
|
/* @__PURE__ */ e("div", { className: "mtx-dialog-overlay", onClick: t }),
|
|
107
|
-
/* @__PURE__ */ i("div", { className: "mtx-image-dialog", ref: R, role: "dialog", "aria-modal": "true", "aria-labelledby":
|
|
109
|
+
/* @__PURE__ */ i("div", { className: "mtx-image-dialog", ref: R, role: "dialog", "aria-modal": "true", "aria-labelledby": N, tabIndex: -1, children: [
|
|
108
110
|
/* @__PURE__ */ i("div", { className: "mtx-image-dialog__header", children: [
|
|
109
|
-
/* @__PURE__ */ e("h3", { id:
|
|
111
|
+
/* @__PURE__ */ e("h3", { id: N, children: "Sisipkan Gambar" }),
|
|
110
112
|
/* @__PURE__ */ e(
|
|
111
113
|
"button",
|
|
112
114
|
{
|
|
@@ -123,7 +125,7 @@ function Z({
|
|
|
123
125
|
{
|
|
124
126
|
className: `mtx-image-dialog__tab ${p === "upload" ? "is-active" : ""}`,
|
|
125
127
|
onClick: () => {
|
|
126
|
-
L("upload"), l(""),
|
|
128
|
+
L("upload"), l(""), c(null);
|
|
127
129
|
},
|
|
128
130
|
children: "📁 Upload File"
|
|
129
131
|
}
|
|
@@ -133,7 +135,7 @@ function Z({
|
|
|
133
135
|
{
|
|
134
136
|
className: `mtx-image-dialog__tab ${p === "url" ? "is-active" : ""}`,
|
|
135
137
|
onClick: () => {
|
|
136
|
-
L("url"), l(""),
|
|
138
|
+
L("url"), l(""), c(null);
|
|
137
139
|
},
|
|
138
140
|
children: "🔗 URL"
|
|
139
141
|
}
|
|
@@ -145,8 +147,8 @@ function Z({
|
|
|
145
147
|
{
|
|
146
148
|
className: `mtx-image-dialog__dropzone ${B ? "is-dragging" : ""}`,
|
|
147
149
|
onDragOver: I,
|
|
148
|
-
onDragLeave:
|
|
149
|
-
onDrop:
|
|
150
|
+
onDragLeave: E,
|
|
151
|
+
onDrop: O,
|
|
150
152
|
onClick: () => F.current?.click(),
|
|
151
153
|
children: [
|
|
152
154
|
/* @__PURE__ */ e(
|
|
@@ -199,7 +201,7 @@ function Z({
|
|
|
199
201
|
src: f,
|
|
200
202
|
alt: "Preview",
|
|
201
203
|
onError: () => {
|
|
202
|
-
|
|
204
|
+
c(null), l("Gambar tidak dapat dimuat dari URL ini");
|
|
203
205
|
}
|
|
204
206
|
}
|
|
205
207
|
) })
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as e, jsxs as t } from "react/jsx-runtime";
|
|
2
2
|
import N, { useId as v, useRef as f, useState as c, useEffect as g, useCallback as w } from "react";
|
|
3
|
-
import { u as y } from "./index-
|
|
3
|
+
import { u as y } from "./index-CakccgVO.js";
|
|
4
4
|
const k = N.memo(({ isOpen: i, onInsert: d, onClose: m }) => {
|
|
5
5
|
const b = v(), u = f(null), [l, n] = c(3), [a, s] = c(3), [o, h] = c(!0);
|
|
6
6
|
g(() => {
|