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,677 +1,677 @@
1
- import EditorInjector from '../../editorInjector';
2
- import { Modal, Controller, FileManager, Figure, _DragHandle } from '../../modules';
3
- import { dom, numbers, env } from '../../helper';
4
- const { NO_EVENT, ON_OVER_COMPONENT } = env;
5
-
6
- /**
7
- * @typedef {import('../../events').AudioInfo} AudioInfo_audio
8
- */
9
-
10
- /**
11
- * @typedef {Object} AudioPluginOptions
12
- * @property {string} [defaultWidth="300px"] - The default width of the audio tag (e.g., "300px").
13
- * @property {string} [defaultHeight="150px"] - The default height of the audio tag (e.g., "150px").
14
- * @property {boolean} [createFileInput] - Whether to create a file input element.
15
- * @property {boolean} [createUrlInput] - Whether to create a URL input element (default is true if file input is not created).
16
- * @property {string} [uploadUrl] - The URL to which files will be uploaded.
17
- * @property {Object<string, string>} [uploadHeaders] - Headers to include in the file upload request.
18
- * @property {number} [uploadSizeLimit] - The total upload size limit in bytes.
19
- * @property {number} [uploadSingleSizeLimit] - The single file size limit in bytes.
20
- * @property {boolean} [allowMultiple] - Whether to allow multiple file uploads.
21
- * @property {string} [acceptedFormats="audio/*"] - Accepted file formats (default is "audio/*").
22
- * @property {Object<string, string>} [audioTagAttributes] - Additional attributes to set on the audio tag.
23
- */
24
-
25
- /**
26
- * @class
27
- * @description Audio modal plugin.
28
- */
29
- class Audio_ extends EditorInjector {
30
- static key = 'audio';
31
- static type = 'modal';
32
- static className = '';
33
- /**
34
- * @this {Audio_}
35
- * @param {HTMLElement} node - The node to check.
36
- * @returns {HTMLElement|null} Returns a node if the node is a valid component.
37
- */
38
- static component(node) {
39
- return /^AUDIO$/i.test(node?.nodeName) ? node : null;
40
- }
41
-
42
- /**
43
- * @constructor
44
- * @param {__se__EditorCore} editor - The root editor instance
45
- * @param {AudioPluginOptions} pluginOptions
46
- */
47
- constructor(editor, pluginOptions) {
48
- // plugin bisic properties
49
- super(editor);
50
- this.title = this.lang.audio;
51
- this.icon = 'audio';
52
-
53
- // define plugin options
54
- this.pluginOptions = {
55
- defaultWidth: !pluginOptions.defaultWidth ? '' : numbers.is(pluginOptions.defaultWidth) ? pluginOptions.defaultWidth + 'px' : pluginOptions.defaultWidth,
56
- defaultHeight: !pluginOptions.defaultHeight ? '' : numbers.is(pluginOptions.defaultHeight) ? pluginOptions.defaultHeight + 'px' : pluginOptions.defaultHeight,
57
- createFileInput: !!pluginOptions.createFileInput,
58
- createUrlInput: pluginOptions.createUrlInput === undefined || !pluginOptions.createFileInput ? true : pluginOptions.createUrlInput,
59
- uploadUrl: typeof pluginOptions.uploadUrl === 'string' ? pluginOptions.uploadUrl : null,
60
- uploadHeaders: pluginOptions.uploadHeaders || null,
61
- uploadSizeLimit: numbers.get(pluginOptions.uploadSizeLimit, 0),
62
- uploadSingleSizeLimit: numbers.get(pluginOptions.uploadSingleSizeLimit, 0),
63
- allowMultiple: !!pluginOptions.allowMultiple,
64
- acceptedFormats: typeof pluginOptions.acceptedFormats !== 'string' || pluginOptions.acceptedFormats.trim() === '*' ? 'audio/*' : pluginOptions.acceptedFormats.trim() || 'audio/*',
65
- audioTagAttributes: pluginOptions.audioTagAttributes || null
66
- };
67
-
68
- // create HTML
69
- const modalEl = CreateHTML_modal(editor, this.pluginOptions);
70
- const controllerEl = CreateHTML_controller(editor);
71
-
72
- // modules
73
- this.modal = new Modal(this, modalEl);
74
- this.controller = new Controller(this, controllerEl, { position: 'bottom', disabled: true });
75
- this.fileManager = new FileManager(this, {
76
- query: 'audio',
77
- loadHandler: this.events.onAudioLoad,
78
- eventHandler: this.events.onAudioAction
79
- });
80
-
81
- // members
82
- this.figure = new Figure(this, null, {});
83
-
84
- /** @type {HTMLElement} */
85
- this.fileModalWrapper = modalEl.querySelector('.se-flex-input-wrapper');
86
- /** @type {HTMLInputElement} */
87
- this.audioInputFile = modalEl.querySelector('.__se__file_input');
88
- /** @type {HTMLInputElement} */
89
- this.audioUrlFile = modalEl.querySelector('.se-input-url');
90
- /** @type {HTMLElement} */
91
- this.preview = modalEl.querySelector('.se-link-preview');
92
- /** @type {HTMLAudioElement} */
93
- this._element = null;
94
-
95
- this.defaultWidth = this.pluginOptions.defaultWidth;
96
- this.defaultHeight = this.pluginOptions.defaultHeight;
97
- this.urlValue = '';
98
-
99
- const galleryButton = modalEl.querySelector('.__se__gallery');
100
- if (galleryButton) this.eventManager.addEvent(galleryButton, 'click', this.#OpenGallery.bind(this));
101
-
102
- // init
103
- if (this.audioInputFile) {
104
- this.eventManager.addEvent(modalEl.querySelector('.se-modal-files-edge-button'), 'click', this.#RemoveSelectedFiles.bind(this, this.audioUrlFile, this.preview));
105
- if (this.audioUrlFile) {
106
- this.eventManager.addEvent(this.audioInputFile, 'change', this.#FileInputChange.bind(this));
107
- }
108
- }
109
- if (this.audioUrlFile) {
110
- this.eventManager.addEvent(this.audioUrlFile, 'input', this.#OnLinkPreview.bind(this));
111
- }
112
- }
113
-
114
- /**
115
- * @editorMethod Modules.Modal
116
- * @description Executes the method that is called when a "Modal" module's is opened.
117
- */
118
- open() {
119
- this.modal.open();
120
- }
121
-
122
- /**
123
- * @editorMethod Modules.Modal
124
- * @description Executes the method that is called when a plugin's modal is opened.
125
- * @param {boolean} isUpdate "Indicates whether the modal is for editing an existing component (true) or registering a new one (false)."
126
- */
127
- on(isUpdate) {
128
- if (!isUpdate) {
129
- if (this.audioInputFile && this.pluginOptions.allowMultiple) this.audioInputFile.setAttribute('multiple', 'multiple');
130
- } else if (this._element) {
131
- this.urlValue = this.preview.textContent = this.audioUrlFile.value = this._element.src;
132
- if (this.audioInputFile && this.pluginOptions.allowMultiple) this.audioInputFile.removeAttribute('multiple');
133
- } else {
134
- if (this.audioInputFile && this.pluginOptions.allowMultiple) this.audioInputFile.removeAttribute('multiple');
135
- }
136
- }
137
-
138
- /**
139
- * @editorMethod Editor.EventManager
140
- * @description Executes the event function of "paste" or "drop".
141
- * @param {Object} params { frameContext, event, file }
142
- * @param {__se__FrameContext} params.frameContext Frame context
143
- * @param {ClipboardEvent} params.event Event object
144
- * @param {File} params.file File object
145
- * @returns {boolean} - If return false, the file upload will be canceled
146
- */
147
- onFilePasteAndDrop({ file }) {
148
- if (!/^audio/.test(file.type)) return;
149
-
150
- this.submitFile([file]);
151
- this.editor.focus();
152
-
153
- return false;
154
- }
155
-
156
- /**
157
- * @editorMethod Modules.Modal
158
- * @description This function is called when a form within a modal window is "submit".
159
- * @returns {Promise<boolean>} Success or failure
160
- */
161
- async modalAction() {
162
- if (this.audioInputFile && this.audioInputFile?.files.length > 0) {
163
- return await this.submitFile(this.audioInputFile.files);
164
- } else if (this.audioUrlFile && this.urlValue.length > 0) {
165
- return await this.submitURL(this.urlValue);
166
- }
167
- return false;
168
- }
169
-
170
- /**
171
- * @editorMethod Modules.Modal
172
- * @description This function is called before the modal window is opened, but before it is closed.
173
- */
174
- init() {
175
- Modal.OnChangeFile(this.fileModalWrapper, []);
176
- if (this.audioInputFile) this.audioInputFile.value = '';
177
- if (this.audioUrlFile) this.urlValue = this.preview.textContent = this.audioUrlFile.value = '';
178
- if (this.audioInputFile && this.audioUrlFile) {
179
- this.audioUrlFile.disabled = false;
180
- this.preview.style.textDecoration = '';
181
- }
182
- }
183
-
184
- /**
185
- * @editorMethod Modules.Controller
186
- * @description Executes the method that is called when a button is clicked in the "controller".
187
- * @param {HTMLButtonElement} target Target button element
188
- */
189
- controllerAction(target) {
190
- switch (target.getAttribute('data-command')) {
191
- case 'update':
192
- if (this.audioUrlFile) this.urlValue = this.preview.textContent = this.audioUrlFile.value = this._element.src;
193
- this.open();
194
- break;
195
- case 'copy': {
196
- const figure = Figure.GetContainer(this._element);
197
- this.component.copy(figure.container);
198
- break;
199
- }
200
- case 'delete':
201
- this.destroy();
202
- break;
203
- }
204
- }
205
-
206
- /**
207
- * @editorMethod Editor.core
208
- * @description This method is used to validate and preserve the format of the component within the editor.
209
- * - It ensures that the structure and attributes of the element are maintained and secure.
210
- * - The method checks if the element is already wrapped in a valid container and updates its attributes if necessary.
211
- * - If the element isn't properly contained, a new container is created to retain the format.
212
- * @returns {{query: string, method: (element: HTMLAudioElement) => void}} The format retention object containing the query and method to process the element.
213
- * - query: The selector query to identify the relevant elements (in this case, 'audio').
214
- * - method:The function to execute on the element to validate and preserve its format.
215
- * - The function takes the element as an argument, checks if it is contained correctly, and applies necessary adjustments.
216
- */
217
- retainFormat() {
218
- return {
219
- query: 'audio',
220
- method: (element) => {
221
- const figureInfo = Figure.GetContainer(element);
222
- if (figureInfo && figureInfo.container && figureInfo.cover) return;
223
-
224
- this._setTagAttrs(element);
225
- const figure = Figure.CreateContainer(element.cloneNode(true), 'se-flex-component');
226
- this.figure.retainFigureFormat(figure.container, element, null, this.fileManager);
227
- }
228
- };
229
- }
230
-
231
- /**
232
- * @editorMethod Editor.Component
233
- * @description Executes the method that is called when a component of a plugin is selected.
234
- * @param {HTMLElement} target Target component element
235
- */
236
- select(target) {
237
- this.figure.open(target, { nonResizing: true, nonSizeInfo: true, nonBorder: true, figureTarget: true, __fileManagerInfo: false });
238
- this._ready(target);
239
- }
240
-
241
- /**
242
- * @private
243
- * @description Prepares the component for selection.
244
- * - Ensures that the controller is properly positioned and initialized.
245
- * - Prevents duplicate event handling if the component is already selected.
246
- * @param {HTMLElement} target - The selected element.
247
- */
248
- _ready(target) {
249
- if (_DragHandle.get('__overInfo') === ON_OVER_COMPONENT) return;
250
- this._element = /** @type {HTMLAudioElement} */ (target);
251
- this.controller.open(target, null, { isWWTarget: false, addOffset: null });
252
- }
253
-
254
- /**
255
- * @editorMethod Editor.Component
256
- * @description Method to delete a component of a plugin, called by the "FileManager", "Controller" module.
257
- * @param {HTMLElement=} target Target element, if null current selected element
258
- * @returns {Promise<void>}
259
- */
260
- async destroy(target) {
261
- const element = target || this._element;
262
- const figure = Figure.GetContainer(element);
263
- const container = figure.container || element;
264
- const focusEl = container.previousElementSibling || container.nextElementSibling;
265
-
266
- const message = await this.triggerEvent('onAudioDeleteBefore', { element: element, container: figure, url: element.getAttribute('src') });
267
- if (message === false) return;
268
-
269
- const emptyDiv = container.parentNode;
270
- dom.utils.removeItem(container);
271
- this.init();
272
- this.controller.close();
273
-
274
- if (emptyDiv !== this.editor.frameContext.get('wysiwyg')) {
275
- this.nodeTransform.removeAllParents(
276
- emptyDiv,
277
- function (current) {
278
- return current.childNodes.length === 0;
279
- },
280
- null
281
- );
282
- }
283
-
284
- // focus
285
- this.editor.focusEdge(focusEl);
286
- this.history.push(false);
287
- }
288
-
289
- /**
290
- * @private
291
- * @description Registers uploaded audio files and creates the corresponding audio elements.
292
- * - Iterates through the uploaded files and inserts them into the editor.
293
- * @param {AudioInfo_audio} info - Upload metadata, including `isUpdate` flag and `element`.
294
- * @param {Object<string, *>} response - Server response containing uploaded file details.
295
- */
296
- _register(info, response) {
297
- const fileList = response.result;
298
-
299
- for (let i = 0, len = fileList.length, file, oAudio; i < len; i++) {
300
- if (info.isUpdate) oAudio = info.element;
301
- else oAudio = this._createAudioTag();
302
-
303
- file = { name: fileList[i].name, size: fileList[i].size };
304
- this._createComp(oAudio, fileList[i].url, file, info.isUpdate);
305
- }
306
- }
307
-
308
- /**
309
- * @description Create an "audio" component using the provided files.
310
- * @param {FileList|File[]} fileList File object list
311
- * @returns {Promise<boolean>} If return false, the file upload will be canceled
312
- */
313
- async submitFile(fileList) {
314
- if (fileList.length === 0) return false;
315
-
316
- let fileSize = 0;
317
- const files = [];
318
- const slngleSizeLimit = this.pluginOptions.uploadSingleSizeLimit;
319
- for (let i = 0, len = fileList.length, f, s; i < len; i++) {
320
- f = fileList[i];
321
- if (!/audio/i.test(f.type)) continue;
322
-
323
- s = f.size;
324
- if (slngleSizeLimit && slngleSizeLimit > s) {
325
- const err = '[SUNEDITOR.audioUpload.fail] Size of uploadable single file: ' + slngleSizeLimit / 1000 + 'KB';
326
- const message = await this.triggerEvent('onAudioUploadError', {
327
- error: err,
328
- limitSize: slngleSizeLimit,
329
- uploadSize: s,
330
- file: f
331
- });
332
-
333
- this.ui.alertOpen(message === NO_EVENT ? err : message || err, 'error');
334
-
335
- return false;
336
- }
337
-
338
- files.push(f);
339
- fileSize += s;
340
- }
341
-
342
- const limitSize = this.pluginOptions.uploadSizeLimit;
343
- if (limitSize > 0 && fileSize + this.fileManager.getSize() > limitSize) {
344
- const err = '[SUNEDITOR.audioUpload.fail] Size of uploadable total audios: ' + limitSize / 1000 + 'KB';
345
- const message = await this.triggerEvent('onAudioUploadError', { error: err, limitSize, currentSize: this.fileManager.getSize(), uploadSize: fileSize });
346
-
347
- this.ui.alertOpen(message === NO_EVENT ? err : message || err, 'error');
348
-
349
- return false;
350
- }
351
-
352
- const audioInfo = {
353
- files,
354
- isUpdate: this.modal.isUpdate,
355
- element: this._element
356
- };
357
-
358
- const handler = function (newInfos, infos) {
359
- infos = newInfos || infos;
360
- this._serverUpload(infos, infos.files);
361
- }.bind(this, audioInfo);
362
-
363
- const result = await this.triggerEvent('onAudioUploadBefore', {
364
- info: audioInfo,
365
- handler
366
- });
367
-
368
- if (typeof result === 'undefined') return true;
369
- if (!result) return false;
370
- if (result !== null && typeof result === 'object') handler(result);
371
-
372
- if (result === true || result === NO_EVENT) handler(null);
373
-
374
- return true;
375
- }
376
-
377
- /**
378
- * @description Create an "audio" component using the provided url.
379
- * @param {string} url File url
380
- * @returns {Promise<boolean>}
381
- */
382
- async submitURL(url) {
383
- if (url.length === 0) return false;
384
-
385
- const file = { name: url.split('/').pop(), size: 0 };
386
- const audioInfo = {
387
- url,
388
- files: file,
389
- isUpdate: this.modal.isUpdate,
390
- element: this._createAudioTag()
391
- };
392
-
393
- const handler = function (newInfos, infos) {
394
- infos = newInfos || infos;
395
- this._createComp(infos.element, infos.url, infos.files, infos.isUpdate);
396
- }.bind(this, audioInfo);
397
-
398
- const result = await this.triggerEvent('onAudioUploadBefore', {
399
- info: audioInfo,
400
- handler
401
- });
402
-
403
- if (typeof result === 'undefined') return true;
404
- if (!result) return false;
405
- if (result !== null && typeof result === 'object') handler(result);
406
-
407
- if (result === true || result === NO_EVENT) handler(null);
408
-
409
- return true;
410
- }
411
-
412
- /**
413
- * @private
414
- * @description Creates or updates an audio component within the editor.
415
- * - If `isUpdate` is `true`, updates the existing element's `src`.
416
- * - Otherwise, inserts a new audio component with the given file.
417
- * @param {HTMLAudioElement} element - The target audio element.
418
- * @param {string} src - The source URL of the audio file.
419
- * @param {{name: string, size: number}} file - The file metadata (name, size).
420
- * @param {boolean} isUpdate - Whether to update an existing element.
421
- */
422
- _createComp(element, src, file, isUpdate) {
423
- // create new tag
424
- if (!isUpdate) {
425
- this.fileManager.setFileData(element, file);
426
- element.src = src;
427
- const figure = Figure.CreateContainer(element, 'se-flex-component');
428
- if (!this.component.insert(figure.container, { skipCharCount: false, skipSelection: !this.options.get('componentAutoSelect'), skipHistory: false })) {
429
- this.editor.focus();
430
- return;
431
- }
432
- if (!this.options.get('componentAutoSelect')) {
433
- const line = this.format.addLine(figure.container, null);
434
- if (line) this.selection.setRange(line, 0, line, 0);
435
- }
436
- } else {
437
- if (this._element) element = this._element;
438
- this.fileManager.setFileData(element, file);
439
- if (element && element.src !== src) {
440
- element.src = src;
441
- this.component.select(element, Audio_.key);
442
- } else {
443
- this.component.select(element, Audio_.key);
444
- return;
445
- }
446
- }
447
-
448
- if (isUpdate) this.history.push(false);
449
- }
450
-
451
- /**
452
- * @private
453
- * @description Creates a new `<audio>` element with default attributes.
454
- * - Applies width, height, and additional attributes from plugin options.
455
- * @returns {HTMLAudioElement} - The newly created `<audio>` element.
456
- */
457
- _createAudioTag() {
458
- const w = this.defaultWidth;
459
- const h = this.defaultHeight;
460
- /** @type {HTMLAudioElement} */
461
- const oAudio = dom.utils.createElement('AUDIO', { style: (w ? 'width:' + w + '; ' : '') + (h ? 'height:' + h + ';' : '') });
462
- this._setTagAttrs(oAudio);
463
- return oAudio;
464
- }
465
-
466
- /**
467
- * @private
468
- * @description Sets attributes on an audio element based on plugin options.
469
- * - Adds the `controls` attribute and applies any custom attributes.
470
- * @param {HTMLElement} element - The `<audio>` element to modify.
471
- */
472
- _setTagAttrs(element) {
473
- element.setAttribute('controls', 'true');
474
-
475
- const attrs = this.pluginOptions.audioTagAttributes;
476
- if (!attrs) return;
477
-
478
- for (const key in attrs) {
479
- element.setAttribute(key, attrs[key]);
480
- }
481
- }
482
-
483
- /**
484
- * @private
485
- * @description Uploads audio files to the server.
486
- * - Sends a request to the configured upload URL and processes the response.
487
- * @param {AudioInfo_audio} info - Upload metadata, including `files` and `isUpdate`.
488
- * @param {FileList|File[]} files - The files to be uploaded.
489
- */
490
- _serverUpload(info, files) {
491
- if (!files) return;
492
-
493
- const uploadFiles = this.modal.isUpdate ? [files[0]] : files;
494
- this.fileManager.upload(this.pluginOptions.uploadUrl, this.pluginOptions.uploadHeaders, uploadFiles, this.#UploadCallBack.bind(this, info), this._error.bind(this));
495
- }
496
-
497
- /**
498
- * @private
499
- * @description Handles errors that occur during the audio upload process.
500
- * - Triggers the `onAudioUploadError` event to allow custom handling of errors.
501
- * - Displays an error message in the editor's UI.
502
- * - Logs the error to the console for debugging.
503
- * @param {Object<string, *>} response - The error response object from the server or upload process.
504
- * @returns {Promise<void>}
505
- */
506
- async _error(response) {
507
- const message = await this.triggerEvent('onAudioUploadError', { error: response });
508
- const err = message === NO_EVENT ? response.errorMessage : message || response.errorMessage;
509
- this.ui.alertOpen(err, 'error');
510
- console.error('[SUNEDITOR.plugin.audio.error]', err);
511
- }
512
-
513
- /**
514
- * @description Handles the server response after a file upload.
515
- * - If the upload is successful, registers the uploaded audio.
516
- * - If an error occurs, triggers an error event.
517
- * @param {AudioInfo_audio} info - Upload metadata.
518
- * @param {XMLHttpRequest} xmlHttp - The completed XHR request.
519
- */
520
- async #UploadCallBack(info, xmlHttp) {
521
- if ((await this.triggerEvent('audioUploadHandler', { xmlHttp, info })) === NO_EVENT) {
522
- const response = JSON.parse(xmlHttp.responseText);
523
- if (response.errorMessage) {
524
- this._error(response);
525
- } else {
526
- this._register(info, response);
527
- }
528
- }
529
- }
530
-
531
- /**
532
- * @description Updates the preview text for the entered audio URL.
533
- * - Formats the URL correctly based on the editor’s settings.
534
- * @param {InputEvent} e - The input event triggered when the user types a URL.
535
- */
536
- #OnLinkPreview(e) {
537
- /** @type {HTMLInputElement} */
538
- const target = dom.query.getEventTarget(e);
539
- const value = target.value.trim();
540
- this.urlValue = this.preview.textContent = !value
541
- ? ''
542
- : this.options.get('defaultUrlProtocol') && !value.includes('://') && value.indexOf('#') !== 0
543
- ? this.options.get('defaultUrlProtocol') + value
544
- : !value.includes('://')
545
- ? '/' + value
546
- : value;
547
- }
548
-
549
- /**
550
- * @description Opens the audio gallery plugin, if available.
551
- * - Calls a function to populate the URL input with the selected audio file.
552
- */
553
- #OpenGallery() {
554
- this.plugins.audioGallery.open(this.#SetUrlInput.bind(this));
555
- }
556
-
557
- /**
558
- * @param {HTMLInputElement} target - The target element.
559
- */
560
- #SetUrlInput(target) {
561
- this.urlValue = this.preview.textContent = this.audioUrlFile.value = target.getAttribute('data-command') || target.src;
562
- this.audioUrlFile.focus();
563
- }
564
-
565
- /**
566
- * @description Clears the selected file input and re-enables the URL input.
567
- * - Ensures that only one input method (file or URL) is used at a time.
568
- * @param {HTMLInputElement} urlInput - The URL input field.
569
- * @param {HTMLElement} preview - The preview text element.
570
- */
571
- #RemoveSelectedFiles(urlInput, preview) {
572
- this.audioInputFile.value = '';
573
- if (urlInput) {
574
- urlInput.disabled = false;
575
- preview.style.textDecoration = '';
576
- }
577
-
578
- // inputFile check
579
- Modal.OnChangeFile(this.fileModalWrapper, []);
580
- }
581
-
582
- /**
583
- * @param {InputEvent} e - Event object
584
- */
585
- #FileInputChange(e) {
586
- /** @type {HTMLInputElement} */
587
- const target = dom.query.getEventTarget(e);
588
- if (!this.audioInputFile.value) {
589
- this.audioUrlFile.disabled = false;
590
- this.preview.style.textDecoration = '';
591
- } else {
592
- this.audioUrlFile.disabled = true;
593
- this.preview.style.textDecoration = 'line-through';
594
- }
595
-
596
- // inputFile check
597
- Modal.OnChangeFile(this.fileModalWrapper, target.files);
598
- }
599
- }
600
-
601
- function CreateHTML_modal({ lang, icons, plugins }, pluginOptions) {
602
- let html = /*html*/ `
603
- <form method="post" enctype="multipart/form-data">
604
- <div class="se-modal-header">
605
- <button type="button" data-command="close" class="se-btn se-close-btn" title="${lang.close}" aria-label="${lang.close}">
606
- ${icons.cancel}
607
- </button>
608
- <span class="se-modal-title">${lang.audio_modal_title}</span>
609
- </div>
610
- <div class="se-modal-body">`;
611
- if (pluginOptions.createFileInput) {
612
- html += /*html*/ `
613
- <div class="se-modal-form">
614
- <label>${lang.audio_modal_file}</label>
615
- ${Modal.CreateFileInput({ lang, icons }, pluginOptions)}
616
- </div>`;
617
- }
618
- if (pluginOptions.createUrlInput) {
619
- html += /*html*/ `
620
- <div class="se-modal-form">
621
- <label>${lang.audio_modal_url}</label>
622
- <div class="se-modal-form-files">
623
- <input class="se-input-form se-input-url" data-focus type="text" />
624
- ${
625
- plugins.audioGallery
626
- ? `<button type="button" class="se-btn se-tooltip se-modal-files-edge-button __se__gallery" aria-label="${lang.audioGallery}">
627
- ${icons.audio_gallery}
628
- ${dom.utils.createTooltipInner(lang.audioGallery)}
629
- </button>`
630
- : ''
631
- }
632
- </div>
633
- <pre class="se-link-preview"></pre>
634
- </div>`;
635
- }
636
- html += /*html*/ `
637
- </div>
638
- <div class="se-modal-footer">
639
- <button type="submit" class="se-btn-primary" title="${lang.submitButton}" aria-label="${lang.submitButton}">
640
- <span>${lang.submitButton}</span>
641
- </button>
642
- </div>
643
- </form>`;
644
-
645
- return dom.utils.createElement('DIV', { class: 'se-modal-content' }, html);
646
- }
647
-
648
- function CreateHTML_controller({ lang, icons }) {
649
- const html = /*html*/ `
650
- <div class="se-arrow se-arrow-up"></div>
651
- <div class="link-content">
652
- <div class="se-btn-group">
653
- <button type="button" data-command="update" tabindex="-1" class="se-btn se-tooltip">
654
- ${icons.edit}
655
- <span class="se-tooltip-inner">
656
- <span class="se-tooltip-text">${lang.edit}</span>
657
- </span>
658
- </button>
659
- <button type="button" data-command="copy" tabindex="-1" class="se-btn se-tooltip">
660
- ${icons.copy}
661
- <span class="se-tooltip-inner">
662
- <span class="se-tooltip-text">${lang.copy}</span>
663
- </span>
664
- </button>
665
- <button type="button" data-command="delete" tabindex="-1" class="se-btn se-tooltip">
666
- ${icons.delete}
667
- <span class="se-tooltip-inner">
668
- <span class="se-tooltip-text">${lang.remove}</span>
669
- </span>
670
- </button>
671
- </div>
672
- </div>`;
673
-
674
- return dom.utils.createElement('DIV', { class: 'se-controller' }, html);
675
- }
676
-
677
- export default Audio_;
1
+ import EditorInjector from '../../editorInjector';
2
+ import { Modal, Controller, FileManager, Figure, _DragHandle } from '../../modules';
3
+ import { dom, numbers, env } from '../../helper';
4
+ const { NO_EVENT, ON_OVER_COMPONENT } = env;
5
+
6
+ /**
7
+ * @typedef {import('../../events').AudioInfo} AudioInfo_audio
8
+ */
9
+
10
+ /**
11
+ * @typedef {Object} AudioPluginOptions
12
+ * @property {string} [defaultWidth="300px"] - The default width of the audio tag (e.g., "300px").
13
+ * @property {string} [defaultHeight="150px"] - The default height of the audio tag (e.g., "150px").
14
+ * @property {boolean} [createFileInput] - Whether to create a file input element.
15
+ * @property {boolean} [createUrlInput] - Whether to create a URL input element (default is true if file input is not created).
16
+ * @property {string} [uploadUrl] - The URL to which files will be uploaded.
17
+ * @property {Object<string, string>} [uploadHeaders] - Headers to include in the file upload request.
18
+ * @property {number} [uploadSizeLimit] - The total upload size limit in bytes.
19
+ * @property {number} [uploadSingleSizeLimit] - The single file size limit in bytes.
20
+ * @property {boolean} [allowMultiple] - Whether to allow multiple file uploads.
21
+ * @property {string} [acceptedFormats="audio/*"] - Accepted file formats (default is "audio/*").
22
+ * @property {Object<string, string>} [audioTagAttributes] - Additional attributes to set on the audio tag.
23
+ */
24
+
25
+ /**
26
+ * @class
27
+ * @description Audio modal plugin.
28
+ */
29
+ class Audio_ extends EditorInjector {
30
+ static key = 'audio';
31
+ static type = 'modal';
32
+ static className = '';
33
+ /**
34
+ * @this {Audio_}
35
+ * @param {HTMLElement} node - The node to check.
36
+ * @returns {HTMLElement|null} Returns a node if the node is a valid component.
37
+ */
38
+ static component(node) {
39
+ return /^AUDIO$/i.test(node?.nodeName) ? node : null;
40
+ }
41
+
42
+ /**
43
+ * @constructor
44
+ * @param {__se__EditorCore} editor - The root editor instance
45
+ * @param {AudioPluginOptions} pluginOptions
46
+ */
47
+ constructor(editor, pluginOptions) {
48
+ // plugin bisic properties
49
+ super(editor);
50
+ this.title = this.lang.audio;
51
+ this.icon = 'audio';
52
+
53
+ // define plugin options
54
+ this.pluginOptions = {
55
+ defaultWidth: !pluginOptions.defaultWidth ? '' : numbers.is(pluginOptions.defaultWidth) ? pluginOptions.defaultWidth + 'px' : pluginOptions.defaultWidth,
56
+ defaultHeight: !pluginOptions.defaultHeight ? '' : numbers.is(pluginOptions.defaultHeight) ? pluginOptions.defaultHeight + 'px' : pluginOptions.defaultHeight,
57
+ createFileInput: !!pluginOptions.createFileInput,
58
+ createUrlInput: pluginOptions.createUrlInput === undefined || !pluginOptions.createFileInput ? true : pluginOptions.createUrlInput,
59
+ uploadUrl: typeof pluginOptions.uploadUrl === 'string' ? pluginOptions.uploadUrl : null,
60
+ uploadHeaders: pluginOptions.uploadHeaders || null,
61
+ uploadSizeLimit: numbers.get(pluginOptions.uploadSizeLimit, 0),
62
+ uploadSingleSizeLimit: numbers.get(pluginOptions.uploadSingleSizeLimit, 0),
63
+ allowMultiple: !!pluginOptions.allowMultiple,
64
+ acceptedFormats: typeof pluginOptions.acceptedFormats !== 'string' || pluginOptions.acceptedFormats.trim() === '*' ? 'audio/*' : pluginOptions.acceptedFormats.trim() || 'audio/*',
65
+ audioTagAttributes: pluginOptions.audioTagAttributes || null
66
+ };
67
+
68
+ // create HTML
69
+ const modalEl = CreateHTML_modal(editor, this.pluginOptions);
70
+ const controllerEl = CreateHTML_controller(editor);
71
+
72
+ // modules
73
+ this.modal = new Modal(this, modalEl);
74
+ this.controller = new Controller(this, controllerEl, { position: 'bottom', disabled: true });
75
+ this.fileManager = new FileManager(this, {
76
+ query: 'audio',
77
+ loadHandler: this.events.onAudioLoad,
78
+ eventHandler: this.events.onAudioAction
79
+ });
80
+
81
+ // members
82
+ this.figure = new Figure(this, null, {});
83
+
84
+ /** @type {HTMLElement} */
85
+ this.fileModalWrapper = modalEl.querySelector('.se-flex-input-wrapper');
86
+ /** @type {HTMLInputElement} */
87
+ this.audioInputFile = modalEl.querySelector('.__se__file_input');
88
+ /** @type {HTMLInputElement} */
89
+ this.audioUrlFile = modalEl.querySelector('.se-input-url');
90
+ /** @type {HTMLElement} */
91
+ this.preview = modalEl.querySelector('.se-link-preview');
92
+ /** @type {HTMLAudioElement} */
93
+ this._element = null;
94
+
95
+ this.defaultWidth = this.pluginOptions.defaultWidth;
96
+ this.defaultHeight = this.pluginOptions.defaultHeight;
97
+ this.urlValue = '';
98
+
99
+ const galleryButton = modalEl.querySelector('.__se__gallery');
100
+ if (galleryButton) this.eventManager.addEvent(galleryButton, 'click', this.#OpenGallery.bind(this));
101
+
102
+ // init
103
+ if (this.audioInputFile) {
104
+ this.eventManager.addEvent(modalEl.querySelector('.se-modal-files-edge-button'), 'click', this.#RemoveSelectedFiles.bind(this, this.audioUrlFile, this.preview));
105
+ if (this.audioUrlFile) {
106
+ this.eventManager.addEvent(this.audioInputFile, 'change', this.#FileInputChange.bind(this));
107
+ }
108
+ }
109
+ if (this.audioUrlFile) {
110
+ this.eventManager.addEvent(this.audioUrlFile, 'input', this.#OnLinkPreview.bind(this));
111
+ }
112
+ }
113
+
114
+ /**
115
+ * @editorMethod Modules.Modal
116
+ * @description Executes the method that is called when a "Modal" module's is opened.
117
+ */
118
+ open() {
119
+ this.modal.open();
120
+ }
121
+
122
+ /**
123
+ * @editorMethod Modules.Modal
124
+ * @description Executes the method that is called when a plugin's modal is opened.
125
+ * @param {boolean} isUpdate "Indicates whether the modal is for editing an existing component (true) or registering a new one (false)."
126
+ */
127
+ on(isUpdate) {
128
+ if (!isUpdate) {
129
+ if (this.audioInputFile && this.pluginOptions.allowMultiple) this.audioInputFile.setAttribute('multiple', 'multiple');
130
+ } else if (this._element) {
131
+ this.urlValue = this.preview.textContent = this.audioUrlFile.value = this._element.src;
132
+ if (this.audioInputFile && this.pluginOptions.allowMultiple) this.audioInputFile.removeAttribute('multiple');
133
+ } else {
134
+ if (this.audioInputFile && this.pluginOptions.allowMultiple) this.audioInputFile.removeAttribute('multiple');
135
+ }
136
+ }
137
+
138
+ /**
139
+ * @editorMethod Editor.EventManager
140
+ * @description Executes the event function of "paste" or "drop".
141
+ * @param {Object} params { frameContext, event, file }
142
+ * @param {__se__FrameContext} params.frameContext Frame context
143
+ * @param {ClipboardEvent} params.event Event object
144
+ * @param {File} params.file File object
145
+ * @returns {boolean} - If return false, the file upload will be canceled
146
+ */
147
+ onFilePasteAndDrop({ file }) {
148
+ if (!/^audio/.test(file.type)) return;
149
+
150
+ this.submitFile([file]);
151
+ this.editor.focus();
152
+
153
+ return false;
154
+ }
155
+
156
+ /**
157
+ * @editorMethod Modules.Modal
158
+ * @description This function is called when a form within a modal window is "submit".
159
+ * @returns {Promise<boolean>} Success or failure
160
+ */
161
+ async modalAction() {
162
+ if (this.audioInputFile && this.audioInputFile?.files.length > 0) {
163
+ return await this.submitFile(this.audioInputFile.files);
164
+ } else if (this.audioUrlFile && this.urlValue.length > 0) {
165
+ return await this.submitURL(this.urlValue);
166
+ }
167
+ return false;
168
+ }
169
+
170
+ /**
171
+ * @editorMethod Modules.Modal
172
+ * @description This function is called before the modal window is opened, but before it is closed.
173
+ */
174
+ init() {
175
+ Modal.OnChangeFile(this.fileModalWrapper, []);
176
+ if (this.audioInputFile) this.audioInputFile.value = '';
177
+ if (this.audioUrlFile) this.urlValue = this.preview.textContent = this.audioUrlFile.value = '';
178
+ if (this.audioInputFile && this.audioUrlFile) {
179
+ this.audioUrlFile.disabled = false;
180
+ this.preview.style.textDecoration = '';
181
+ }
182
+ }
183
+
184
+ /**
185
+ * @editorMethod Modules.Controller
186
+ * @description Executes the method that is called when a button is clicked in the "controller".
187
+ * @param {HTMLButtonElement} target Target button element
188
+ */
189
+ controllerAction(target) {
190
+ switch (target.getAttribute('data-command')) {
191
+ case 'update':
192
+ if (this.audioUrlFile) this.urlValue = this.preview.textContent = this.audioUrlFile.value = this._element.src;
193
+ this.open();
194
+ break;
195
+ case 'copy': {
196
+ const figure = Figure.GetContainer(this._element);
197
+ this.component.copy(figure.container);
198
+ break;
199
+ }
200
+ case 'delete':
201
+ this.destroy();
202
+ break;
203
+ }
204
+ }
205
+
206
+ /**
207
+ * @editorMethod Editor.core
208
+ * @description This method is used to validate and preserve the format of the component within the editor.
209
+ * - It ensures that the structure and attributes of the element are maintained and secure.
210
+ * - The method checks if the element is already wrapped in a valid container and updates its attributes if necessary.
211
+ * - If the element isn't properly contained, a new container is created to retain the format.
212
+ * @returns {{query: string, method: (element: HTMLAudioElement) => void}} The format retention object containing the query and method to process the element.
213
+ * - query: The selector query to identify the relevant elements (in this case, 'audio').
214
+ * - method:The function to execute on the element to validate and preserve its format.
215
+ * - The function takes the element as an argument, checks if it is contained correctly, and applies necessary adjustments.
216
+ */
217
+ retainFormat() {
218
+ return {
219
+ query: 'audio',
220
+ method: (element) => {
221
+ const figureInfo = Figure.GetContainer(element);
222
+ if (figureInfo && figureInfo.container && figureInfo.cover) return;
223
+
224
+ this._setTagAttrs(element);
225
+ const figure = Figure.CreateContainer(element.cloneNode(true), 'se-flex-component');
226
+ this.figure.retainFigureFormat(figure.container, element, null, this.fileManager);
227
+ }
228
+ };
229
+ }
230
+
231
+ /**
232
+ * @editorMethod Editor.Component
233
+ * @description Executes the method that is called when a component of a plugin is selected.
234
+ * @param {HTMLElement} target Target component element
235
+ */
236
+ select(target) {
237
+ this.figure.open(target, { nonResizing: true, nonSizeInfo: true, nonBorder: true, figureTarget: true, __fileManagerInfo: false });
238
+ this._ready(target);
239
+ }
240
+
241
+ /**
242
+ * @private
243
+ * @description Prepares the component for selection.
244
+ * - Ensures that the controller is properly positioned and initialized.
245
+ * - Prevents duplicate event handling if the component is already selected.
246
+ * @param {HTMLElement} target - The selected element.
247
+ */
248
+ _ready(target) {
249
+ if (_DragHandle.get('__overInfo') === ON_OVER_COMPONENT) return;
250
+ this._element = /** @type {HTMLAudioElement} */ (target);
251
+ this.controller.open(target, null, { isWWTarget: false, addOffset: null });
252
+ }
253
+
254
+ /**
255
+ * @editorMethod Editor.Component
256
+ * @description Method to delete a component of a plugin, called by the "FileManager", "Controller" module.
257
+ * @param {HTMLElement=} target Target element, if null current selected element
258
+ * @returns {Promise<void>}
259
+ */
260
+ async destroy(target) {
261
+ const element = target || this._element;
262
+ const figure = Figure.GetContainer(element);
263
+ const container = figure.container || element;
264
+ const focusEl = container.previousElementSibling || container.nextElementSibling;
265
+
266
+ const message = await this.triggerEvent('onAudioDeleteBefore', { element: element, container: figure, url: element.getAttribute('src') });
267
+ if (message === false) return;
268
+
269
+ const emptyDiv = container.parentNode;
270
+ dom.utils.removeItem(container);
271
+ this.init();
272
+ this.controller.close();
273
+
274
+ if (emptyDiv !== this.editor.frameContext.get('wysiwyg')) {
275
+ this.nodeTransform.removeAllParents(
276
+ emptyDiv,
277
+ function (current) {
278
+ return current.childNodes.length === 0;
279
+ },
280
+ null
281
+ );
282
+ }
283
+
284
+ // focus
285
+ this.editor.focusEdge(focusEl);
286
+ this.history.push(false);
287
+ }
288
+
289
+ /**
290
+ * @private
291
+ * @description Registers uploaded audio files and creates the corresponding audio elements.
292
+ * - Iterates through the uploaded files and inserts them into the editor.
293
+ * @param {AudioInfo_audio} info - Upload metadata, including `isUpdate` flag and `element`.
294
+ * @param {Object<string, *>} response - Server response containing uploaded file details.
295
+ */
296
+ _register(info, response) {
297
+ const fileList = response.result;
298
+
299
+ for (let i = 0, len = fileList.length, file, oAudio; i < len; i++) {
300
+ if (info.isUpdate) oAudio = info.element;
301
+ else oAudio = this._createAudioTag();
302
+
303
+ file = { name: fileList[i].name, size: fileList[i].size };
304
+ this._createComp(oAudio, fileList[i].url, file, info.isUpdate);
305
+ }
306
+ }
307
+
308
+ /**
309
+ * @description Create an "audio" component using the provided files.
310
+ * @param {FileList|File[]} fileList File object list
311
+ * @returns {Promise<boolean>} If return false, the file upload will be canceled
312
+ */
313
+ async submitFile(fileList) {
314
+ if (fileList.length === 0) return false;
315
+
316
+ let fileSize = 0;
317
+ const files = [];
318
+ const slngleSizeLimit = this.pluginOptions.uploadSingleSizeLimit;
319
+ for (let i = 0, len = fileList.length, f, s; i < len; i++) {
320
+ f = fileList[i];
321
+ if (!/audio/i.test(f.type)) continue;
322
+
323
+ s = f.size;
324
+ if (slngleSizeLimit && slngleSizeLimit > s) {
325
+ const err = '[SUNEDITOR.audioUpload.fail] Size of uploadable single file: ' + slngleSizeLimit / 1000 + 'KB';
326
+ const message = await this.triggerEvent('onAudioUploadError', {
327
+ error: err,
328
+ limitSize: slngleSizeLimit,
329
+ uploadSize: s,
330
+ file: f
331
+ });
332
+
333
+ this.ui.alertOpen(message === NO_EVENT ? err : message || err, 'error');
334
+
335
+ return false;
336
+ }
337
+
338
+ files.push(f);
339
+ fileSize += s;
340
+ }
341
+
342
+ const limitSize = this.pluginOptions.uploadSizeLimit;
343
+ if (limitSize > 0 && fileSize + this.fileManager.getSize() > limitSize) {
344
+ const err = '[SUNEDITOR.audioUpload.fail] Size of uploadable total audios: ' + limitSize / 1000 + 'KB';
345
+ const message = await this.triggerEvent('onAudioUploadError', { error: err, limitSize, currentSize: this.fileManager.getSize(), uploadSize: fileSize });
346
+
347
+ this.ui.alertOpen(message === NO_EVENT ? err : message || err, 'error');
348
+
349
+ return false;
350
+ }
351
+
352
+ const audioInfo = {
353
+ files,
354
+ isUpdate: this.modal.isUpdate,
355
+ element: this._element
356
+ };
357
+
358
+ const handler = function (newInfos, infos) {
359
+ infos = newInfos || infos;
360
+ this._serverUpload(infos, infos.files);
361
+ }.bind(this, audioInfo);
362
+
363
+ const result = await this.triggerEvent('onAudioUploadBefore', {
364
+ info: audioInfo,
365
+ handler
366
+ });
367
+
368
+ if (typeof result === 'undefined') return true;
369
+ if (!result) return false;
370
+ if (result !== null && typeof result === 'object') handler(result);
371
+
372
+ if (result === true || result === NO_EVENT) handler(null);
373
+
374
+ return true;
375
+ }
376
+
377
+ /**
378
+ * @description Create an "audio" component using the provided url.
379
+ * @param {string} url File url
380
+ * @returns {Promise<boolean>}
381
+ */
382
+ async submitURL(url) {
383
+ if (url.length === 0) return false;
384
+
385
+ const file = { name: url.split('/').pop(), size: 0 };
386
+ const audioInfo = {
387
+ url,
388
+ files: file,
389
+ isUpdate: this.modal.isUpdate,
390
+ element: this._createAudioTag()
391
+ };
392
+
393
+ const handler = function (newInfos, infos) {
394
+ infos = newInfos || infos;
395
+ this._createComp(infos.element, infos.url, infos.files, infos.isUpdate);
396
+ }.bind(this, audioInfo);
397
+
398
+ const result = await this.triggerEvent('onAudioUploadBefore', {
399
+ info: audioInfo,
400
+ handler
401
+ });
402
+
403
+ if (typeof result === 'undefined') return true;
404
+ if (!result) return false;
405
+ if (result !== null && typeof result === 'object') handler(result);
406
+
407
+ if (result === true || result === NO_EVENT) handler(null);
408
+
409
+ return true;
410
+ }
411
+
412
+ /**
413
+ * @private
414
+ * @description Creates or updates an audio component within the editor.
415
+ * - If `isUpdate` is `true`, updates the existing element's `src`.
416
+ * - Otherwise, inserts a new audio component with the given file.
417
+ * @param {HTMLAudioElement} element - The target audio element.
418
+ * @param {string} src - The source URL of the audio file.
419
+ * @param {{name: string, size: number}} file - The file metadata (name, size).
420
+ * @param {boolean} isUpdate - Whether to update an existing element.
421
+ */
422
+ _createComp(element, src, file, isUpdate) {
423
+ // create new tag
424
+ if (!isUpdate) {
425
+ this.fileManager.setFileData(element, file);
426
+ element.src = src;
427
+ const figure = Figure.CreateContainer(element, 'se-flex-component');
428
+ if (!this.component.insert(figure.container, { skipCharCount: false, skipSelection: !this.options.get('componentAutoSelect'), skipHistory: false })) {
429
+ this.editor.focus();
430
+ return;
431
+ }
432
+ if (!this.options.get('componentAutoSelect')) {
433
+ const line = this.format.addLine(figure.container, null);
434
+ if (line) this.selection.setRange(line, 0, line, 0);
435
+ }
436
+ } else {
437
+ if (this._element) element = this._element;
438
+ this.fileManager.setFileData(element, file);
439
+ if (element && element.src !== src) {
440
+ element.src = src;
441
+ this.component.select(element, Audio_.key);
442
+ } else {
443
+ this.component.select(element, Audio_.key);
444
+ return;
445
+ }
446
+ }
447
+
448
+ if (isUpdate) this.history.push(false);
449
+ }
450
+
451
+ /**
452
+ * @private
453
+ * @description Creates a new `<audio>` element with default attributes.
454
+ * - Applies width, height, and additional attributes from plugin options.
455
+ * @returns {HTMLAudioElement} - The newly created `<audio>` element.
456
+ */
457
+ _createAudioTag() {
458
+ const w = this.defaultWidth;
459
+ const h = this.defaultHeight;
460
+ /** @type {HTMLAudioElement} */
461
+ const oAudio = dom.utils.createElement('AUDIO', { style: (w ? 'width:' + w + '; ' : '') + (h ? 'height:' + h + ';' : '') });
462
+ this._setTagAttrs(oAudio);
463
+ return oAudio;
464
+ }
465
+
466
+ /**
467
+ * @private
468
+ * @description Sets attributes on an audio element based on plugin options.
469
+ * - Adds the `controls` attribute and applies any custom attributes.
470
+ * @param {HTMLElement} element - The `<audio>` element to modify.
471
+ */
472
+ _setTagAttrs(element) {
473
+ element.setAttribute('controls', 'true');
474
+
475
+ const attrs = this.pluginOptions.audioTagAttributes;
476
+ if (!attrs) return;
477
+
478
+ for (const key in attrs) {
479
+ element.setAttribute(key, attrs[key]);
480
+ }
481
+ }
482
+
483
+ /**
484
+ * @private
485
+ * @description Uploads audio files to the server.
486
+ * - Sends a request to the configured upload URL and processes the response.
487
+ * @param {AudioInfo_audio} info - Upload metadata, including `files` and `isUpdate`.
488
+ * @param {FileList|File[]} files - The files to be uploaded.
489
+ */
490
+ _serverUpload(info, files) {
491
+ if (!files) return;
492
+
493
+ const uploadFiles = this.modal.isUpdate ? [files[0]] : files;
494
+ this.fileManager.upload(this.pluginOptions.uploadUrl, this.pluginOptions.uploadHeaders, uploadFiles, this.#UploadCallBack.bind(this, info), this._error.bind(this));
495
+ }
496
+
497
+ /**
498
+ * @private
499
+ * @description Handles errors that occur during the audio upload process.
500
+ * - Triggers the `onAudioUploadError` event to allow custom handling of errors.
501
+ * - Displays an error message in the editor's UI.
502
+ * - Logs the error to the console for debugging.
503
+ * @param {Object<string, *>} response - The error response object from the server or upload process.
504
+ * @returns {Promise<void>}
505
+ */
506
+ async _error(response) {
507
+ const message = await this.triggerEvent('onAudioUploadError', { error: response });
508
+ const err = message === NO_EVENT ? response.errorMessage : message || response.errorMessage;
509
+ this.ui.alertOpen(err, 'error');
510
+ console.error('[SUNEDITOR.plugin.audio.error]', err);
511
+ }
512
+
513
+ /**
514
+ * @description Handles the server response after a file upload.
515
+ * - If the upload is successful, registers the uploaded audio.
516
+ * - If an error occurs, triggers an error event.
517
+ * @param {AudioInfo_audio} info - Upload metadata.
518
+ * @param {XMLHttpRequest} xmlHttp - The completed XHR request.
519
+ */
520
+ async #UploadCallBack(info, xmlHttp) {
521
+ if ((await this.triggerEvent('audioUploadHandler', { xmlHttp, info })) === NO_EVENT) {
522
+ const response = JSON.parse(xmlHttp.responseText);
523
+ if (response.errorMessage) {
524
+ this._error(response);
525
+ } else {
526
+ this._register(info, response);
527
+ }
528
+ }
529
+ }
530
+
531
+ /**
532
+ * @description Updates the preview text for the entered audio URL.
533
+ * - Formats the URL correctly based on the editor’s settings.
534
+ * @param {InputEvent} e - The input event triggered when the user types a URL.
535
+ */
536
+ #OnLinkPreview(e) {
537
+ /** @type {HTMLInputElement} */
538
+ const target = dom.query.getEventTarget(e);
539
+ const value = target.value.trim();
540
+ this.urlValue = this.preview.textContent = !value
541
+ ? ''
542
+ : this.options.get('defaultUrlProtocol') && !value.includes('://') && value.indexOf('#') !== 0
543
+ ? this.options.get('defaultUrlProtocol') + value
544
+ : !value.includes('://')
545
+ ? '/' + value
546
+ : value;
547
+ }
548
+
549
+ /**
550
+ * @description Opens the audio gallery plugin, if available.
551
+ * - Calls a function to populate the URL input with the selected audio file.
552
+ */
553
+ #OpenGallery() {
554
+ this.plugins.audioGallery.open(this.#SetUrlInput.bind(this));
555
+ }
556
+
557
+ /**
558
+ * @param {HTMLInputElement} target - The target element.
559
+ */
560
+ #SetUrlInput(target) {
561
+ this.urlValue = this.preview.textContent = this.audioUrlFile.value = target.getAttribute('data-command') || target.src;
562
+ this.audioUrlFile.focus();
563
+ }
564
+
565
+ /**
566
+ * @description Clears the selected file input and re-enables the URL input.
567
+ * - Ensures that only one input method (file or URL) is used at a time.
568
+ * @param {HTMLInputElement} urlInput - The URL input field.
569
+ * @param {HTMLElement} preview - The preview text element.
570
+ */
571
+ #RemoveSelectedFiles(urlInput, preview) {
572
+ this.audioInputFile.value = '';
573
+ if (urlInput) {
574
+ urlInput.disabled = false;
575
+ preview.style.textDecoration = '';
576
+ }
577
+
578
+ // inputFile check
579
+ Modal.OnChangeFile(this.fileModalWrapper, []);
580
+ }
581
+
582
+ /**
583
+ * @param {InputEvent} e - Event object
584
+ */
585
+ #FileInputChange(e) {
586
+ /** @type {HTMLInputElement} */
587
+ const target = dom.query.getEventTarget(e);
588
+ if (!this.audioInputFile.value) {
589
+ this.audioUrlFile.disabled = false;
590
+ this.preview.style.textDecoration = '';
591
+ } else {
592
+ this.audioUrlFile.disabled = true;
593
+ this.preview.style.textDecoration = 'line-through';
594
+ }
595
+
596
+ // inputFile check
597
+ Modal.OnChangeFile(this.fileModalWrapper, target.files);
598
+ }
599
+ }
600
+
601
+ function CreateHTML_modal({ lang, icons, plugins }, pluginOptions) {
602
+ let html = /*html*/ `
603
+ <form method="post" enctype="multipart/form-data">
604
+ <div class="se-modal-header">
605
+ <button type="button" data-command="close" class="se-btn se-close-btn" title="${lang.close}" aria-label="${lang.close}">
606
+ ${icons.cancel}
607
+ </button>
608
+ <span class="se-modal-title">${lang.audio_modal_title}</span>
609
+ </div>
610
+ <div class="se-modal-body">`;
611
+ if (pluginOptions.createFileInput) {
612
+ html += /*html*/ `
613
+ <div class="se-modal-form">
614
+ <label>${lang.audio_modal_file}</label>
615
+ ${Modal.CreateFileInput({ lang, icons }, pluginOptions)}
616
+ </div>`;
617
+ }
618
+ if (pluginOptions.createUrlInput) {
619
+ html += /*html*/ `
620
+ <div class="se-modal-form">
621
+ <label>${lang.audio_modal_url}</label>
622
+ <div class="se-modal-form-files">
623
+ <input class="se-input-form se-input-url" data-focus type="text" />
624
+ ${
625
+ plugins.audioGallery
626
+ ? `<button type="button" class="se-btn se-tooltip se-modal-files-edge-button __se__gallery" aria-label="${lang.audioGallery}">
627
+ ${icons.audio_gallery}
628
+ ${dom.utils.createTooltipInner(lang.audioGallery)}
629
+ </button>`
630
+ : ''
631
+ }
632
+ </div>
633
+ <pre class="se-link-preview"></pre>
634
+ </div>`;
635
+ }
636
+ html += /*html*/ `
637
+ </div>
638
+ <div class="se-modal-footer">
639
+ <button type="submit" class="se-btn-primary" title="${lang.submitButton}" aria-label="${lang.submitButton}">
640
+ <span>${lang.submitButton}</span>
641
+ </button>
642
+ </div>
643
+ </form>`;
644
+
645
+ return dom.utils.createElement('DIV', { class: 'se-modal-content' }, html);
646
+ }
647
+
648
+ function CreateHTML_controller({ lang, icons }) {
649
+ const html = /*html*/ `
650
+ <div class="se-arrow se-arrow-up"></div>
651
+ <div class="link-content">
652
+ <div class="se-btn-group">
653
+ <button type="button" data-command="update" tabindex="-1" class="se-btn se-tooltip">
654
+ ${icons.edit}
655
+ <span class="se-tooltip-inner">
656
+ <span class="se-tooltip-text">${lang.edit}</span>
657
+ </span>
658
+ </button>
659
+ <button type="button" data-command="copy" tabindex="-1" class="se-btn se-tooltip">
660
+ ${icons.copy}
661
+ <span class="se-tooltip-inner">
662
+ <span class="se-tooltip-text">${lang.copy}</span>
663
+ </span>
664
+ </button>
665
+ <button type="button" data-command="delete" tabindex="-1" class="se-btn se-tooltip">
666
+ ${icons.delete}
667
+ <span class="se-tooltip-inner">
668
+ <span class="se-tooltip-text">${lang.remove}</span>
669
+ </span>
670
+ </button>
671
+ </div>
672
+ </div>`;
673
+
674
+ return dom.utils.createElement('DIV', { class: 'se-controller' }, html);
675
+ }
676
+
677
+ export default Audio_;