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.
- package/CONTRIBUTING.md +186 -184
- package/LICENSE +21 -21
- package/README.md +157 -180
- package/dist/suneditor.min.css +1 -1
- package/dist/suneditor.min.js +1 -1
- package/package.json +126 -123
- package/src/assets/design/color.css +131 -121
- package/src/assets/design/index.css +3 -3
- package/src/assets/design/size.css +37 -35
- package/src/assets/design/typography.css +37 -37
- package/src/assets/icons/defaultIcons.js +247 -232
- package/src/assets/suneditor-contents.css +779 -778
- package/src/assets/suneditor.css +43 -35
- package/src/core/base/eventHandlers/handler_toolbar.js +135 -135
- package/src/core/base/eventHandlers/handler_ww_clipboard.js +56 -56
- package/src/core/base/eventHandlers/handler_ww_dragDrop.js +115 -113
- package/src/core/base/eventHandlers/handler_ww_key_input.js +1200 -1200
- package/src/core/base/eventHandlers/handler_ww_mouse.js +194 -194
- package/src/core/base/eventManager.js +1550 -1484
- package/src/core/base/history.js +355 -355
- package/src/core/class/char.js +163 -162
- package/src/core/class/component.js +856 -842
- package/src/core/class/format.js +3433 -3422
- package/src/core/class/html.js +1927 -1890
- package/src/core/class/menu.js +357 -346
- package/src/core/class/nodeTransform.js +424 -424
- package/src/core/class/offset.js +858 -891
- package/src/core/class/selection.js +710 -620
- package/src/core/class/shortcuts.js +98 -98
- package/src/core/class/toolbar.js +438 -430
- package/src/core/class/ui.js +424 -422
- package/src/core/class/viewer.js +750 -750
- package/src/core/editor.js +1810 -1708
- package/src/core/section/actives.js +268 -241
- package/src/core/section/constructor.js +1348 -1661
- package/src/core/section/context.js +102 -102
- package/src/core/section/documentType.js +582 -561
- package/src/core/section/options.js +367 -0
- package/src/core/util/instanceCheck.js +59 -0
- package/src/editorInjector/_classes.js +36 -36
- package/src/editorInjector/_core.js +92 -92
- package/src/editorInjector/index.js +75 -75
- package/src/events.js +634 -622
- package/src/helper/clipboard.js +59 -59
- package/src/helper/converter.js +586 -564
- package/src/helper/dom/domCheck.js +304 -304
- package/src/helper/dom/domQuery.js +677 -669
- package/src/helper/dom/domUtils.js +618 -557
- package/src/helper/dom/index.js +12 -12
- package/src/helper/env.js +249 -240
- package/src/helper/index.js +25 -25
- package/src/helper/keyCodeMap.js +183 -183
- package/src/helper/numbers.js +72 -72
- package/src/helper/unicode.js +47 -47
- package/src/langs/ckb.js +231 -231
- package/src/langs/cs.js +231 -231
- package/src/langs/da.js +231 -231
- package/src/langs/de.js +231 -231
- package/src/langs/en.js +230 -230
- package/src/langs/es.js +231 -231
- package/src/langs/fa.js +231 -231
- package/src/langs/fr.js +231 -231
- package/src/langs/he.js +231 -231
- package/src/langs/hu.js +230 -230
- package/src/langs/index.js +28 -28
- package/src/langs/it.js +231 -231
- package/src/langs/ja.js +230 -230
- package/src/langs/km.js +230 -230
- package/src/langs/ko.js +230 -230
- package/src/langs/lv.js +231 -231
- package/src/langs/nl.js +231 -231
- package/src/langs/pl.js +231 -231
- package/src/langs/pt_br.js +231 -231
- package/src/langs/ro.js +231 -231
- package/src/langs/ru.js +231 -231
- package/src/langs/se.js +231 -231
- package/src/langs/tr.js +231 -231
- package/src/langs/uk.js +231 -231
- package/src/langs/ur.js +231 -231
- package/src/langs/zh_cn.js +231 -231
- package/src/modules/ApiManager.js +191 -191
- package/src/modules/Browser.js +669 -667
- package/src/modules/ColorPicker.js +364 -362
- package/src/modules/Controller.js +474 -454
- package/src/modules/Figure.js +1620 -1617
- package/src/modules/FileManager.js +359 -359
- package/src/modules/HueSlider.js +577 -565
- package/src/modules/Modal.js +346 -346
- package/src/modules/ModalAnchorEditor.js +643 -643
- package/src/modules/SelectMenu.js +549 -549
- package/src/modules/_DragHandle.js +17 -17
- package/src/modules/index.js +14 -14
- package/src/plugins/browser/audioGallery.js +83 -83
- package/src/plugins/browser/fileBrowser.js +103 -103
- package/src/plugins/browser/fileGallery.js +83 -83
- package/src/plugins/browser/imageGallery.js +81 -81
- package/src/plugins/browser/videoGallery.js +103 -103
- package/src/plugins/command/blockquote.js +61 -60
- package/src/plugins/command/exportPDF.js +134 -134
- package/src/plugins/command/fileUpload.js +456 -456
- package/src/plugins/command/list_bulleted.js +149 -148
- package/src/plugins/command/list_numbered.js +152 -151
- package/src/plugins/dropdown/align.js +157 -155
- package/src/plugins/dropdown/backgroundColor.js +108 -104
- package/src/plugins/dropdown/font.js +141 -137
- package/src/plugins/dropdown/fontColor.js +109 -105
- package/src/plugins/dropdown/formatBlock.js +170 -178
- package/src/plugins/dropdown/hr.js +152 -152
- package/src/plugins/dropdown/layout.js +83 -83
- package/src/plugins/dropdown/lineHeight.js +131 -130
- package/src/plugins/dropdown/list.js +123 -122
- package/src/plugins/dropdown/paragraphStyle.js +138 -138
- package/src/plugins/dropdown/table.js +4110 -4000
- package/src/plugins/dropdown/template.js +83 -83
- package/src/plugins/dropdown/textStyle.js +149 -149
- package/src/plugins/field/mention.js +242 -242
- package/src/plugins/index.js +120 -120
- package/src/plugins/input/fontSize.js +414 -410
- package/src/plugins/input/pageNavigator.js +71 -70
- package/src/plugins/modal/audio.js +677 -677
- package/src/plugins/modal/drawing.js +537 -531
- package/src/plugins/modal/embed.js +886 -886
- package/src/plugins/modal/image.js +1377 -1376
- package/src/plugins/modal/link.js +248 -240
- package/src/plugins/modal/math.js +563 -563
- package/src/plugins/modal/video.js +1226 -1226
- package/src/plugins/popup/anchor.js +224 -222
- package/src/suneditor.js +114 -107
- package/src/themes/dark.css +132 -122
- package/src/typedef.js +132 -130
- package/types/assets/icons/defaultIcons.d.ts +8 -0
- package/types/core/base/eventManager.d.ts +29 -4
- package/types/core/class/char.d.ts +2 -1
- package/types/core/class/component.d.ts +1 -2
- package/types/core/class/format.d.ts +8 -1
- package/types/core/class/html.d.ts +8 -0
- package/types/core/class/menu.d.ts +8 -0
- package/types/core/class/offset.d.ts +24 -26
- package/types/core/class/selection.d.ts +2 -0
- package/types/core/class/toolbar.d.ts +6 -0
- package/types/core/class/ui.d.ts +1 -1
- package/types/core/editor.d.ts +34 -12
- package/types/core/section/constructor.d.ts +5 -638
- package/types/core/section/documentType.d.ts +12 -2
- package/types/core/section/options.d.ts +740 -0
- package/types/core/util/instanceCheck.d.ts +50 -0
- package/types/editorInjector/_core.d.ts +5 -5
- package/types/editorInjector/index.d.ts +2 -2
- package/types/events.d.ts +2 -0
- package/types/helper/converter.d.ts +9 -0
- package/types/helper/dom/domQuery.d.ts +5 -5
- package/types/helper/dom/domUtils.d.ts +8 -0
- package/types/helper/env.d.ts +6 -1
- package/types/helper/index.d.ts +4 -1
- package/types/index.d.ts +122 -120
- package/types/langs/_Lang.d.ts +194 -194
- package/types/modules/ColorPicker.d.ts +5 -1
- package/types/modules/Controller.d.ts +8 -4
- package/types/modules/Figure.d.ts +2 -1
- package/types/modules/HueSlider.d.ts +4 -1
- package/types/modules/SelectMenu.d.ts +1 -1
- package/types/plugins/command/blockquote.d.ts +1 -0
- package/types/plugins/command/list_bulleted.d.ts +1 -0
- package/types/plugins/command/list_numbered.d.ts +1 -0
- package/types/plugins/dropdown/align.d.ts +1 -0
- package/types/plugins/dropdown/backgroundColor.d.ts +1 -0
- package/types/plugins/dropdown/font.d.ts +1 -0
- package/types/plugins/dropdown/fontColor.d.ts +1 -0
- package/types/plugins/dropdown/formatBlock.d.ts +3 -2
- package/types/plugins/dropdown/lineHeight.d.ts +1 -0
- package/types/plugins/dropdown/list.d.ts +1 -0
- package/types/plugins/dropdown/table.d.ts +6 -0
- package/types/plugins/input/fontSize.d.ts +1 -0
- package/types/plugins/modal/drawing.d.ts +4 -0
- package/types/plugins/modal/link.d.ts +32 -15
- package/types/suneditor.d.ts +13 -9
- 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_;
|