@mbs-dev/react-editor 1.6.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 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,26 +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, _b, _c;
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
- 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
140
  var tagName = 'a';
116
141
  var elm = fn.createInside.element(tagName);
117
142
  elm.setAttribute('href', src);
@@ -119,13 +144,14 @@ var uploaderConfig = function (apiUrl, imageUrl) { return ({
119
144
  elm.setAttribute('rel', 'noopener noreferrer');
120
145
  elm.textContent = getDisplayNameFromPath(filename);
121
146
  fn.s.insertNode(elm);
122
- if ((_c = fn === null || fn === void 0 ? void 0 : fn.s) === null || _c === void 0 ? void 0 : _c.setCursorAfter) {
147
+ if ((_a = fn === null || fn === void 0 ? void 0 : fn.s) === null || _a === void 0 ? void 0 : _a.setCursorAfter) {
123
148
  try {
124
149
  fn.s.setCursorAfter(elm);
125
150
  }
126
- catch (_e) {
151
+ catch (_b) {
127
152
  }
128
153
  }
154
+ refreshSavedRange();
129
155
  }
130
156
  });
131
157
  }
@@ -154,6 +180,7 @@ var uploaderConfig = function (apiUrl, imageUrl) { return ({
154
180
  exports.uploaderConfig = uploaderConfig;
155
181
  var config = function (_a) {
156
182
  var _b = _a === void 0 ? {} : _a, includeUploader = _b.includeUploader, apiUrl = _b.apiUrl, imageUrl = _b.imageUrl, onDeleteImage = _b.onDeleteImage;
183
+ var selectionRef = { current: null };
157
184
  var base = {
158
185
  readonly: false,
159
186
  placeholder: 'Start typing...',
@@ -167,6 +194,7 @@ var config = function (_a) {
167
194
  showCharsCounter: true,
168
195
  showWordsCounter: true,
169
196
  showXPathInStatusbar: false,
197
+ saveSelectionOnBlur: true,
170
198
  buttons: [
171
199
  'source',
172
200
  '|',
@@ -200,38 +228,95 @@ var config = function (_a) {
200
228
  'fullsize',
201
229
  ],
202
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
+ };
203
239
  if (includeUploader) {
204
- base.uploader = (0, exports.uploaderConfig)(apiUrl, imageUrl);
240
+ base.uploader = (0, exports.uploaderConfig)(apiUrl, imageUrl, selectionRef);
241
+ var selectionCaptureAfterInit = function (editor) {
242
+ var _a, _b, _c, _d;
243
+ var capture = function () {
244
+ try {
245
+ var r = editor.s.range;
246
+ selectionRef.current = r ? r.cloneRange() : null;
247
+ }
248
+ catch (_a) {
249
+ selectionRef.current = null;
250
+ }
251
+ };
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();
271
+ }
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);
205
289
  }
206
290
  if (onDeleteImage) {
207
- base.events = __assign(__assign({}, (base.events || {})), { afterInit: function (editor) {
208
- var _this = this;
209
- var extractImageSrcs = function (html) {
210
- var container = document.createElement('div');
211
- container.innerHTML = html || '';
212
- var imgs = Array.from(container.getElementsByTagName('img'));
213
- return new Set(imgs.map(function (img) { return img.getAttribute('src') || ''; }).filter(function (src) { return !!src; }));
214
- };
215
- var prevValue = editor.value || '';
216
- var prevSrcs = extractImageSrcs(prevValue);
217
- editor.events.on('change', function () { return __awaiter(_this, void 0, void 0, function () {
218
- var currentValue, currentSrcs;
219
- return __generator(this, function (_a) {
220
- currentValue = editor.value || '';
221
- currentSrcs = extractImageSrcs(currentValue);
222
- prevSrcs.forEach(function (src) {
223
- if (!currentSrcs.has(src)) {
224
- if (!imageUrl || src.startsWith(imageUrl)) {
225
- void onDeleteImage(src);
226
- }
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);
227
310
  }
228
- });
229
- prevValue = currentValue;
230
- prevSrcs = currentSrcs;
231
- return [2];
311
+ }
232
312
  });
233
- }); });
234
- } });
313
+ prevValue = currentValue;
314
+ prevSrcs = currentSrcs;
315
+ return [2];
316
+ });
317
+ }); });
318
+ };
319
+ base.events.afterInit = composeAfterInit(base.events.afterInit, deleteImageAfterInit);
235
320
  }
236
321
  return base;
237
322
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mbs-dev/react-editor",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "react editor",
5
5
  "main": "dist/index.js",
6
6
  "types": "types/index.d.ts",
