@mbs-dev/react-editor 1.7.0 → 1.8.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 +124 -73
- package/package.json +1 -1
- package/src/Editor.tsx +150 -86
- 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 + ']';
|
|
@@ -92,34 +92,51 @@ var uploaderConfig = function (apiUrl, imageUrl) { return ({
|
|
|
92
92
|
var _this = this;
|
|
93
93
|
var _a;
|
|
94
94
|
var fn = this.jodit;
|
|
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
|
+
};
|
|
95
127
|
if (((_a = e === null || e === void 0 ? void 0 : e.data) === null || _a === void 0 ? void 0 : _a.files) && e.data.files.length) {
|
|
96
128
|
e.data.files.forEach(function (filename) {
|
|
97
|
-
var _a
|
|
129
|
+
var _a;
|
|
98
130
|
var src = imageUrl ? "".concat(imageUrl, "/").concat(filename) : filename;
|
|
131
|
+
restoreToLastCaret();
|
|
99
132
|
if (isImageByExtension(filename, _this.imagesExtensions || ['jpg', 'png', 'jpeg', 'gif', 'webp'])) {
|
|
100
133
|
var tagName = 'img';
|
|
101
134
|
var elm = fn.createInside.element(tagName);
|
|
102
135
|
elm.setAttribute('src', src);
|
|
103
136
|
fn.s.insertImage(elm, null, fn.o.imageDefaultWidth);
|
|
137
|
+
refreshSavedRange();
|
|
104
138
|
}
|
|
105
139
|
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
140
|
var tagName = 'a';
|
|
124
141
|
var elm = fn.createInside.element(tagName);
|
|
125
142
|
elm.setAttribute('href', src);
|
|
@@ -127,13 +144,14 @@ var uploaderConfig = function (apiUrl, imageUrl) { return ({
|
|
|
127
144
|
elm.setAttribute('rel', 'noopener noreferrer');
|
|
128
145
|
elm.textContent = getDisplayNameFromPath(filename);
|
|
129
146
|
fn.s.insertNode(elm);
|
|
130
|
-
if ((
|
|
147
|
+
if ((_a = fn === null || fn === void 0 ? void 0 : fn.s) === null || _a === void 0 ? void 0 : _a.setCursorAfter) {
|
|
131
148
|
try {
|
|
132
149
|
fn.s.setCursorAfter(elm);
|
|
133
150
|
}
|
|
134
|
-
catch (
|
|
151
|
+
catch (_b) {
|
|
135
152
|
}
|
|
136
153
|
}
|
|
154
|
+
refreshSavedRange();
|
|
137
155
|
}
|
|
138
156
|
});
|
|
139
157
|
}
|
|
@@ -141,9 +159,7 @@ var uploaderConfig = function (apiUrl, imageUrl) { return ({
|
|
|
141
159
|
},
|
|
142
160
|
getMessage: function (e) {
|
|
143
161
|
var _a;
|
|
144
|
-
return ((_a = e === null || e === void 0 ? void 0 : e.data) === null || _a === void 0 ? void 0 : _a.messages) && Array.isArray(e.data.messages)
|
|
145
|
-
? e.data.messages.join('')
|
|
146
|
-
: '';
|
|
162
|
+
return ((_a = e === null || e === void 0 ? void 0 : e.data) === null || _a === void 0 ? void 0 : _a.messages) && Array.isArray(e.data.messages) ? e.data.messages.join('') : '';
|
|
147
163
|
},
|
|
148
164
|
process: function (resp) {
|
|
149
165
|
var files = [];
|
|
@@ -163,8 +179,8 @@ var uploaderConfig = function (apiUrl, imageUrl) { return ({
|
|
|
163
179
|
}); };
|
|
164
180
|
exports.uploaderConfig = uploaderConfig;
|
|
165
181
|
var config = function (_a) {
|
|
166
|
-
var _b;
|
|
167
|
-
var
|
|
182
|
+
var _b = _a === void 0 ? {} : _a, includeUploader = _b.includeUploader, apiUrl = _b.apiUrl, imageUrl = _b.imageUrl, onDeleteImage = _b.onDeleteImage;
|
|
183
|
+
var selectionRef = { current: null };
|
|
168
184
|
var base = {
|
|
169
185
|
readonly: false,
|
|
170
186
|
placeholder: 'Start typing...',
|
|
@@ -178,6 +194,7 @@ var config = function (_a) {
|
|
|
178
194
|
showCharsCounter: true,
|
|
179
195
|
showWordsCounter: true,
|
|
180
196
|
showXPathInStatusbar: false,
|
|
197
|
+
saveSelectionOnBlur: true,
|
|
181
198
|
buttons: [
|
|
182
199
|
'source',
|
|
183
200
|
'|',
|
|
@@ -211,61 +228,95 @@ var config = function (_a) {
|
|
|
211
228
|
'fullsize',
|
|
212
229
|
],
|
|
213
230
|
};
|
|
231
|
+
var composeAfterInit = function (a, b) {
|
|
232
|
+
return function (editor) {
|
|
233
|
+
if (a)
|
|
234
|
+
a(editor);
|
|
235
|
+
if (b)
|
|
236
|
+
b(editor);
|
|
237
|
+
};
|
|
238
|
+
};
|
|
214
239
|
if (includeUploader) {
|
|
215
|
-
base.uploader = (0, exports.uploaderConfig)(apiUrl, imageUrl);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
var
|
|
219
|
-
var _a;
|
|
240
|
+
base.uploader = (0, exports.uploaderConfig)(apiUrl, imageUrl, selectionRef);
|
|
241
|
+
var selectionCaptureAfterInit = function (editor) {
|
|
242
|
+
var _a, _b, _c, _d;
|
|
243
|
+
var capture = function () {
|
|
220
244
|
try {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
245
|
+
var r = editor.s.range;
|
|
246
|
+
selectionRef.current = r ? r.cloneRange() : null;
|
|
224
247
|
}
|
|
225
|
-
catch (
|
|
248
|
+
catch (_a) {
|
|
249
|
+
selectionRef.current = null;
|
|
226
250
|
}
|
|
227
251
|
};
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
252
|
+
var editorEl = (_a = editor === null || editor === void 0 ? void 0 : editor.editor) !== null && _a !== void 0 ? _a : null;
|
|
253
|
+
var onMouseUp = function () { return capture(); };
|
|
254
|
+
var onKeyUp = function () { return capture(); };
|
|
255
|
+
var onTouchEnd = function () { return capture(); };
|
|
256
|
+
if (editorEl) {
|
|
257
|
+
editorEl.addEventListener('mouseup', onMouseUp);
|
|
258
|
+
editorEl.addEventListener('keyup', onKeyUp);
|
|
259
|
+
editorEl.addEventListener('touchend', onTouchEnd);
|
|
260
|
+
}
|
|
261
|
+
var toolbarEl = (_b = editor === null || editor === void 0 ? void 0 : editor.toolbarContainer) !== null && _b !== void 0 ? _b : null;
|
|
262
|
+
var onToolbarMouseDownCapture = function (ev) {
|
|
263
|
+
var target = ev.target;
|
|
264
|
+
if (!target)
|
|
265
|
+
return;
|
|
266
|
+
var isFileBtn = !!target.closest('[data-ref="file"]') ||
|
|
267
|
+
!!target.closest('[data-name="file"]') ||
|
|
268
|
+
!!target.closest('.jodit-toolbar-button_file');
|
|
269
|
+
if (isFileBtn) {
|
|
270
|
+
capture();
|
|
240
271
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
272
|
+
};
|
|
273
|
+
if (toolbarEl) {
|
|
274
|
+
toolbarEl.addEventListener('mousedown', onToolbarMouseDownCapture, true);
|
|
275
|
+
}
|
|
276
|
+
(_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 () {
|
|
277
|
+
if (editorEl) {
|
|
278
|
+
editorEl.removeEventListener('mouseup', onMouseUp);
|
|
279
|
+
editorEl.removeEventListener('keyup', onKeyUp);
|
|
280
|
+
editorEl.removeEventListener('touchend', onTouchEnd);
|
|
281
|
+
}
|
|
282
|
+
if (toolbarEl) {
|
|
283
|
+
toolbarEl.removeEventListener('mousedown', onToolbarMouseDownCapture, true);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
};
|
|
287
|
+
base.events = __assign({}, (base.events || {}));
|
|
288
|
+
base.events.afterInit = composeAfterInit(base.events.afterInit, selectionCaptureAfterInit);
|
|
289
|
+
}
|
|
290
|
+
if (onDeleteImage) {
|
|
291
|
+
base.events = __assign({}, (base.events || {}));
|
|
292
|
+
var deleteImageAfterInit = function (editor) {
|
|
293
|
+
var extractImageSrcs = function (html) {
|
|
294
|
+
var container = document.createElement('div');
|
|
295
|
+
container.innerHTML = html || '';
|
|
296
|
+
var imgs = Array.from(container.getElementsByTagName('img'));
|
|
297
|
+
return new Set(imgs.map(function (img) { return img.getAttribute('src') || ''; }).filter(function (src) { return !!src; }));
|
|
298
|
+
};
|
|
299
|
+
var prevValue = editor.value || '';
|
|
300
|
+
var prevSrcs = extractImageSrcs(prevValue);
|
|
301
|
+
editor.events.on('change', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
302
|
+
var currentValue, currentSrcs;
|
|
303
|
+
return __generator(this, function (_a) {
|
|
304
|
+
currentValue = editor.value || '';
|
|
305
|
+
currentSrcs = extractImageSrcs(currentValue);
|
|
306
|
+
prevSrcs.forEach(function (src) {
|
|
307
|
+
if (!currentSrcs.has(src)) {
|
|
308
|
+
if (!imageUrl || src.startsWith(imageUrl)) {
|
|
309
|
+
void onDeleteImage(src);
|
|
261
310
|
}
|
|
262
|
-
}
|
|
263
|
-
prevValue = currentValue;
|
|
264
|
-
prevSrcs = currentSrcs;
|
|
265
|
-
return [2];
|
|
311
|
+
}
|
|
266
312
|
});
|
|
267
|
-
|
|
268
|
-
|
|
313
|
+
prevValue = currentValue;
|
|
314
|
+
prevSrcs = currentSrcs;
|
|
315
|
+
return [2];
|
|
316
|
+
});
|
|
317
|
+
}); });
|
|
318
|
+
};
|
|
319
|
+
base.events.afterInit = composeAfterInit(base.events.afterInit, deleteImageAfterInit);
|
|
269
320
|
}
|
|
270
321
|
return base;
|
|
271
322
|
};
|
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,35 +58,55 @@ export const uploaderConfig = (apiUrl?: string, imageUrl?: string) => ({
|
|
|
56
58
|
isSuccess(this: any, e: any): boolean {
|
|
57
59
|
const fn = this.jodit;
|
|
58
60
|
|
|
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
|
+
|
|
59
95
|
if (e?.data?.files && e.data.files.length) {
|
|
60
96
|
e.data.files.forEach((filename: string) => {
|
|
61
97
|
const src = imageUrl ? `${imageUrl}/${filename}` : filename;
|
|
62
98
|
|
|
63
|
-
//
|
|
99
|
+
// ✅ Restore caret BEFORE inserting anything (image or file)
|
|
100
|
+
restoreToLastCaret();
|
|
101
|
+
|
|
102
|
+
// ✅ If it's an image => insert <img>, otherwise insert <a href="...">
|
|
64
103
|
if (isImageByExtension(filename, this.imagesExtensions || ['jpg', 'png', 'jpeg', 'gif', 'webp'])) {
|
|
65
104
|
const tagName = 'img';
|
|
66
105
|
const elm = fn.createInside.element(tagName);
|
|
67
106
|
elm.setAttribute('src', src);
|
|
68
107
|
fn.s.insertImage(elm as HTMLImageElement, null, fn.o.imageDefaultWidth);
|
|
108
|
+
refreshSavedRange();
|
|
69
109
|
} 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
110
|
const tagName = 'a';
|
|
89
111
|
const elm = fn.createInside.element(tagName);
|
|
90
112
|
elm.setAttribute('href', src);
|
|
@@ -101,6 +123,8 @@ export const uploaderConfig = (apiUrl?: string, imageUrl?: string) => ({
|
|
|
101
123
|
// ignore
|
|
102
124
|
}
|
|
103
125
|
}
|
|
126
|
+
|
|
127
|
+
refreshSavedRange();
|
|
104
128
|
}
|
|
105
129
|
});
|
|
106
130
|
}
|
|
@@ -108,9 +132,7 @@ export const uploaderConfig = (apiUrl?: string, imageUrl?: string) => ({
|
|
|
108
132
|
return !!e?.success;
|
|
109
133
|
},
|
|
110
134
|
getMessage(e: any): string {
|
|
111
|
-
return e?.data?.messages && Array.isArray(e.data.messages)
|
|
112
|
-
? e.data.messages.join('')
|
|
113
|
-
: '';
|
|
135
|
+
return e?.data?.messages && Array.isArray(e.data.messages) ? e.data.messages.join('') : '';
|
|
114
136
|
},
|
|
115
137
|
process(resp: any): { files: any[]; error: string; msg: string } {
|
|
116
138
|
const files: any[] = [];
|
|
@@ -141,12 +163,12 @@ type ConfigParams = {
|
|
|
141
163
|
onDeleteImage?: (imageUrl: string) => void | Promise<void>;
|
|
142
164
|
};
|
|
143
165
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
166
|
+
/**
|
|
167
|
+
* Build Jodit config for ReactEditor
|
|
168
|
+
*/
|
|
169
|
+
export const config = ({ includeUploader, apiUrl, imageUrl, onDeleteImage }: ConfigParams = {}) => {
|
|
170
|
+
const selectionRef: SelectionRef = { current: null };
|
|
171
|
+
|
|
150
172
|
const base: any = {
|
|
151
173
|
readonly: false,
|
|
152
174
|
placeholder: 'Start typing...',
|
|
@@ -161,6 +183,9 @@ export const config = ({
|
|
|
161
183
|
showWordsCounter: true,
|
|
162
184
|
showXPathInStatusbar: false,
|
|
163
185
|
|
|
186
|
+
// ✅ Helps preserve selection when editor loses focus (e.g., file dialog)
|
|
187
|
+
saveSelectionOnBlur: true,
|
|
188
|
+
|
|
164
189
|
buttons: [
|
|
165
190
|
'source',
|
|
166
191
|
'|',
|
|
@@ -195,76 +220,115 @@ export const config = ({
|
|
|
195
220
|
],
|
|
196
221
|
};
|
|
197
222
|
|
|
223
|
+
const composeAfterInit =
|
|
224
|
+
(a?: (editor: any) => void, b?: (editor: any) => void) =>
|
|
225
|
+
(editor: any) => {
|
|
226
|
+
if (a) a(editor);
|
|
227
|
+
if (b) b(editor);
|
|
228
|
+
};
|
|
229
|
+
|
|
198
230
|
if (includeUploader) {
|
|
199
|
-
base.uploader = uploaderConfig(apiUrl, imageUrl);
|
|
200
|
-
}
|
|
231
|
+
base.uploader = uploaderConfig(apiUrl, imageUrl, selectionRef);
|
|
201
232
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
...(base.events || {}),
|
|
205
|
-
afterInit(editor: any) {
|
|
206
|
-
const saveRange = () => {
|
|
233
|
+
const selectionCaptureAfterInit = (editor: any) => {
|
|
234
|
+
const capture = () => {
|
|
207
235
|
try {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
236
|
+
const r: Range = editor.s.range;
|
|
237
|
+
selectionRef.current = r ? r.cloneRange() : null;
|
|
211
238
|
} catch {
|
|
212
|
-
|
|
239
|
+
selectionRef.current = null;
|
|
213
240
|
}
|
|
214
241
|
};
|
|
215
242
|
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
editor.events.on('change', saveRange);
|
|
222
|
-
},
|
|
223
|
-
};
|
|
243
|
+
// Capture during normal editing (helps even without toolbar click)
|
|
244
|
+
const editorEl: HTMLElement | null = editor?.editor ?? null;
|
|
245
|
+
const onMouseUp = () => capture();
|
|
246
|
+
const onKeyUp = () => capture();
|
|
247
|
+
const onTouchEnd = () => capture();
|
|
224
248
|
|
|
225
|
-
|
|
226
|
-
|
|
249
|
+
if (editorEl) {
|
|
250
|
+
editorEl.addEventListener('mouseup', onMouseUp);
|
|
251
|
+
editorEl.addEventListener('keyup', onKeyUp);
|
|
252
|
+
editorEl.addEventListener('touchend', onTouchEnd);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ✅ IMPORTANT: capture BEFORE toolbar steals focus.
|
|
256
|
+
// We listen on toolbar mousedown in CAPTURE phase.
|
|
257
|
+
const toolbarEl: HTMLElement | null = editor?.toolbarContainer ?? null;
|
|
258
|
+
const onToolbarMouseDownCapture = (ev: MouseEvent) => {
|
|
259
|
+
const target = ev.target as HTMLElement | null;
|
|
260
|
+
if (!target) return;
|
|
261
|
+
|
|
262
|
+
const isFileBtn =
|
|
263
|
+
!!target.closest('[data-ref="file"]') ||
|
|
264
|
+
!!target.closest('[data-name="file"]') ||
|
|
265
|
+
!!target.closest('.jodit-toolbar-button_file');
|
|
266
|
+
|
|
267
|
+
if (isFileBtn) {
|
|
268
|
+
capture();
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
if (toolbarEl) {
|
|
273
|
+
toolbarEl.addEventListener('mousedown', onToolbarMouseDownCapture, true);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// cleanup
|
|
277
|
+
editor?.e?.on?.('beforeDestruct', () => {
|
|
278
|
+
if (editorEl) {
|
|
279
|
+
editorEl.removeEventListener('mouseup', onMouseUp);
|
|
280
|
+
editorEl.removeEventListener('keyup', onKeyUp);
|
|
281
|
+
editorEl.removeEventListener('touchend', onTouchEnd);
|
|
282
|
+
}
|
|
283
|
+
if (toolbarEl) {
|
|
284
|
+
toolbarEl.removeEventListener('mousedown', onToolbarMouseDownCapture, true);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
};
|
|
227
288
|
|
|
228
289
|
base.events = {
|
|
229
290
|
...(base.events || {}),
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
prevAfterInit(editor);
|
|
234
|
-
}
|
|
291
|
+
};
|
|
292
|
+
base.events.afterInit = composeAfterInit(base.events.afterInit, selectionCaptureAfterInit);
|
|
293
|
+
}
|
|
235
294
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return new Set(
|
|
242
|
-
imgs
|
|
243
|
-
.map((img) => img.getAttribute('src') || '')
|
|
244
|
-
.filter((src) => !!src)
|
|
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
|
-
});
|
|
295
|
+
if (onDeleteImage) {
|
|
296
|
+
base.events = {
|
|
297
|
+
...(base.events || {}),
|
|
298
|
+
};
|
|
262
299
|
|
|
263
|
-
|
|
264
|
-
|
|
300
|
+
const deleteImageAfterInit = (editor: any) => {
|
|
301
|
+
const extractImageSrcs = (html: string): Set<string> => {
|
|
302
|
+
const container = document.createElement('div');
|
|
303
|
+
container.innerHTML = html || '';
|
|
304
|
+
const imgs = Array.from(container.getElementsByTagName('img')) as HTMLImageElement[];
|
|
305
|
+
|
|
306
|
+
return new Set(imgs.map((img) => img.getAttribute('src') || '').filter((src) => !!src));
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
let prevValue: string = editor.value || '';
|
|
310
|
+
let prevSrcs: Set<string> = extractImageSrcs(prevValue);
|
|
311
|
+
|
|
312
|
+
editor.events.on('change', async () => {
|
|
313
|
+
const currentValue: string = editor.value || '';
|
|
314
|
+
const currentSrcs = extractImageSrcs(currentValue);
|
|
315
|
+
|
|
316
|
+
// src present before, not present now -> deleted
|
|
317
|
+
prevSrcs.forEach((src) => {
|
|
318
|
+
if (!currentSrcs.has(src)) {
|
|
319
|
+
// If imageUrl is defined, you can filter to only your own assets
|
|
320
|
+
if (!imageUrl || src.startsWith(imageUrl)) {
|
|
321
|
+
void onDeleteImage(src);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
265
324
|
});
|
|
266
|
-
|
|
325
|
+
|
|
326
|
+
prevValue = currentValue;
|
|
327
|
+
prevSrcs = currentSrcs;
|
|
328
|
+
});
|
|
267
329
|
};
|
|
330
|
+
|
|
331
|
+
base.events.afterInit = composeAfterInit(base.events.afterInit, deleteImageAfterInit);
|
|
268
332
|
}
|
|
269
333
|
|
|
270
334
|
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;
|