@mbs-dev/react-editor 1.2.0 → 1.4.0

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