@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 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
- if (((_a = e === null || e === void 0 ? void 0 : e.data) === null || _a === void 0 ? void 0 : _a.files) && e.data.files.length) {
96
- e.data.files.forEach(function (filename) {
97
- var _a, _b, _c, _d;
98
- var src = imageUrl ? "".concat(imageUrl, "/").concat(filename) : filename;
99
- if (isImageByExtension(filename, _this.imagesExtensions || ['jpg', 'png', 'jpeg', 'gif', 'webp'])) {
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(filename);
164
+ elm.textContent = origineFileName || getDisplayNameFromPath(file);
129
165
  fn.s.insertNode(elm);
130
- if ((_d = fn === null || fn === void 0 ? void 0 : fn.s) === null || _d === void 0 ? void 0 : _d.setCursorAfter) {
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 (_g) {
170
+ catch (_c) {
135
171
  }
136
172
  }
173
+ refreshSavedRange();
137
174
  }
138
175
  });
139
176
  }
140
- return !!(e === null || e === void 0 ? void 0 : e.success);
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
- 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
- : '';
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 files = [];
150
- files.unshift(resp === null || resp === void 0 ? void 0 : resp.data);
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: resp === null || resp === void 0 ? void 0 : resp.data,
153
- error: resp === null || resp === void 0 ? void 0 : resp.msg,
154
- msg: resp === null || resp === void 0 ? void 0 : resp.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 _c = _a === void 0 ? {} : _a, includeUploader = _c.includeUploader, apiUrl = _c.apiUrl, imageUrl = _c.imageUrl, onDeleteImage = _c.onDeleteImage;
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
- base.events = __assign(__assign({}, (base.events || {})), { afterInit: function (editor) {
218
- var saveRange = function () {
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
- if ((_a = editor === null || editor === void 0 ? void 0 : editor.s) === null || _a === void 0 ? void 0 : _a.range) {
222
- editor.__mbs_lastRange = editor.s.range.cloneRange();
223
- }
272
+ var r = editor.s.range;
273
+ selectionRef.current = r ? r.cloneRange() : null;
224
274
  }
225
- catch (_b) {
275
+ catch (_a) {
276
+ selectionRef.current = null;
226
277
  }
227
278
  };
228
- saveRange();
229
- editor.events.on('mouseup', saveRange);
230
- editor.events.on('keyup', saveRange);
231
- editor.events.on('focus', saveRange);
232
- editor.events.on('change', saveRange);
233
- } });
234
- if (onDeleteImage) {
235
- var prevAfterInit_1 = (_b = base.events) === null || _b === void 0 ? void 0 : _b.afterInit;
236
- base.events = __assign(__assign({}, (base.events || {})), { afterInit: function (editor) {
237
- var _this = this;
238
- if (typeof prevAfterInit_1 === 'function') {
239
- prevAfterInit_1(editor);
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
- var extractImageSrcs = function (html) {
242
- var container = document.createElement('div');
243
- container.innerHTML = html || '';
244
- var imgs = Array.from(container.getElementsByTagName('img'));
245
- return new Set(imgs
246
- .map(function (img) { return img.getAttribute('src') || ''; })
247
- .filter(function (src) { return !!src; }));
248
- };
249
- var prevValue = editor.value || '';
250
- var prevSrcs = extractImageSrcs(prevValue);
251
- editor.events.on('change', function () { return __awaiter(_this, void 0, void 0, function () {
252
- var currentValue, currentSrcs;
253
- return __generator(this, function (_a) {
254
- currentValue = editor.value || '';
255
- currentSrcs = extractImageSrcs(currentValue);
256
- prevSrcs.forEach(function (src) {
257
- if (!currentSrcs.has(src)) {
258
- if (!imageUrl || src.startsWith(imageUrl)) {
259
- void onDeleteImage(src);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mbs-dev/react-editor",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "react editor",
5
5
  "main": "dist/index.js",
6
6
  "types": "types/index.d.ts",
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
- if (e?.data?.files && e.data.files.length) {
60
- e.data.files.forEach((filename: string) => {
61
- const src = imageUrl ? `${imageUrl}/${filename}` : filename;
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(filename, this.imagesExtensions || ['jpg', 'png', 'jpeg', 'gif', 'webp'])) {
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
- elm.textContent = getDisplayNameFromPath(filename);
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
- return !!e?.success;
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
- return e?.data?.messages && Array.isArray(e.data.messages)
112
- ? e.data.messages.join('')
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 files: any[] = [];
117
- files.unshift(resp?.data);
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: resp?.data,
121
- error: resp?.msg,
122
- msg: resp?.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
- export const config = ({
145
- includeUploader,
146
- apiUrl,
147
- imageUrl,
148
- onDeleteImage,
149
- }: ConfigParams = {}) => {
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
- // Always keep last caret/selection (needed for file uploads to insert in the right cell)
203
- base.events = {
204
- ...(base.events || {}),
205
- afterInit(editor: any) {
206
- const saveRange = () => {
266
+ const selectionCaptureAfterInit = (editor: any) => {
267
+ const capture = () => {
207
268
  try {
208
- if (editor?.s?.range) {
209
- (editor as any).__mbs_lastRange = editor.s.range.cloneRange();
210
- }
269
+ const r: Range = editor.s.range;
270
+ selectionRef.current = r ? r.cloneRange() : null;
211
271
  } catch {
212
- // ignore
272
+ selectionRef.current = null;
213
273
  }
214
274
  };
215
275
 
216
- // initial + common interactions
217
- saveRange();
218
- editor.events.on('mouseup', saveRange);
219
- editor.events.on('keyup', saveRange);
220
- editor.events.on('focus', saveRange);
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
- if (onDeleteImage) {
226
- const prevAfterInit = base.events?.afterInit;
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
- afterInit(editor: any) {
231
- // keep existing selection saver
232
- if (typeof prevAfterInit === 'function') {
233
- prevAfterInit(editor);
234
- }
324
+ };
325
+ base.events.afterInit = composeAfterInit(base.events.afterInit, selectionCaptureAfterInit);
326
+ }
235
327
 
236
- const extractImageSrcs = (html: string): Set<string> => {
237
- const container = document.createElement('div');
238
- container.innerHTML = html || '';
239
- const imgs = Array.from(container.getElementsByTagName('img')) as HTMLImageElement[];
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
- });
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
- prevValue = currentValue;
264
- prevSrcs = currentSrcs;
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
- export declare const uploaderConfig: (apiUrl?: string, imageUrl?: string) => {
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, }?: ConfigParams) => any;
31
+ export declare const config: ({ includeUploader, apiUrl, imageUrl, onDeleteImage }?: ConfigParams) => any;
29
32
  export default ReactEditor;