suneditor 3.0.0-beta.2 → 3.0.0-beta.20

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.
Files changed (177) hide show
  1. package/CONTRIBUTING.md +186 -184
  2. package/LICENSE +21 -21
  3. package/README.md +157 -180
  4. package/dist/suneditor.min.css +1 -1
  5. package/dist/suneditor.min.js +1 -1
  6. package/package.json +126 -123
  7. package/src/assets/design/color.css +131 -121
  8. package/src/assets/design/index.css +3 -3
  9. package/src/assets/design/size.css +37 -35
  10. package/src/assets/design/typography.css +37 -37
  11. package/src/assets/icons/defaultIcons.js +247 -232
  12. package/src/assets/suneditor-contents.css +779 -778
  13. package/src/assets/suneditor.css +43 -35
  14. package/src/core/base/eventHandlers/handler_toolbar.js +135 -135
  15. package/src/core/base/eventHandlers/handler_ww_clipboard.js +56 -56
  16. package/src/core/base/eventHandlers/handler_ww_dragDrop.js +115 -113
  17. package/src/core/base/eventHandlers/handler_ww_key_input.js +1200 -1200
  18. package/src/core/base/eventHandlers/handler_ww_mouse.js +194 -194
  19. package/src/core/base/eventManager.js +1550 -1484
  20. package/src/core/base/history.js +355 -355
  21. package/src/core/class/char.js +163 -162
  22. package/src/core/class/component.js +856 -842
  23. package/src/core/class/format.js +3433 -3422
  24. package/src/core/class/html.js +1927 -1890
  25. package/src/core/class/menu.js +357 -346
  26. package/src/core/class/nodeTransform.js +424 -424
  27. package/src/core/class/offset.js +858 -891
  28. package/src/core/class/selection.js +710 -620
  29. package/src/core/class/shortcuts.js +98 -98
  30. package/src/core/class/toolbar.js +438 -430
  31. package/src/core/class/ui.js +424 -422
  32. package/src/core/class/viewer.js +750 -750
  33. package/src/core/editor.js +1810 -1708
  34. package/src/core/section/actives.js +268 -241
  35. package/src/core/section/constructor.js +1348 -1661
  36. package/src/core/section/context.js +102 -102
  37. package/src/core/section/documentType.js +582 -561
  38. package/src/core/section/options.js +367 -0
  39. package/src/core/util/instanceCheck.js +59 -0
  40. package/src/editorInjector/_classes.js +36 -36
  41. package/src/editorInjector/_core.js +92 -92
  42. package/src/editorInjector/index.js +75 -75
  43. package/src/events.js +634 -622
  44. package/src/helper/clipboard.js +59 -59
  45. package/src/helper/converter.js +586 -564
  46. package/src/helper/dom/domCheck.js +304 -304
  47. package/src/helper/dom/domQuery.js +677 -669
  48. package/src/helper/dom/domUtils.js +618 -557
  49. package/src/helper/dom/index.js +12 -12
  50. package/src/helper/env.js +249 -240
  51. package/src/helper/index.js +25 -25
  52. package/src/helper/keyCodeMap.js +183 -183
  53. package/src/helper/numbers.js +72 -72
  54. package/src/helper/unicode.js +47 -47
  55. package/src/langs/ckb.js +231 -231
  56. package/src/langs/cs.js +231 -231
  57. package/src/langs/da.js +231 -231
  58. package/src/langs/de.js +231 -231
  59. package/src/langs/en.js +230 -230
  60. package/src/langs/es.js +231 -231
  61. package/src/langs/fa.js +231 -231
  62. package/src/langs/fr.js +231 -231
  63. package/src/langs/he.js +231 -231
  64. package/src/langs/hu.js +230 -230
  65. package/src/langs/index.js +28 -28
  66. package/src/langs/it.js +231 -231
  67. package/src/langs/ja.js +230 -230
  68. package/src/langs/km.js +230 -230
  69. package/src/langs/ko.js +230 -230
  70. package/src/langs/lv.js +231 -231
  71. package/src/langs/nl.js +231 -231
  72. package/src/langs/pl.js +231 -231
  73. package/src/langs/pt_br.js +231 -231
  74. package/src/langs/ro.js +231 -231
  75. package/src/langs/ru.js +231 -231
  76. package/src/langs/se.js +231 -231
  77. package/src/langs/tr.js +231 -231
  78. package/src/langs/uk.js +231 -231
  79. package/src/langs/ur.js +231 -231
  80. package/src/langs/zh_cn.js +231 -231
  81. package/src/modules/ApiManager.js +191 -191
  82. package/src/modules/Browser.js +669 -667
  83. package/src/modules/ColorPicker.js +364 -362
  84. package/src/modules/Controller.js +474 -454
  85. package/src/modules/Figure.js +1620 -1617
  86. package/src/modules/FileManager.js +359 -359
  87. package/src/modules/HueSlider.js +577 -565
  88. package/src/modules/Modal.js +346 -346
  89. package/src/modules/ModalAnchorEditor.js +643 -643
  90. package/src/modules/SelectMenu.js +549 -549
  91. package/src/modules/_DragHandle.js +17 -17
  92. package/src/modules/index.js +14 -14
  93. package/src/plugins/browser/audioGallery.js +83 -83
  94. package/src/plugins/browser/fileBrowser.js +103 -103
  95. package/src/plugins/browser/fileGallery.js +83 -83
  96. package/src/plugins/browser/imageGallery.js +81 -81
  97. package/src/plugins/browser/videoGallery.js +103 -103
  98. package/src/plugins/command/blockquote.js +61 -60
  99. package/src/plugins/command/exportPDF.js +134 -134
  100. package/src/plugins/command/fileUpload.js +456 -456
  101. package/src/plugins/command/list_bulleted.js +149 -148
  102. package/src/plugins/command/list_numbered.js +152 -151
  103. package/src/plugins/dropdown/align.js +157 -155
  104. package/src/plugins/dropdown/backgroundColor.js +108 -104
  105. package/src/plugins/dropdown/font.js +141 -137
  106. package/src/plugins/dropdown/fontColor.js +109 -105
  107. package/src/plugins/dropdown/formatBlock.js +170 -178
  108. package/src/plugins/dropdown/hr.js +152 -152
  109. package/src/plugins/dropdown/layout.js +83 -83
  110. package/src/plugins/dropdown/lineHeight.js +131 -130
  111. package/src/plugins/dropdown/list.js +123 -122
  112. package/src/plugins/dropdown/paragraphStyle.js +138 -138
  113. package/src/plugins/dropdown/table.js +4110 -4000
  114. package/src/plugins/dropdown/template.js +83 -83
  115. package/src/plugins/dropdown/textStyle.js +149 -149
  116. package/src/plugins/field/mention.js +242 -242
  117. package/src/plugins/index.js +120 -120
  118. package/src/plugins/input/fontSize.js +414 -410
  119. package/src/plugins/input/pageNavigator.js +71 -70
  120. package/src/plugins/modal/audio.js +677 -677
  121. package/src/plugins/modal/drawing.js +537 -531
  122. package/src/plugins/modal/embed.js +886 -886
  123. package/src/plugins/modal/image.js +1377 -1376
  124. package/src/plugins/modal/link.js +248 -240
  125. package/src/plugins/modal/math.js +563 -563
  126. package/src/plugins/modal/video.js +1226 -1226
  127. package/src/plugins/popup/anchor.js +224 -222
  128. package/src/suneditor.js +114 -107
  129. package/src/themes/dark.css +132 -122
  130. package/src/typedef.js +132 -130
  131. package/types/assets/icons/defaultIcons.d.ts +8 -0
  132. package/types/core/base/eventManager.d.ts +29 -4
  133. package/types/core/class/char.d.ts +2 -1
  134. package/types/core/class/component.d.ts +1 -2
  135. package/types/core/class/format.d.ts +8 -1
  136. package/types/core/class/html.d.ts +8 -0
  137. package/types/core/class/menu.d.ts +8 -0
  138. package/types/core/class/offset.d.ts +24 -26
  139. package/types/core/class/selection.d.ts +2 -0
  140. package/types/core/class/toolbar.d.ts +6 -0
  141. package/types/core/class/ui.d.ts +1 -1
  142. package/types/core/editor.d.ts +34 -12
  143. package/types/core/section/constructor.d.ts +5 -638
  144. package/types/core/section/documentType.d.ts +12 -2
  145. package/types/core/section/options.d.ts +740 -0
  146. package/types/core/util/instanceCheck.d.ts +50 -0
  147. package/types/editorInjector/_core.d.ts +5 -5
  148. package/types/editorInjector/index.d.ts +2 -2
  149. package/types/events.d.ts +2 -0
  150. package/types/helper/converter.d.ts +9 -0
  151. package/types/helper/dom/domQuery.d.ts +5 -5
  152. package/types/helper/dom/domUtils.d.ts +8 -0
  153. package/types/helper/env.d.ts +6 -1
  154. package/types/helper/index.d.ts +4 -1
  155. package/types/index.d.ts +122 -120
  156. package/types/langs/_Lang.d.ts +194 -194
  157. package/types/modules/ColorPicker.d.ts +5 -1
  158. package/types/modules/Controller.d.ts +8 -4
  159. package/types/modules/Figure.d.ts +2 -1
  160. package/types/modules/HueSlider.d.ts +4 -1
  161. package/types/modules/SelectMenu.d.ts +1 -1
  162. package/types/plugins/command/blockquote.d.ts +1 -0
  163. package/types/plugins/command/list_bulleted.d.ts +1 -0
  164. package/types/plugins/command/list_numbered.d.ts +1 -0
  165. package/types/plugins/dropdown/align.d.ts +1 -0
  166. package/types/plugins/dropdown/backgroundColor.d.ts +1 -0
  167. package/types/plugins/dropdown/font.d.ts +1 -0
  168. package/types/plugins/dropdown/fontColor.d.ts +1 -0
  169. package/types/plugins/dropdown/formatBlock.d.ts +3 -2
  170. package/types/plugins/dropdown/lineHeight.d.ts +1 -0
  171. package/types/plugins/dropdown/list.d.ts +1 -0
  172. package/types/plugins/dropdown/table.d.ts +6 -0
  173. package/types/plugins/input/fontSize.d.ts +1 -0
  174. package/types/plugins/modal/drawing.d.ts +4 -0
  175. package/types/plugins/modal/link.d.ts +32 -15
  176. package/types/suneditor.d.ts +13 -9
  177. package/types/typedef.d.ts +8 -0
