@mbs-dev/react-editor 1.1.0 → 1.2.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,6 +46,31 @@ 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
+ };
49
74
  var __importDefault = (this && this.__importDefault) || function (mod) {
50
75
  return (mod && mod.__esModule) ? mod : { "default": mod };
51
76
  };
@@ -57,8 +82,147 @@ var ReactEditor = function (_a) {
57
82
  var onChange = _a.onChange, onBlur = _a.onBlur, value = _a.value, config = _a.config;
58
83
  return (react_1.default.createElement(jodit_react_1.default, { value: value, config: config, onBlur: onBlur, onChange: onChange }));
59
84
  };
60
- var uploaderConfig = function (apiUrl, imageUrl) { return ({
61
- imagesExtensions: ['jpg', 'png', 'jpeg', 'gif', 'webp'],
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 [];
184
+ };
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;
199
+ };
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),
62
226
  filesVariableName: function (t) {
63
227
  return 'files[' + t + ']';
64
228
  },
@@ -69,45 +233,69 @@ var uploaderConfig = function (apiUrl, imageUrl) { return ({
69
233
  prepareData: function (formdata) {
70
234
  return formdata;
71
235
  },
72
- isSuccess: function (e) {
73
- var _a;
74
- var fn = this.jodit;
75
- if (((_a = e === null || e === void 0 ? void 0 : e.data) === null || _a === void 0 ? void 0 : _a.files) && e.data.files.length) {
76
- var tagName_1 = 'img';
77
- e.data.files.forEach(function (filename) {
78
- var elm = fn.createInside.element(tagName_1);
79
- var src = imageUrl ? "".concat(imageUrl, "/").concat(filename) : filename;
80
- elm.setAttribute('src', src);
81
- fn.s.insertImage(elm, null, fn.o.imageDefaultWidth);
82
- });
83
- }
84
- return !!(e === null || e === void 0 ? void 0 : e.success);
236
+ isSuccess: function (resp) {
237
+ return isSuccessResponse(resp);
85
238
  },
86
- getMessage: function (e) {
87
- var _a;
88
- return ((_a = e === null || e === void 0 ? void 0 : e.data) === null || _a === void 0 ? void 0 : _a.messages) && Array.isArray(e.data.messages)
89
- ? e.data.messages.join('')
90
- : '';
239
+ getMessage: function (resp) {
240
+ var msgs = extractMessages(resp);
241
+ return msgs.length ? msgs.join(' ') : '';
91
242
  },
92
243
  process: function (resp) {
93
- var files = [];
94
- files.unshift(resp === null || resp === void 0 ? void 0 : resp.data);
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);
95
251
  return {
96
- files: resp === null || resp === void 0 ? void 0 : resp.data,
97
- error: resp === null || resp === void 0 ? void 0 : resp.msg,
98
- msg: resp === null || resp === void 0 ? void 0 : resp.msg,
252
+ files: files,
253
+ path: '',
254
+ baseurl: '',
255
+ messages: messages,
256
+ msg: messages.join(' '),
99
257
  };
100
258
  },
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
+ },
101
287
  error: function (e) {
102
- this.j.e.fire('errorMessage', e.message, 'error', 4000);
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);
103
290
  },
104
291
  defaultHandlerError: function (e) {
105
- this.j.e.fire('errorMessage', (e === null || e === void 0 ? void 0 : e.message) || 'Upload error');
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');
106
294
  },
107
295
  }); };
108
296
  exports.uploaderConfig = uploaderConfig;