package/src/Editor.tsx CHANGED
@@ -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,27 +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
 
99
+ // ✅ Restore caret BEFORE inserting anything (image or file)
100
+ restoreToLastCaret();
101
+
63
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 caret/selection before inserting file link
71
- if (fn?.s?.focus) fn.s.focus();
72
- if (fn?.s?.restore) {
73
- try {
74
- fn.s.restore();
75
- } catch {
76
- // ignore
77
- }
78
- }
79
-
80
110
  const tagName = 'a';
81
111
  const elm = fn.createInside.element(tagName);
82
112
  elm.setAttribute('href', src);
@@ -93,6 +123,8 @@ export const uploaderConfig = (apiUrl?: string, imageUrl?: string) => ({
93
123
  // ignore
94
124
  }
95
125
  }
126
+
127
+ refreshSavedRange();
96
128
  }
97
129
  });
98
130
  }
@@ -135,6 +167,8 @@ type ConfigParams = {
135
167
  * Build Jodit config for ReactEditor
136
168
  */
137
169
  export const config = ({ includeUploader, apiUrl, imageUrl, onDeleteImage }: ConfigParams = {}) => {
170
+ const selectionRef: SelectionRef = { current: null };
171
+
138
172
  const base: any = {
139
173
  readonly: false,
140
174
  placeholder: 'Start typing...',
@@ -149,6 +183,9 @@ export const config = ({ includeUploader, apiUrl, imageUrl, onDeleteImage }: Con
149
183
  showWordsCounter: true,
150
184
  showXPathInStatusbar: false,
151
185
 
186
+ // ✅ Helps preserve selection when editor loses focus (e.g., file dialog)
187
+ saveSelectionOnBlur: true,
188
+
152
189
  buttons: [
153
190
  'source',
154
191
  '|',
@@ -183,48 +220,115 @@ export const config = ({ includeUploader, apiUrl, imageUrl, onDeleteImage }: Con
183
220
  ],
184
221
  };
185
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
+
186
230
  if (includeUploader) {
187
- base.uploader = uploaderConfig(apiUrl, imageUrl);
231
+ base.uploader = uploaderConfig(apiUrl, imageUrl, selectionRef);
232
+
233
+ const selectionCaptureAfterInit = (editor: any) => {
234
+ const capture = () => {
235
+ try {
236
+ const r: Range = editor.s.range;
237
+ selectionRef.current = r ? r.cloneRange() : null;
238
+ } catch {
239
+ selectionRef.current = null;
240
+ }
241
+ };
242
+
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();
248
+
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
+ };
288
+
289
+ base.events = {
290
+ ...(base.events || {}),
291
+ };
292
+ base.events.afterInit = composeAfterInit(base.events.afterInit, selectionCaptureAfterInit);
188
293
  }
189
294
 
190
295
  if (onDeleteImage) {
191
296
  base.events = {
192
297
  ...(base.events || {}),
193
- /**
194
- * We use the value diff to detect removed <img> src.
195
- * This avoids false calls during upload (where DOM nodes can be replaced).
196
- */
197
- afterInit(editor: any) {
198
- const extractImageSrcs = (html: string): Set<string> => {
199
- const container = document.createElement('div');
200
- container.innerHTML = html || '';
201
- const imgs = Array.from(container.getElementsByTagName('img')) as HTMLImageElement[];
202
-
203
- return new Set(imgs.map((img) => img.getAttribute('src') || '').filter((src) => !!src));
204
- };
205
-
206
- let prevValue: string = editor.value || '';
207
- let prevSrcs: Set<string> = extractImageSrcs(prevValue);
208
-
209
- editor.events.on('change', async () => {
210
- const currentValue: string = editor.value || '';
211
- const currentSrcs = extractImageSrcs(currentValue);
212
-
213
- // src present before, not present now -> deleted
214
- prevSrcs.forEach((src) => {
215
- if (!currentSrcs.has(src)) {
216
- // If imageUrl is defined, you can filter to only your own assets
217
- if (!imageUrl || src.startsWith(imageUrl)) {
218
- void onDeleteImage(src);
219
- }
220
- }
221
- });
298
+ };
299
+
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);
222
311
 
223
- prevValue = currentValue;
224
- prevSrcs = currentSrcs;
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
+ }
225
324
  });
226
- },
325
+
326
+ prevValue = currentValue;
327
+ prevSrcs = currentSrcs;
328
+ });
227
329
  };
330
+
331
+ base.events.afterInit = composeAfterInit(base.events.afterInit, deleteImageAfterInit);
228
332
  }
229
333
 
230
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
- 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;