@mbs-dev/react-editor 1.2.0 → 1.4.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/dist/Editor.js +66 -230
- package/package.json +1 -1
- package/src/Editor.tsx +75 -311
- package/src/Editor.types.ts +2 -3
- package/types/Editor.d.ts +8 -22
- package/types/Editor.types.d.ts +0 -1
package/dist/Editor.js
CHANGED
|
@@ -46,31 +46,6 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
|
46
46
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
|
-
var __read = (this && this.__read) || function (o, n) {
|
|
50
|
-
var m = typeof Symbol === "function" && o[Symbol.iterator];
|
|
51
|
-
if (!m) return o;
|
|
52
|
-
var i = m.call(o), r, ar = [], e;
|
|
53
|
-
try {
|
|
54
|
-
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
|
|
55
|
-
}
|
|
56
|
-
catch (error) { e = { error: error }; }
|
|
57
|
-
finally {
|
|
58
|
-
try {
|
|
59
|
-
if (r && !r.done && (m = i["return"])) m.call(i);
|
|
60
|
-
}
|
|
61
|
-
finally { if (e) throw e.error; }
|
|
62
|
-
}
|
|
63
|
-
return ar;
|
|
64
|
-
};
|
|
65
|
-
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
66
|
-
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
67
|
-
if (ar || !(i in from)) {
|
|
68
|
-
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
69
|
-
ar[i] = from[i];
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return to.concat(ar || Array.prototype.slice.call(from));
|
|
73
|
-
};
|
|
74
49
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
75
50
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
76
51
|
};
|
|
@@ -82,147 +57,20 @@ var ReactEditor = function (_a) {
|
|
|
82
57
|
var onChange = _a.onChange, onBlur = _a.onBlur, value = _a.value, config = _a.config;
|
|
83
58
|
return (react_1.default.createElement(jodit_react_1.default, { value: value, config: config, onBlur: onBlur, onChange: onChange }));
|
|
84
59
|
};
|
|
85
|
-
var
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
var
|
|
89
|
-
|
|
90
|
-
return /^https?:\/\//i.test(url) || /^\/\//.test(url);
|
|
91
|
-
};
|
|
92
|
-
var normalizeJoinUrl = function (base, path) {
|
|
93
|
-
if (!base)
|
|
94
|
-
return path;
|
|
95
|
-
if (!path)
|
|
96
|
-
return base;
|
|
97
|
-
if (isAbsoluteUrl(path))
|
|
98
|
-
return path;
|
|
99
|
-
return "".concat(base.replace(/\/+$/, ''), "/").concat(path.replace(/^\/+/, ''));
|
|
100
|
-
};
|
|
101
|
-
var getExtensionLower = function (filenameOrUrl) {
|
|
102
|
-
var clean = filenameOrUrl.split('?')[0].split('#')[0];
|
|
103
|
-
var lastDot = clean.lastIndexOf('.');
|
|
104
|
-
if (lastDot === -1)
|
|
105
|
-
return '';
|
|
106
|
-
return clean.slice(lastDot + 1).toLowerCase();
|
|
107
|
-
};
|
|
108
|
-
var escapeHtml = function (s) {
|
|
109
|
-
return s
|
|
110
|
-
.replace(/&/g, '&')
|
|
111
|
-
.replace(/</g, '<')
|
|
112
|
-
.replace(/>/g, '>')
|
|
113
|
-
.replace(/"/g, '"')
|
|
114
|
-
.replace(/'/g, ''');
|
|
115
|
-
};
|
|
116
|
-
var DEFAULT_IMAGE_EXTENSIONS = [
|
|
117
|
-
'jpg',
|
|
118
|
-
'jpeg',
|
|
119
|
-
'png',
|
|
120
|
-
'gif',
|
|
121
|
-
'webp',
|
|
122
|
-
'avif',
|
|
123
|
-
'svg',
|
|
124
|
-
];
|
|
125
|
-
var DEFAULT_FILE_LINK_EXTENSIONS = [
|
|
126
|
-
'pdf',
|
|
127
|
-
'doc',
|
|
128
|
-
'docx',
|
|
129
|
-
'xls',
|
|
130
|
-
'xlsx',
|
|
131
|
-
'ppt',
|
|
132
|
-
'pptx',
|
|
133
|
-
'csv',
|
|
134
|
-
'txt',
|
|
135
|
-
'7z',
|
|
136
|
-
];
|
|
137
|
-
var asJoditLike = function (v) {
|
|
138
|
-
if (!isRecord(v))
|
|
139
|
-
return null;
|
|
140
|
-
return v;
|
|
141
|
-
};
|
|
142
|
-
var extractUploadedFiles = function (resp) {
|
|
143
|
-
if (!isRecord(resp))
|
|
144
|
-
return [];
|
|
145
|
-
var r = resp;
|
|
146
|
-
var data = isRecord(r.data) ? r.data : undefined;
|
|
147
|
-
var files1 = data === null || data === void 0 ? void 0 : data.files;
|
|
148
|
-
if (Array.isArray(files1))
|
|
149
|
-
return files1.map(toStringSafe).filter(Boolean);
|
|
150
|
-
if (typeof files1 === 'string')
|
|
151
|
-
return [files1];
|
|
152
|
-
var files2 = r.files;
|
|
153
|
-
if (Array.isArray(files2))
|
|
154
|
-
return files2.map(toStringSafe).filter(Boolean);
|
|
155
|
-
if (typeof files2 === 'string')
|
|
156
|
-
return [files2];
|
|
157
|
-
if (isRecord(r.data) && isRecord(r.data.data)) {
|
|
158
|
-
var d2 = r.data.data;
|
|
159
|
-
var f = d2.files;
|
|
160
|
-
if (Array.isArray(f))
|
|
161
|
-
return f.map(toStringSafe).filter(Boolean);
|
|
162
|
-
if (typeof f === 'string')
|
|
163
|
-
return [f];
|
|
164
|
-
}
|
|
165
|
-
return [];
|
|
166
|
-
};
|
|
167
|
-
var extractMessages = function (resp) {
|
|
168
|
-
if (!isRecord(resp))
|
|
169
|
-
return [];
|
|
170
|
-
var r = resp;
|
|
171
|
-
var data = isRecord(r.data) ? r.data : undefined;
|
|
172
|
-
var messages = data === null || data === void 0 ? void 0 : data.messages;
|
|
173
|
-
if (Array.isArray(messages))
|
|
174
|
-
return messages.map(toStringSafe).filter(Boolean);
|
|
175
|
-
if (typeof messages === 'string')
|
|
176
|
-
return [messages];
|
|
177
|
-
var msg1 = data === null || data === void 0 ? void 0 : data.msg;
|
|
178
|
-
if (typeof msg1 === 'string' && msg1.trim())
|
|
179
|
-
return [msg1];
|
|
180
|
-
var msg2 = r.msg;
|
|
181
|
-
if (typeof msg2 === 'string' && msg2.trim())
|
|
182
|
-
return [msg2];
|
|
183
|
-
return [];
|
|
60
|
+
var isImageByExtension = function (filename, imageExts) {
|
|
61
|
+
var _a, _b;
|
|
62
|
+
var clean = (_b = (_a = (filename || '').split('?')[0]) === null || _a === void 0 ? void 0 : _a.split('#')[0]) !== null && _b !== void 0 ? _b : '';
|
|
63
|
+
var ext = (clean.split('.').pop() || '').toLowerCase();
|
|
64
|
+
return !!ext && imageExts.includes(ext);
|
|
184
65
|
};
|
|
185
|
-
var
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
var
|
|
189
|
-
|
|
190
|
-
return true;
|
|
191
|
-
if (isRecord(r.data) && r.data.success === true)
|
|
192
|
-
return true;
|
|
193
|
-
if (isRecord(r.data) && r.data.error === 0)
|
|
194
|
-
return true;
|
|
195
|
-
if (r.error === 0)
|
|
196
|
-
return true;
|
|
197
|
-
var files = extractUploadedFiles(resp);
|
|
198
|
-
return files.length > 0;
|
|
66
|
+
var getDisplayNameFromPath = function (filename) {
|
|
67
|
+
var _a, _b;
|
|
68
|
+
var clean = (_b = (_a = (filename || '').split('?')[0]) === null || _a === void 0 ? void 0 : _a.split('#')[0]) !== null && _b !== void 0 ? _b : '';
|
|
69
|
+
var last = clean.split('/').pop();
|
|
70
|
+
return last ? decodeURIComponent(last) : filename;
|
|
199
71
|
};
|
|
200
|
-
var
|
|
201
|
-
|
|
202
|
-
return { images: new Set(), files: new Set() };
|
|
203
|
-
}
|
|
204
|
-
var container = document.createElement('div');
|
|
205
|
-
container.innerHTML = html || '';
|
|
206
|
-
var images = new Set();
|
|
207
|
-
var files = new Set();
|
|
208
|
-
Array.from(container.getElementsByTagName('img')).forEach(function (img) {
|
|
209
|
-
var src = img.getAttribute('src') || '';
|
|
210
|
-
if (src)
|
|
211
|
-
images.add(src);
|
|
212
|
-
});
|
|
213
|
-
Array.from(container.getElementsByTagName('a')).forEach(function (a) {
|
|
214
|
-
var href = a.getAttribute('href') || '';
|
|
215
|
-
if (!href)
|
|
216
|
-
return;
|
|
217
|
-
var ext = getExtensionLower(href);
|
|
218
|
-
if (ext && fileLinkExtensions.includes(ext))
|
|
219
|
-
files.add(href);
|
|
220
|
-
});
|
|
221
|
-
return { images: images, files: files };
|
|
222
|
-
};
|
|
223
|
-
var uploaderConfig = function (apiUrl, assetBaseUrl) { return ({
|
|
224
|
-
insertImageAsBase64URI: false,
|
|
225
|
-
imagesExtensions: __spreadArray([], __read(DEFAULT_IMAGE_EXTENSIONS), false),
|
|
72
|
+
var uploaderConfig = function (apiUrl, imageUrl) { return ({
|
|
73
|
+
imagesExtensions: ['jpg', 'png', 'jpeg', 'gif', 'webp'],
|
|
226
74
|
filesVariableName: function (t) {
|
|
227
75
|
return 'files[' + t + ']';
|
|
228
76
|
},
|
|
@@ -233,69 +81,57 @@ var uploaderConfig = function (apiUrl, assetBaseUrl) { return ({
|
|
|
233
81
|
prepareData: function (formdata) {
|
|
234
82
|
return formdata;
|
|
235
83
|
},
|
|
236
|
-
isSuccess: function (
|
|
237
|
-
|
|
84
|
+
isSuccess: function (e) {
|
|
85
|
+
var _this = this;
|
|
86
|
+
var _a;
|
|
87
|
+
var fn = this.jodit;
|
|
88
|
+
if (((_a = e === null || e === void 0 ? void 0 : e.data) === null || _a === void 0 ? void 0 : _a.files) && e.data.files.length) {
|
|
89
|
+
e.data.files.forEach(function (filename) {
|
|
90
|
+
var src = imageUrl ? "".concat(imageUrl, "/").concat(filename) : filename;
|
|
91
|
+
if (isImageByExtension(filename, _this.imagesExtensions || ['jpg', 'png', 'jpeg', 'gif', 'webp'])) {
|
|
92
|
+
var tagName = 'img';
|
|
93
|
+
var elm = fn.createInside.element(tagName);
|
|
94
|
+
elm.setAttribute('src', src);
|
|
95
|
+
fn.s.insertImage(elm, null, fn.o.imageDefaultWidth);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
var tagName = 'a';
|
|
99
|
+
var elm = fn.createInside.element(tagName);
|
|
100
|
+
elm.setAttribute('href', src);
|
|
101
|
+
elm.setAttribute('target', '_blank');
|
|
102
|
+
elm.setAttribute('rel', 'noopener noreferrer');
|
|
103
|
+
elm.textContent = getDisplayNameFromPath(filename);
|
|
104
|
+
fn.s.insertNode(elm);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return !!(e === null || e === void 0 ? void 0 : e.success);
|
|
238
109
|
},
|
|
239
|
-
getMessage: function (
|
|
240
|
-
var
|
|
241
|
-
return
|
|
110
|
+
getMessage: function (e) {
|
|
111
|
+
var _a;
|
|
112
|
+
return ((_a = e === null || e === void 0 ? void 0 : e.data) === null || _a === void 0 ? void 0 : _a.messages) && Array.isArray(e.data.messages)
|
|
113
|
+
? e.data.messages.join('')
|
|
114
|
+
: '';
|
|
242
115
|
},
|
|
243
116
|
process: function (resp) {
|
|
244
|
-
var
|
|
245
|
-
|
|
246
|
-
if (!assetBaseUrl)
|
|
247
|
-
return filenameOrUrl;
|
|
248
|
-
return normalizeJoinUrl(assetBaseUrl, filenameOrUrl);
|
|
249
|
-
});
|
|
250
|
-
var messages = extractMessages(resp);
|
|
117
|
+
var files = [];
|
|
118
|
+
files.unshift(resp === null || resp === void 0 ? void 0 : resp.data);
|
|
251
119
|
return {
|
|
252
|
-
files:
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
messages: messages,
|
|
256
|
-
msg: messages.join(' '),
|
|
120
|
+
files: resp === null || resp === void 0 ? void 0 : resp.data,
|
|
121
|
+
error: resp === null || resp === void 0 ? void 0 : resp.msg,
|
|
122
|
+
msg: resp === null || resp === void 0 ? void 0 : resp.msg,
|
|
257
123
|
};
|
|
258
124
|
},
|
|
259
|
-
defaultHandlerSuccess: function (data) {
|
|
260
|
-
var jodit = asJoditLike(this.jodit);
|
|
261
|
-
if (!jodit)
|
|
262
|
-
return;
|
|
263
|
-
var selection = jodit.s;
|
|
264
|
-
var creator = jodit.createInside;
|
|
265
|
-
var files = isRecord(data) && Array.isArray(data.files)
|
|
266
|
-
? data.files.map(toStringSafe).filter(Boolean)
|
|
267
|
-
: [];
|
|
268
|
-
files.forEach(function (url) {
|
|
269
|
-
var _a, _b, _c, _d;
|
|
270
|
-
var ext = getExtensionLower(url);
|
|
271
|
-
var isImage = DEFAULT_IMAGE_EXTENSIONS.includes(ext);
|
|
272
|
-
if (isImage) {
|
|
273
|
-
if ((creator === null || creator === void 0 ? void 0 : creator.element) && (selection === null || selection === void 0 ? void 0 : selection.insertImage)) {
|
|
274
|
-
var img = creator.element('img');
|
|
275
|
-
img.setAttribute('src', url);
|
|
276
|
-
selection.insertImage(img, null, (_a = jodit.o) === null || _a === void 0 ? void 0 : _a.imageDefaultWidth);
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
(_b = selection === null || selection === void 0 ? void 0 : selection.insertHTML) === null || _b === void 0 ? void 0 : _b.call(selection, "<img src=\"".concat(escapeHtml(url), "\" alt=\"\" />"));
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
var label = escapeHtml((_c = url.split('/').pop()) !== null && _c !== void 0 ? _c : url);
|
|
283
|
-
var linkHtml = "<a href=\"".concat(escapeHtml(url), "\" target=\"_blank\" rel=\"noopener noreferrer\">").concat(label, "</a>");
|
|
284
|
-
(_d = selection === null || selection === void 0 ? void 0 : selection.insertHTML) === null || _d === void 0 ? void 0 : _d.call(selection, linkHtml);
|
|
285
|
-
});
|
|
286
|
-
},
|
|
287
125
|
error: function (e) {
|
|
288
|
-
|
|
289
|
-
(_c = (_b = (_a = this.j) === null || _a === void 0 ? void 0 : _a.e) === null || _b === void 0 ? void 0 : _b.fire) === null || _c === void 0 ? void 0 : _c.call(_b, 'errorMessage', e.message, 'error', 4000);
|
|
126
|
+
this.j.e.fire('errorMessage', e.message, 'error', 4000);
|
|
290
127
|
},
|
|
291
128
|
defaultHandlerError: function (e) {
|
|
292
|
-
|
|
293
|
-
(_c = (_b = (_a = this.j) === null || _a === void 0 ? void 0 : _a.e) === null || _b === void 0 ? void 0 : _b.fire) === null || _c === void 0 ? void 0 : _c.call(_b, 'errorMessage', (e === null || e === void 0 ? void 0 : e.message) || 'Upload error');
|
|
129
|
+
this.j.e.fire('errorMessage', (e === null || e === void 0 ? void 0 : e.message) || 'Upload error');
|
|
294
130
|
},
|
|
295
131
|
}); };
|
|
296
132
|
exports.uploaderConfig = uploaderConfig;
|
|
297
133
|
var config = function (_a) {
|
|
298
|
-
var _b = _a === void 0 ? {} : _a, includeUploader = _b.includeUploader, apiUrl = _b.apiUrl, imageUrl = _b.imageUrl, onDeleteImage = _b.onDeleteImage
|
|
134
|
+
var _b = _a === void 0 ? {} : _a, includeUploader = _b.includeUploader, apiUrl = _b.apiUrl, imageUrl = _b.imageUrl, onDeleteImage = _b.onDeleteImage;
|
|
299
135
|
var base = {
|
|
300
136
|
readonly: false,
|
|
301
137
|
placeholder: 'Start typing...',
|
|
@@ -345,33 +181,33 @@ var config = function (_a) {
|
|
|
345
181
|
if (includeUploader) {
|
|
346
182
|
base.uploader = (0, exports.uploaderConfig)(apiUrl, imageUrl);
|
|
347
183
|
}
|
|
348
|
-
|
|
349
|
-
if (hasDelete) {
|
|
184
|
+
if (onDeleteImage) {
|
|
350
185
|
base.events = __assign(__assign({}, (base.events || {})), { afterInit: function (editor) {
|
|
351
186
|
var _this = this;
|
|
187
|
+
var extractImageSrcs = function (html) {
|
|
188
|
+
var container = document.createElement('div');
|
|
189
|
+
container.innerHTML = html || '';
|
|
190
|
+
var imgs = Array.from(container.getElementsByTagName('img'));
|
|
191
|
+
return new Set(imgs
|
|
192
|
+
.map(function (img) { return img.getAttribute('src') || ''; })
|
|
193
|
+
.filter(function (src) { return !!src; }));
|
|
194
|
+
};
|
|
352
195
|
var prevValue = editor.value || '';
|
|
353
|
-
var
|
|
196
|
+
var prevSrcs = extractImageSrcs(prevValue);
|
|
354
197
|
editor.events.on('change', function () { return __awaiter(_this, void 0, void 0, function () {
|
|
355
|
-
var currentValue,
|
|
198
|
+
var currentValue, currentSrcs;
|
|
356
199
|
return __generator(this, function (_a) {
|
|
357
200
|
currentValue = editor.value || '';
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if (!
|
|
201
|
+
currentSrcs = extractImageSrcs(currentValue);
|
|
202
|
+
prevSrcs.forEach(function (src) {
|
|
203
|
+
if (!currentSrcs.has(src)) {
|
|
361
204
|
if (!imageUrl || src.startsWith(imageUrl)) {
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
|
-
prevAssets.files.forEach(function (href) {
|
|
367
|
-
if (!currentAssets.files.has(href)) {
|
|
368
|
-
if (!imageUrl || href.startsWith(imageUrl)) {
|
|
369
|
-
onDeleteFile === null || onDeleteFile === void 0 ? void 0 : onDeleteFile(href);
|
|
205
|
+
void onDeleteImage(src);
|
|
370
206
|
}
|
|
371
207
|
}
|
|
372
208
|
});
|
|
373
209
|
prevValue = currentValue;
|
|
374
|
-
|
|
210
|
+
prevSrcs = currentSrcs;
|
|
375
211
|
return [2];
|
|
376
212
|
});
|
|
377
213
|
}); });
|
package/package.json
CHANGED
package/src/Editor.tsx
CHANGED
|
@@ -2,9 +2,6 @@ import React from 'react';
|
|
|
2
2
|
import JoditEditor from 'jodit-react';
|
|
3
3
|
import { EditorProps } from './Editor.types';
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Keep the component unchanged
|
|
7
|
-
*/
|
|
8
5
|
const ReactEditor: React.FC<EditorProps> = ({ onChange, onBlur, value, config }) => {
|
|
9
6
|
return (
|
|
10
7
|
<JoditEditor
|
|
@@ -16,303 +13,84 @@ const ReactEditor: React.FC<EditorProps> = ({ onChange, onBlur, value, config })
|
|
|
16
13
|
);
|
|
17
14
|
};
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const toStringSafe = (v: unknown): string => (typeof v === 'string' ? v : '');
|
|
24
|
-
|
|
25
|
-
const isAbsoluteUrl = (url: string): boolean =>
|
|
26
|
-
/^https?:\/\//i.test(url) || /^\/\//.test(url);
|
|
27
|
-
|
|
28
|
-
const normalizeJoinUrl = (base: string, path: string): string => {
|
|
29
|
-
if (!base) return path;
|
|
30
|
-
if (!path) return base;
|
|
31
|
-
if (isAbsoluteUrl(path)) return path;
|
|
32
|
-
return `${base.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const getExtensionLower = (filenameOrUrl: string): string => {
|
|
36
|
-
const clean = filenameOrUrl.split('?')[0].split('#')[0];
|
|
37
|
-
const lastDot = clean.lastIndexOf('.');
|
|
38
|
-
if (lastDot === -1) return '';
|
|
39
|
-
return clean.slice(lastDot + 1).toLowerCase();
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const escapeHtml = (s: string): string =>
|
|
43
|
-
s
|
|
44
|
-
.replace(/&/g, '&')
|
|
45
|
-
.replace(/</g, '<')
|
|
46
|
-
.replace(/>/g, '>')
|
|
47
|
-
.replace(/"/g, '"')
|
|
48
|
-
.replace(/'/g, ''');
|
|
49
|
-
|
|
50
|
-
const DEFAULT_IMAGE_EXTENSIONS: readonly string[] = [
|
|
51
|
-
'jpg',
|
|
52
|
-
'jpeg',
|
|
53
|
-
'png',
|
|
54
|
-
'gif',
|
|
55
|
-
'webp',
|
|
56
|
-
'avif',
|
|
57
|
-
'svg',
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
const DEFAULT_FILE_LINK_EXTENSIONS: readonly string[] = [
|
|
61
|
-
'pdf',
|
|
62
|
-
'doc',
|
|
63
|
-
'docx',
|
|
64
|
-
'xls',
|
|
65
|
-
'xlsx',
|
|
66
|
-
'ppt',
|
|
67
|
-
'pptx',
|
|
68
|
-
'csv',
|
|
69
|
-
'txt',
|
|
70
|
-
'7z',
|
|
71
|
-
];
|
|
72
|
-
|
|
73
|
-
type JoditSelectionLike = {
|
|
74
|
-
insertImage?: (img: HTMLImageElement, arg2?: unknown, width?: unknown) => void;
|
|
75
|
-
insertHTML?: (html: string) => void;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
type JoditCreateInsideLike = {
|
|
79
|
-
element: (tag: string) => HTMLElement;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
type JoditLike = {
|
|
83
|
-
createInside?: JoditCreateInsideLike;
|
|
84
|
-
s?: JoditSelectionLike;
|
|
85
|
-
o?: {
|
|
86
|
-
imageDefaultWidth?: unknown;
|
|
87
|
-
};
|
|
88
|
-
events?: {
|
|
89
|
-
on?: (eventName: string, cb: () => void) => void;
|
|
90
|
-
};
|
|
91
|
-
value?: string;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
type UploaderThis = {
|
|
95
|
-
jodit?: unknown;
|
|
96
|
-
j?: {
|
|
97
|
-
e?: {
|
|
98
|
-
fire?: (...args: unknown[]) => void;
|
|
99
|
-
};
|
|
100
|
-
};
|
|
16
|
+
const isImageByExtension = (filename: string, imageExts: string[]): boolean => {
|
|
17
|
+
const clean = (filename || '').split('?')[0]?.split('#')[0] ?? '';
|
|
18
|
+
const ext = (clean.split('.').pop() || '').toLowerCase();
|
|
19
|
+
return !!ext && imageExts.includes(ext);
|
|
101
20
|
};
|
|
102
21
|
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
type UploadResponseShape = {
|
|
109
|
-
success?: unknown;
|
|
110
|
-
msg?: unknown;
|
|
111
|
-
data?: {
|
|
112
|
-
files?: unknown;
|
|
113
|
-
messages?: unknown;
|
|
114
|
-
error?: unknown;
|
|
115
|
-
msg?: unknown;
|
|
116
|
-
};
|
|
117
|
-
error?: unknown;
|
|
118
|
-
files?: unknown;
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const extractUploadedFiles = (resp: unknown): string[] => {
|
|
122
|
-
if (!isRecord(resp)) return [];
|
|
123
|
-
const r = resp as UploadResponseShape;
|
|
124
|
-
|
|
125
|
-
// Common: axios => { data: <json> }, but here we already receive <json>
|
|
126
|
-
const data = isRecord(r.data) ? r.data : undefined;
|
|
127
|
-
|
|
128
|
-
const files1 = data?.files;
|
|
129
|
-
if (Array.isArray(files1)) return files1.map(toStringSafe).filter(Boolean);
|
|
130
|
-
if (typeof files1 === 'string') return [files1];
|
|
131
|
-
|
|
132
|
-
const files2 = r.files;
|
|
133
|
-
if (Array.isArray(files2)) return files2.map(toStringSafe).filter(Boolean);
|
|
134
|
-
if (typeof files2 === 'string') return [files2];
|
|
135
|
-
|
|
136
|
-
// Sometimes: resp = { data: { data: { files: [...] } } }
|
|
137
|
-
if (isRecord(r.data) && isRecord((r.data as UnknownRecord).data)) {
|
|
138
|
-
const d2 = (r.data as UnknownRecord).data as UnknownRecord;
|
|
139
|
-
const f = d2.files;
|
|
140
|
-
if (Array.isArray(f)) return f.map(toStringSafe).filter(Boolean);
|
|
141
|
-
if (typeof f === 'string') return [f];
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return [];
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
const extractMessages = (resp: unknown): string[] => {
|
|
148
|
-
if (!isRecord(resp)) return [];
|
|
149
|
-
const r = resp as UploadResponseShape;
|
|
150
|
-
|
|
151
|
-
const data = isRecord(r.data) ? r.data : undefined;
|
|
152
|
-
|
|
153
|
-
const messages = data?.messages;
|
|
154
|
-
if (Array.isArray(messages)) return messages.map(toStringSafe).filter(Boolean);
|
|
155
|
-
if (typeof messages === 'string') return [messages];
|
|
156
|
-
|
|
157
|
-
const msg1 = data?.msg;
|
|
158
|
-
if (typeof msg1 === 'string' && msg1.trim()) return [msg1];
|
|
159
|
-
|
|
160
|
-
const msg2 = r.msg;
|
|
161
|
-
if (typeof msg2 === 'string' && msg2.trim()) return [msg2];
|
|
162
|
-
|
|
163
|
-
return [];
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const isSuccessResponse = (resp: unknown): boolean => {
|
|
167
|
-
if (!isRecord(resp)) return false;
|
|
168
|
-
const r = resp as UploadResponseShape;
|
|
169
|
-
|
|
170
|
-
// success flag (if present)
|
|
171
|
-
if (r.success === true) return true;
|
|
172
|
-
if (isRecord(r.data) && (r.data as UnknownRecord).success === true) return true;
|
|
173
|
-
|
|
174
|
-
// Symfony-style: data.error === 0
|
|
175
|
-
if (isRecord(r.data) && (r.data as UnknownRecord).error === 0) return true;
|
|
176
|
-
if (r.error === 0) return true;
|
|
177
|
-
|
|
178
|
-
// fallback: if files exist, treat as success
|
|
179
|
-
const files = extractUploadedFiles(resp);
|
|
180
|
-
return files.length > 0;
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
const extractAssetsFromHtml = (
|
|
184
|
-
html: string,
|
|
185
|
-
fileLinkExtensions: readonly string[]
|
|
186
|
-
): { images: Set<string>; files: Set<string> } => {
|
|
187
|
-
if (typeof document === 'undefined') {
|
|
188
|
-
return { images: new Set<string>(), files: new Set<string>() };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const container = document.createElement('div');
|
|
192
|
-
container.innerHTML = html || '';
|
|
193
|
-
|
|
194
|
-
const images = new Set<string>();
|
|
195
|
-
const files = new Set<string>();
|
|
196
|
-
|
|
197
|
-
Array.from(container.getElementsByTagName('img')).forEach((img) => {
|
|
198
|
-
const src = img.getAttribute('src') || '';
|
|
199
|
-
if (src) images.add(src);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
Array.from(container.getElementsByTagName('a')).forEach((a) => {
|
|
203
|
-
const href = a.getAttribute('href') || '';
|
|
204
|
-
if (!href) return;
|
|
205
|
-
|
|
206
|
-
const ext = getExtensionLower(href);
|
|
207
|
-
if (ext && fileLinkExtensions.includes(ext)) files.add(href);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
return { images, files };
|
|
22
|
+
const getDisplayNameFromPath = (filename: string): string => {
|
|
23
|
+
const clean = (filename || '').split('?')[0]?.split('#')[0] ?? '';
|
|
24
|
+
const last = clean.split('/').pop();
|
|
25
|
+
return last ? decodeURIComponent(last) : filename;
|
|
211
26
|
};
|
|
212
27
|
|
|
213
28
|
/**
|
|
214
29
|
* Uploader configuration for Jodit
|
|
215
|
-
* Handles image
|
|
30
|
+
* Handles image upload + insertion in the editor
|
|
216
31
|
*/
|
|
217
|
-
export const uploaderConfig = (
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
32
|
+
export const uploaderConfig = (
|
|
33
|
+
apiUrl?: string,
|
|
34
|
+
imageUrl?: string
|
|
35
|
+
) => ({
|
|
36
|
+
imagesExtensions: ['jpg', 'png', 'jpeg', 'gif', 'webp'],
|
|
222
37
|
filesVariableName(t: number): string {
|
|
223
38
|
return 'files[' + t + ']';
|
|
224
39
|
},
|
|
225
|
-
|
|
226
40
|
url: apiUrl,
|
|
227
41
|
withCredentials: false,
|
|
228
42
|
format: 'json',
|
|
229
43
|
method: 'POST',
|
|
230
|
-
|
|
231
44
|
prepareData(formdata: FormData): FormData {
|
|
232
45
|
return formdata;
|
|
233
46
|
},
|
|
47
|
+
isSuccess(this: any, e: any): boolean {
|
|
48
|
+
const fn = this.jodit;
|
|
49
|
+
|
|
50
|
+
if (e?.data?.files && e.data.files.length) {
|
|
51
|
+
e.data.files.forEach((filename: string) => {
|
|
52
|
+
const src = imageUrl ? `${imageUrl}/${filename}` : filename;
|
|
53
|
+
|
|
54
|
+
// ✅ If it's an image => insert <img>, otherwise insert <a href="...">
|
|
55
|
+
if (isImageByExtension(filename, this.imagesExtensions || ['jpg', 'png', 'jpeg', 'gif', 'webp'])) {
|
|
56
|
+
const tagName = 'img';
|
|
57
|
+
const elm = fn.createInside.element(tagName);
|
|
58
|
+
elm.setAttribute('src', src);
|
|
59
|
+
fn.s.insertImage(elm as HTMLImageElement, null, fn.o.imageDefaultWidth);
|
|
60
|
+
} else {
|
|
61
|
+
const tagName = 'a';
|
|
62
|
+
const elm = fn.createInside.element(tagName);
|
|
63
|
+
elm.setAttribute('href', src);
|
|
64
|
+
elm.setAttribute('target', '_blank');
|
|
65
|
+
elm.setAttribute('rel', 'noopener noreferrer');
|
|
66
|
+
elm.textContent = getDisplayNameFromPath(filename);
|
|
67
|
+
fn.s.insertNode(elm);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
234
71
|
|
|
235
|
-
|
|
236
|
-
* ✅ Robust success detection (supports: success=true OR data.error=0 OR files exist)
|
|
237
|
-
*/
|
|
238
|
-
isSuccess(this: unknown, resp: unknown): boolean {
|
|
239
|
-
return isSuccessResponse(resp);
|
|
72
|
+
return !!e?.success;
|
|
240
73
|
},
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
74
|
+
getMessage(e: any): string {
|
|
75
|
+
return e?.data?.messages && Array.isArray(e.data.messages)
|
|
76
|
+
? e.data.messages.join('')
|
|
77
|
+
: '';
|
|
245
78
|
},
|
|
79
|
+
process(resp: any): { files: any[]; error: string; msg: string } {
|
|
80
|
+
const files: any[] = [];
|
|
81
|
+
files.unshift(resp?.data);
|
|
246
82
|
|
|
247
|
-
/**
|
|
248
|
-
* ✅ Normalize backend response to the shape Jodit expects.
|
|
249
|
-
* Convert to absolute URLs (so editor can display immediately).
|
|
250
|
-
*/
|
|
251
|
-
process(resp: unknown): { files: string[]; path: string; baseurl: string; messages?: string[]; msg?: string } {
|
|
252
|
-
const rawFiles = extractUploadedFiles(resp);
|
|
253
|
-
|
|
254
|
-
const files = rawFiles.map((filenameOrUrl) => {
|
|
255
|
-
if (!assetBaseUrl) return filenameOrUrl;
|
|
256
|
-
return normalizeJoinUrl(assetBaseUrl, filenameOrUrl);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
const messages = extractMessages(resp);
|
|
260
83
|
return {
|
|
261
|
-
files,
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
messages,
|
|
265
|
-
msg: messages.join(' '),
|
|
84
|
+
files: resp?.data,
|
|
85
|
+
error: resp?.msg,
|
|
86
|
+
msg: resp?.msg,
|
|
266
87
|
};
|
|
267
88
|
},
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* ✅ Insert uploaded assets:
|
|
271
|
-
* - images => <img>
|
|
272
|
-
* - other files => <a href="...">filename</a>
|
|
273
|
-
*/
|
|
274
|
-
defaultHandlerSuccess(this: UploaderThis, data: unknown): void {
|
|
275
|
-
const jodit = asJoditLike(this.jodit);
|
|
276
|
-
if (!jodit) return;
|
|
277
|
-
|
|
278
|
-
const selection = jodit.s;
|
|
279
|
-
const creator = jodit.createInside;
|
|
280
|
-
|
|
281
|
-
const files = isRecord(data) && Array.isArray((data as UnknownRecord).files)
|
|
282
|
-
? ((data as UnknownRecord).files as unknown[]).map(toStringSafe).filter(Boolean)
|
|
283
|
-
: [];
|
|
284
|
-
|
|
285
|
-
files.forEach((url) => {
|
|
286
|
-
const ext = getExtensionLower(url);
|
|
287
|
-
const isImage = DEFAULT_IMAGE_EXTENSIONS.includes(ext);
|
|
288
|
-
|
|
289
|
-
if (isImage) {
|
|
290
|
-
// Prefer insertImage if available
|
|
291
|
-
if (creator?.element && selection?.insertImage) {
|
|
292
|
-
const img = creator.element('img') as HTMLImageElement;
|
|
293
|
-
img.setAttribute('src', url);
|
|
294
|
-
selection.insertImage(img, null, jodit.o?.imageDefaultWidth);
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Fallback
|
|
299
|
-
selection?.insertHTML?.(`<img src="${escapeHtml(url)}" alt="" />`);
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// File link insertion
|
|
304
|
-
const label = escapeHtml(url.split('/').pop() ?? url);
|
|
305
|
-
const linkHtml = `<a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer">${label}</a>`;
|
|
306
|
-
selection?.insertHTML?.(linkHtml);
|
|
307
|
-
});
|
|
308
|
-
},
|
|
309
|
-
|
|
310
89
|
error(this: any, e: Error): void {
|
|
311
|
-
this.j
|
|
90
|
+
this.j.e.fire('errorMessage', e.message, 'error', 4000);
|
|
312
91
|
},
|
|
313
|
-
|
|
314
92
|
defaultHandlerError(this: any, e: any): void {
|
|
315
|
-
this.j
|
|
93
|
+
this.j.e.fire('errorMessage', e?.message || 'Upload error');
|
|
316
94
|
},
|
|
317
95
|
});
|
|
318
96
|
|
|
@@ -320,22 +98,11 @@ type ConfigParams = {
|
|
|
320
98
|
includeUploader?: boolean;
|
|
321
99
|
apiUrl?: string;
|
|
322
100
|
imageUrl?: string;
|
|
323
|
-
|
|
324
101
|
/**
|
|
325
102
|
* Called when an image is really removed from the editor content.
|
|
326
103
|
* You receive the image src URL and can call your API to delete it on server.
|
|
327
104
|
*/
|
|
328
105
|
onDeleteImage?: (imageUrl: string) => void | Promise<void>;
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* ✅ Called when a tracked file link (<a href="...">) is removed.
|
|
332
|
-
*/
|
|
333
|
-
onDeleteFile?: (fileHref: string) => void | Promise<void>;
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* ✅ Which file extensions to track for delete-sync when removed from content.
|
|
337
|
-
*/
|
|
338
|
-
fileLinkExtensions?: readonly string[];
|
|
339
106
|
};
|
|
340
107
|
|
|
341
108
|
/**
|
|
@@ -346,8 +113,6 @@ export const config = ({
|
|
|
346
113
|
apiUrl,
|
|
347
114
|
imageUrl,
|
|
348
115
|
onDeleteImage,
|
|
349
|
-
onDeleteFile,
|
|
350
|
-
fileLinkExtensions = DEFAULT_FILE_LINK_EXTENSIONS,
|
|
351
116
|
}: ConfigParams = {}) => {
|
|
352
117
|
const base: any = {
|
|
353
118
|
readonly: false,
|
|
@@ -374,7 +139,7 @@ export const config = ({
|
|
|
374
139
|
'ol',
|
|
375
140
|
'|',
|
|
376
141
|
'image',
|
|
377
|
-
'file', // ✅ added
|
|
142
|
+
'file', // ✅ added
|
|
378
143
|
'|',
|
|
379
144
|
'video',
|
|
380
145
|
'|',
|
|
@@ -398,49 +163,48 @@ export const config = ({
|
|
|
398
163
|
};
|
|
399
164
|
|
|
400
165
|
if (includeUploader) {
|
|
401
|
-
// ✅ Use imageUrl as base for BOTH images + files (no new required props)
|
|
402
166
|
base.uploader = uploaderConfig(apiUrl, imageUrl);
|
|
403
167
|
}
|
|
404
168
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (hasDelete) {
|
|
169
|
+
if (onDeleteImage) {
|
|
408
170
|
base.events = {
|
|
409
171
|
...(base.events || {}),
|
|
410
|
-
|
|
411
172
|
/**
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
* - removed <a href="..."> (matching fileLinkExtensions) => onDeleteFile
|
|
173
|
+
* We use the value diff to detect removed <img> src.
|
|
174
|
+
* This avoids false calls during upload (where DOM nodes can be replaced).
|
|
415
175
|
*/
|
|
416
176
|
afterInit(editor: any) {
|
|
177
|
+
const extractImageSrcs = (html: string): Set<string> => {
|
|
178
|
+
const container = document.createElement('div');
|
|
179
|
+
container.innerHTML = html || '';
|
|
180
|
+
const imgs = Array.from(container.getElementsByTagName('img')) as HTMLImageElement[];
|
|
181
|
+
|
|
182
|
+
return new Set(
|
|
183
|
+
imgs
|
|
184
|
+
.map((img) => img.getAttribute('src') || '')
|
|
185
|
+
.filter((src) => !!src)
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
|
|
417
189
|
let prevValue: string = editor.value || '';
|
|
418
|
-
let
|
|
190
|
+
let prevSrcs: Set<string> = extractImageSrcs(prevValue);
|
|
419
191
|
|
|
420
192
|
editor.events.on('change', async () => {
|
|
421
193
|
const currentValue: string = editor.value || '';
|
|
422
|
-
const
|
|
194
|
+
const currentSrcs = extractImageSrcs(currentValue);
|
|
423
195
|
|
|
424
|
-
//
|
|
425
|
-
|
|
426
|
-
if (!
|
|
196
|
+
// src present before, not present now -> deleted
|
|
197
|
+
prevSrcs.forEach((src) => {
|
|
198
|
+
if (!currentSrcs.has(src)) {
|
|
199
|
+
// If imageUrl is defined, you can filter to only your own assets
|
|
427
200
|
if (!imageUrl || src.startsWith(imageUrl)) {
|
|
428
|
-
onDeleteImage
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
// Deleted files
|
|
434
|
-
prevAssets.files.forEach((href) => {
|
|
435
|
-
if (!currentAssets.files.has(href)) {
|
|
436
|
-
if (!imageUrl || href.startsWith(imageUrl)) {
|
|
437
|
-
onDeleteFile?.(href);
|
|
201
|
+
void onDeleteImage(src);
|
|
438
202
|
}
|
|
439
203
|
}
|
|
440
204
|
});
|
|
441
205
|
|
|
442
206
|
prevValue = currentValue;
|
|
443
|
-
|
|
207
|
+
prevSrcs = currentSrcs;
|
|
444
208
|
});
|
|
445
209
|
},
|
|
446
210
|
};
|
package/src/Editor.types.ts
CHANGED
|
@@ -5,9 +5,8 @@ export interface EditorProps {
|
|
|
5
5
|
useUploadImage?: boolean;
|
|
6
6
|
apiUrl?: string;
|
|
7
7
|
imageUrl?: string;
|
|
8
|
-
config?: any
|
|
8
|
+
config?: any
|
|
9
9
|
// ref?: any;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export type OnDeleteImage = (src: string) => void | Promise<void>;
|
|
13
|
-
export type OnDeleteFile = (href: string) => void | Promise<void>;
|
|
12
|
+
export type OnDeleteImage = (src: string) => void | Promise<void>;
|
package/types/Editor.d.ts
CHANGED
|
@@ -1,16 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { EditorProps } from './Editor.types';
|
|
3
3
|
declare const ReactEditor: React.FC<EditorProps>;
|
|
4
|
-
|
|
5
|
-
jodit?: unknown;
|
|
6
|
-
j?: {
|
|
7
|
-
e?: {
|
|
8
|
-
fire?: (...args: unknown[]) => void;
|
|
9
|
-
};
|
|
10
|
-
};
|
|
11
|
-
};
|
|
12
|
-
export declare const uploaderConfig: (apiUrl?: string, assetBaseUrl?: string) => {
|
|
13
|
-
insertImageAsBase64URI: boolean;
|
|
4
|
+
export declare const uploaderConfig: (apiUrl?: string, imageUrl?: string) => {
|
|
14
5
|
imagesExtensions: string[];
|
|
15
6
|
filesVariableName(t: number): string;
|
|
16
7
|
url: string | undefined;
|
|
@@ -18,16 +9,13 @@ export declare const uploaderConfig: (apiUrl?: string, assetBaseUrl?: string) =>
|
|
|
18
9
|
format: string;
|
|
19
10
|
method: string;
|
|
20
11
|
prepareData(formdata: FormData): FormData;
|
|
21
|
-
isSuccess(this:
|
|
22
|
-
getMessage(
|
|
23
|
-
process(resp:
|
|
24
|
-
files:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
messages?: string[] | undefined;
|
|
28
|
-
msg?: string | undefined;
|
|
12
|
+
isSuccess(this: any, e: any): boolean;
|
|
13
|
+
getMessage(e: any): string;
|
|
14
|
+
process(resp: any): {
|
|
15
|
+
files: any[];
|
|
16
|
+
error: string;
|
|
17
|
+
msg: string;
|
|
29
18
|
};
|
|
30
|
-
defaultHandlerSuccess(this: UploaderThis, data: unknown): void;
|
|
31
19
|
error(this: any, e: Error): void;
|
|
32
20
|
defaultHandlerError(this: any, e: any): void;
|
|
33
21
|
};
|
|
@@ -36,8 +24,6 @@ type ConfigParams = {
|
|
|
36
24
|
apiUrl?: string;
|
|
37
25
|
imageUrl?: string;
|
|
38
26
|
onDeleteImage?: (imageUrl: string) => void | Promise<void>;
|
|
39
|
-
onDeleteFile?: (fileHref: string) => void | Promise<void>;
|
|
40
|
-
fileLinkExtensions?: readonly string[];
|
|
41
27
|
};
|
|
42
|
-
export declare const config: ({ includeUploader, apiUrl, imageUrl, onDeleteImage,
|
|
28
|
+
export declare const config: ({ includeUploader, apiUrl, imageUrl, onDeleteImage, }?: ConfigParams) => any;
|
|
43
29
|
export default ReactEditor;
|
package/types/Editor.types.d.ts
CHANGED