109
297
  var config = function (_a) {
110
- var _b = _a === void 0 ? {} : _a, includeUploader = _b.includeUploader, apiUrl = _b.apiUrl, imageUrl = _b.imageUrl, onDeleteImage = _b.onDeleteImage;
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;
111
299
  var base = {
112
300
  readonly: false,
113
301
  placeholder: 'Start typing...',
@@ -132,6 +320,7 @@ var config = function (_a) {
132
320
  'ol',
133
321
  '|',
134
322
  'image',
323
+ 'file',
135
324
  '|',
136
325
  'video',
137
326
  '|',
@@ -156,33 +345,33 @@ var config = function (_a) {
156
345
  if (includeUploader) {
157
346
  base.uploader = (0, exports.uploaderConfig)(apiUrl, imageUrl);
158
347
  }
159
- if (onDeleteImage) {
348
+ var hasDelete = Boolean(onDeleteImage || onDeleteFile);
349
+ if (hasDelete) {
160
350
  base.events = __assign(__assign({}, (base.events || {})), { afterInit: function (editor) {
161
351
  var _this = this;
162
- var extractImageSrcs = function (html) {
163
- var container = document.createElement('div');
164
- container.innerHTML = html || '';
165
- var imgs = Array.from(container.getElementsByTagName('img'));
166
- return new Set(imgs
167
- .map(function (img) { return img.getAttribute('src') || ''; })
168
- .filter(function (src) { return !!src; }));
169
- };
170
352
  var prevValue = editor.value || '';
171
- var prevSrcs = extractImageSrcs(prevValue);
353
+ var prevAssets = extractAssetsFromHtml(prevValue, fileLinkExtensions);
172
354
  editor.events.on('change', function () { return __awaiter(_this, void 0, void 0, function () {
173
- var currentValue, currentSrcs;
355
+ var currentValue, currentAssets;
174
356
  return __generator(this, function (_a) {
175
357
  currentValue = editor.value || '';
176
- currentSrcs = extractImageSrcs(currentValue);
177
- prevSrcs.forEach(function (src) {
178
- if (!currentSrcs.has(src)) {
358
+ currentAssets = extractAssetsFromHtml(currentValue, fileLinkExtensions);
359
+ prevAssets.images.forEach(function (src) {
360
+ if (!currentAssets.images.has(src)) {
179
361
  if (!imageUrl || src.startsWith(imageUrl)) {
180
- void onDeleteImage(src);
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);
181
370
  }
182
371
  }
183
372
  });
184
373
  prevValue = currentValue;
185
- prevSrcs = currentSrcs;
374
+ prevAssets = currentAssets;
186
375
  return [2];
187
376
  });
188
377
  }); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mbs-dev/react-editor",
3
- "version": "1.1.0",
3
+ "version": "1.2.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,6 +2,9 @@ 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
+ */
5
8
  const ReactEditor: React.FC<EditorProps> = ({ onChange, onBlur, value, config }) => {
6
9
  return (
7
10
  <JoditEditor
@@ -13,61 +16,303 @@ const ReactEditor: React.FC<EditorProps> = ({ onChange, onBlur, value, config })
13
16
  );
14
17
  };
15
18
 
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
+ };
101
+ };
102
+
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 };
211
+ };
212
+
16
213
  /**
17
214
  * Uploader configuration for Jodit
18
- * Handles image upload + insertion in the editor
215
+ * Handles image + file upload + insertion in the editor
19
216
  */
20
- export const uploaderConfig = (
21
- apiUrl?: string,
22
- imageUrl?: string
23
- ) => ({
24
- imagesExtensions: ['jpg', 'png', 'jpeg', 'gif', 'webp'],
217
+ export const uploaderConfig = (apiUrl?: string, assetBaseUrl?: string) => ({
218
+ insertImageAsBase64URI: false,
219
+
220
+ imagesExtensions: [...DEFAULT_IMAGE_EXTENSIONS],
221
+
25
222
  filesVariableName(t: number): string {
26
223
  return 'files[' + t + ']';
27
224
  },
225
+
28
226
  url: apiUrl,
29
227
  withCredentials: false,
30
228
  format: 'json',
31
229
  method: 'POST',
230
+
32
231
  prepareData(formdata: FormData): FormData {
33
232
  return formdata;
34
233
  },
35
- isSuccess(this: any, e: any): boolean {
36
- const fn = this.jodit;
37
-
38
- if (e?.data?.files && e.data.files.length) {
39
- const tagName = 'img';
40
-
41
- e.data.files.forEach((filename: string) => {
42
- const elm = fn.createInside.element(tagName);
43
- const src = imageUrl ? `${imageUrl}/${filename}` : filename;
44
- elm.setAttribute('src', src);
45
- fn.s.insertImage(elm as HTMLImageElement, null, fn.o.imageDefaultWidth);
46
- });
47
- }
48
234
 
49
- return !!e?.success;
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);
50
240
  },
51
- getMessage(e: any): string {
52
- return e?.data?.messages && Array.isArray(e.data.messages)
53
- ? e.data.messages.join('')
54
- : '';
241
+
242
+ getMessage(resp: unknown): string {
243
+ const msgs = extractMessages(resp);
244
+ return msgs.length ? msgs.join(' ') : '';
55
245
  },
56
- process(resp: any): { files: any[]; error: string; msg: string } {
57
- const files: any[] = [];
58
- files.unshift(resp?.data);
59
246
 
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);
60
260
  return {
61
- files: resp?.data,
62
- error: resp?.msg,
63
- msg: resp?.msg,
261
+ files,
262
+ path: '',
263
+ baseurl: '',
264
+ messages,
265
+ msg: messages.join(' '),
64
266
  };
65
267
  },
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
+
66
310
  error(this: any, e: Error): void {
67
- this.j.e.fire('errorMessage', e.message, 'error', 4000);
311
+ this.j?.e?.fire?.('errorMessage', e.message, 'error', 4000);
68
312
  },
313
+
69
314
  defaultHandlerError(this: any, e: any): void {
70
- this.j.e.fire('errorMessage', e?.message || 'Upload error');
315
+ this.j?.e?.fire?.('errorMessage', e?.message || 'Upload error');
71
316
  },
72
317
  });
73
318
 
@@ -75,11 +320,22 @@ type ConfigParams = {
75
320
  includeUploader?: boolean;
76
321
  apiUrl?: string;
77
322
  imageUrl?: string;
323
+
78
324
  /**
79
325
  * Called when an image is really removed from the editor content.
80
326
  * You receive the image src URL and can call your API to delete it on server.
81
327
  */
82
328
  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[];
83
339
  };
84
340
 
85
341
  /**
@@ -90,6 +346,8 @@ export const config = ({
90
346
  apiUrl,
91
347
  imageUrl,
92
348
  onDeleteImage,
349
+ onDeleteFile,
350
+ fileLinkExtensions = DEFAULT_FILE_LINK_EXTENSIONS,
93
351
  }: ConfigParams = {}) => {
94
352
  const base: any = {
95
353
  readonly: false,
@@ -116,6 +374,7 @@ export const config = ({
116
374
  'ol',
117
375
  '|',
118
376
  'image',
377
+ 'file', // ✅ added (file uploads)
119
378
  '|',
120
379
  'video',
121
380
  '|',
@@ -139,48 +398,49 @@ export const config = ({
139
398
  };
140
399
 
141
400
  if (includeUploader) {
401
+ // ✅ Use imageUrl as base for BOTH images + files (no new required props)
142
402
  base.uploader = uploaderConfig(apiUrl, imageUrl);
143
403
  }
144
404
 
145
- if (onDeleteImage) {
405
+ const hasDelete = Boolean(onDeleteImage || onDeleteFile);
406
+
407
+ if (hasDelete) {
146
408
  base.events = {
147
409
  ...(base.events || {}),
410
+
148
411
  /**
149
- * We use the value diff to detect removed <img> src.
150
- * This avoids false calls during upload (where DOM nodes can be replaced).
412
+ * Detect removed assets by diffing previous HTML vs current HTML.
413
+ * - removed <img src="..."> => onDeleteImage
414
+ * - removed <a href="..."> (matching fileLinkExtensions) => onDeleteFile
151
415
  */
152
416
  afterInit(editor: any) {
153
- const extractImageSrcs = (html: string): Set<string> => {
154
- const container = document.createElement('div');
155
- container.innerHTML = html || '';
156
- const imgs = Array.from(container.getElementsByTagName('img')) as HTMLImageElement[];
157
-
158
- return new Set(
159
- imgs
160
- .map((img) => img.getAttribute('src') || '')
161
- .filter((src) => !!src)
162
- );
163
- };
164
-
165
417
  let prevValue: string = editor.value || '';
166
- let prevSrcs: Set<string> = extractImageSrcs(prevValue);
418
+ let prevAssets = extractAssetsFromHtml(prevValue, fileLinkExtensions);
167
419
 
168
420
  editor.events.on('change', async () => {
169
421
  const currentValue: string = editor.value || '';
170
- const currentSrcs = extractImageSrcs(currentValue);
422
+ const currentAssets = extractAssetsFromHtml(currentValue, fileLinkExtensions);
171
423
 
172
- // src present before, not present now -> deleted
173
- prevSrcs.forEach((src) => {
174
- if (!currentSrcs.has(src)) {
175
- // If imageUrl is defined, you can filter to only your own assets
424
+ // Deleted images
425
+ prevAssets.images.forEach((src) => {
426
+ if (!currentAssets.images.has(src)) {
176
427
  if (!imageUrl || src.startsWith(imageUrl)) {
177
- void onDeleteImage(src);
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);
178
438
  }
179
439
  }
180
440
  });
181
441
 
182
442
  prevValue = currentValue;
183
- prevSrcs = currentSrcs;
443
+ prevAssets = currentAssets;
184
444
  });
185
445
  },
186
446
  };
@@ -5,8 +5,9 @@ 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>;
12
+ export type OnDeleteImage = (src: string) => void | Promise<void>;
13
+ export type OnDeleteFile = (href: string) => void | Promise<void>;
package/types/Editor.d.ts CHANGED
@@ -1,7 +1,16 @@
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 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;
5
14
  imagesExtensions: string[];
6
15
  filesVariableName(t: number): string;
7
16
  url: string | undefined;
@@ -9,13 +18,16 @@ export declare const uploaderConfig: (apiUrl?: string, imageUrl?: string) => {
9
18
  format: string;
10
19
  method: string;
11
20
  prepareData(formdata: FormData): FormData;
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;
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;
18
29
  };
30
+ defaultHandlerSuccess(this: UploaderThis, data: unknown): void;
19
31
  error(this: any, e: Error): void;
20
32
  defaultHandlerError(this: any, e: any): void;
21
33
  };
@@ -24,6 +36,8 @@ type ConfigParams = {
24
36
  apiUrl?: string;
25
37
  imageUrl?: string;
26
38
  onDeleteImage?: (imageUrl: string) => void | Promise<void>;
39
+ onDeleteFile?: (fileHref: string) => void | Promise<void>;
40
+ fileLinkExtensions?: readonly string[];
27
41
  };
28
- export declare const config: ({ includeUploader, apiUrl, imageUrl, onDeleteImage, }?: ConfigParams) => any;
42
+ export declare const config: ({ includeUploader, apiUrl, imageUrl, onDeleteImage, onDeleteFile, fileLinkExtensions, }?: ConfigParams) => any;
29
43
  export default ReactEditor;
@@ -8,3 +8,4 @@ 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>;