erl-mathtextx-editor 0.1.9 → 0.2.0
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/CHANGELOG.md +32 -0
- package/README.md +294 -109
- package/dist/{CellPropertiesDialogImpl-DBgs-7H9.js → CellPropertiesDialogImpl-5BptFKaE.js} +1 -1
- package/dist/{ContentViewer-CsFSAN_B.js → ContentViewer-RijJ5nlJ.js} +15 -14
- package/dist/{ImageInsertDialog-B24KHrgt.js → ImageInsertDialog-Cc7wpIjM.js} +27 -25
- package/dist/{InsertTableDialogImpl-B6_PRu5m.js → InsertTableDialogImpl-BJFXRnQX.js} +1 -1
- package/dist/{LinkDialogImpl-BTA8u_qQ.js → LinkDialogImpl-Cu032Nc7.js} +1 -1
- package/dist/MathTextXEditor.d.ts +1 -1
- package/dist/{TablePropertiesDialogImpl-CuRRWS4H.js → TablePropertiesDialogImpl-YBNdKM7k.js} +1 -1
- package/dist/{TableTemplatesDialogImpl-CU8seEdV.js → TableTemplatesDialogImpl-P54y5q-u.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-C9scFI1r.js +16549 -0
- package/dist/{index-UCSefQk0.js → index-D0Rzm7Tg.js} +4338 -3839
- package/dist/{index-CB1g0gXh.js → index-DJrUW7HG.js} +1 -1
- package/dist/toolbar/MainToolbar.d.ts +2 -0
- package/dist/toolbar/MathToolbar.d.ts +3 -1
- package/dist/types/index.d.ts +3 -1
- 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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,38 @@ All notable changes to `erl-mathtextx-editor` will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.0] - 2026-05-13
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **🏆 Olimpiade Toolbar Mode** — Toolbar khusus untuk olimpiade/kompetisi matematika
|
|
13
|
+
- Mode switch 3 opsi: Basic / Advanced / Olimpiade
|
|
14
|
+
- Toolbar minimal: Undo/Redo, Bold/Italic/Underline, Paragraph/H1/H2, Lists, Math Formula, Block Math
|
|
15
|
+
- Math Toolbar khusus olimpiade: Basic, Relation, Set (extended), Greek, Structure
|
|
16
|
+
- Block Math button untuk insert display-style math node langsung
|
|
17
|
+
- **Block Math Direct Insert** — Prop `onInsertBlockMath` untuk insert block math tanpa dialog
|
|
18
|
+
- **Empty Inline Math Visibility** — Empty math fields sekarang terlihat sebagai kotak dashed abu-abu
|
|
19
|
+
- `min-width: 50px`, `min-height: 26px` untuk empty math inline
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **Math Node Serialization** — Perbaikan kritis: DOMPurify strip `latex` attribute
|
|
24
|
+
- `addAttributes` sekarang render sebagai `data-latex` / `data-display` (bukan `latex` / `display`)
|
|
25
|
+
- `data-display` ditambahkan ke `ALLOWED_ATTR` di DOMPurify
|
|
26
|
+
- Math nodes persist saat tab switch editor ↔ viewer
|
|
27
|
+
- **MathTypeDialog Enter Conflict** — Enter tidak lagi submit dialog
|
|
28
|
+
- Solusi: `\sqrt` + Enter → MathLive auto-convert ke √, dialog tetap terbuka
|
|
29
|
+
- Submit hanya via klik tombol "Sisipkan"
|
|
30
|
+
- **ESLint Cleanup** — Fixed `let` → `const` di MathFieldView, fixed regex escape di MathTypeDialog
|
|
31
|
+
- **Edit Button Overlap** — Tombol Edit pada math node hanya muncul saat hover, hilang saat focus
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- **Toolbar Cleanup** — Advanced mode jadi lebih ringkas
|
|
36
|
+
- Image editing (Edit, Wrap) dipindah ke right-click context menu
|
|
37
|
+
- Table operation buttons dipindah ke Table Operations dropdown
|
|
38
|
+
- **Ctrl+M** — Insert inline math langsung (tanpa dialog MathType)
|
|
39
|
+
|
|
8
40
|
## [0.1.9] - 2026-04-01
|
|
9
41
|
|
|
10
42
|
### Fixed
|
package/README.md
CHANGED
|
@@ -11,18 +11,18 @@ Embeddable visual math editor widget untuk CMS dan platform edukasi. User tidak
|
|
|
11
11
|
|
|
12
12
|
## ✨ Fitur Utama
|
|
13
13
|
|
|
14
|
-
- 🎯 **Visual Math
|
|
14
|
+
- 🎯 **Visual Math Input** — Insert math langsung inline tanpa dialog (Ctrl+M)
|
|
15
15
|
- 📝 **Rich Text Editor** — Bold, italic, tables, lists, links, images
|
|
16
|
-
- 🧮 **
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
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
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
@@ -36,7 +36,7 @@ npm install erl-mathtextx-editor
|
|
|
36
36
|
|
|
37
37
|
## 🚀 Quick Start
|
|
38
38
|
|
|
39
|
-
### Basic
|
|
39
|
+
### 1. Basic Editor
|
|
40
40
|
|
|
41
41
|
```tsx
|
|
42
42
|
import { MathTextXEditor } from 'erl-mathtextx-editor'
|
|
@@ -52,14 +52,19 @@ function App() {
|
|
|
52
52
|
}
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
###
|
|
55
|
+
### 2. Editor dengan Save Handler
|
|
56
56
|
|
|
57
57
|
```tsx
|
|
58
|
-
import {
|
|
58
|
+
import { useRef } from 'react'
|
|
59
|
+
import { MathTextXEditor, getHTML } from 'erl-mathtextx-editor'
|
|
59
60
|
import 'erl-mathtextx-editor/styles'
|
|
60
61
|
|
|
61
62
|
function QuestionForm() {
|
|
62
|
-
const
|
|
63
|
+
const editorRef = useRef<HTMLDivElement>(null)
|
|
64
|
+
|
|
65
|
+
const handleSave = () => {
|
|
66
|
+
if (!editorRef.current) return
|
|
67
|
+
const html = getHTML(editorRef.current)
|
|
63
68
|
fetch('/api/questions', {
|
|
64
69
|
method: 'POST',
|
|
65
70
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -67,32 +72,239 @@ function QuestionForm() {
|
|
|
67
72
|
})
|
|
68
73
|
}
|
|
69
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 }) {
|
|
70
91
|
return (
|
|
71
92
|
<MathTextXEditor
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
placeholder="Tulis pertanyaan..."
|
|
93
|
+
content={existingHtml}
|
|
94
|
+
onChange={(html) => console.log('Updated:', html)}
|
|
75
95
|
/>
|
|
76
96
|
)
|
|
77
97
|
}
|
|
78
98
|
```
|
|
79
99
|
|
|
80
|
-
###
|
|
100
|
+
### 4. Read-Only Mode (Viewer)
|
|
81
101
|
|
|
82
102
|
```tsx
|
|
83
103
|
import { ContentViewer } from 'erl-mathtextx-editor/viewer'
|
|
84
104
|
import 'erl-mathtextx-editor/viewer/styles'
|
|
85
105
|
|
|
86
|
-
function
|
|
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
|
+
|
|
87
124
|
return (
|
|
88
|
-
<div
|
|
89
|
-
<
|
|
90
|
-
<
|
|
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
|
+
))}
|
|
91
144
|
</div>
|
|
92
145
|
)
|
|
93
146
|
}
|
|
94
147
|
```
|
|
95
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
|
+
|
|
96
308
|
---
|
|
97
309
|
|
|
98
310
|
## 📋 Props API
|
|
@@ -102,15 +314,18 @@ function QuestionCard({ questionHtml }) {
|
|
|
102
314
|
| Prop | Type | Default | Description |
|
|
103
315
|
|------|------|---------|-------------|
|
|
104
316
|
| `content` | `string` | `''` | Initial HTML content |
|
|
105
|
-
| `onChange` | `(html: string) => void` | — | Callback on content change |
|
|
317
|
+
| `onChange` | `(html: string) => void` | — | Callback on content change (debounced 150ms) |
|
|
106
318
|
| `onSave` | `(html: string) => void` | — | Callback on Ctrl+S |
|
|
107
319
|
| `onImageUpload` | `(file: File) => Promise<string>` | — | Custom image upload handler |
|
|
320
|
+
| `onBeforePasteHTML` | `(html: string) => Promise<string>` | — | Transform pasted HTML (re-upload images) |
|
|
108
321
|
| `placeholder` | `string` | `'Tulis soal...'` | Placeholder text |
|
|
109
322
|
| `minHeight` | `string` | `'200px'` | Minimum editor height |
|
|
110
323
|
| `maxHeight` | `string` | — | Maximum editor height |
|
|
111
324
|
| `autoFocus` | `boolean` | `false` | Auto-focus editor on mount |
|
|
112
|
-
| `toolbarMode` | `'basic' \| 'advanced'` | `'basic'` | Toolbar preset |
|
|
113
|
-
| `
|
|
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 |
|
|
114
329
|
|
|
115
330
|
### ContentViewer
|
|
116
331
|
|
|
@@ -127,17 +342,35 @@ function QuestionCard({ questionHtml }) {
|
|
|
127
342
|
|
|
128
343
|
```tsx
|
|
129
344
|
import {
|
|
130
|
-
MathTextXEditor,
|
|
131
|
-
ContentViewer,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
141
374
|
} from 'erl-mathtextx-editor'
|
|
142
375
|
|
|
143
376
|
import 'erl-mathtextx-editor/styles'
|
|
@@ -160,87 +393,35 @@ import 'erl-mathtextx-editor/viewer/styles'
|
|
|
160
393
|
| `Ctrl+I` | Italic |
|
|
161
394
|
| `Ctrl+U` | Underline |
|
|
162
395
|
| `Ctrl+K` | Insert/edit link |
|
|
396
|
+
| `Ctrl+M` | Insert inline math directly (without dialog) |
|
|
163
397
|
| `Ctrl+Shift+T` | Insert table |
|
|
164
398
|
| `Ctrl+S` | Save document |
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `Backspace` | Outdent (if indented) / Join with previous paragraph |
|
|
399
|
+
| `Shift+Ctrl+V` | Paste as plain text |
|
|
400
|
+
| `Esc` (equation editor dialog) | Close dialog |
|
|
168
401
|
|
|
169
402
|
---
|
|
170
403
|
|
|
171
|
-
##
|
|
172
|
-
|
|
173
|
-
```tsx
|
|
174
|
-
import { useState } from 'react'
|
|
175
|
-
import { MathTextXEditor } from 'erl-mathtextx-editor'
|
|
176
|
-
import 'erl-mathtextx-editor/styles'
|
|
404
|
+
## ⚠️ Troubleshooting
|
|
177
405
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
}
|
|
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/` |
|
|
194
414
|
|
|
195
|
-
|
|
196
|
-
<div>
|
|
197
|
-
<h2>Buat Soal Pilihan Ganda</h2>
|
|
415
|
+
---
|
|
198
416
|
|
|
199
|
-
|
|
200
|
-
content={question}
|
|
201
|
-
onChange={setQuestion}
|
|
202
|
-
placeholder="Tulis pertanyaan..."
|
|
203
|
-
minHeight="150px"
|
|
204
|
-
/>
|
|
417
|
+
## ✅ Verified Import Paths
|
|
205
418
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
))}
|
|
238
|
-
|
|
239
|
-
<button onClick={handleSubmit}>Simpan Soal</button>
|
|
240
|
-
</div>
|
|
241
|
-
)
|
|
242
|
-
}
|
|
243
|
-
```
|
|
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` |
|
|
244
425
|
|
|
245
426
|
---
|
|
246
427
|
|
|
@@ -250,8 +431,11 @@ export default function QuestionForm() {
|
|
|
250
431
|
- **Editor Engine:** TipTap / ProseMirror
|
|
251
432
|
- **Math Input:** MathLive (WYSIWYG math)
|
|
252
433
|
- **Math Rendering:** KaTeX
|
|
434
|
+
- **DOCX Import:** mammoth.js
|
|
253
435
|
- **XSS Protection:** DOMPurify
|
|
254
436
|
- **Graph Plotting:** Function Plot
|
|
437
|
+
- **Syntax Highlight:** lowlight (100+ languages)
|
|
438
|
+
- **Build:** Vite (Library Mode)
|
|
255
439
|
|
|
256
440
|
---
|
|
257
441
|
|
|
@@ -266,3 +450,4 @@ export default function QuestionForm() {
|
|
|
266
450
|
- **NPM:** [erl-mathtextx-editor](https://www.npmjs.com/package/erl-mathtextx-editor)
|
|
267
451
|
- **Source:** [GitHub Repository](https://github.com/erlangga/richtext-editor-research)
|
|
268
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,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-D0Rzm7Tg.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
|
};
|