@mbs-dev/react-editor 1.7.0 → 1.9.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 +164 -86
- package/package.json +1 -1
- package/src/Editor.tsx +194 -97
- package/types/Editor.d.ts +5 -2
package/dist/Editor.js
CHANGED
|
@@ -76,7 +76,7 @@ var getDisplayNameFromPath = function (filename) {
|
|
|
76
76
|
}
|
|
77
77
|
return noExt;
|
|
78
78
|
};
|
|
79
|
-
var uploaderConfig = function (apiUrl, imageUrl) { return ({
|
|
79
|
+
var uploaderConfig = function (apiUrl, imageUrl, selectionRef) { return ({
|
|
80
80
|
imagesExtensions: ['jpg', 'png', 'jpeg', 'gif', 'webp'],
|
|
81
81
|
filesVariableName: function (t) {
|
|
82
82
|
return 'files[' + t + ']';
|
|
@@ -90,68 +90,111 @@ var uploaderConfig = function (apiUrl, imageUrl) { return ({
|
|
|
90
90
|
},
|
|
91
91
|
isSuccess: function (e) {
|
|
92
92
|
var _this = this;
|
|
93
|
-
var _a;
|
|
93
|
+
var _a, _b;
|
|
94
94
|
var fn = this.jodit;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
var restoreToLastCaret = function () {
|
|
96
|
+
var _a, _b;
|
|
97
|
+
var saved = selectionRef === null || selectionRef === void 0 ? void 0 : selectionRef.current;
|
|
98
|
+
if (saved) {
|
|
99
|
+
try {
|
|
100
|
+
fn.s.selectRange(saved, true);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
catch (_c) {
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if ((_a = fn === null || fn === void 0 ? void 0 : fn.s) === null || _a === void 0 ? void 0 : _a.focus)
|
|
107
|
+
fn.s.focus();
|
|
108
|
+
if ((_b = fn === null || fn === void 0 ? void 0 : fn.s) === null || _b === void 0 ? void 0 : _b.restore) {
|
|
109
|
+
try {
|
|
110
|
+
fn.s.restore();
|
|
111
|
+
}
|
|
112
|
+
catch (_d) {
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
var refreshSavedRange = function () {
|
|
117
|
+
if (!selectionRef)
|
|
118
|
+
return;
|
|
119
|
+
try {
|
|
120
|
+
var r = fn.s.range;
|
|
121
|
+
selectionRef.current = r ? r.cloneRange() : null;
|
|
122
|
+
}
|
|
123
|
+
catch (_a) {
|
|
124
|
+
selectionRef.current = null;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
var normalizedFiles = (function () {
|
|
128
|
+
var _a, _b, _c;
|
|
129
|
+
var raw = (_c = (_a = (Array.isArray(e === null || e === void 0 ? void 0 : e.files) ? e.files : null)) !== null && _a !== void 0 ? _a : (Array.isArray((_b = e === null || e === void 0 ? void 0 : e.data) === null || _b === void 0 ? void 0 : _b.files) ? e.data.files : null)) !== null && _c !== void 0 ? _c : [];
|
|
130
|
+
return raw
|
|
131
|
+
.map(function (item) {
|
|
132
|
+
var _a;
|
|
133
|
+
if (typeof item === 'string')
|
|
134
|
+
return { file: item };
|
|
135
|
+
if (item && typeof item === 'object') {
|
|
136
|
+
return {
|
|
137
|
+
file: String((_a = item.file) !== null && _a !== void 0 ? _a : ''),
|
|
138
|
+
origineFileName: item.origineFileName ? String(item.origineFileName) : undefined,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return { file: '' };
|
|
142
|
+
})
|
|
143
|
+
.filter(function (x) { return !!x.file; });
|
|
144
|
+
})();
|
|
145
|
+
if (normalizedFiles.length) {
|
|
146
|
+
normalizedFiles.forEach(function (_a) {
|
|
147
|
+
var _b;
|
|
148
|
+
var file = _a.file, origineFileName = _a.origineFileName;
|
|
149
|
+
var src = imageUrl ? "".concat(imageUrl, "/").concat(file) : file;
|
|
150
|
+
restoreToLastCaret();
|
|
151
|
+
if (isImageByExtension(file, _this.imagesExtensions || ['jpg', 'png', 'jpeg', 'gif', 'webp'])) {
|
|
100
152
|
var tagName = 'img';
|
|
101
153
|
var elm = fn.createInside.element(tagName);
|
|
102
154
|
elm.setAttribute('src', src);
|
|
103
155
|
fn.s.insertImage(elm, null, fn.o.imageDefaultWidth);
|
|
156
|
+
refreshSavedRange();
|
|
104
157
|
}
|
|
105
158
|
else {
|
|
106
|
-
if ((_a = fn === null || fn === void 0 ? void 0 : fn.s) === null || _a === void 0 ? void 0 : _a.focus)
|
|
107
|
-
fn.s.focus();
|
|
108
|
-
var savedRange = fn && fn.__mbs_lastRange;
|
|
109
|
-
if (savedRange && ((_b = fn === null || fn === void 0 ? void 0 : fn.s) === null || _b === void 0 ? void 0 : _b.selectRange)) {
|
|
110
|
-
try {
|
|
111
|
-
fn.s.selectRange(savedRange, true);
|
|
112
|
-
}
|
|
113
|
-
catch (_e) {
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
else if ((_c = fn === null || fn === void 0 ? void 0 : fn.s) === null || _c === void 0 ? void 0 : _c.restore) {
|
|
117
|
-
try {
|
|
118
|
-
fn.s.restore();
|
|
119
|
-
}
|
|
120
|
-
catch (_f) {
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
159
|
var tagName = 'a';
|
|
124
160
|
var elm = fn.createInside.element(tagName);
|
|
125
161
|
elm.setAttribute('href', src);
|
|
126
162
|
elm.setAttribute('target', '_blank');
|
|
127
163
|
elm.setAttribute('rel', 'noopener noreferrer');
|
|
128
|
-
elm.textContent = getDisplayNameFromPath(
|
|
164
|
+
elm.textContent = origineFileName || getDisplayNameFromPath(file);
|
|
129
165
|
fn.s.insertNode(elm);
|
|
130
|
-
if ((
|
|
166
|
+
if ((_b = fn === null || fn === void 0 ? void 0 : fn.s) === null || _b === void 0 ? void 0 : _b.setCursorAfter) {
|
|
131
167
|
try {
|
|
132
168
|
fn.s.setCursorAfter(elm);
|
|
133
169
|
}
|
|
134
|
-
catch (
|
|
170
|
+
catch (_c) {
|
|
135
171
|
}
|
|
136
172
|
}
|
|
173
|
+
refreshSavedRange();
|
|
137
174
|
}
|
|
138
175
|
});
|
|
139
176
|
}
|
|
140
|
-
|
|
177
|
+
var err = (_a = (typeof (e === null || e === void 0 ? void 0 : e.error) === 'number' ? e.error : undefined)) !== null && _a !== void 0 ? _a : (typeof ((_b = e === null || e === void 0 ? void 0 : e.data) === null || _b === void 0 ? void 0 : _b.error) === 'number' ? e.data.error : undefined);
|
|
178
|
+
if ((e === null || e === void 0 ? void 0 : e.success) === true)
|
|
179
|
+
return true;
|
|
180
|
+
if (typeof err === 'number')
|
|
181
|
+
return err === 0;
|
|
182
|
+
return false;
|
|
141
183
|
},
|
|
142
184
|
getMessage: function (e) {
|
|
143
|
-
var _a;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
: '';
|
|
185
|
+
var _a, _b;
|
|
186
|
+
var msg = (_a = e === null || e === void 0 ? void 0 : e.msg) !== null && _a !== void 0 ? _a : (_b = e === null || e === void 0 ? void 0 : e.data) === null || _b === void 0 ? void 0 : _b.msg;
|
|
187
|
+
return typeof msg === 'string' ? msg : '';
|
|
147
188
|
},
|
|
148
189
|
process: function (resp) {
|
|
149
|
-
var
|
|
150
|
-
files.
|
|
190
|
+
var payload = (resp === null || resp === void 0 ? void 0 : resp.data) ? resp.data : resp;
|
|
191
|
+
var files = Array.isArray(payload === null || payload === void 0 ? void 0 : payload.files) ? payload.files : [];
|
|
192
|
+
var error = payload === null || payload === void 0 ? void 0 : payload.error;
|
|
193
|
+
var msg = payload === null || payload === void 0 ? void 0 : payload.msg;
|
|
151
194
|
return {
|
|
152
|
-
files:
|
|
153
|
-
error:
|
|
154
|
-
msg:
|
|
195
|
+
files: files,
|
|
196
|
+
error: typeof error === 'number' ? String(error) : (error !== null && error !== void 0 ? error : ''),
|
|
197
|
+
msg: typeof msg === 'string' ? msg : '',
|
|
155
198
|
};
|
|
156
199
|
},
|
|
157
200
|
error: function (e) {
|
|
@@ -163,8 +206,8 @@ var uploaderConfig = function (apiUrl, imageUrl) { return ({
|
|
|
163
206
|
}); };
|
|
164
207
|
exports.uploaderConfig = uploaderConfig;
|
|
165
208
|
var config = function (_a) {
|
|
166
|
-
var _b;
|
|
167
|
-
var
|
|
209
|
+
var _b = _a === void 0 ? {} : _a, includeUploader = _b.includeUploader, apiUrl = _b.apiUrl, imageUrl = _b.imageUrl, onDeleteImage = _b.onDeleteImage;
|
|
210
|
+
var selectionRef = { current: null };
|
|
168
211
|
var base = {
|
|
169
212
|
readonly: false,
|
|
170
213
|
placeholder: 'Start typing...',
|
|
@@ -178,6 +221,7 @@ var config = function (_a) {
|
|
|
178
221
|
showCharsCounter: true,
|
|
179
222
|
showWordsCounter: true,
|
|
180
223
|
showXPathInStatusbar: false,
|
|
224
|
+
saveSelectionOnBlur: true,
|
|
181
225
|
buttons: [
|
|
182
226
|
'source',
|
|
183
227
|
'|',
|
|
@@ -211,61 +255,95 @@ var config = function (_a) {
|
|
|
211
255
|
'fullsize',
|
|
212
256
|
],
|
|
213
257
|
};
|
|
258
|
+
var composeAfterInit = function (a, b) {
|
|
259
|
+
return function (editor) {
|
|
260
|
+
if (a)
|
|
261
|
+
a(editor);
|
|
262
|
+
if (b)
|
|
263
|
+
b(editor);
|
|
264
|
+
};
|
|
265
|
+
};
|
|
214
266
|
if (includeUploader) {
|
|
215
|
-
base.uploader = (0, exports.uploaderConfig)(apiUrl, imageUrl);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
var
|
|
219
|
-
var _a;
|
|
267
|
+
base.uploader = (0, exports.uploaderConfig)(apiUrl, imageUrl, selectionRef);
|
|
268
|
+
var selectionCaptureAfterInit = function (editor) {
|
|
269
|
+
var _a, _b, _c, _d;
|
|
270
|
+
var capture = function () {
|
|
220
271
|
try {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
272
|
+
var r = editor.s.range;
|
|
273
|
+
selectionRef.current = r ? r.cloneRange() : null;
|
|
224
274
|
}
|
|
225
|
-
catch (
|
|
275
|
+
catch (_a) {
|
|
276
|
+
selectionRef.current = null;
|
|
226
277
|
}
|
|
227
278
|
};
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
279
|
+
var editorEl = (_a = editor === null || editor === void 0 ? void 0 : editor.editor) !== null && _a !== void 0 ? _a : null;
|
|
280
|
+
var onMouseUp = function () { return capture(); };
|
|
281
|
+
var onKeyUp = function () { return capture(); };
|
|
282
|
+
var onTouchEnd = function () { return capture(); };
|
|
283
|
+
if (editorEl) {
|
|
284
|
+
editorEl.addEventListener('mouseup', onMouseUp);
|
|
285
|
+
editorEl.addEventListener('keyup', onKeyUp);
|
|
286
|
+
editorEl.addEventListener('touchend', onTouchEnd);
|
|
287
|
+
}
|
|
288
|
+
var toolbarEl = (_b = editor === null || editor === void 0 ? void 0 : editor.toolbarContainer) !== null && _b !== void 0 ? _b : null;
|
|
289
|
+
var onToolbarMouseDownCapture = function (ev) {
|
|
290
|
+
var target = ev.target;
|
|
291
|
+
if (!target)
|
|
292
|
+
return;
|
|
293
|
+
var isFileBtn = !!target.closest('[data-ref="file"]') ||
|
|
294
|
+
!!target.closest('[data-name="file"]') ||
|
|
295
|
+
!!target.closest('.jodit-toolbar-button_file');
|
|
296
|
+
if (isFileBtn) {
|
|
297
|
+
capture();
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
if (toolbarEl) {
|
|
301
|
+
toolbarEl.addEventListener('mousedown', onToolbarMouseDownCapture, true);
|
|
302
|
+
}
|
|
303
|
+
(_d = (_c = editor === null || editor === void 0 ? void 0 : editor.e) === null || _c === void 0 ? void 0 : _c.on) === null || _d === void 0 ? void 0 : _d.call(_c, 'beforeDestruct', function () {
|
|
304
|
+
if (editorEl) {
|
|
305
|
+
editorEl.removeEventListener('mouseup', onMouseUp);
|
|
306
|
+
editorEl.removeEventListener('keyup', onKeyUp);
|
|
307
|
+
editorEl.removeEventListener('touchend', onTouchEnd);
|
|
308
|
+
}
|
|
309
|
+
if (toolbarEl) {
|
|
310
|
+
toolbarEl.removeEventListener('mousedown', onToolbarMouseDownCapture, true);
|
|
240
311
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
var
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
312
|
+
});
|
|
313
|
+
};
|
|
314
|
+
base.events = __assign({}, (base.events || {}));
|
|
315
|
+
base.events.afterInit = composeAfterInit(base.events.afterInit, selectionCaptureAfterInit);
|
|
316
|
+
}
|
|
317
|
+
if (onDeleteImage) {
|
|
318
|
+
base.events = __assign({}, (base.events || {}));
|
|
319
|
+
var deleteImageAfterInit = function (editor) {
|
|
320
|
+
var extractImageSrcs = function (html) {
|
|
321
|
+
var container = document.createElement('div');
|
|
322
|
+
container.innerHTML = html || '';
|
|
323
|
+
var imgs = Array.from(container.getElementsByTagName('img'));
|
|
324
|
+
return new Set(imgs.map(function (img) { return img.getAttribute('src') || ''; }).filter(function (src) { return !!src; }));
|
|
325
|
+
};
|
|
326
|
+
var prevValue = editor.value || '';
|
|
327
|
+
var prevSrcs = extractImageSrcs(prevValue);
|
|
328
|
+
editor.events.on('change', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
329
|
+
var currentValue, currentSrcs;
|
|
330
|
+
return __generator(this, function (_a) {
|
|
331
|
+
currentValue = editor.value || '';
|
|
332
|
+
currentSrcs = extractImageSrcs(currentValue);
|
|
333
|
+
prevSrcs.forEach(function (src) {
|
|
334
|
+
if (!currentSrcs.has(src)) {
|
|
335
|
+
if (!imageUrl || src.startsWith(imageUrl)) {
|
|
336
|
+
void onDeleteImage(src);
|
|
261
337
|
}
|
|
262
|
-
}
|
|
263
|
-
prevValue = currentValue;
|
|
264
|
-
prevSrcs = currentSrcs;
|
|
265
|
-
return [2];
|
|
338
|
+
}
|
|
266
339
|
});
|
|
267
|
-
|
|
268
|
-
|
|
340
|
+
prevValue = currentValue;
|
|
341
|
+
prevSrcs = currentSrcs;
|
|
342
|
+
return [2];
|
|
343
|
+
});
|
|
344
|
+
}); });
|
|
345
|
+
};
|
|
346
|
+
base.events.afterInit = composeAfterInit(base.events.afterInit, deleteImageAfterInit);
|
|
269
347
|
}
|
|
270
348
|
return base;
|
|
271
349
|
};
|
package/package.json
CHANGED
package/src/Editor.tsx
CHANGED
|
@@ -19,16 +19,16 @@ const isImageByExtension = (filename: string, imageExts: string[]): boolean => {
|
|
|
19
19
|
return !!ext && imageExts.includes(ext);
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
// display name without extension and without last "-..."
|
|
23
|
-
// Example: recu-202600004-2-69956651a3b98099024323.pdf -> recu-202600004-2
|
|
24
22
|
const getDisplayNameFromPath = (filename: string): string => {
|
|
25
23
|
const clean = (filename || '').split('?')[0]?.split('#')[0] ?? '';
|
|
26
24
|
const last = clean.split('/').pop();
|
|
27
25
|
const base = last ? decodeURIComponent(last) : filename;
|
|
28
26
|
|
|
27
|
+
// remove extension
|
|
29
28
|
const dotIndex = base.lastIndexOf('.');
|
|
30
29
|
const noExt = dotIndex > 0 ? base.slice(0, dotIndex) : base;
|
|
31
30
|
|
|
31
|
+
// remove last "-..." suffix
|
|
32
32
|
const dashIndex = noExt.lastIndexOf('-');
|
|
33
33
|
if (dashIndex > 0) {
|
|
34
34
|
return noExt.slice(0, dashIndex);
|
|
@@ -37,11 +37,13 @@ const getDisplayNameFromPath = (filename: string): string => {
|
|
|
37
37
|
return noExt;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
+
type SelectionRef = { current: Range | null };
|
|
41
|
+
|
|
40
42
|
/**
|
|
41
43
|
* Uploader configuration for Jodit
|
|
42
44
|
* Handles image upload + insertion in the editor
|
|
43
45
|
*/
|
|
44
|
-
export const uploaderConfig = (apiUrl?: string, imageUrl?: string) => ({
|
|
46
|
+
export const uploaderConfig = (apiUrl?: string, imageUrl?: string, selectionRef?: SelectionRef) => ({
|
|
45
47
|
imagesExtensions: ['jpg', 'png', 'jpeg', 'gif', 'webp'],
|
|
46
48
|
filesVariableName(t: number): string {
|
|
47
49
|
return 'files[' + t + ']';
|
|
@@ -56,41 +58,83 @@ export const uploaderConfig = (apiUrl?: string, imageUrl?: string) => ({
|
|
|
56
58
|
isSuccess(this: any, e: any): boolean {
|
|
57
59
|
const fn = this.jodit;
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
const restoreToLastCaret = () => {
|
|
62
|
+
const saved = selectionRef?.current;
|
|
63
|
+
|
|
64
|
+
if (saved) {
|
|
65
|
+
try {
|
|
66
|
+
fn.s.selectRange(saved, true);
|
|
67
|
+
return;
|
|
68
|
+
} catch {
|
|
69
|
+
// ignore and fallback
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// fallback: jodit internal save/restore
|
|
74
|
+
if (fn?.s?.focus) fn.s.focus();
|
|
75
|
+
if (fn?.s?.restore) {
|
|
76
|
+
try {
|
|
77
|
+
fn.s.restore();
|
|
78
|
+
} catch {
|
|
79
|
+
// ignore
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const refreshSavedRange = () => {
|
|
85
|
+
if (!selectionRef) return;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const r: Range = fn.s.range;
|
|
89
|
+
selectionRef.current = r ? r.cloneRange() : null;
|
|
90
|
+
} catch {
|
|
91
|
+
selectionRef.current = null;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const normalizedFiles: Array<{ file: string; origineFileName?: string }> = (() => {
|
|
96
|
+
const raw =
|
|
97
|
+
(Array.isArray(e?.files) ? e.files : null) ??
|
|
98
|
+
(Array.isArray(e?.data?.files) ? e.data.files : null) ??
|
|
99
|
+
[];
|
|
100
|
+
|
|
101
|
+
return raw
|
|
102
|
+
.map((item: any) => {
|
|
103
|
+
if (typeof item === 'string') return { file: item };
|
|
104
|
+
if (item && typeof item === 'object') {
|
|
105
|
+
return {
|
|
106
|
+
file: String(item.file ?? ''),
|
|
107
|
+
origineFileName: item.origineFileName ? String(item.origineFileName) : undefined,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return { file: '' };
|
|
111
|
+
})
|
|
112
|
+
.filter((x: { file: any; }) => !!x.file);
|
|
113
|
+
})();
|
|
114
|
+
|
|
115
|
+
if (normalizedFiles.length) {
|
|
116
|
+
normalizedFiles.forEach(({ file, origineFileName }) => {
|
|
117
|
+
const src = imageUrl ? `${imageUrl}/${file}` : file;
|
|
118
|
+
|
|
119
|
+
// ✅ Restore caret BEFORE inserting anything (image or file)
|
|
120
|
+
restoreToLastCaret();
|
|
62
121
|
|
|
63
|
-
// If it's an image => insert <img>, otherwise insert <a href="...">
|
|
64
|
-
if (isImageByExtension(
|
|
122
|
+
// ✅ If it's an image => insert <img>, otherwise insert <a href="...">
|
|
123
|
+
if (isImageByExtension(file, this.imagesExtensions || ['jpg', 'png', 'jpeg', 'gif', 'webp'])) {
|
|
65
124
|
const tagName = 'img';
|
|
66
125
|
const elm = fn.createInside.element(tagName);
|
|
67
126
|
elm.setAttribute('src', src);
|
|
68
127
|
fn.s.insertImage(elm as HTMLImageElement, null, fn.o.imageDefaultWidth);
|
|
128
|
+
refreshSavedRange();
|
|
69
129
|
} else {
|
|
70
|
-
// ✅ FIX: restore the last known caret/selection inside the editor (works in table cells)
|
|
71
|
-
if (fn?.s?.focus) fn.s.focus();
|
|
72
|
-
|
|
73
|
-
const savedRange = fn && (fn as any).__mbs_lastRange;
|
|
74
|
-
if (savedRange && fn?.s?.selectRange) {
|
|
75
|
-
try {
|
|
76
|
-
fn.s.selectRange(savedRange, true);
|
|
77
|
-
} catch {
|
|
78
|
-
// ignore
|
|
79
|
-
}
|
|
80
|
-
} else if (fn?.s?.restore) {
|
|
81
|
-
try {
|
|
82
|
-
fn.s.restore();
|
|
83
|
-
} catch {
|
|
84
|
-
// ignore
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
130
|
const tagName = 'a';
|
|
89
131
|
const elm = fn.createInside.element(tagName);
|
|
90
132
|
elm.setAttribute('href', src);
|
|
91
133
|
elm.setAttribute('target', '_blank');
|
|
92
134
|
elm.setAttribute('rel', 'noopener noreferrer');
|
|
93
|
-
|
|
135
|
+
|
|
136
|
+
// ✅ Display origineFileName (new response), fallback to previous logic
|
|
137
|
+
elm.textContent = origineFileName || getDisplayNameFromPath(file);
|
|
94
138
|
|
|
95
139
|
fn.s.insertNode(elm);
|
|
96
140
|
|
|
@@ -101,25 +145,36 @@ export const uploaderConfig = (apiUrl?: string, imageUrl?: string) => ({
|
|
|
101
145
|
// ignore
|
|
102
146
|
}
|
|
103
147
|
}
|
|
148
|
+
|
|
149
|
+
refreshSavedRange();
|
|
104
150
|
}
|
|
105
151
|
});
|
|
106
152
|
}
|
|
107
153
|
|
|
108
|
-
|
|
154
|
+
const err =
|
|
155
|
+
(typeof e?.error === 'number' ? e.error : undefined) ??
|
|
156
|
+
(typeof e?.data?.error === 'number' ? e.data.error : undefined);
|
|
157
|
+
|
|
158
|
+
if (e?.success === true) return true;
|
|
159
|
+
if (typeof err === 'number') return err === 0;
|
|
160
|
+
|
|
161
|
+
return false;
|
|
109
162
|
},
|
|
110
163
|
getMessage(e: any): string {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
: '';
|
|
164
|
+
const msg = e?.msg ?? e?.data?.msg;
|
|
165
|
+
return typeof msg === 'string' ? msg : '';
|
|
114
166
|
},
|
|
115
167
|
process(resp: any): { files: any[]; error: string; msg: string } {
|
|
116
|
-
const
|
|
117
|
-
|
|
168
|
+
const payload = resp?.data ? resp.data : resp;
|
|
169
|
+
|
|
170
|
+
const files = Array.isArray(payload?.files) ? payload.files : [];
|
|
171
|
+
const error = payload?.error;
|
|
172
|
+
const msg = payload?.msg;
|
|
118
173
|
|
|
119
174
|
return {
|
|
120
|
-
files
|
|
121
|
-
error:
|
|
122
|
-
msg:
|
|
175
|
+
files,
|
|
176
|
+
error: typeof error === 'number' ? String(error) : (error ?? ''),
|
|
177
|
+
msg: typeof msg === 'string' ? msg : '',
|
|
123
178
|
};
|
|
124
179
|
},
|
|
125
180
|
error(this: any, e: Error): void {
|
|
@@ -141,12 +196,12 @@ type ConfigParams = {
|
|
|
141
196
|
onDeleteImage?: (imageUrl: string) => void | Promise<void>;
|
|
142
197
|
};
|
|
143
198
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Build Jodit config for ReactEditor
|
|
201
|
+
*/
|
|
202
|
+
export const config = ({ includeUploader, apiUrl, imageUrl, onDeleteImage }: ConfigParams = {}) => {
|
|
203
|
+
const selectionRef: SelectionRef = { current: null };
|
|
204
|
+
|
|
150
205
|
const base: any = {
|
|
151
206
|
readonly: false,
|
|
152
207
|
placeholder: 'Start typing...',
|
|
@@ -161,6 +216,9 @@ export const config = ({
|
|
|
161
216
|
showWordsCounter: true,
|
|
162
217
|
showXPathInStatusbar: false,
|
|
163
218
|
|
|
219
|
+
// ✅ Helps preserve selection when editor loses focus (e.g., file dialog)
|
|
220
|
+
saveSelectionOnBlur: true,
|
|
221
|
+
|
|
164
222
|
buttons: [
|
|
165
223
|
'source',
|
|
166
224
|
'|',
|
|
@@ -195,76 +253,115 @@ export const config = ({
|
|
|
195
253
|
],
|
|
196
254
|
};
|
|
197
255
|
|
|
256
|
+
const composeAfterInit =
|
|
257
|
+
(a?: (editor: any) => void, b?: (editor: any) => void) =>
|
|
258
|
+
(editor: any) => {
|
|
259
|
+
if (a) a(editor);
|
|
260
|
+
if (b) b(editor);
|
|
261
|
+
};
|
|
262
|
+
|
|
198
263
|
if (includeUploader) {
|
|
199
|
-
base.uploader = uploaderConfig(apiUrl, imageUrl);
|
|
200
|
-
}
|
|
264
|
+
base.uploader = uploaderConfig(apiUrl, imageUrl, selectionRef);
|
|
201
265
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
...(base.events || {}),
|
|
205
|
-
afterInit(editor: any) {
|
|
206
|
-
const saveRange = () => {
|
|
266
|
+
const selectionCaptureAfterInit = (editor: any) => {
|
|
267
|
+
const capture = () => {
|
|
207
268
|
try {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
269
|
+
const r: Range = editor.s.range;
|
|
270
|
+
selectionRef.current = r ? r.cloneRange() : null;
|
|
211
271
|
} catch {
|
|
212
|
-
|
|
272
|
+
selectionRef.current = null;
|
|
213
273
|
}
|
|
214
274
|
};
|
|
215
275
|
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
editor.events.on('change', saveRange);
|
|
222
|
-
},
|
|
223
|
-
};
|
|
276
|
+
// Capture during normal editing (helps even without toolbar click)
|
|
277
|
+
const editorEl: HTMLElement | null = editor?.editor ?? null;
|
|
278
|
+
const onMouseUp = () => capture();
|
|
279
|
+
const onKeyUp = () => capture();
|
|
280
|
+
const onTouchEnd = () => capture();
|
|
224
281
|
|
|
225
|
-
|
|
226
|
-
|
|
282
|
+
if (editorEl) {
|
|
283
|
+
editorEl.addEventListener('mouseup', onMouseUp);
|
|
284
|
+
editorEl.addEventListener('keyup', onKeyUp);
|
|
285
|
+
editorEl.addEventListener('touchend', onTouchEnd);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ✅ IMPORTANT: capture BEFORE toolbar steals focus.
|
|
289
|
+
// We listen on toolbar mousedown in CAPTURE phase.
|
|
290
|
+
const toolbarEl: HTMLElement | null = editor?.toolbarContainer ?? null;
|
|
291
|
+
const onToolbarMouseDownCapture = (ev: MouseEvent) => {
|
|
292
|
+
const target = ev.target as HTMLElement | null;
|
|
293
|
+
if (!target) return;
|
|
294
|
+
|
|
295
|
+
const isFileBtn =
|
|
296
|
+
!!target.closest('[data-ref="file"]') ||
|
|
297
|
+
!!target.closest('[data-name="file"]') ||
|
|
298
|
+
!!target.closest('.jodit-toolbar-button_file');
|
|
299
|
+
|
|
300
|
+
if (isFileBtn) {
|
|
301
|
+
capture();
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
if (toolbarEl) {
|
|
306
|
+
toolbarEl.addEventListener('mousedown', onToolbarMouseDownCapture, true);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// cleanup
|
|
310
|
+
editor?.e?.on?.('beforeDestruct', () => {
|
|
311
|
+
if (editorEl) {
|
|
312
|
+
editorEl.removeEventListener('mouseup', onMouseUp);
|
|
313
|
+
editorEl.removeEventListener('keyup', onKeyUp);
|
|
314
|
+
editorEl.removeEventListener('touchend', onTouchEnd);
|
|
315
|
+
}
|
|
316
|
+
if (toolbarEl) {
|
|
317
|
+
toolbarEl.removeEventListener('mousedown', onToolbarMouseDownCapture, true);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
};
|
|
227
321
|
|
|
228
322
|
base.events = {
|
|
229
323
|
...(base.events || {}),
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
prevAfterInit(editor);
|
|
234
|
-
}
|
|
324
|
+
};
|
|
325
|
+
base.events.afterInit = composeAfterInit(base.events.afterInit, selectionCaptureAfterInit);
|
|
326
|
+
}
|
|
235
327
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
let prevValue: string = editor.value || '';
|
|
249
|
-
let prevSrcs: Set<string> = extractImageSrcs(prevValue);
|
|
250
|
-
|
|
251
|
-
editor.events.on('change', async () => {
|
|
252
|
-
const currentValue: string = editor.value || '';
|
|
253
|
-
const currentSrcs = extractImageSrcs(currentValue);
|
|
254
|
-
|
|
255
|
-
prevSrcs.forEach((src) => {
|
|
256
|
-
if (!currentSrcs.has(src)) {
|
|
257
|
-
if (!imageUrl || src.startsWith(imageUrl)) {
|
|
258
|
-
void onDeleteImage(src);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
});
|
|
328
|
+
if (onDeleteImage) {
|
|
329
|
+
base.events = {
|
|
330
|
+
...(base.events || {}),
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const deleteImageAfterInit = (editor: any) => {
|
|
334
|
+
const extractImageSrcs = (html: string): Set<string> => {
|
|
335
|
+
const container = document.createElement('div');
|
|
336
|
+
container.innerHTML = html || '';
|
|
337
|
+
const imgs = Array.from(container.getElementsByTagName('img')) as HTMLImageElement[];
|
|
262
338
|
|
|
263
|
-
|
|
264
|
-
|
|
339
|
+
return new Set(imgs.map((img) => img.getAttribute('src') || '').filter((src) => !!src));
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
let prevValue: string = editor.value || '';
|
|
343
|
+
let prevSrcs: Set<string> = extractImageSrcs(prevValue);
|
|
344
|
+
|
|
345
|
+
editor.events.on('change', async () => {
|
|
346
|
+
const currentValue: string = editor.value || '';
|
|
347
|
+
const currentSrcs = extractImageSrcs(currentValue);
|
|
348
|
+
|
|
349
|
+
// src present before, not present now -> deleted
|
|
350
|
+
prevSrcs.forEach((src) => {
|
|
351
|
+
if (!currentSrcs.has(src)) {
|
|
352
|
+
// If imageUrl is defined, you can filter to only your own assets
|
|
353
|
+
if (!imageUrl || src.startsWith(imageUrl)) {
|
|
354
|
+
void onDeleteImage(src);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
265
357
|
});
|
|
266
|
-
|
|
358
|
+
|
|
359
|
+
prevValue = currentValue;
|
|
360
|
+
prevSrcs = currentSrcs;
|
|
361
|
+
});
|
|
267
362
|
};
|
|
363
|
+
|
|
364
|
+
base.events.afterInit = composeAfterInit(base.events.afterInit, deleteImageAfterInit);
|
|
268
365
|
}
|
|
269
366
|
|
|
270
367
|
return base;
|
package/types/Editor.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { EditorProps } from './Editor.types';
|
|
3
3
|
declare const ReactEditor: React.FC<EditorProps>;
|
|
4
|
-
|
|
4
|
+
type SelectionRef = {
|
|
5
|
+
current: Range | null;
|
|
6
|
+
};
|
|
7
|
+
export declare const uploaderConfig: (apiUrl?: string, imageUrl?: string, selectionRef?: SelectionRef) => {
|
|
5
8
|
imagesExtensions: string[];
|
|
6
9
|
filesVariableName(t: number): string;
|
|
7
10
|
url: string | undefined;
|
|
@@ -25,5 +28,5 @@ type ConfigParams = {
|
|
|
25
28
|
imageUrl?: string;
|
|
26
29
|
onDeleteImage?: (imageUrl: string) => void | Promise<void>;
|
|
27
30
|
};
|
|
28
|
-
export declare const config: ({ includeUploader, apiUrl, imageUrl, onDeleteImage
|
|
31
|
+
export declare const config: ({ includeUploader, apiUrl, imageUrl, onDeleteImage }?: ConfigParams) => any;
|
|
29
32
|
export default ReactEditor;
|