@mbs-dev/react-editor 1.2.0 → 1.3.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...',
@@ -320,7 +156,6 @@ var config = function (_a) {
320
156
  'ol',
321
157
  '|',
322
158
  'image',
323
- 'file',
324
159
  '|',
325
160
  'video',
326
161
  '|',
@@ -345,33 +180,33 @@ var config = function (_a) {
345
180
  if (includeUploader) {
346
181
  base.uploader = (0, exports.uploaderConfig)(apiUrl, imageUrl);
347
182
  }
348
- var hasDelete = Boolean(onDeleteImage || onDeleteFile);
349
- if (hasDelete) {
183
+ if (onDeleteImage) {
350
184
  base.events = __assign(__assign({}, (base.events || {})), { afterInit: function (editor) {
351
185
  var _this = this;
186
+ var extractImageSrcs = function (html) {
187
+ var container = document.createElement('div');
188
+ container.innerHTML = html || '';
189
+ var imgs = Array.from(container.getElementsByTagName('img'));
190
+ return new Set(imgs
191
+ .map(function (img) { return img.getAttribute('src') || ''; })
192
+ .filter(function (src) { return !!src; }));
193
+ };
352
194
  var prevValue = editor.value || '';
353
- var prevAssets = extractAssetsFromHtml(prevValue, fileLinkExtensions);
195
+ var prevSrcs = extractImageSrcs(prevValue);
354
196
  editor.events.on('change', function () { return __awaiter(_this, void 0, void 0, function () {
355
- var currentValue, currentAssets;
197
+ var currentValue, currentSrcs;
356
198
  return __generator(this, function (_a) {
357
199
  currentValue = editor.value || '';
358
- currentAssets = extractAssetsFromHtml(currentValue, fileLinkExtensions);
359
- prevAssets.images.forEach(function (src) {
360
- if (!currentAssets.images.has(src)) {
200
+ currentSrcs = extractImageSrcs(currentValue);
201
+ prevSrcs.forEach(function (src) {
202
+ if (!currentSrcs.has(src)) {
361
203
  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);
204
+ void onDeleteImage(src);
370
205
  }
371
206
  }
372
207
  });
373
208
  prevValue = currentValue;
374
- prevAssets = currentAssets;
209
+ prevSrcs = currentSrcs;
375
210
  return [2];
376
211
  });
377
212
  }); });
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.3.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,6 @@ export const config = ({
374
139
  'ol',
375
140
  '|',
376
141
  'image',
377
- 'file', // ✅ added (file uploads)
378
142
  '|',
379
143
  'video',
380
144
  '|',
@@ -398,49 +162,48 @@ export const config = ({
398
162
  };
399
163
 
400
164
  if (includeUploader) {
401
- // ✅ Use imageUrl as base for BOTH images + files (no new required props)
402
165
  base.uploader = uploaderConfig(apiUrl, imageUrl);
403
166
  }
404
167
 
405
- const hasDelete = Boolean(onDeleteImage || onDeleteFile);
406
-
407
- if (hasDelete) {
168
+ if (onDeleteImage) {
408
169
  base.events = {
409
170
  ...(base.events || {}),
410
-
411
171
  /**
412
- * Detect removed assets by diffing previous HTML vs current HTML.
413
- * - removed <img src="..."> => onDeleteImage
414
- * - removed <a href="..."> (matching fileLinkExtensions) => onDeleteFile
172
+ * We use the value diff to detect removed <img> src.
173
+ * This avoids false calls during upload (where DOM nodes can be replaced).
415
174
  */
416
175
  afterInit(editor: any) {
176
+ const extractImageSrcs = (html: string): Set<string> => {
177
+ const container = document.createElement('div');
178
+ container.innerHTML = html || '';
179
+ const imgs = Array.from(container.getElementsByTagName('img')) as HTMLImageElement[];
180
+
181
+ return new Set(
182
+ imgs
183
+ .map((img) => img.getAttribute('src') || '')
184
+ .filter((src) => !!src)
185
+ );
186
+ };
187
+
417
188
  let prevValue: string = editor.value || '';
418
- let prevAssets = extractAssetsFromHtml(prevValue, fileLinkExtensions);
189
+ let prevSrcs: Set<string> = extractImageSrcs(prevValue);
419
190
 
420
191
  editor.events.on('change', async () => {
421
192
  const currentValue: string = editor.value || '';
422
- const currentAssets = extractAssetsFromHtml(currentValue, fileLinkExtensions);
193
+ const currentSrcs = extractImageSrcs(currentValue);
423
194
 
424
- // Deleted images
425
- prevAssets.images.forEach((src) => {
426
- if (!currentAssets.images.has(src)) {
195
+ // src present before, not present now -> deleted
196
+ prevSrcs.forEach((src) => {
197
+ if (!currentSrcs.has(src)) {
198
+ // If imageUrl is defined, you can filter to only your own assets
427
199
  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);
200
+ void onDeleteImage(src);
438
201
  }
439
202
  }
440
203
  });
441
204
 
442
205
  prevValue = currentValue;
443
- prevAssets = currentAssets;
206
+ prevSrcs = currentSrcs;
444
207
  });
445
208
  },
446
209
  };
@@ -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>;