@mbs-dev/react-editor 1.0.7 → 1.1.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/README.md +210 -56
- package/dist/Editor.js +91 -63
- package/package.json +1 -1
- package/src/Editor.tsx +120 -100
- package/types/Editor.d.ts +3 -38
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
|
|
2
1
|
# @mbs-dev/react-editor
|
|
3
2
|
|
|
4
|
-
A lightweight, typed React wrapper around **Jodit** rich
|
|
3
|
+
A lightweight, typed React wrapper around **Jodit** rich‑text editor, designed for real‑world CRUD forms (blogs, products, CMS pages, etc.).
|
|
5
4
|
|
|
6
5
|
It provides:
|
|
7
6
|
|
|
8
7
|
- A **simple React component** `<ReactEditor />`
|
|
9
8
|
- A reusable **`config()` helper** for configuration
|
|
10
|
-
-
|
|
9
|
+
- A reusable **`uploaderConfig()` helper**
|
|
10
|
+
- A new **`onDeleteImage` callback** to delete images on server when removed from editor
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
@@ -19,11 +19,12 @@ It provides:
|
|
|
19
19
|
|
|
20
20
|
## ✨ Features
|
|
21
21
|
|
|
22
|
-
- 🧩
|
|
23
|
-
- ⚙️
|
|
24
|
-
- 🖼️
|
|
25
|
-
-
|
|
26
|
-
-
|
|
22
|
+
- 🧩 Simple React API
|
|
23
|
+
- ⚙️ Powerful config builder (`config()`)
|
|
24
|
+
- 🖼️ Image upload support
|
|
25
|
+
- 🗑️ **New: Server‑side image delete support (`onDeleteImage`)**
|
|
26
|
+
- 🔥 Fully typed (TS/TSX)
|
|
27
|
+
- 📦 Built for npm libraries
|
|
27
28
|
|
|
28
29
|
---
|
|
29
30
|
|
|
@@ -38,19 +39,19 @@ npm install @mbs-dev/react-editor
|
|
|
38
39
|
## 🚀 Quick Start
|
|
39
40
|
|
|
40
41
|
```tsx
|
|
41
|
-
import React, { useMemo, useState } from
|
|
42
|
-
import ReactEditor, { config } from
|
|
42
|
+
import React, { useMemo, useState } from "react";
|
|
43
|
+
import ReactEditor, { config } from "@mbs-dev/react-editor";
|
|
43
44
|
|
|
44
45
|
const MyPage = () => {
|
|
45
|
-
const [content, setContent] = useState(
|
|
46
|
+
const [content, setContent] = useState("");
|
|
46
47
|
|
|
47
|
-
const editorConfig = useMemo(() => config(
|
|
48
|
+
const editorConfig = useMemo(() => config({}), []);
|
|
48
49
|
|
|
49
50
|
return (
|
|
50
51
|
<ReactEditor
|
|
51
52
|
config={editorConfig}
|
|
52
53
|
value={content}
|
|
53
|
-
onChange={
|
|
54
|
+
onChange={setContent}
|
|
54
55
|
/>
|
|
55
56
|
);
|
|
56
57
|
};
|
|
@@ -58,82 +59,235 @@ const MyPage = () => {
|
|
|
58
59
|
|
|
59
60
|
---
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
# 🔧 Configuration (Full API)
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
## `config(params: ConfigParams)`
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
```ts
|
|
67
|
+
type ConfigParams = {
|
|
68
|
+
includeUploader?: boolean;
|
|
69
|
+
apiUrl?: string;
|
|
70
|
+
imageUrl?: string;
|
|
71
|
+
onDeleteImage?: (imageUrl: string) => void | Promise<void>;
|
|
72
|
+
};
|
|
73
|
+
```
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
### Example
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
const editorConfig = useMemo(
|
|
79
|
+
() =>
|
|
80
|
+
config({
|
|
81
|
+
includeUploader: true,
|
|
82
|
+
apiUrl: `${apiUrl}/upload`,
|
|
83
|
+
imageUrl: blogPostImgUrl,
|
|
84
|
+
onDeleteImage: handleDeleteImage,
|
|
85
|
+
}),
|
|
86
|
+
[]
|
|
87
|
+
);
|
|
88
|
+
```
|
|
68
89
|
|
|
69
90
|
---
|
|
70
91
|
|
|
71
|
-
|
|
92
|
+
# 🖼️ Image Uploading
|
|
93
|
+
|
|
94
|
+
The backend must return:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"success": true,
|
|
99
|
+
"data": {
|
|
100
|
+
"files": ["uploaded.webp"],
|
|
101
|
+
"messages": []
|
|
102
|
+
},
|
|
103
|
+
"msg": "Upload successful"
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Images get inserted automatically:
|
|
108
|
+
|
|
109
|
+
```html
|
|
110
|
+
<img src="https://domain.com/uploads/images/filename.webp" />
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
72
114
|
|
|
73
|
-
|
|
115
|
+
# 🗑️ NEW — Image Delete (Server Sync)
|
|
74
116
|
|
|
75
|
-
|
|
117
|
+
The editor detects removed `<img>` tags and calls your function:
|
|
76
118
|
|
|
77
|
-
|
|
119
|
+
### **TSX Implementation**
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
const handleDeleteImage = async (imageSrc: string) => {
|
|
123
|
+
const filename = imageSrc.split("/").pop();
|
|
124
|
+
if (!filename) return;
|
|
125
|
+
|
|
126
|
+
await axios.delete(`${apiUrl}/delete/${filename}`);
|
|
127
|
+
};
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
# 🧩 Full TSX Example (Add Blog)
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
const AddBlog = () => {
|
|
136
|
+
const [description, setDescription] = useState("");
|
|
137
|
+
|
|
138
|
+
const handleDeleteImage = async (imageSrc: string) => {
|
|
139
|
+
const filename = imageSrc.split("/").pop();
|
|
140
|
+
if (!filename) return;
|
|
141
|
+
await axios.delete(`${apiUrl}/delete/${filename}`);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const editorConfig = useMemo(
|
|
145
|
+
() =>
|
|
146
|
+
config({
|
|
147
|
+
includeUploader: true,
|
|
148
|
+
apiUrl: `${apiUrl}/upload`,
|
|
149
|
+
imageUrl: blogPostImgUrl,
|
|
150
|
+
onDeleteImage: handleDeleteImage,
|
|
151
|
+
}),
|
|
152
|
+
[]
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<ReactEditor
|
|
157
|
+
config={editorConfig}
|
|
158
|
+
value={description}
|
|
159
|
+
onChange={setDescription}
|
|
160
|
+
/>
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
# 🧩 Symfony API Platform Full Example
|
|
168
|
+
|
|
169
|
+
## **1. Upload base directory**
|
|
170
|
+
|
|
171
|
+
`config/services.yaml`
|
|
78
172
|
|
|
79
173
|
```yaml
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
controller: App\Controller\ImageUploadController
|
|
83
|
-
methods: [POST]
|
|
174
|
+
parameters:
|
|
175
|
+
blog_post_images: '%kernel.project_dir%/public/uploads/images_dir'
|
|
84
176
|
```
|
|
85
177
|
|
|
86
|
-
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## **2. Symfony Upload & Delete Service**
|
|
87
181
|
|
|
88
182
|
```php
|
|
89
|
-
|
|
183
|
+
<?php
|
|
90
184
|
|
|
91
|
-
|
|
185
|
+
final class UploaderService
|
|
92
186
|
{
|
|
93
|
-
|
|
94
|
-
|
|
187
|
+
public function __construct(
|
|
188
|
+
private readonly EntityManagerInterface $entityManager,
|
|
189
|
+
private readonly ParameterBagInterface $params
|
|
190
|
+
) {}
|
|
191
|
+
|
|
192
|
+
public function uploadBlogPostImages(array $uploadedFiles): array
|
|
193
|
+
{
|
|
194
|
+
if ($uploadedFiles === []) {
|
|
195
|
+
throw new BadRequestHttpException('Les fichiers sont requis.');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
$uploadedFileNames = [];
|
|
199
|
+
|
|
200
|
+
foreach ($uploadedFiles as $file) {
|
|
201
|
+
if (!$file instanceof UploadedFile) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
$image = new BlogPostImages();
|
|
206
|
+
$image->setImageFile($file);
|
|
207
|
+
|
|
208
|
+
$this->entityManager->persist($image);
|
|
209
|
+
$uploadedFileNames[] = $image->getImage();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if ($uploadedFileNames === []) {
|
|
213
|
+
throw new BadRequestHttpException('Aucun fichier valide n’a été fourni.');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
$this->entityManager->flush();
|
|
217
|
+
|
|
218
|
+
return $uploadedFileNames;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
public function deleteBlogPostImage(string $filename): void
|
|
222
|
+
{
|
|
223
|
+
$cleanName = basename($filename);
|
|
224
|
+
|
|
225
|
+
$imageEntity = $this->entityManager
|
|
226
|
+
->getRepository(BlogPostImages::class)
|
|
227
|
+
->findOneBy(['image' => $cleanName]);
|
|
228
|
+
|
|
229
|
+
if (!$imageEntity) {
|
|
230
|
+
throw new NotFoundHttpException("L'image demandée n'existe pas.");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
$fileDir = str_replace('\\', '/', $this->params->get('blog_post_images'));
|
|
234
|
+
$filePath = rtrim($fileDir, '/') . '/' . $cleanName;
|
|
235
|
+
|
|
236
|
+
if (is_file($filePath)) {
|
|
237
|
+
unlink($filePath);
|
|
238
|
+
}
|
|
95
239
|
|
|
96
|
-
|
|
97
|
-
$
|
|
98
|
-
$file->move($this->getParameter('kernel.project_dir').'/public/uploads/images', $newName);
|
|
99
|
-
$storedFiles[] = $newName;
|
|
240
|
+
$this->entityManager->remove($imageEntity);
|
|
241
|
+
$this->entityManager->flush();
|
|
100
242
|
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## **3. Delete endpoint**
|
|
249
|
+
|
|
250
|
+
```php
|
|
251
|
+
public function deleteImage(string $filename): JsonResponse
|
|
252
|
+
{
|
|
253
|
+
$this->uploaderService->deleteBlogPostImage($filename);
|
|
101
254
|
|
|
102
255
|
return new JsonResponse([
|
|
103
256
|
'success' => true,
|
|
104
|
-
'
|
|
105
|
-
'files' => $storedFiles,
|
|
106
|
-
'messages' => [],
|
|
107
|
-
],
|
|
108
|
-
'msg' => 'Upload successful'
|
|
257
|
+
'message' => "L'image a été supprimée avec succès.",
|
|
109
258
|
]);
|
|
110
259
|
}
|
|
111
260
|
```
|
|
112
261
|
|
|
113
|
-
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## **4. React Integration**
|
|
114
265
|
|
|
115
266
|
```ts
|
|
116
|
-
export const
|
|
117
|
-
|
|
267
|
+
export const blogPostImgUrl = `${apiUrl}/uploads/post`;
|
|
268
|
+
```
|
|
118
269
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
[]
|
|
126
|
-
);
|
|
270
|
+
```tsx
|
|
271
|
+
const handleDeleteImage = async (src: string) => {
|
|
272
|
+
const filename = src.split("/").pop();
|
|
273
|
+
if (filename)
|
|
274
|
+
await axios.delete(`${apiUrl}/post/delete/${filename}`);
|
|
275
|
+
};
|
|
127
276
|
```
|
|
128
277
|
|
|
129
|
-
|
|
278
|
+
---
|
|
130
279
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
280
|
+
# 📘 API Summary
|
|
281
|
+
|
|
282
|
+
| Feature | Supported |
|
|
283
|
+
|-------------------|-----------|
|
|
284
|
+
| Image upload | ✅ |
|
|
285
|
+
| Image delete sync | ✅ |
|
|
286
|
+
| TypeScript ready | ✅ |
|
|
287
|
+
| Config builder | ✅ |
|
|
134
288
|
|
|
135
289
|
---
|
|
136
290
|
|
|
137
|
-
|
|
291
|
+
# 📝 License
|
|
138
292
|
|
|
139
|
-
MIT —
|
|
293
|
+
MIT — free for commercial and private use.
|
package/dist/Editor.js
CHANGED
|
@@ -1,28 +1,50 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
catch (error) { e = { error: error }; }
|
|
10
|
-
finally {
|
|
11
|
-
try {
|
|
12
|
-
if (r && !r.done && (m = i["return"])) m.call(i);
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
13
8
|
}
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
return
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
17
12
|
};
|
|
18
|
-
var
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
13
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
14
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
15
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
16
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
17
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
18
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
19
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
23
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
24
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
25
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
26
|
+
function step(op) {
|
|
27
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
28
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
29
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
30
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
31
|
+
switch (op[0]) {
|
|
32
|
+
case 0: case 1: t = op; break;
|
|
33
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
34
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
35
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
36
|
+
default:
|
|
37
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
38
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
39
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
40
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
41
|
+
if (t[2]) _.ops.pop();
|
|
42
|
+
_.trys.pop(); continue;
|
|
43
|
+
}
|
|
44
|
+
op = body.call(thisArg, _);
|
|
45
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
46
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
24
47
|
}
|
|
25
|
-
return to.concat(ar || Array.prototype.slice.call(from));
|
|
26
48
|
};
|
|
27
49
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
28
50
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -48,42 +70,45 @@ var uploaderConfig = function (apiUrl, imageUrl) { return ({
|
|
|
48
70
|
return formdata;
|
|
49
71
|
},
|
|
50
72
|
isSuccess: function (e) {
|
|
73
|
+
var _a;
|
|
51
74
|
var fn = this.jodit;
|
|
52
|
-
if (e.data.files && e.data.files.length) {
|
|
75
|
+
if (((_a = e === null || e === void 0 ? void 0 : e.data) === null || _a === void 0 ? void 0 : _a.files) && e.data.files.length) {
|
|
53
76
|
var tagName_1 = 'img';
|
|
54
77
|
e.data.files.forEach(function (filename) {
|
|
55
78
|
var elm = fn.createInside.element(tagName_1);
|
|
56
|
-
|
|
79
|
+
var src = imageUrl ? "".concat(imageUrl, "/").concat(filename) : filename;
|
|
80
|
+
elm.setAttribute('src', src);
|
|
57
81
|
fn.s.insertImage(elm, null, fn.o.imageDefaultWidth);
|
|
58
82
|
});
|
|
59
83
|
}
|
|
60
|
-
return e.success;
|
|
84
|
+
return !!(e === null || e === void 0 ? void 0 : e.success);
|
|
61
85
|
},
|
|
62
86
|
getMessage: function (e) {
|
|
63
|
-
|
|
87
|
+
var _a;
|
|
88
|
+
return ((_a = e === null || e === void 0 ? void 0 : e.data) === null || _a === void 0 ? void 0 : _a.messages) && Array.isArray(e.data.messages)
|
|
64
89
|
? e.data.messages.join('')
|
|
65
90
|
: '';
|
|
66
91
|
},
|
|
67
92
|
process: function (resp) {
|
|
68
93
|
var files = [];
|
|
69
|
-
files.unshift(resp.data);
|
|
94
|
+
files.unshift(resp === null || resp === void 0 ? void 0 : resp.data);
|
|
70
95
|
return {
|
|
71
|
-
files: resp.data,
|
|
72
|
-
error: resp.msg,
|
|
73
|
-
msg: resp.msg,
|
|
96
|
+
files: resp === null || resp === void 0 ? void 0 : resp.data,
|
|
97
|
+
error: resp === null || resp === void 0 ? void 0 : resp.msg,
|
|
98
|
+
msg: resp === null || resp === void 0 ? void 0 : resp.msg,
|
|
74
99
|
};
|
|
75
100
|
},
|
|
76
101
|
error: function (e) {
|
|
77
102
|
this.j.e.fire('errorMessage', e.message, 'error', 4000);
|
|
78
103
|
},
|
|
79
104
|
defaultHandlerError: function (e) {
|
|
80
|
-
this.j.e.fire('errorMessage', e.message);
|
|
105
|
+
this.j.e.fire('errorMessage', (e === null || e === void 0 ? void 0 : e.message) || 'Upload error');
|
|
81
106
|
},
|
|
82
107
|
}); };
|
|
83
108
|
exports.uploaderConfig = uploaderConfig;
|
|
84
109
|
var config = function (_a) {
|
|
85
110
|
var _b = _a === void 0 ? {} : _a, includeUploader = _b.includeUploader, apiUrl = _b.apiUrl, imageUrl = _b.imageUrl, onDeleteImage = _b.onDeleteImage;
|
|
86
|
-
|
|
111
|
+
var base = {
|
|
87
112
|
readonly: false,
|
|
88
113
|
placeholder: 'Start typing...',
|
|
89
114
|
toolbarAdaptive: false,
|
|
@@ -127,40 +152,43 @@ var config = function (_a) {
|
|
|
127
152
|
'table',
|
|
128
153
|
'fullsize',
|
|
129
154
|
],
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
155
|
+
};
|
|
156
|
+
if (includeUploader) {
|
|
157
|
+
base.uploader = (0, exports.uploaderConfig)(apiUrl, imageUrl);
|
|
158
|
+
}
|
|
159
|
+
if (onDeleteImage) {
|
|
160
|
+
base.events = __assign(__assign({}, (base.events || {})), { afterInit: function (editor) {
|
|
161
|
+
var _this = this;
|
|
162
|
+
var extractImageSrcs = function (html) {
|
|
163
|
+
var container = document.createElement('div');
|
|
164
|
+
container.innerHTML = html || '';
|
|
165
|
+
var imgs = Array.from(container.getElementsByTagName('img'));
|
|
166
|
+
return new Set(imgs
|
|
167
|
+
.map(function (img) { return img.getAttribute('src') || ''; })
|
|
168
|
+
.filter(function (src) { return !!src; }));
|
|
169
|
+
};
|
|
170
|
+
var prevValue = editor.value || '';
|
|
171
|
+
var prevSrcs = extractImageSrcs(prevValue);
|
|
172
|
+
editor.events.on('change', function () { return __awaiter(_this, void 0, void 0, function () {
|
|
173
|
+
var currentValue, currentSrcs;
|
|
174
|
+
return __generator(this, function (_a) {
|
|
175
|
+
currentValue = editor.value || '';
|
|
176
|
+
currentSrcs = extractImageSrcs(currentValue);
|
|
177
|
+
prevSrcs.forEach(function (src) {
|
|
178
|
+
if (!currentSrcs.has(src)) {
|
|
179
|
+
if (!imageUrl || src.startsWith(imageUrl)) {
|
|
180
|
+
void onDeleteImage(src);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
151
183
|
});
|
|
184
|
+
prevValue = currentValue;
|
|
185
|
+
prevSrcs = currentSrcs;
|
|
186
|
+
return [2];
|
|
152
187
|
});
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
editor.events.on('beforeDestruct', function () {
|
|
158
|
-
observer.disconnect();
|
|
159
|
-
});
|
|
160
|
-
},
|
|
161
|
-
}
|
|
162
|
-
: undefined,
|
|
163
|
-
});
|
|
188
|
+
}); });
|
|
189
|
+
} });
|
|
190
|
+
}
|
|
191
|
+
return base;
|
|
164
192
|
};
|
|
165
193
|
exports.config = config;
|
|
166
194
|
exports.default = ReactEditor;
|
package/package.json
CHANGED
package/src/Editor.tsx
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
// Editor.tsx
|
|
2
1
|
import React from 'react';
|
|
3
2
|
import JoditEditor from 'jodit-react';
|
|
4
|
-
import { EditorProps
|
|
3
|
+
import { EditorProps } from './Editor.types';
|
|
5
4
|
|
|
6
5
|
const ReactEditor: React.FC<EditorProps> = ({ onChange, onBlur, value, config }) => {
|
|
7
6
|
return (
|
|
@@ -14,7 +13,14 @@ const ReactEditor: React.FC<EditorProps> = ({ onChange, onBlur, value, config })
|
|
|
14
13
|
);
|
|
15
14
|
};
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Uploader configuration for Jodit
|
|
18
|
+
* Handles image upload + insertion in the editor
|
|
19
|
+
*/
|
|
20
|
+
export const uploaderConfig = (
|
|
21
|
+
apiUrl?: string,
|
|
22
|
+
imageUrl?: string
|
|
23
|
+
) => ({
|
|
18
24
|
imagesExtensions: ['jpg', 'png', 'jpeg', 'gif', 'webp'],
|
|
19
25
|
filesVariableName(t: number): string {
|
|
20
26
|
return 'files[' + t + ']';
|
|
@@ -29,38 +35,39 @@ export const uploaderConfig = (apiUrl?: string, imageUrl?: string) => ({
|
|
|
29
35
|
isSuccess(this: any, e: any): boolean {
|
|
30
36
|
const fn = this.jodit;
|
|
31
37
|
|
|
32
|
-
if (e
|
|
38
|
+
if (e?.data?.files && e.data.files.length) {
|
|
33
39
|
const tagName = 'img';
|
|
34
40
|
|
|
35
41
|
e.data.files.forEach((filename: string) => {
|
|
36
42
|
const elm = fn.createInside.element(tagName);
|
|
37
|
-
|
|
43
|
+
const src = imageUrl ? `${imageUrl}/${filename}` : filename;
|
|
44
|
+
elm.setAttribute('src', src);
|
|
38
45
|
fn.s.insertImage(elm as HTMLImageElement, null, fn.o.imageDefaultWidth);
|
|
39
46
|
});
|
|
40
47
|
}
|
|
41
48
|
|
|
42
|
-
return e
|
|
49
|
+
return !!e?.success;
|
|
43
50
|
},
|
|
44
51
|
getMessage(e: any): string {
|
|
45
|
-
return e
|
|
52
|
+
return e?.data?.messages && Array.isArray(e.data.messages)
|
|
46
53
|
? e.data.messages.join('')
|
|
47
54
|
: '';
|
|
48
55
|
},
|
|
49
56
|
process(resp: any): { files: any[]; error: string; msg: string } {
|
|
50
57
|
const files: any[] = [];
|
|
51
|
-
files.unshift(resp
|
|
58
|
+
files.unshift(resp?.data);
|
|
52
59
|
|
|
53
60
|
return {
|
|
54
|
-
files: resp
|
|
55
|
-
error: resp
|
|
56
|
-
msg: resp
|
|
61
|
+
files: resp?.data,
|
|
62
|
+
error: resp?.msg,
|
|
63
|
+
msg: resp?.msg,
|
|
57
64
|
};
|
|
58
65
|
},
|
|
59
66
|
error(this: any, e: Error): void {
|
|
60
67
|
this.j.e.fire('errorMessage', e.message, 'error', 4000);
|
|
61
68
|
},
|
|
62
69
|
defaultHandlerError(this: any, e: any): void {
|
|
63
|
-
this.j.e.fire('errorMessage', e
|
|
70
|
+
this.j.e.fire('errorMessage', e?.message || 'Upload error');
|
|
64
71
|
},
|
|
65
72
|
});
|
|
66
73
|
|
|
@@ -68,105 +75,118 @@ type ConfigParams = {
|
|
|
68
75
|
includeUploader?: boolean;
|
|
69
76
|
apiUrl?: string;
|
|
70
77
|
imageUrl?: string;
|
|
71
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Called when an image is really removed from the editor content.
|
|
80
|
+
* You receive the image src URL and can call your API to delete it on server.
|
|
81
|
+
*/
|
|
82
|
+
onDeleteImage?: (imageUrl: string) => void | Promise<void>;
|
|
72
83
|
};
|
|
73
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Build Jodit config for ReactEditor
|
|
87
|
+
*/
|
|
74
88
|
export const config = ({
|
|
75
89
|
includeUploader,
|
|
76
90
|
apiUrl,
|
|
77
91
|
imageUrl,
|
|
78
92
|
onDeleteImage,
|
|
79
|
-
}: ConfigParams = {}) =>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
93
|
+
}: ConfigParams = {}) => {
|
|
94
|
+
const base: any = {
|
|
95
|
+
readonly: false,
|
|
96
|
+
placeholder: 'Start typing...',
|
|
97
|
+
toolbarAdaptive: false,
|
|
98
|
+
useSearch: false,
|
|
99
|
+
language: 'en',
|
|
100
|
+
allowResizeX: false,
|
|
101
|
+
allowResizeY: false,
|
|
102
|
+
height: 400,
|
|
103
|
+
enableDragAndDropFileToEditor: true,
|
|
104
|
+
showCharsCounter: true,
|
|
105
|
+
showWordsCounter: true,
|
|
106
|
+
showXPathInStatusbar: false,
|
|
107
|
+
|
|
108
|
+
buttons: [
|
|
109
|
+
'source',
|
|
110
|
+
'|',
|
|
111
|
+
'bold',
|
|
112
|
+
'italic',
|
|
113
|
+
'underline',
|
|
114
|
+
'|',
|
|
115
|
+
'ul',
|
|
116
|
+
'ol',
|
|
117
|
+
'|',
|
|
118
|
+
'image',
|
|
119
|
+
'|',
|
|
120
|
+
'video',
|
|
121
|
+
'|',
|
|
122
|
+
'link',
|
|
123
|
+
'|',
|
|
124
|
+
'undo',
|
|
125
|
+
'redo',
|
|
126
|
+
'|',
|
|
127
|
+
'hr',
|
|
128
|
+
'|',
|
|
129
|
+
'eraser',
|
|
130
|
+
'|',
|
|
131
|
+
'font',
|
|
132
|
+
'fontsize',
|
|
133
|
+
'paragraph',
|
|
134
|
+
'brush',
|
|
135
|
+
'|',
|
|
136
|
+
'table',
|
|
137
|
+
'fullsize',
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (includeUploader) {
|
|
142
|
+
base.uploader = uploaderConfig(apiUrl, imageUrl);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (onDeleteImage) {
|
|
146
|
+
base.events = {
|
|
147
|
+
...(base.events || {}),
|
|
148
|
+
/**
|
|
149
|
+
* We use the value diff to detect removed <img> src.
|
|
150
|
+
* This avoids false calls during upload (where DOM nodes can be replaced).
|
|
151
|
+
*/
|
|
130
152
|
afterInit(editor: any) {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
const extractImageSrcs = (html: string): Set<string> => {
|
|
154
|
+
const container = document.createElement('div');
|
|
155
|
+
container.innerHTML = html || '';
|
|
156
|
+
const imgs = Array.from(container.getElementsByTagName('img')) as HTMLImageElement[];
|
|
157
|
+
|
|
158
|
+
return new Set(
|
|
159
|
+
imgs
|
|
160
|
+
.map((img) => img.getAttribute('src') || '')
|
|
161
|
+
.filter((src) => !!src)
|
|
162
|
+
);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
let prevValue: string = editor.value || '';
|
|
166
|
+
let prevSrcs: Set<string> = extractImageSrcs(prevValue);
|
|
167
|
+
|
|
168
|
+
editor.events.on('change', async () => {
|
|
169
|
+
const currentValue: string = editor.value || '';
|
|
170
|
+
const currentSrcs = extractImageSrcs(currentValue);
|
|
171
|
+
|
|
172
|
+
// src present before, not present now -> deleted
|
|
173
|
+
prevSrcs.forEach((src) => {
|
|
174
|
+
if (!currentSrcs.has(src)) {
|
|
175
|
+
// If imageUrl is defined, you can filter to only your own assets
|
|
176
|
+
if (!imageUrl || src.startsWith(imageUrl)) {
|
|
177
|
+
void onDeleteImage(src);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
155
180
|
});
|
|
156
|
-
});
|
|
157
181
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
subtree: true,
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
// Cleanup when editor is destroyed
|
|
164
|
-
editor.events.on('beforeDestruct', () => {
|
|
165
|
-
observer.disconnect();
|
|
182
|
+
prevValue = currentValue;
|
|
183
|
+
prevSrcs = currentSrcs;
|
|
166
184
|
});
|
|
167
185
|
},
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return base;
|
|
190
|
+
};
|
|
171
191
|
|
|
172
192
|
export default ReactEditor;
|
package/types/Editor.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { EditorProps
|
|
2
|
+
import { EditorProps } from './Editor.types';
|
|
3
3
|
declare const ReactEditor: React.FC<EditorProps>;
|
|
4
4
|
export declare const uploaderConfig: (apiUrl?: string, imageUrl?: string) => {
|
|
5
5
|
imagesExtensions: string[];
|
|
@@ -23,42 +23,7 @@ type ConfigParams = {
|
|
|
23
23
|
includeUploader?: boolean;
|
|
24
24
|
apiUrl?: string;
|
|
25
25
|
imageUrl?: string;
|
|
26
|
-
onDeleteImage?:
|
|
27
|
-
};
|
|
28
|
-
export declare const config: ({ includeUploader, apiUrl, imageUrl, onDeleteImage, }?: ConfigParams) => {
|
|
29
|
-
readonly: boolean;
|
|
30
|
-
placeholder: string;
|
|
31
|
-
toolbarAdaptive: boolean;
|
|
32
|
-
useSearch: boolean;
|
|
33
|
-
language: string;
|
|
34
|
-
allowResizeX: boolean;
|
|
35
|
-
allowResizeY: boolean;
|
|
36
|
-
height: number;
|
|
37
|
-
enableDragAndDropFileToEditor: boolean;
|
|
38
|
-
showCharsCounter: boolean;
|
|
39
|
-
showWordsCounter: boolean;
|
|
40
|
-
showXPathInStatusbar: boolean;
|
|
41
|
-
buttons: string[];
|
|
42
|
-
uploader: {
|
|
43
|
-
imagesExtensions: string[];
|
|
44
|
-
filesVariableName(t: number): string;
|
|
45
|
-
url: string | undefined;
|
|
46
|
-
withCredentials: boolean;
|
|
47
|
-
format: string;
|
|
48
|
-
method: string;
|
|
49
|
-
prepareData(formdata: FormData): FormData;
|
|
50
|
-
isSuccess(this: any, e: any): boolean;
|
|
51
|
-
getMessage(e: any): string;
|
|
52
|
-
process(resp: any): {
|
|
53
|
-
files: any[];
|
|
54
|
-
error: string;
|
|
55
|
-
msg: string;
|
|
56
|
-
};
|
|
57
|
-
error(this: any, e: Error): void;
|
|
58
|
-
defaultHandlerError(this: any, e: any): void;
|
|
59
|
-
} | undefined;
|
|
60
|
-
events: {
|
|
61
|
-
afterInit(editor: any): void;
|
|
62
|
-
} | undefined;
|
|
26
|
+
onDeleteImage?: (imageUrl: string) => void | Promise<void>;
|
|
63
27
|
};
|
|
28
|
+
export declare const config: ({ includeUploader, apiUrl, imageUrl, onDeleteImage, }?: ConfigParams) => any;
|
|
64
29
|
export default ReactEditor;
|