@@ -1,456 +1,456 @@
1
- import EditorInjector from '../../editorInjector';
2
- import { dom, env, numbers } from '../../helper';
3
- import { FileManager, Figure, Controller } from '../../modules';
4
-
5
- const { NO_EVENT } = env;
6
-
7
- /**
8
- * @class
9
- * @description File upload plugin
10
- */
11
- class FileUpload extends EditorInjector {
12
- static key = 'fileUpload';
13
- static type = 'command';
14
- static className = '';
15
- static options = { eventIndex: 10000 };
16
- /**
17
- * @this {FileUpload}
18
- * @param {HTMLElement} node - The node to check.
19
- * @returns {HTMLElement|null} Returns a node if the node is a valid component.
20
- */
21
- static component(node) {
22
- return dom.check.isAnchor(node) && node.hasAttribute('data-se-file-download') ? node : null;
23
- }
24
-
25
- /**
26
- * @constructor
27
- * @param {__se__EditorCore} editor - The root editor instance
28
- * @param {Object} pluginOptions - plugin options
29
- * @param {string} pluginOptions.uploadUrl - server request url
30
- * @param {Object<string, string>=} pluginOptions.uploadHeaders - server request headers
31
- * @param {string=} pluginOptions.uploadSizeLimit - upload size limit
32
- * @param {string=} pluginOptions.uploadSingleSizeLimit - upload single size limit
33
- * @param {boolean=} pluginOptions.allowMultiple - allow multiple files
34
- * @param {string=} pluginOptions.acceptedFormats - accepted formats
35
- * @param {string=} pluginOptions.as - Whether to use the 'Box' or 'Link' conversion button
36
- * @param {Array<string>} pluginOptions.controls - Additional controls to be added to the figure
37
- */
38
- constructor(editor, pluginOptions) {
39
- super(editor);
40
- // plugin basic properties
41
- this.title = this.lang.fileUpload;
42
- this.icon = 'file_upload';
43
-
44
- if (!pluginOptions.uploadUrl) console.warn('[SUNEDITOR.fileUpload.warn] "fileUpload" plugin must be have "uploadUrl" option.');
45
-
46
- // members
47
- this.uploadUrl = pluginOptions.uploadUrl;
48
- this.uploadHeaders = pluginOptions.uploadHeaders;
49
- this.uploadSizeLimit = numbers.get(pluginOptions.uploadSizeLimit, 0);
50
- this.uploadSingleSizeLimit = numbers.get(pluginOptions.uploadSingleSizeLimit, 0);
51
- this.allowMultiple = !!pluginOptions.allowMultiple;
52
- this.acceptedFormats = typeof pluginOptions.acceptedFormats !== 'string' ? '*' : pluginOptions.acceptedFormats.trim() || '*';
53
- this._acceptedCheck = this.acceptedFormats.split(', ');
54
- this.as = pluginOptions.as || 'box';
55
- this.input = dom.utils.createElement('input', { type: 'file', accept: this.acceptedFormats });
56
- if (this.allowMultiple) {
57
- this.input.setAttribute('multiple', 'multiple');
58
- }
59
- this._element = null;
60
-
61
- // figure
62
- const customItems = {
63
- 'custom-download': {
64
- command: 'download',
65
- title: this.lang.download,
66
- icon: 'download',
67
- action: (target) => {
68
- const url = target.getAttribute('href');
69
- if (url) dom.utils.createElement('A', { href: url }, null).click();
70
- }
71
- },
72
- 'custom-as': {
73
- command: 'as',
74
- value: 'link', // 'block' or 'link'
75
- title: this.lang.asLink,
76
- icon: 'reduction',
77
- action: (target, value) => {
78
- this.convertFormat(target, value);
79
- }
80
- }
81
- };
82
-
83
- const figureControls = (pluginOptions.controls || [['custom-as', 'align', 'edit', 'custom-download', 'copy', 'remove']]).map((subArray) => subArray.map((item) => (item.startsWith('custom-') ? customItems[item] : item)));
84
- this.figure = new Figure(this, figureControls, {});
85
-
86
- // file manager
87
- this.fileManager = new FileManager(this, {
88
- query: 'a[download][data-se-file-download]',
89
- loadHandler: this.events.onFileLoad,
90
- eventHandler: this.events.onFileAction
91
- });
92
-
93
- // controller
94
- if (/\bedit\b/.test(JSON.stringify(figureControls))) {
95
- const controllerEl = CreateHTML_controller(this);
96
- this.controller = new Controller(this, controllerEl, { position: 'bottom', disabled: true }, FileUpload.key);
97
- this.editInput = controllerEl.querySelector('input');
98
- }
99
-
100
- // init
101
- this.eventManager.addEvent(this.input, 'change', this.#OnChangeFile.bind(this));
102
- }
103
-
104
- /**
105
- * @editorMethod Editor.core
106
- * @description Executes the main execution method of the plugin.
107
- * - It is executed by clicking a toolbar "command" button or calling an API.
108
- */
109
- action() {
110
- this.editor._preventBlur = true;
111
- this.input.click();
112
- }
113
-
114
- /**
115
- * @editorMethod Editor.Component
116
- * @description Executes the method that is called when a component of a plugin is selected.
117
- * @param {HTMLElement} target Target component element
118
- */
119
- select(target) {
120
- this._element = target;
121
- const asBtn = this.figure.controller.form.querySelector('[data-command="__c__as"]');
122
- if (dom.check.isFigure(target.parentElement)) {
123
- asBtn.innerHTML = this.icons.reduction + dom.utils.createTooltipInner(this.lang.asLink);
124
- asBtn.setAttribute('data-value', 'link');
125
- this.figure.open(target, { nonResizing: true, nonSizeInfo: true, nonBorder: true, figureTarget: true, __fileManagerInfo: false });
126
- } else {
127
- asBtn.innerHTML = this.icons.expansion + dom.utils.createTooltipInner(this.lang.asBlock);
128
- asBtn.setAttribute('data-value', 'box');
129
- this.figure.controllerOpen(target, { isWWTarget: true });
130
- return true;
131
- }
132
- }
133
-
134
- /**
135
- * @editorMethod Editor.EventManager
136
- * @description Executes the event function of "paste" or "drop".
137
- * @param {Object} params { frameContext, event, file }
138
- * @param {__se__FrameContext} params.frameContext Frame context
139
- * @param {ClipboardEvent} params.event Event object
140
- * @param {File} params.file File object
141
- * @returns {boolean} - If return false, the file upload will be canceled
142
- */
143
- onFilePasteAndDrop({ file }) {
144
- const fileType = file.type;
145
- if (
146
- !this._acceptedCheck.some((format) => {
147
- if (format.startsWith('*')) return true;
148
- if (format.startsWith(fileType)) return true;
149
- })
150
- ) {
151
- return;
152
- }
153
-
154
- this.submitFile([file]);
155
- this.editor.focus();
156
-
157
- return false;
158
- }
159
-
160
- /**
161
- * @editorMethod Modules.Controller
162
- * @description Executes the method that is called when a target component is edited.
163
- * @param {HTMLElement|Text} target Target element
164
- */
165
- edit(target) {
166
- this.editInput.value = target.textContent;
167
- this.figure.controllerHide();
168
- this.controller.open(target, null, { isWWTarget: !dom.check.isFigure(target.parentElement), initMethod: null, addOffset: null });
169
- this.editInput.focus();
170
- }
171
-
172
- /**
173
- * @editorMethod Modules.Controller
174
- * @description Executes the method that is called when a button is clicked in the "controller".
175
- * @param {HTMLButtonElement} target Target button element
176
- */
177
- controllerAction(target) {
178
- const command = target.getAttribute('data-command');
179
- if (!command) return;
180
-
181
- if (command === 'edit') {
182
- if (this.editInput.value.trim().length === 0) return;
183
- this._element.textContent = this.editInput.value;
184
- }
185
-
186
- this.controller.close();
187
- this.component.select(this._element, FileUpload.key);
188
- }
189
-
190
- /**
191
- * @editorMethod Editor.Component
192
- * @description Method to delete a component of a plugin, called by the "FileManager", "Controller" module.
193
- * @param {HTMLElement} target Target element
194
- * @returns {Promise<void>}
195
- */
196
- async destroy(target) {
197
- if (!target) return;
198
-
199
- const figure = Figure.GetContainer(target);
200
- const containerTarget = dom.query.getParentElement(target, '.se-component') || target;
201
-
202
- const message = await this.triggerEvent('onFileDeleteBefore', { element: figure.target, container: figure, url: figure.target.getAttribute('href') });
203
- if (message === false) return;
204
-
205
- const isInlineComp = this.component.isInline(containerTarget);
206
- const focusEl = isInlineComp ? containerTarget.previousSibling || containerTarget.nextSibling : containerTarget.previousElementSibling || containerTarget.nextElementSibling;
207
- dom.utils.removeItem(containerTarget);
208
- this.ui._offCurrentController();
209
-
210
- this.editor.focusEdge(focusEl);
211
- this.history.push(false);
212
- }
213
-
214
- /**
215
- * @description Create an "file" component using the provided files.
216
- * @param {File[]|FileList} fileList File object list
217
- * @returns {Promise<boolean>} If return false, the file upload will be canceled
218
- */
219
- async submitFile(fileList) {
220
- if (fileList.length === 0) return;
221
-
222
- let fileSize = 0;
223
- const files = [];
224
- const slngleSizeLimit = this.uploadSingleSizeLimit;
225
- for (let i = 0, len = fileList.length, f, s; i < len; i++) {
226
- f = fileList[i];
227
- s = f.size;
228
- if (slngleSizeLimit && slngleSizeLimit > s) {
229
- const err = '[SUNEDITOR.fileUpload.fail] Size of uploadable single file: ' + slngleSizeLimit / 1000 + 'KB';
230
- const message = await this.triggerEvent('onFileUploadError', {
231
- error: err,
232
- limitSize: slngleSizeLimit,
233
- uploadSize: s,
234
- file: f
235
- });
236
-
237
- this.ui.alertOpen(message === NO_EVENT ? err : message || err, 'error');
238
-
239
- return false;
240
- }
241
-
242
- files.push(f);
243
- fileSize += s;
244
- }
245
-
246
- const limitSize = this.uploadSizeLimit;
247
- const currentSize = this.fileManager.getSize();
248
- if (limitSize > 0 && fileSize + currentSize > limitSize) {
249
- const err = '[SUNEDITOR.fileUpload.fail] Size of uploadable total files: ' + limitSize / 1000 + 'KB';
250
- const message = await this.triggerEvent('onFileUploadError', {
251
- error: err,
252
- limitSize,
253
- currentSize,
254
- uploadSize: fileSize
255
- });
256
-
257
- this.ui.alertOpen(message === NO_EVENT ? err : message || err, 'error');
258
-
259
- return false;
260
- }
261
-
262
- const fileInfo = {
263
- url: this.uploadUrl,
264
- uploadHeaders: this.uploadHeaders,
265
- files
266
- };
267
-
268
- const handler = async function (uploadCallback, infos, newInfos) {
269
- infos = newInfos || infos;
270
- const xmlHttp = await this.fileManager.asyncUpload(infos.url, infos.uploadHeaders, infos.files);
271
- uploadCallback(xmlHttp);
272
- }.bind(this, this.#_uploadCallBack.bind(this), fileInfo);
273
-
274
- const result = await this.triggerEvent('onFileUploadBefore', {
275
- info: fileInfo,
276
- handler
277
- });
278
-
279
- if (result === undefined) return true;
280
- if (result === false) return false;
281
- if (result !== null && typeof result === 'object') handler(result);
282
-
283
- if (result === true || result === NO_EVENT) handler(null);
284
- }
285
-
286
- /**
287
- * @description Convert format to link or block
288
- * @param {HTMLElement} target Target element
289
- * @param {string} value 'link' or 'block'
290
- */
291
- convertFormat(target, value) {
292
- if (value === 'link') {
293
- this.figure.close();
294
- const { container } = Figure.GetContainer(target);
295
- const next = container.nextElementSibling;
296
- const parent = container.parentElement;
297
-
298
- target.removeAttribute('data-se-non-focus');
299
- target.setAttribute('contenteditable', 'false');
300
- dom.utils.addClass(target, 'se-component|se-inline-component');
301
-
302
- const line = dom.utils.createElement(this.options.get('defaultLine'), null, target);
303
- parent.insertBefore(line, next);
304
- dom.utils.removeItem(container);
305
- } else {
306
- // block
307
- this.selection.setRange(target, 0, target, 1);
308
- const r = this.html.remove();
309
- const s = this.nodeTransform.split(r.container, r.offset, 0);
310
-
311
- if (s?.previousElementSibling && dom.check.isZeroWidth(s.previousElementSibling)) {
312
- dom.utils.removeItem(s.previousElementSibling);
313
- }
314
-
315
- target.setAttribute('data-se-non-focus', 'true');
316
- target.removeAttribute('contenteditable');
317
- dom.utils.removeClass(target, 'se-component|se-component-selected|se-inline-component');
318
-
319
- const figure = Figure.CreateContainer(target, 'se-file-figure se-flex-component');
320
- (s || r.container).parentElement.insertBefore(figure.container, s);
321
- }
322
-
323
- this.history.push(false);
324
- this.component.select(target, FileUpload.key);
325
- }
326
-
327
- /**
328
- * @description Create file element
329
- * @param {string} url File URL
330
- * @param {File|{name: string, size: number}} file File object
331
- * @param {boolean} isLast Is last file
332
- */
333
- create(url, file, isLast) {
334
- const name = file.name || url;
335
- const a = dom.utils.createElement(
336
- 'A',
337
- {
338
- href: url,
339
- title: name,
340
- download: name,
341
- 'data-se-file-download': '',
342
- contenteditable: 'false',
343
- 'data-se-non-focus': 'true',
344
- 'data-se-non-link': 'true'
345
- },
346
- name
347
- );
348
-
349
- this.fileManager.setFileData(a, file);
350
-
351
- if (this.as === 'link') {
352
- a.className = 'se-component se-inline-component';
353
- this.component.insert(a, { skipCharCount: false, skipSelection: false, skipHistory: false });
354
- return;
355
- }
356
-
357
- const figure = Figure.CreateContainer(a);
358
- dom.utils.addClass(figure.container, 'se-file-figure|se-flex-component');
359
-
360
- if (!this.component.insert(figure.container, { skipCharCount: false, skipSelection: isLast ? !this.options.get('componentAutoSelect') : true, skipHistory: false })) {
361
- this.editor.focus();
362
- return;
363
- }
364
-
365
- if (!isLast) return;
366
-
367
- if (!this.options.get('componentAutoSelect')) {
368
- const line = this.format.addLine(figure.container, null);
369
- if (line) this.selection.setRange(line, 0, line, 0);
370
- } else {
371
- this.component.select(a, FileUpload.key);
372
- }
373
- }
374
-
375
- /**
376
- * @private
377
- * @description Processes the server response after file upload.
378
- * - Registers the uploaded files in the editor.
379
- * @param {Object<string, *>} response - The response object from the server.
380
- */
381
- _register(response) {
382
- response.result.forEach((file, i, a) => {
383
- this.create(
384
- file.url,
385
- {
386
- name: file.name,
387
- size: file.size
388
- },
389
- i === a.length - 1
390
- );
391
- });
392
- }
393
-
394
- /**
395
- * @private
396
- * @description Handles file upload errors.
397
- * - Displays an error message if the upload fails.
398
- * @param {Object<string, *>} response - The error response from the server.
399
- * @returns {Promise<void>}
400
- */
401
- async _error(response) {
402
- const message = await this.triggerEvent('onFileUploadError', { error: response });
403
- if (message === false) return;
404
- const err = message === NO_EVENT ? response.errorMessage : message || response.errorMessage;
405
- this.ui.alertOpen(err, 'error');
406
- console.error('[SUNEDITOR.plugin.fileUpload.error]', err);
407
- }
408
-
409
- /**
410
- * @description Handles the file upload completion callback.
411
- * - Parses the response and registers the uploaded file.
412
- * @param {XMLHttpRequest} xmlHttp - The completed XHR request.
413
- */
414
- #_uploadCallBack(xmlHttp) {
415
- const response = JSON.parse(xmlHttp.responseText);
416
- if (response.errorMessage) {
417
- this._error(response);
418
- } else {
419
- this._register(response);
420
- }
421
- }
422
-
423
- /**
424
- * @description Handles the change event when a file is selected.
425
- * - Triggers the file upload process.
426
- * @param {InputEvent} e - The change event object.
427
- */
428
- async #OnChangeFile(e) {
429
- /** @type {HTMLInputElement} */
430
- const eventTarget = dom.query.getEventTarget(e);
431
- await this.submitFile(eventTarget.files);
432
- }
433
- }
434
-
435
- function CreateHTML_controller({ lang, icons }) {
436
- const html = /*html*/ `
437
- <div class="se-arrow se-arrow-up"></div>
438
- <form>
439
- <div class="se-btn-group se-form-group">
440
- <input type="text" />
441
- <button type="submit" data-command="edit" class="se-btn se-tooltip se-btn-success">
442
- ${icons.checked}
443
- <span class="se-tooltip-inner"><span class="se-tooltip-text">${lang.save}</span></span>
444
- </button>
445
- <button type="button" data-command="cancel" class="se-btn se-tooltip se-btn-danger">
446
- ${icons.cancel}
447
- <span class="se-tooltip-inner"><span class="se-tooltip-text">${lang.cancel}</span></span>
448
- </button>
449
- </div>
450
- </form>
451
- `;
452
-
453
- return dom.utils.createElement('DIV', { class: 'se-controller se-controller-simple-input' }, html);
454
- }
455
-
456
- export default FileUpload;
1
+ import EditorInjector from '../../editorInjector';
2
+ import { dom, env, numbers } from '../../helper';
3
+ import { FileManager, Figure, Controller } from '../../modules';
4
+
5
+ const { NO_EVENT } = env;
6
+
7
+ /**
8
+ * @class
9
+ * @description File upload plugin
10
+ */
11
+ class FileUpload extends EditorInjector {
12
+ static key = 'fileUpload';
13
+ static type = 'command';
14
+ static className = '';
15
+ static options = { eventIndex: 10000 };
16
+ /**
17
+ * @this {FileUpload}
18
+ * @param {HTMLElement} node - The node to check.
19
+ * @returns {HTMLElement|null} Returns a node if the node is a valid component.
20
+ */
21
+ static component(node) {
22
+ return dom.check.isAnchor(node) && node.hasAttribute('data-se-file-download') ? node : null;
23
+ }
24
+
25
+ /**
26
+ * @constructor
27
+ * @param {__se__EditorCore} editor - The root editor instance
28
+ * @param {Object} pluginOptions - plugin options
29
+ * @param {string} pluginOptions.uploadUrl - server request url
30
+ * @param {Object<string, string>=} pluginOptions.uploadHeaders - server request headers
31
+ * @param {string=} pluginOptions.uploadSizeLimit - upload size limit
32
+ * @param {string=} pluginOptions.uploadSingleSizeLimit - upload single size limit
33
+ * @param {boolean=} pluginOptions.allowMultiple - allow multiple files
34
+ * @param {string=} pluginOptions.acceptedFormats - accepted formats
35
+ * @param {string=} pluginOptions.as - Whether to use the 'Box' or 'Link' conversion button
36
+ * @param {Array<string>} pluginOptions.controls - Additional controls to be added to the figure
37
+ */
38
+ constructor(editor, pluginOptions) {
39
+ super(editor);
40
+ // plugin basic properties
41
+ this.title = this.lang.fileUpload;
42
+ this.icon = 'file_upload';
43
+
44
+ if (!pluginOptions.uploadUrl) console.warn('[SUNEDITOR.fileUpload.warn] "fileUpload" plugin must be have "uploadUrl" option.');
45
+
46
+ // members
47
+ this.uploadUrl = pluginOptions.uploadUrl;
48
+ this.uploadHeaders = pluginOptions.uploadHeaders;
49
+ this.uploadSizeLimit = numbers.get(pluginOptions.uploadSizeLimit, 0);
50
+ this.uploadSingleSizeLimit = numbers.get(pluginOptions.uploadSingleSizeLimit, 0);
51
+ this.allowMultiple = !!pluginOptions.allowMultiple;
52
+ this.acceptedFormats = typeof pluginOptions.acceptedFormats !== 'string' ? '*' : pluginOptions.acceptedFormats.trim() || '*';
53
+ this._acceptedCheck = this.acceptedFormats.split(', ');
54
+ this.as = pluginOptions.as || 'box';
55
+ this.input = dom.utils.createElement('input', { type: 'file', accept: this.acceptedFormats });
56
+ if (this.allowMultiple) {
57
+ this.input.setAttribute('multiple', 'multiple');
58
+ }
59
+ this._element = null;
60
+
61
+ // figure
62
+ const customItems = {
63
+ 'custom-download': {
64
+ command: 'download',
65
+ title: this.lang.download,
66
+ icon: 'download',
67
+ action: (target) => {
68
+ const url = target.getAttribute('href');
69
+ if (url) dom.utils.createElement('A', { href: url }, null).click();
70
+ }
71
+ },
72
+ 'custom-as': {
73
+ command: 'as',
74
+ value: 'link', // 'block' or 'link'
75
+ title: this.lang.asLink,
76
+ icon: 'reduction',
77
+ action: (target, value) => {
78
+ this.convertFormat(target, value);
79
+ }
80
+ }
81
+ };
82
+
83
+ const figureControls = (pluginOptions.controls || [['custom-as', 'align', 'edit', 'custom-download', 'copy', 'remove']]).map((subArray) => subArray.map((item) => (item.startsWith('custom-') ? customItems[item] : item)));
84
+ this.figure = new Figure(this, figureControls, {});
85
+
86
+ // file manager
87
+ this.fileManager = new FileManager(this, {
88
+ query: 'a[download][data-se-file-download]',
89
+ loadHandler: this.events.onFileLoad,
90
+ eventHandler: this.events.onFileAction
91
+ });
92
+
93
+ // controller
94
+ if (/\bedit\b/.test(JSON.stringify(figureControls))) {
95
+ const controllerEl = CreateHTML_controller(this);
96
+ this.controller = new Controller(this, controllerEl, { position: 'bottom', disabled: true }, FileUpload.key);
97
+ this.editInput = controllerEl.querySelector('input');
98
+ }
99
+
100
+ // init
101
+ this.eventManager.addEvent(this.input, 'change', this.#OnChangeFile.bind(this));
102
+ }
103
+
104
+ /**
105
+ * @editorMethod Editor.core
106
+ * @description Executes the main execution method of the plugin.
107
+ * - It is executed by clicking a toolbar "command" button or calling an API.
108
+ */
109
+ action() {
110
+ this.editor._preventBlur = true;
111
+ this.input.click();
112
+ }
113
+
114
+ /**
115
+ * @editorMethod Editor.Component
116
+ * @description Executes the method that is called when a component of a plugin is selected.
117
+ * @param {HTMLElement} target Target component element
118
+ */
119
+ select(target) {
120
+ this._element = target;
121
+ const asBtn = this.figure.controller.form.querySelector('[data-command="__c__as"]');
122
+ if (dom.check.isFigure(target.parentElement)) {
123
+ asBtn.innerHTML = this.icons.reduction + dom.utils.createTooltipInner(this.lang.asLink);
124
+ asBtn.setAttribute('data-value', 'link');
125
+ this.figure.open(target, { nonResizing: true, nonSizeInfo: true, nonBorder: true, figureTarget: true, __fileManagerInfo: false });
126
+ } else {
127
+ asBtn.innerHTML = this.icons.expansion + dom.utils.createTooltipInner(this.lang.asBlock);
128
+ asBtn.setAttribute('data-value', 'box');
129
+ this.figure.controllerOpen(target, { isWWTarget: true });
130
+ return true;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * @editorMethod Editor.EventManager
136
+ * @description Executes the event function of "paste" or "drop".
137
+ * @param {Object} params { frameContext, event, file }
138
+ * @param {__se__FrameContext} params.frameContext Frame context
139
+ * @param {ClipboardEvent} params.event Event object
140
+ * @param {File} params.file File object
141
+ * @returns {boolean} - If return false, the file upload will be canceled
142
+ */
143
+ onFilePasteAndDrop({ file }) {
144
+ const fileType = file.type;
145
+ if (
146
+ !this._acceptedCheck.some((format) => {
147
+ if (format.startsWith('*')) return true;
148
+ if (format.startsWith(fileType)) return true;
149
+ })
150
+ ) {
151
+ return;
152
+ }
153
+
154
+ this.submitFile([file]);
155
+ this.editor.focus();
156
+
157
+ return false;
158
+ }
159
+
160
+ /**
161
+ * @editorMethod Modules.Controller
162
+ * @description Executes the method that is called when a target component is edited.
163
+ * @param {HTMLElement|Text} target Target element
164
+ */
165
+ edit(target) {
166
+ this.editInput.value = target.textContent;
167
+ this.figure.controllerHide();
168
+ this.controller.open(target, null, { isWWTarget: !dom.check.isFigure(target.parentElement), initMethod: null, addOffset: null });
169
+ this.editInput.focus();
170
+ }
171
+
172
+ /**
173
+ * @editorMethod Modules.Controller
174
+ * @description Executes the method that is called when a button is clicked in the "controller".
175
+ * @param {HTMLButtonElement} target Target button element
176
+ */
177
+ controllerAction(target) {
178
+ const command = target.getAttribute('data-command');
179
+ if (!command) return;
180
+
181
+ if (command === 'edit') {
182
+ if (this.editInput.value.trim().length === 0) return;
183
+ this._element.textContent = this.editInput.value;
184
+ }
185
+
186
+ this.controller.close();
187
+ this.component.select(this._element, FileUpload.key);
188
+ }
189
+
190
+ /**
191
+ * @editorMethod Editor.Component
192
+ * @description Method to delete a component of a plugin, called by the "FileManager", "Controller" module.
193
+ * @param {HTMLElement} target Target element
194
+ * @returns {Promise<void>}
195
+ */
196
+ async destroy(target) {
197
+ if (!target) return;
198
+
199
+ const figure = Figure.GetContainer(target);
200
+ const containerTarget = dom.query.getParentElement(target, '.se-component') || target;
201
+
202
+ const message = await this.triggerEvent('onFileDeleteBefore', { element: figure.target, container: figure, url: figure.target.getAttribute('href') });
203
+ if (message === false) return;
204
+
205
+ const isInlineComp = this.component.isInline(containerTarget);
206
+ const focusEl = isInlineComp ? containerTarget.previousSibling || containerTarget.nextSibling : containerTarget.previousElementSibling || containerTarget.nextElementSibling;
207
+ dom.utils.removeItem(containerTarget);
208
+ this.ui._offCurrentController();
209
+
210
+ this.editor.focusEdge(focusEl);
211
+ this.history.push(false);
212
+ }
213
+
214
+ /**
215
+ * @description Create an "file" component using the provided files.
216
+ * @param {File[]|FileList} fileList File object list
217
+ * @returns {Promise<boolean>} If return false, the file upload will be canceled
218
+ */
219
+ async submitFile(fileList) {
220
+ if (fileList.length === 0) return;
221
+
222
+ let fileSize = 0;
223
+ const files = [];
224
+ const slngleSizeLimit = this.uploadSingleSizeLimit;
225
+ for (let i = 0, len = fileList.length, f, s; i < len; i++) {
226
+ f = fileList[i];
227
+ s = f.size;
228
+ if (slngleSizeLimit && slngleSizeLimit > s) {
229
+ const err = '[SUNEDITOR.fileUpload.fail] Size of uploadable single file: ' + slngleSizeLimit / 1000 + 'KB';
230
+ const message = await this.triggerEvent('onFileUploadError', {
231
+ error: err,
232
+ limitSize: slngleSizeLimit,
233
+ uploadSize: s,
234
+ file: f
235
+ });
236
+
237
+ this.ui.alertOpen(message === NO_EVENT ? err : message || err, 'error');
238
+
239
+ return false;
240
+ }
241
+
242
+ files.push(f);
243
+ fileSize += s;
244
+ }
245
+
246
+ const limitSize = this.uploadSizeLimit;
247
+ const currentSize = this.fileManager.getSize();
248
+ if (limitSize > 0 && fileSize + currentSize > limitSize) {
249
+ const err = '[SUNEDITOR.fileUpload.fail] Size of uploadable total files: ' + limitSize / 1000 + 'KB';
250
+ const message = await this.triggerEvent('onFileUploadError', {
251
+ error: err,
252
+ limitSize,
253
+ currentSize,
254
+ uploadSize: fileSize
255
+ });
256
+
257
+ this.ui.alertOpen(message === NO_EVENT ? err : message || err, 'error');
258
+
259
+ return false;
260
+ }
261
+
262
+ const fileInfo = {
263
+ url: this.uploadUrl,
264
+ uploadHeaders: this.uploadHeaders,
265
+ files
266
+ };
267
+
268
+ const handler = async function (uploadCallback, infos, newInfos) {
269
+ infos = newInfos || infos;
270
+ const xmlHttp = await this.fileManager.asyncUpload(infos.url, infos.uploadHeaders, infos.files);
271
+ uploadCallback(xmlHttp);
272
+ }.bind(this, this.#_uploadCallBack.bind(this), fileInfo);
273
+
274
+ const result = await this.triggerEvent('onFileUploadBefore', {
275
+ info: fileInfo,
276
+ handler
277
+ });
278
+
279
+ if (result === undefined) return true;
280
+ if (result === false) return false;
281
+ if (result !== null && typeof result === 'object') handler(result);
282
+
283
+ if (result === true || result === NO_EVENT) handler(null);
284
+ }
285
+
286
+ /**
287
+ * @description Convert format to link or block
288
+ * @param {HTMLElement} target Target element
289
+ * @param {string} value 'link' or 'block'
290
+ */
291
+ convertFormat(target, value) {
292
+ if (value === 'link') {
293
+ this.figure.close();
294
+ const { container } = Figure.GetContainer(target);
295
+ const next = container.nextElementSibling;
296
+ const parent = container.parentElement;
297
+
298
+ target.removeAttribute('data-se-non-focus');
299
+ target.setAttribute('contenteditable', 'false');
300
+ dom.utils.addClass(target, 'se-component|se-inline-component');
301
+
302
+ const line = dom.utils.createElement(this.options.get('defaultLine'), null, target);
303
+ parent.insertBefore(line, next);
304
+ dom.utils.removeItem(container);
305
+ } else {
306
+ // block
307
+ this.selection.setRange(target, 0, target, 1);
308
+ const r = this.html.remove();
309
+ const s = this.nodeTransform.split(r.container, r.offset, 0);
310
+
311
+ if (s?.previousElementSibling && dom.check.isZeroWidth(s.previousElementSibling)) {
312
+ dom.utils.removeItem(s.previousElementSibling);
313
+ }
314
+
315
+ target.setAttribute('data-se-non-focus', 'true');
316
+ target.removeAttribute('contenteditable');
317
+ dom.utils.removeClass(target, 'se-component|se-component-selected|se-inline-component');
318
+
319
+ const figure = Figure.CreateContainer(target, 'se-file-figure se-flex-component');
320
+ (s || r.container).parentElement.insertBefore(figure.container, s);
321
+ }
322
+
323
+ this.history.push(false);
324
+ this.component.select(target, FileUpload.key);
325
+ }
326
+
327
+ /**
328
+ * @description Create file element
329
+ * @param {string} url File URL
330
+ * @param {File|{name: string, size: number}} file File object
331
+ * @param {boolean} isLast Is last file
332
+ */
333
+ create(url, file, isLast) {
334
+ const name = file.name || url;
335
+ const a = dom.utils.createElement(
336
+ 'A',
337
+ {
338
+ href: url,
339
+ title: name,
340
+ download: name,
341
+ 'data-se-file-download': '',
342
+ contenteditable: 'false',
343
+ 'data-se-non-focus': 'true',
344
+ 'data-se-non-link': 'true'
345
+ },
346
+ name
347
+ );
348
+
349
+ this.fileManager.setFileData(a, file);
350
+
351
+ if (this.as === 'link') {
352
+ a.className = 'se-component se-inline-component';
353
+ this.component.insert(a, { skipCharCount: false, skipSelection: false, skipHistory: false });
354
+ return;
355
+ }
356
+
357
+ const figure = Figure.CreateContainer(a);
358
+ dom.utils.addClass(figure.container, 'se-file-figure|se-flex-component');
359
+
360
+ if (!this.component.insert(figure.container, { skipCharCount: false, skipSelection: isLast ? !this.options.get('componentAutoSelect') : true, skipHistory: false })) {
361
+ this.editor.focus();
362
+ return;
363
+ }
364
+
365
+ if (!isLast) return;
366
+
367
+ if (!this.options.get('componentAutoSelect')) {
368
+ const line = this.format.addLine(figure.container, null);
369
+ if (line) this.selection.setRange(line, 0, line, 0);
370
+ } else {
371
+ this.component.select(a, FileUpload.key);
372
+ }
373
+ }
374
+
375
+ /**
376
+ * @private
377
+ * @description Processes the server response after file upload.
378
+ * - Registers the uploaded files in the editor.
379
+ * @param {Object<string, *>} response - The response object from the server.
380
+ */
381
+ _register(response) {
382
+ response.result.forEach((file, i, a) => {
383
+ this.create(
384
+ file.url,
385
+ {
386
+ name: file.name,
387
+ size: file.size
388
+ },
389
+ i === a.length - 1
390
+ );
391
+ });
392
+ }
393
+
394
+ /**
395
+ * @private
396
+ * @description Handles file upload errors.
397
+ * - Displays an error message if the upload fails.
398
+ * @param {Object<string, *>} response - The error response from the server.
399
+ * @returns {Promise<void>}
400
+ */
401
+ async _error(response) {
402
+ const message = await this.triggerEvent('onFileUploadError', { error: response });
403
+ if (message === false) return;
404
+ const err = message === NO_EVENT ? response.errorMessage : message || response.errorMessage;
405
+ this.ui.alertOpen(err, 'error');
406
+ console.error('[SUNEDITOR.plugin.fileUpload.error]', err);
407
+ }
408
+
409
+ /**
410
+ * @description Handles the file upload completion callback.
411
+ * - Parses the response and registers the uploaded file.
412
+ * @param {XMLHttpRequest} xmlHttp - The completed XHR request.
413
+ */
414
+ #_uploadCallBack(xmlHttp) {
415
+ const response = JSON.parse(xmlHttp.responseText);
416
+ if (response.errorMessage) {
417
+ this._error(response);
418
+ } else {
419
+ this._register(response);
420
+ }
421
+ }
422
+
423
+ /**
424
+ * @description Handles the change event when a file is selected.
425
+ * - Triggers the file upload process.
426
+ * @param {InputEvent} e - The change event object.
427
+ */
428
+ async #OnChangeFile(e) {
429
+ /** @type {HTMLInputElement} */
430
+ const eventTarget = dom.query.getEventTarget(e);
431
+ await this.submitFile(eventTarget.files);
432
+ }
433
+ }
434
+
435
+ function CreateHTML_controller({ lang, icons }) {
436
+ const html = /*html*/ `
437
+ <div class="se-arrow se-arrow-up"></div>
438
+ <form>
439
+ <div class="se-btn-group se-form-group">
440
+ <input type="text" />
441
+ <button type="submit" data-command="edit" class="se-btn se-tooltip se-btn-success">
442
+ ${icons.checked}
443
+ <span class="se-tooltip-inner"><span class="se-tooltip-text">${lang.save}</span></span>
444
+ </button>
445
+ <button type="button" data-command="cancel" class="se-btn se-tooltip se-btn-danger">
446
+ ${icons.cancel}
447
+ <span class="se-tooltip-inner"><span class="se-tooltip-text">${lang.cancel}</span></span>
448
+ </button>
449
+ </div>
450
+ </form>
451
+ `;
452
+
453
+ return dom.utils.createElement('DIV', { class: 'se-controller se-controller-simple-input' }, html);
454
+ }
455
+
456
+ export default FileUpload;