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,1226 +1,1226 @@
1
- import EditorInjector from '../../editorInjector';
2
- import { Modal, Figure, FileManager } from '../../modules';
3
- import { dom, numbers, env, converter, keyCodeMap } from '../../helper';
4
- const { NO_EVENT } = env;
5
-
6
- /**
7
- * @typedef {import('../../events').VideoInfo} VideoInfo_video
8
- */
9
-
10
- /**
11
- * @typedef {import('../../modules/Figure').FigureControls} FigureControls_video
12
- */
13
-
14
- /**
15
- * @typedef {Object} VideoPluginOptions
16
- * @property {boolean} [canResize=true] - Whether the video element can be resized.
17
- * @property {boolean} [showHeightInput=true] - Whether to display the height input field.
18
- * @property {string} [defaultWidth] - The default width of the video element. If a number is provided, "px" will be appended.
19
- * @property {string} [defaultHeight] - The default height of the video element. If a number is provided, "px" will be appended.
20
- * @property {boolean} [percentageOnlySize=false] - Whether to allow only percentage-based sizing.
21
- * @property {boolean} [createFileInput=false] - Whether to create a file input element for video uploads.
22
- * @property {boolean} [createUrlInput=true] - Whether to create a URL input element for video embedding.
23
- * @property {string} [uploadUrl] - The URL endpoint for video file uploads.
24
- * @property {Object<string, string>} [uploadHeaders] - Additional headers to include in the video upload request.
25
- * @property {number} [uploadSizeLimit] - The total upload size limit for videos in bytes.
26
- * @property {number} [uploadSingleSizeLimit] - The single file upload size limit for videos in bytes.
27
- * @property {boolean} [allowMultiple=false] - Whether multiple video uploads are allowed.
28
- * @property {string} [acceptedFormats="video/*"] - Accepted file formats for video uploads.
29
- * @property {number} [defaultRatio=0.5625] - The default aspect ratio for the video (e.g., 16:9 is 0.5625).
30
- * @property {boolean} [showRatioOption=true] - Whether to display the ratio option in the modal.
31
- * @property {Array} [ratioOptions] - Custom ratio options for video resizing.
32
- * @property {Object<string, string>} [videoTagAttributes] - Additional attributes to set on the video tag.
33
- * @property {Object<string, string>} [iframeTagAttributes] - Additional attributes to set on the iframe tag.
34
- * @property {string} [query_youtube=""] - Additional query parameters for YouTube embedding.
35
- * @property {string} [query_vimeo=""] - Additional query parameters for Vimeo embedding.
36
- * @property {Object<string, {pattern: RegExp, action: (url: string) => string, tag: string}>} [embedQuery] - Custom query objects for additional embedding services.
37
- * @property {Array<RegExp>} [urlPatterns] - Additional URL patterns for video embedding.
38
- * @property {Array<string>} [extensions] - Additional file extensions to be recognized for video uploads.
39
- * @property {FigureControls_video} [controls] - Figure controls.
40
- */
41
-
42
- /**
43
- * @class
44
- * @description Video plugin.
45
- * - This plugin provides video embedding functionality within the editor.
46
- * - It also supports embedding from popular video services
47
- */
48
- class Video extends EditorInjector {
49
- static key = 'video';
50
- static type = 'modal';
51
- static className = '';
52
- /**
53
- * @this {Video}
54
- * @param {HTMLElement} node - The node to check.
55
- * @returns {HTMLElement|null} Returns a node if the node is a valid component.
56
- */
57
- static component(node) {
58
- if (/^(VIDEO)$/i.test(node?.nodeName)) {
59
- return node;
60
- } else if (/^(IFRAME)$/i.test(node?.nodeName)) {
61
- return this.checkContentType(/** @type {HTMLIFrameElement} */ (node).src) ? node : null;
62
- }
63
- return null;
64
- }
65
-
66
- /**
67
- * @constructor
68
- * @param {__se__EditorCore} editor - The root editor instance
69
- * @param {VideoPluginOptions} pluginOptions
70
- */
71
- constructor(editor, pluginOptions) {
72
- // plugin bisic properties
73
- super(editor);
74
- this.title = this.lang.video;
75
- this.icon = 'video';
76
-
77
- // define plugin options
78
- this.pluginOptions = {
79
- canResize: pluginOptions.canResize === undefined ? true : pluginOptions.canResize,
80
- showHeightInput: pluginOptions.showHeightInput === undefined ? true : !!pluginOptions.showHeightInput,
81
- defaultWidth: !pluginOptions.defaultWidth || !numbers.get(pluginOptions.defaultWidth, 0) ? '' : numbers.is(pluginOptions.defaultWidth) ? pluginOptions.defaultWidth + 'px' : pluginOptions.defaultWidth,
82
- defaultHeight: !pluginOptions.defaultHeight || !numbers.get(pluginOptions.defaultHeight, 0) ? '' : numbers.is(pluginOptions.defaultHeight) ? pluginOptions.defaultHeight + 'px' : pluginOptions.defaultHeight,
83
- percentageOnlySize: !!pluginOptions.percentageOnlySize,
84
- createFileInput: !!pluginOptions.createFileInput,
85
- createUrlInput: pluginOptions.createUrlInput === undefined || !pluginOptions.createFileInput ? true : pluginOptions.createUrlInput,
86
- uploadUrl: typeof pluginOptions.uploadUrl === 'string' ? pluginOptions.uploadUrl : null,
87
- uploadHeaders: pluginOptions.uploadHeaders || null,
88
- uploadSizeLimit: numbers.get(pluginOptions.uploadSizeLimit, 0),
89
- uploadSingleSizeLimit: numbers.get(pluginOptions.uploadSingleSizeLimit, 0),
90
- allowMultiple: !!pluginOptions.allowMultiple,
91
- acceptedFormats: typeof pluginOptions.acceptedFormats !== 'string' || pluginOptions.acceptedFormats.trim() === '*' ? 'video/*' : pluginOptions.acceptedFormats.trim() || 'video/*',
92
- defaultRatio: numbers.get(pluginOptions.defaultRatio, 4) || 0.5625,
93
- showRatioOption: pluginOptions.showRatioOption === undefined ? true : !!pluginOptions.showRatioOption,
94
- ratioOptions: !pluginOptions.ratioOptions ? null : pluginOptions.ratioOptions,
95
- videoTagAttributes: pluginOptions.videoTagAttributes || null,
96
- iframeTagAttributes: pluginOptions.iframeTagAttributes || null,
97
- query_youtube: pluginOptions.query_youtube || '',
98
- query_vimeo: pluginOptions.query_vimeo || ''
99
- };
100
-
101
- // create HTML
102
- const sizeUnit = this.pluginOptions.percentageOnlySize ? '%' : 'px';
103
- const modalEl = CreateHTML_modal(editor, this.pluginOptions);
104
- const figureControls = pluginOptions.controls || !this.pluginOptions.canResize ? [['align', 'edit', 'copy', 'remove']] : [['resize_auto,75,50', 'align', 'edit', 'revert', 'copy', 'remove']];
105
-
106
- // show align
107
- if (!figureControls.some((subArray) => subArray.includes('align'))) modalEl.alignForm.style.display = 'none';
108
-
109
- // modules
110
- const defaultRatio = this.pluginOptions.defaultRatio * 100 + '%';
111
- this.modal = new Modal(this, modalEl.html);
112
- this.figure = new Figure(this, figureControls, { sizeUnit: sizeUnit, autoRatio: { current: defaultRatio, default: defaultRatio } });
113
- this.fileManager = new FileManager(this, {
114
- query: 'iframe, video',
115
- loadHandler: this.events.onVideoLoad,
116
- eventHandler: this.events.onVideoAction
117
- });
118
-
119
- // members
120
- this.fileModalWrapper = modalEl.fileModalWrapper;
121
- this.videoInputFile = modalEl.videoInputFile;
122
- this.videoUrlFile = modalEl.videoUrlFile;
123
- this.focusElement = this.videoUrlFile || this.videoInputFile;
124
- this.previewSrc = modalEl.previewSrc;
125
- this._linkValue = '';
126
- this._align = 'none';
127
- this._frameRatio = defaultRatio;
128
- this._defaultRatio = defaultRatio;
129
- this._defaultSizeX = '100%';
130
- this._defaultSizeY = this.pluginOptions.defaultRatio * 100 + '%';
131
- this.sizeUnit = sizeUnit;
132
- this.proportion = null;
133
- this.frameRatioOption = null;
134
- this.inputX = null;
135
- this.inputY = null;
136
- this._element = null;
137
- this._cover = null;
138
- this._container = null;
139
- this._ratio = { w: 1, h: 1 };
140
- this._origin_w = this.pluginOptions.defaultWidth === '100%' ? '' : this.pluginOptions.defaultWidth;
141
- this._origin_h = this.pluginOptions.defaultHeight === defaultRatio ? '' : this.pluginOptions.defaultHeight;
142
- this._resizing = this.pluginOptions.canResize;
143
- this._onlyPercentage = this.pluginOptions.percentageOnlySize;
144
- this._nonResizing = !this._resizing || !this.pluginOptions.showHeightInput || this._onlyPercentage;
145
- this.query = {
146
- youtube: {
147
- pattern: /youtu\.?be/i,
148
- action: (url) => {
149
- url = this.convertUrlYoutube(url);
150
- return converter.addUrlQuery(url, this.pluginOptions.query_youtube);
151
- },
152
- tag: 'iframe'
153
- },
154
- vimeo: {
155
- pattern: /vimeo\.com/i,
156
- action: (url) => {
157
- url = this.convertUrlVimeo(url);
158
- return converter.addUrlQuery(url, this.pluginOptions.query_vimeo);
159
- },
160
- tag: 'iframe'
161
- },
162
- ...pluginOptions.embedQuery
163
- };
164
-
165
- const urlPatterns = [];
166
- for (const key in this.query) {
167
- urlPatterns.push(this.query[key].pattern);
168
- }
169
- this.extensions = ['.mp4', '.avi', '.mov', '.webm', '.flv', '.mkv', '.m4v', '.ogv'].concat(this.pluginOptions.extensions || []);
170
- this.urlPatterns = urlPatterns
171
- .concat([
172
- /youtu\.?be/,
173
- /vimeo\.com\//,
174
- /dailymotion\.com\/video\//,
175
- /facebook\.com\/.+\/videos\//,
176
- /facebook\.com\/watch\/\?v=/,
177
- /twitter\.com\/.+\/status\//,
178
- /twitch\.tv\/videos\//,
179
- /twitch\.tv\/[^/]+$/,
180
- /tiktok\.com\/@[^/]+\/video\//,
181
- /instagram\.com\/p\//,
182
- /instagram\.com\/tv\//,
183
- /instagram\.com\/reel\//,
184
- /linkedin\.com\/posts\//,
185
- /\.(wistia\.com|wi\.st)\/(medias|embed)\//,
186
- /loom\.com\/share\//
187
- ])
188
- .concat(pluginOptions.urlPatterns || []);
189
-
190
- const galleryButton = modalEl.galleryButton;
191
- if (galleryButton) this.eventManager.addEvent(galleryButton, 'click', this.#OpenGallery.bind(this));
192
-
193
- // init
194
- if (this.videoInputFile) this.eventManager.addEvent(modalEl.fileRemoveBtn, 'click', this.#RemoveSelectedFiles.bind(this));
195
- if (this.videoUrlFile) this.eventManager.addEvent(this.videoUrlFile, 'input', this.#OnLinkPreview.bind(this));
196
- if (this.videoInputFile && this.videoUrlFile) this.eventManager.addEvent(this.videoInputFile, 'change', this.#OnfileInputChange.bind(this));
197
-
198
- if (this._resizing) {
199
- this.proportion = modalEl.proportion;
200
- this.frameRatioOption = modalEl.frameRatioOption;
201
- this.inputX = modalEl.inputX;
202
- this.inputY = modalEl.inputY;
203
- this.inputX.value = this.pluginOptions.defaultWidth;
204
- this.inputY.value = this.pluginOptions.defaultHeight;
205
-
206
- const ratioChange = this.#OnChangeRatio.bind(this);
207
- this.eventManager.addEvent(this.inputX, 'keyup', this.#OnInputSize.bind(this, 'x'));
208
- this.eventManager.addEvent(this.inputY, 'keyup', this.#OnInputSize.bind(this, 'y'));
209
- this.eventManager.addEvent(this.inputX, 'change', ratioChange);
210
- this.eventManager.addEvent(this.inputY, 'change', ratioChange);
211
- this.eventManager.addEvent(this.proportion, 'change', ratioChange);
212
- this.eventManager.addEvent(this.frameRatioOption, 'change', this.#SetRatio.bind(this));
213
- this.eventManager.addEvent(modalEl.revertBtn, 'click', this.#OnClickRevert.bind(this));
214
- }
215
- }
216
-
217
- /**
218
- * @editorMethod Modules.Modal
219
- * @description Executes the method that is called when a "Modal" module's is opened.
220
- */
221
- open() {
222
- this.modal.open();
223
- }
224
-
225
- /**
226
- * @editorMethod Modules.Controller(Figure)
227
- * @description Executes the method that is called when a target component is edited.
228
- */
229
- edit() {
230
- this.modal.open();
231
- }
232
-
233
- /**
234
- * @editorMethod Modules.Modal
235
- * @description Executes the method that is called when a plugin's modal is opened.
236
- * @param {boolean} isUpdate "Indicates whether the modal is for editing an existing component (true) or registering a new one (false)."
237
- */
238
- on(isUpdate) {
239
- if (!isUpdate) {
240
- if (this._resizing) {
241
- this.inputX.value = this._origin_w = this.pluginOptions.defaultWidth === this._defaultSizeX ? '' : this.pluginOptions.defaultWidth;
242
- this.inputY.value = this._origin_h = this.pluginOptions.defaultHeight === this._defaultSizeY ? '' : this.pluginOptions.defaultHeight;
243
- this.proportion.disabled = true;
244
- }
245
- if (this.videoInputFile && this.pluginOptions.allowMultiple) this.videoInputFile.setAttribute('multiple', 'multiple');
246
- } else {
247
- if (this.videoInputFile && this.pluginOptions.allowMultiple) this.videoInputFile.removeAttribute('multiple');
248
- }
249
-
250
- if (this._resizing) {
251
- this._setRatioSelect(this._origin_h || this._defaultRatio);
252
- }
253
- }
254
-
255
- /**
256
- * @editorMethod Editor.EventManager
257
- * @description Executes the event function of "paste" or "drop".
258
- * @param {Object} params { frameContext, event, file }
259
- * @param {__se__FrameContext} params.frameContext Frame context
260
- * @param {ClipboardEvent} params.event Event object
261
- * @param {File} params.file File object
262
- * @returns {boolean} - If return false, the file upload will be canceled
263
- */
264
- onFilePasteAndDrop({ file }) {
265
- if (!/^video/.test(file.type)) return;
266
-
267
- this.submitFile([file]);
268
- this.editor.focus();
269
-
270
- return false;
271
- }
272
-
273
- /**
274
- * @editorMethod Modules.Modal
275
- * @description This function is called when a form within a modal window is "submit".
276
- * @returns {Promise<boolean>} Success / failure
277
- */
278
- async modalAction() {
279
- this._align = /** @type {HTMLInputElement} */ (this.modal.form.querySelector('input[name="suneditor_video_radio"]:checked')).value;
280
-
281
- let result = false;
282
- if (this.videoInputFile && this.videoInputFile.files.length > 0) {
283
- result = await this.submitFile(this.videoInputFile.files);
284
- } else if (this.videoUrlFile && this._linkValue.length > 0) {
285
- result = await this.submitURL(this._linkValue);
286
- }
287
-
288
- if (result) this._w.setTimeout(this.component.select.bind(this.component, this._element, Video.key), 0);
289
-
290
- return result;
291
- }
292
-
293
- /**
294
- * @editorMethod Editor.core
295
- * @description This method is used to validate and preserve the format of the component within the editor.
296
- * - It ensures that the structure and attributes of the element are maintained and secure.
297
- * - The method checks if the element is already wrapped in a valid container and updates its attributes if necessary.
298
- * - If the element isn't properly contained, a new container is created to retain the format.
299
- * @returns {{query: string, method: (element: HTMLIFrameElement|HTMLVideoElement) => void}} The format retention object containing the query and method to process the element.
300
- * - query: The selector query to identify the relevant elements (in this case, 'audio').
301
- * - method:The function to execute on the element to validate and preserve its format.
302
- * - The function takes the element as an argument, checks if it is contained correctly, and applies necessary adjustments.
303
- */
304
- retainFormat() {
305
- return {
306
- query: 'iframe, video',
307
- method: async (element) => {
308
- if (/^(iframe)$/i.test(element?.nodeName)) {
309
- if (!this.checkContentType(element.src)) return;
310
- }
311
-
312
- const figureInfo = Figure.GetContainer(element);
313
- if (figureInfo && figureInfo.container && figureInfo.cover) return;
314
-
315
- this._ready(element);
316
- const line = this.format.getLine(element);
317
- if (line) this._align = line.style.textAlign || line.style.float;
318
-
319
- this._update(element);
320
- }
321
- };
322
- }
323
-
324
- /**
325
- * @editorMethod Modules.Modal
326
- * @description This function is called before the modal window is opened, but before it is closed.
327
- */
328
- init() {
329
- Modal.OnChangeFile(this.fileModalWrapper, []);
330
- if (this.videoInputFile) this.videoInputFile.value = '';
331
- if (this.videoUrlFile) this._linkValue = this.previewSrc.textContent = this.videoUrlFile.value = '';
332
- if (this.videoInputFile && this.videoUrlFile) {
333
- this.videoUrlFile.disabled = false;
334
- this.previewSrc.style.textDecoration = '';
335
- }
336
-
337
- /** @type {HTMLInputElement} */ (this.modal.form.querySelector('input[name="suneditor_video_radio"][value="none"]')).checked = true;
338
- this._ratio = { w: 1, h: 1 };
339
- this._nonResizing = false;
340
-
341
- if (this._resizing) {
342
- this.inputX.value = this.pluginOptions.defaultWidth === this._defaultSizeX ? '' : this.pluginOptions.defaultWidth;
343
- this.inputY.value = this.pluginOptions.defaultHeight === this._defaultSizeY ? '' : this.pluginOptions.defaultHeight;
344
- this.proportion.checked = false;
345
- this.proportion.disabled = true;
346
- this._setRatioSelect(this._defaultRatio);
347
- }
348
- }
349
-
350
- /**
351
- * @editorMethod Editor.component
352
- * @description Executes the method that is called when a component of a plugin is selected.
353
- * @param {HTMLIFrameElement|HTMLVideoElement} target Target component element
354
- */
355
- select(target) {
356
- this._ready(target);
357
- }
358
-
359
- /**
360
- * @private
361
- * @description Prepares the component for selection.
362
- * - Ensures that the controller is properly positioned and initialized.
363
- * - Prevents duplicate event handling if the component is already selected.
364
- * @param {HTMLIFrameElement|HTMLVideoElement} target - The selected element.
365
- */
366
- _ready(target) {
367
- if (!target) return;
368
- const figureInfo = this.figure.open(target, { nonResizing: this._nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, __fileManagerInfo: false });
369
-
370
- this._element = target;
371
- this._cover = figureInfo.cover;
372
- this._container = figureInfo.container;
373
- this._align = figureInfo.align;
374
- target.style.float = '';
375
-
376
- this._origin_w = String(figureInfo.width || figureInfo.originWidth || figureInfo.w || '');
377
- this._origin_h = String(figureInfo.height || figureInfo.originHeight || figureInfo.h || '');
378
-
379
- let w = figureInfo.width || figureInfo.w || this._origin_w || '';
380
- const h = figureInfo.height || figureInfo.h || this._origin_h || '';
381
-
382
- if (this.videoUrlFile) this._linkValue = this.previewSrc.textContent = this.videoUrlFile.value = this._element.src || this._element.querySelector('source')?.src || '';
383
-
384
- /** @type {HTMLInputElement} */
385
- const activeAlgin = this.modal.form.querySelector('input[name="suneditor_video_radio"][value="' + this._align + '"]') || this.modal.form.querySelector('input[name="suneditor_video_radio"][value="none"]');
386
- activeAlgin.checked = true;
387
-
388
- if (!this._resizing) return;
389
-
390
- const percentageRotation = this._onlyPercentage && this.figure.isVertical;
391
- if (this._onlyPercentage) {
392
- w = numbers.get(w, 2);
393
- if (w > 100) w = 100;
394
- }
395
- this.inputX.value = String(w === 'auto' ? '' : w);
396
-
397
- if (!this._onlyPercentage) {
398
- const infoH = percentageRotation ? '' : figureInfo.height;
399
- this.inputY.value = String(infoH === 'auto' ? '' : infoH);
400
- }
401
-
402
- if (!this._setRatioSelect(h)) this.inputY.value = String(this._onlyPercentage ? numbers.get(h, 2) : h);
403
-
404
- this.proportion.checked = true;
405
- this.inputX.disabled = percentageRotation ? true : false;
406
- this.inputY.disabled = percentageRotation ? true : false;
407
- this.proportion.disabled = percentageRotation ? true : false;
408
-
409
- this._ratio = this.proportion.checked ? figureInfo.ratio : { w: 1, h: 1 };
410
- }
411
-
412
- /**
413
- * @editorMethod Editor.Component
414
- * @description Method to delete a component of a plugin, called by the "FileManager", "Controller" module.
415
- * @param {HTMLElement} target Target element
416
- * @returns {Promise<void>}
417
- */
418
- async destroy(target) {
419
- const targetEl = target || this._element;
420
- const container = dom.query.getParentElement(targetEl, Figure.is) || targetEl;
421
- const focusEl = container.previousElementSibling || container.nextElementSibling;
422
- const emptyDiv = container.parentNode;
423
-
424
- const message = await this.triggerEvent('onVideoDeleteBefore', { element: targetEl, container, align: this._align, url: this._linkValue });
425
- if (message === false) return;
426
-
427
- dom.utils.removeItem(container);
428
- this.init();
429
-
430
- if (emptyDiv !== this.editor.frameContext.get('wysiwyg')) {
431
- this.nodeTransform.removeAllParents(
432
- emptyDiv,
433
- function (current) {
434
- return current.childNodes.length === 0;
435
- },
436
- null
437
- );
438
- }
439
-
440
- // focus
441
- this.editor.focusEdge(focusEl);
442
- this.history.push(false);
443
- }
444
-
445
- /**
446
- * @description Checks if the given URL matches any of the defined URL patterns.
447
- * @param {string} url - The URL to check.
448
- * @returns {boolean} True if the URL matches a known pattern; otherwise, false.
449
- */
450
- checkContentType(url) {
451
- url = url?.toLowerCase() || '';
452
- if (this.extensions.some((ext) => url.endsWith(ext)) || this.urlPatterns.some((pattern) => pattern.test(url))) {
453
- return true;
454
- }
455
-
456
- return false;
457
- }
458
-
459
- /**
460
- * @description Finds and processes the URL for video by matching it against known service patterns.
461
- * @param {string} url - The original URL.
462
- * @returns {{origin: string, url: string, tag: string}|null} An object containing the original URL, the processed URL, and the tag type (e.g., 'iframe'),
463
- * or null if no matching pattern is found.
464
- */
465
- findProcessUrl(url) {
466
- const query = this.query;
467
- for (const key in query) {
468
- const service = query[key];
469
- if (service.pattern.test(url)) {
470
- return {
471
- origin: url,
472
- url: service.action(url),
473
- tag: service.tag
474
- };
475
- }
476
- }
477
-
478
- return null;
479
- }
480
-
481
- /**
482
- * @description Converts a YouTube URL into an embeddable URL.
483
- * - If the URL does not start with "http", it prepends "https://". It also replaces "watch?v=" with the embed path.
484
- * @param {string} url - The original YouTube URL.
485
- * @returns {string} The converted YouTube embed URL.
486
- */
487
- convertUrlYoutube(url) {
488
- if (!/^http/.test(url)) url = 'https://' + url;
489
- url = url.replace('watch?v=', '');
490
- if (!/^\/\/.+\/embed\//.test(url)) {
491
- url = url.replace(url.match(/\/\/.+\//)[0], '//www.youtube.com/embed/').replace('&', '?&');
492
- }
493
- return url;
494
- }
495
-
496
- /**
497
- * @description Converts a Vimeo URL into an embeddable URL.
498
- * - Removes any trailing slash and extracts the video ID from the URL.
499
- * @param {string} url - The original Vimeo URL.
500
- * @returns {string} The converted Vimeo embed URL.
501
- */
502
- convertUrlVimeo(url) {
503
- if (url.endsWith('/')) {
504
- url = url.slice(0, -1);
505
- }
506
- url = 'https://player.vimeo.com/video/' + url.slice(url.lastIndexOf('/') + 1);
507
- return url;
508
- }
509
-
510
- /**
511
- * @description Adds query parameters to a URL.
512
- * - If the URL already contains a query string, the provided query is appended with an "&".
513
- * @param {string} url - The original URL.
514
- * @param {string} query - The query string to append.
515
- * @returns {string} The URL with the appended query parameters.
516
- */
517
- addQuery(url, query) {
518
- if (query.length > 0) {
519
- if (/\?/.test(url)) {
520
- const splitUrl = url.split('?');
521
- url = splitUrl[0] + '?' + query + '&' + splitUrl[1];
522
- } else {
523
- url += '?' + query;
524
- }
525
- }
526
- return url;
527
- }
528
-
529
- /**
530
- * @description Creates or updates a video embed component.
531
- * - When updating, it replaces the existing element if necessary and applies the new source, size, and alignment.
532
- * - When creating, it wraps the provided element in a figure container.
533
- * @param {HTMLIFrameElement|HTMLVideoElement} oFrame - The existing video element (for update) or a newly created one.
534
- * @param {string} src - The source URL for the video.
535
- * @param {string} width - The desired width for the video element.
536
- * @param {string} height - The desired height for the video element.
537
- * @param {string} align - The alignment to apply to the video element (e.g., 'left', 'center', 'right').
538
- * @param {boolean} isUpdate - Indicates whether this is an update to an existing component (true) or a new creation (false).
539
- * @param {{name: string, size: number}} file - File metadata associated with the video
540
- */
541
- create(oFrame, src, width, height, align, isUpdate, file) {
542
- let cover = null;
543
- let container = null;
544
-
545
- /** update */
546
- if (isUpdate) {
547
- oFrame = this._element;
548
- if (oFrame.src !== src) {
549
- const processUrl = this.findProcessUrl(src);
550
- if (/^iframe$/i.test(processUrl?.tag) && !/^iframe$/i.test(oFrame.nodeName)) {
551
- const newTag = this.createIframeTag();
552
- newTag.src = src;
553
- oFrame.replaceWith(newTag);
554
- this._element = oFrame = newTag;
555
- } else if (/^video$/i.test(processUrl?.tag) && !/^video$/i.test(oFrame.nodeName)) {
556
- const newTag = this.createVideoTag();
557
- newTag.src = src;
558
- oFrame.replaceWith(newTag);
559
- this._element = oFrame = newTag;
560
- } else {
561
- oFrame.src = src;
562
- }
563
- }
564
- container = this._container;
565
- cover = dom.query.getParentElement(oFrame, 'FIGURE');
566
- } else {
567
- /** create */
568
- oFrame.src = src;
569
- this._element = oFrame;
570
- const figure = Figure.CreateContainer(oFrame, 'se-video-container');
571
- cover = figure.cover;
572
- container = figure.container;
573
- }
574
-
575
- /** rendering */
576
- this._element = oFrame;
577
- this._cover = cover;
578
- this._container = container;
579
- this.figure.open(oFrame, { nonResizing: this._nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, __fileManagerInfo: true });
580
-
581
- width = width || this._defaultSizeX;
582
- height = height || this._frameRatio;
583
- const size = this.figure.getSize(oFrame);
584
- const inputUpdate = size.w !== width || size.h !== height;
585
- const changeSize = !isUpdate || inputUpdate;
586
-
587
- // set size
588
- if (changeSize) {
589
- this._applySize(width, height);
590
- }
591
-
592
- // align
593
- this.figure.setAlign(oFrame, align);
594
-
595
- // select figure
596
- // oFrame.onload = OnloadVideo.bind(this, oFrame);
597
- this.fileManager.setFileData(oFrame, file);
598
-
599
- if (!isUpdate) {
600
- this.component.insert(container, { skipCharCount: false, skipSelection: true, skipHistory: false });
601
- if (!this.options.get('componentAutoSelect')) {
602
- const line = this.format.addLine(container, null);
603
- if (line) this.selection.setRange(line, 0, line, 0);
604
- }
605
- return;
606
- }
607
-
608
- if (this._resizing && changeSize && this.figure.isVertical) this.figure.setTransform(oFrame, width, height, 0);
609
- this.history.push(false);
610
- }
611
-
612
- /**
613
- * @description Creates a new iframe element for video embedding.
614
- * - Applies any additional properties provided and sets the necessary attributes for embedding.
615
- * @param {Object<string, string>} [props] - An optional object containing properties to assign to the iframe.
616
- * @returns {HTMLIFrameElement} The newly created iframe element.
617
- */
618
- createIframeTag(props) {
619
- /** @type {HTMLIFrameElement} */
620
- const iframeTag = dom.utils.createElement('IFRAME');
621
- if (props) {
622
- for (const key in props) {
623
- iframeTag[key] = props[key];
624
- }
625
- }
626
- this._setIframeAttrs(iframeTag);
627
- return iframeTag;
628
- }
629
-
630
- /**
631
- * @description Creates a new video element for video embedding.
632
- * - Applies any additional properties provided and sets the necessary attributes.
633
- * @param {Object<string, string>} [props] - An optional object containing properties to assign to the video element.
634
- * @returns {HTMLVideoElement} The newly created video element.
635
- */
636
- createVideoTag(props) {
637
- /** @type {HTMLVideoElement} */
638
- const videoTag = dom.utils.createElement('VIDEO');
639
- if (props) {
640
- for (const key in props) {
641
- videoTag[key] = props[key];
642
- }
643
- }
644
- this._setTagAttrs(videoTag);
645
- return videoTag;
646
- }
647
-
648
- /**
649
- * @private
650
- * @description Sets the size of the video element.
651
- * @param {string|number} w - The width of the video.
652
- * @param {string|number} h - The height of the video.
653
- */
654
- _applySize(w, h) {
655
- if (!w) w = this.inputX?.value || this.pluginOptions.defaultWidth;
656
- if (!h) h = this.inputY?.value || this.pluginOptions.defaultHeight;
657
- if (this._onlyPercentage) {
658
- if (!w) w = '100%';
659
- else if (/%$/.test(w + '')) w += '%';
660
- }
661
- this.figure.setSize(w, h);
662
- }
663
-
664
- /**
665
- * @private
666
- * @description Retrieves video information including size and alignment.
667
- * @returns {*} Video information object.
668
- */
669
- _getInfo() {
670
- return {
671
- inputWidth: this.inputX?.value || '',
672
- inputHeight: this.inputY?.value || '',
673
- align: this._align,
674
- isUpdate: this.modal.isUpdate,
675
- element: this._element
676
- };
677
- }
678
-
679
- /**
680
- * @description Create an "video" component using the provided files.
681
- * @param {FileList|File[]} fileList File object list
682
- * @returns {Promise<boolean>} If return false, the file upload will be canceled
683
- */
684
- async submitFile(fileList) {
685
- if (fileList.length === 0) return;
686
-
687
- let fileSize = 0;
688
- const files = [];
689
- const slngleSizeLimit = this.pluginOptions.uploadSingleSizeLimit;
690
- for (let i = 0, len = fileList.length, f, s; i < len; i++) {
691
- f = fileList[i];
692
- if (!/video/i.test(f.type)) continue;
693
-
694
- s = f.size;
695
- if (slngleSizeLimit && slngleSizeLimit > s) {
696
- const err = '[SUNEDITOR.videoUpload.fail] Size of uploadable single file: ' + slngleSizeLimit / 1000 + 'KB';
697
- const message = await this.triggerEvent('onVideoUploadError', {
698
- error: err,
699
- limitSize: slngleSizeLimit,
700
- uploadSize: s,
701
- file: f
702
- });
703
-
704
- this.ui.alertOpen(message === NO_EVENT ? err : message || err, 'error');
705
-
706
- return false;
707
- }
708
-
709
- files.push(f);
710
- fileSize += s;
711
- }
712
-
713
- const limitSize = this.pluginOptions.uploadSizeLimit;
714
- const currentSize = this.fileManager.getSize();
715
- if (limitSize > 0 && fileSize + currentSize > limitSize) {
716
- const err = '[SUNEDITOR.videoUpload.fail] Size of uploadable total videos: ' + limitSize / 1000 + 'KB';
717
- const message = await this.triggerEvent('onVideoUploadError', { error: err, limitSize, currentSize, uploadSize: fileSize });
718
-
719
- this.ui.alertOpen(message === NO_EVENT ? err : message || err, 'error');
720
-
721
- return false;
722
- }
723
-
724
- const videoInfo = {
725
- url: null,
726
- files,
727
- ...this._getInfo()
728
- };
729
-
730
- const handler = function (infos, newInfos) {
731
- infos = newInfos || infos;
732
- this._serverUpload(infos, infos.files);
733
- }.bind(this, videoInfo);
734
- // se-ts-ignore
735
- this._serverUpload;
736
-
737
- const result = await this.triggerEvent('onVideoUploadBefore', {
738
- info: videoInfo,
739
- handler
740
- });
741
-
742
- if (result === undefined) return true;
743
- if (result === false) return false;
744
- if (result !== null && typeof result === 'object') handler(result);
745
-
746
- if (result === true || result === NO_EVENT) handler(null);
747
- }
748
-
749
- /**
750
- * @description Create an "video" component using the provided url.
751
- * @param {string} url File url
752
- * @returns {Promise<boolean>} If return false, the file upload will be canceled
753
- */
754
- async submitURL(url) {
755
- if (!url) url = this._linkValue;
756
- if (!url) return false;
757
-
758
- /** iframe source */
759
- if (/^<iframe.*\/iframe>$/.test(url)) {
760
- const oIframe = new DOMParser().parseFromString(url, 'text/html').querySelector('iframe');
761
- url = oIframe.src;
762
- if (url.length === 0) return false;
763
- }
764
-
765
- const processUrl = this.findProcessUrl(url);
766
- if (processUrl) {
767
- url = processUrl.url;
768
- }
769
-
770
- const file = { name: url.split('/').pop(), size: 0 };
771
- const videoInfo = { url, files: file, ...this._getInfo(), process: processUrl };
772
-
773
- const handler = function (infos, newInfos) {
774
- infos = newInfos || infos;
775
- this.create(this[/^iframe$/i.test(infos.process?.tag) ? 'createIframeTag' : 'createVideoTag'](), infos.url, infos.inputWidth, infos.inputHeight, infos.align, infos.isUpdate, infos.files);
776
- }.bind(this, videoInfo);
777
-
778
- const result = await this.triggerEvent('onVideoUploadBefore', {
779
- info: videoInfo,
780
- handler
781
- });
782
-
783
- if (result === undefined) return true;
784
- if (result === false) return false;
785
- if (result !== null && typeof result === 'object') handler(result);
786
-
787
- if (result === true || result === NO_EVENT) handler(null);
788
-
789
- return true;
790
- }
791
-
792
- /**
793
- * @private
794
- * @description Updates the video component within the editor.
795
- * @param {HTMLIFrameElement|HTMLVideoElement} oFrame - The video element to update.
796
- */
797
- _update(oFrame) {
798
- if (!oFrame) return;
799
-
800
- if (/^video$/i.test(oFrame.nodeName)) {
801
- this._setTagAttrs(/** @type {HTMLVideoElement} */ (oFrame));
802
- } else if (/^iframe$/i.test(oFrame.nodeName)) {
803
- this._setIframeAttrs(/** @type {HTMLIFrameElement} */ (oFrame));
804
- }
805
-
806
- let existElement = this.format.isBlock(oFrame.parentNode) || dom.check.isWysiwygFrame(oFrame.parentNode) ? oFrame : this.format.getLine(oFrame) || oFrame;
807
-
808
- const prevFrame = oFrame;
809
- const cloneFrame = /** @type {HTMLIFrameElement|HTMLVideoElement} */ (oFrame.cloneNode(true));
810
- const figure = Figure.CreateContainer(cloneFrame, 'se-video-container');
811
- const container = figure.container;
812
-
813
- const figcaption = existElement.querySelector('figcaption');
814
- let caption = null;
815
- if (figcaption) {
816
- caption = dom.utils.createElement('DIV');
817
- caption.innerHTML = figcaption.innerHTML;
818
- dom.utils.removeItem(figcaption);
819
- }
820
-
821
- // size
822
- this.figure.open(cloneFrame, { nonResizing: this._nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, __fileManagerInfo: true });
823
- const size = (cloneFrame.getAttribute('data-se-size') || ',').split(',');
824
- this._applySize(size[0] || prevFrame.style.width || prevFrame.width || '', size[1] || prevFrame.style.height || prevFrame.height || '');
825
-
826
- // align
827
- const format = this.format.getLine(prevFrame);
828
- if (format) this._align = format.style.textAlign || format.style.float;
829
- this.figure.setAlign(cloneFrame, this._align);
830
-
831
- if (dom.query.getParentElement(prevFrame, dom.check.isExcludeFormat)) {
832
- prevFrame.replaceWith(container);
833
- } else if (dom.check.isListCell(existElement)) {
834
- const refer = dom.query.getParentElement(prevFrame, (current) => current.parentNode === existElement);
835
- existElement.insertBefore(container, refer);
836
- dom.utils.removeItem(prevFrame);
837
- this.nodeTransform.removeEmptyNode(refer, null, true);
838
- } else if (this.format.isLine(existElement)) {
839
- const refer = dom.query.getParentElement(prevFrame, (current) => current.parentNode === existElement);
840
- existElement = this.nodeTransform.split(existElement, refer);
841
- existElement.parentNode.insertBefore(container, existElement);
842
- dom.utils.removeItem(prevFrame);
843
- this.nodeTransform.removeEmptyNode(existElement, null, true);
844
- } else {
845
- /** @type {Element} */ (existElement).replaceWith(container);
846
- }
847
-
848
- if (caption) existElement.parentNode.insertBefore(caption, container.nextElementSibling);
849
-
850
- return cloneFrame;
851
- }
852
-
853
- /**
854
- * @private
855
- * @description Registers the uploaded video in the editor.
856
- * @param {VideoInfo_video} info - Video information object.
857
- * @param {Object<string, *>} response - Server response containing video data.
858
- */
859
- _register(info, response) {
860
- const fileList = response.result;
861
- const videoTag = this.createVideoTag();
862
-
863
- for (let i = 0, len = fileList.length; i < len; i++) {
864
- const ctag = info.isUpdate ? info.element : /** @type {HTMLIFrameElement|HTMLVideoElement} */ (videoTag.cloneNode(false));
865
- this.create(ctag, fileList[i].url, info.inputWidth, info.inputHeight, info.align, info.isUpdate, {
866
- name: fileList[i].name,
867
- size: fileList[i].size
868
- });
869
- }
870
- }
871
-
872
- /**
873
- * @private
874
- * @description Uploads a video to the server using an external upload handler.
875
- * @param {VideoInfo_video} info - Video information object.
876
- * @param {FileList} files - The video files to upload.
877
- */
878
- _serverUpload(info, files) {
879
- if (!files) return;
880
-
881
- const videoUploadUrl = this.pluginOptions.uploadUrl;
882
- if (typeof videoUploadUrl === 'string' && videoUploadUrl.length > 0) {
883
- this.fileManager.upload(videoUploadUrl, this.pluginOptions.uploadHeaders, files, this.#UploadCallBack.bind(this, info), this._error.bind(this));
884
- }
885
- }
886
-
887
- /**
888
- * @private
889
- * @description Sets attributes for the video tag.
890
- * @param {HTMLVideoElement} element - The video element.
891
- */
892
- _setTagAttrs(element) {
893
- element.setAttribute('controls', 'true');
894
-
895
- const attrs = this.pluginOptions.videoTagAttributes;
896
- if (!attrs) return;
897
-
898
- for (const key in attrs) {
899
- element.setAttribute(key, attrs[key]);
900
- }
901
- }
902
-
903
- /**
904
- * @private
905
- * @description Sets attributes for the iframe tag.
906
- * @param {HTMLIFrameElement} element - The iframe element.
907
- */
908
- _setIframeAttrs(element) {
909
- element.frameBorder = '0';
910
- element.allowFullscreen = true;
911
-
912
- const attrs = this.pluginOptions.iframeTagAttributes;
913
- if (!attrs) return;
914
-
915
- for (const key in attrs) {
916
- element.setAttribute(key, attrs[key]);
917
- }
918
- }
919
-
920
- /**
921
- * @private
922
- * @description Selects a ratio option in the ratio dropdown.
923
- * @param {string|number} value - The selected ratio value.
924
- * @returns {boolean} Returns true if a ratio was selected.
925
- */
926
- _setRatioSelect(value) {
927
- let ratioSelected = false;
928
- const ratioOption = this.frameRatioOption.options;
929
-
930
- if (/%$/.test(value + '') || this._onlyPercentage) value = numbers.get(value, 2) / 100 + '';
931
- else if (!numbers.is(value) || Number(value) >= 1) value = '';
932
-
933
- this.inputY.placeholder = '';
934
- for (let i = 0, len = ratioOption.length; i < len; i++) {
935
- if (ratioOption[i].value === value) {
936
- ratioSelected = ratioOption[i].selected = true;
937
- this.inputY.placeholder = !value ? '' : Number(value) * 100 + '%';
938
- } else ratioOption[i].selected = false;
939
- }
940
-
941
- return ratioSelected;
942
- }
943
-
944
- /**
945
- * @private
946
- * @description Handles video upload errors.
947
- * @param {Object<string, *>} response - The error response object.
948
- * @returns {Promise<void>}
949
- */
950
- async _error(response) {
951
- const message = await this.triggerEvent('onVideoUploadError', { error: response });
952
- const err = message === NO_EVENT ? response.errorMessage : message || response.errorMessage;
953
- this.ui.alertOpen(err, 'error');
954
- console.error('[SUNEDITOR.plugin.video.error]', message);
955
- }
956
-
957
- /**
958
- * @description Handles the callback function for video upload completion.
959
- * @param {VideoInfo_video} info - Video information.
960
- * @param {XMLHttpRequest} xmlHttp - The XMLHttpRequest object.
961
- */
962
- async #UploadCallBack(info, xmlHttp) {
963
- if ((await this.triggerEvent('videoUploadHandler', { xmlHttp, info })) === NO_EVENT) {
964
- const response = JSON.parse(xmlHttp.responseText);
965
- if (response.errorMessage) {
966
- this._error(response);
967
- } else {
968
- this._register(info, response);
969
- }
970
- }
971
- }
972
-
973
- /**
974
- * @description Removes selected files from the file input.
975
- */
976
- #RemoveSelectedFiles() {
977
- this.videoInputFile.value = '';
978
- if (this.videoUrlFile) {
979
- this.videoUrlFile.disabled = false;
980
- this.previewSrc.style.textDecoration = '';
981
- }
982
-
983
- // inputFile check
984
- Modal.OnChangeFile(this.fileModalWrapper, []);
985
- }
986
-
987
- /**
988
- * @description Handles link preview input changes.
989
- * @param {InputEvent} e - Event object
990
- */
991
- #OnLinkPreview(e) {
992
- /** @type {HTMLInputElement} */
993
- const eventTarget = dom.query.getEventTarget(e);
994
- const value = eventTarget.value.trim();
995
- if (/^<iframe.*\/iframe>$/.test(value)) {
996
- this._linkValue = value;
997
- this.previewSrc.textContent = '<IFrame :src=".."></IFrame>';
998
- } else {
999
- this._linkValue = this.previewSrc.textContent = !value
1000
- ? ''
1001
- : this.options.get('defaultUrlProtocol') && !value.includes('://') && value.indexOf('#') !== 0
1002
- ? this.options.get('defaultUrlProtocol') + value
1003
- : !value.includes('://')
1004
- ? '/' + value
1005
- : value;
1006
- }
1007
- }
1008
-
1009
- /**
1010
- * @description Opens the video gallery.
1011
- */
1012
- #OpenGallery() {
1013
- this.plugins.videoGallery.open(this.#SetUrlInput.bind(this));
1014
- }
1015
-
1016
- /**
1017
- * @description Sets the URL input value when selecting from the gallery.
1018
- * @param {HTMLInputElement} target - The selected video element.
1019
- */
1020
- #SetUrlInput(target) {
1021
- this._linkValue = this.previewSrc.textContent = this.videoUrlFile.value = target.getAttribute('data-command') || target.src;
1022
- this.videoUrlFile.focus();
1023
- }
1024
-
1025
- /**
1026
- * @param {InputEvent} e - Event object
1027
- */
1028
- #OnfileInputChange(e) {
1029
- if (!this.videoInputFile.value) {
1030
- this.videoUrlFile.disabled = false;
1031
- this.previewSrc.style.textDecoration = '';
1032
- } else {
1033
- this.videoUrlFile.disabled = true;
1034
- this.previewSrc.style.textDecoration = 'line-through';
1035
- }
1036
-
1037
- // inputFile check
1038
- /** @type {HTMLInputElement} */
1039
- const eventTarget = dom.query.getEventTarget(e);
1040
- Modal.OnChangeFile(this.fileModalWrapper, eventTarget.files);
1041
- }
1042
-
1043
- #OnClickRevert() {
1044
- if (this._onlyPercentage) {
1045
- this.inputX.value = Number(this._origin_w) > 100 ? '100' : this._origin_w;
1046
- } else {
1047
- this.inputX.value = this._origin_w;
1048
- this.inputY.value = this._origin_h;
1049
- }
1050
- }
1051
-
1052
- /**
1053
- * @param {InputEvent} e - Event object
1054
- */
1055
- #SetRatio(e) {
1056
- /** @type {HTMLSelectElement} */
1057
- const eventTarget = dom.query.getEventTarget(e);
1058
- const value = eventTarget.options[eventTarget.selectedIndex].value;
1059
- this._defaultSizeY = this.figure.autoRatio.current = this._frameRatio = !value ? this._defaultSizeY : Number(value) * 100 + '%';
1060
- this.inputY.placeholder = !value ? '' : Number(value) * 100 + '%';
1061
- this.inputY.value = '';
1062
- }
1063
-
1064
- #OnChangeRatio() {
1065
- this._ratio = this.proportion.checked ? Figure.GetRatio(this.inputX.value, this.inputY.value, this.sizeUnit) : { w: 1, h: 1 };
1066
- }
1067
-
1068
- /**
1069
- * @param {"x"|"y"} xy - x or y
1070
- * @param {KeyboardEvent} e - Event object
1071
- */
1072
- #OnInputSize(xy, e) {
1073
- if (keyCodeMap.isSpace(e.code)) {
1074
- e.preventDefault();
1075
- return;
1076
- }
1077
-
1078
- /** @type {HTMLInputElement} */
1079
- const eventTarget = dom.query.getEventTarget(e);
1080
- if (xy === 'x' && this._onlyPercentage && Number(eventTarget.value) > 100) {
1081
- eventTarget.value = '100';
1082
- } else if (this.proportion.checked) {
1083
- const ratioSize = Figure.CalcRatio(this.inputX.value, this.inputY.value, this.sizeUnit, this._ratio);
1084
- if (xy === 'x') {
1085
- this.inputY.value = String(ratioSize.h);
1086
- } else {
1087
- this.inputX.value = String(ratioSize.w);
1088
- }
1089
- }
1090
-
1091
- if (xy === 'y') {
1092
- this._setRatioSelect(eventTarget.value || this._defaultRatio);
1093
- }
1094
- }
1095
- }
1096
-
1097
- /**
1098
- * @typedef {object} ModalReturns_video
1099
- * @property {HTMLElement} html
1100
- * @property {HTMLElement} alignForm
1101
- * @property {HTMLElement} fileModalWrapper
1102
- * @property {HTMLInputElement} videoInputFile
1103
- * @property {HTMLInputElement} videoUrlFile
1104
- * @property {HTMLElement} previewSrc
1105
- * @property {HTMLButtonElement} galleryButton
1106
- * @property {HTMLInputElement} proportion
1107
- * @property {HTMLSelectElement} frameRatioOption
1108
- * @property {HTMLInputElement} inputX
1109
- * @property {HTMLInputElement} inputY
1110
- * @property {HTMLButtonElement} revertBtn
1111
- * @property {HTMLButtonElement} fileRemoveBtn
1112
- *
1113
- * @param {__se__EditorCore} editor
1114
- * @param {*} pluginOptions
1115
- * @returns {ModalReturns_video}
1116
- */
1117
- function CreateHTML_modal({ lang, icons, plugins }, pluginOptions) {
1118
- let html = /*html*/ `
1119
- <form method="post" enctype="multipart/form-data">
1120
- <div class="se-modal-header">
1121
- <button type="button" data-command="close" class="se-btn se-close-btn" title="${lang.close}" aria-label="${lang.close}">
1122
- ${icons.cancel}
1123
- </button>
1124
- <span class="se-modal-title">${lang.video_modal_title}</span>
1125
- </div>
1126
- <div class="se-modal-body">`;
1127
-
1128
- if (pluginOptions.createFileInput) {
1129
- html += /*html*/ `
1130
- <div class="se-modal-form">
1131
- <label>${lang.video_modal_file}</label>
1132
- ${Modal.CreateFileInput({ lang, icons }, pluginOptions)}
1133
- </div>`;
1134
- }
1135
-
1136
- if (pluginOptions.createUrlInput) {
1137
- html += /*html*/ `
1138
- <div class="se-modal-form">
1139
- <label>${lang.video_modal_url}</label>
1140
- <div class="se-modal-form-files">
1141
- <input class="se-input-form se-input-url" type="text" data-focus />
1142
- ${
1143
- plugins.videoGallery
1144
- ? `<button type="button" class="se-btn se-tooltip se-modal-files-edge-button __se__gallery" aria-label="${lang.videoGallery}">
1145
- ${icons.video_gallery}
1146
- ${dom.utils.createTooltipInner(lang.videoGallery)}
1147
- </button>`
1148
- : ''
1149
- }
1150
- </div>
1151
- <pre class="se-link-preview"></pre>
1152
- </div>`;
1153
- }
1154
-
1155
- if (pluginOptions.canResize) {
1156
- const ratioList = pluginOptions.ratioOptions || [
1157
- { name: '16:9', value: 0.5625 },
1158
- { name: '4:3', value: 0.75 },
1159
- { name: '21:9', value: 0.4285 },
1160
- { name: '9:16', value: 1.78 }
1161
- ];
1162
- const ratio = pluginOptions.defaultRatio;
1163
- const onlyPercentage = pluginOptions.percentageOnlySize;
1164
- const onlyPercentDisplay = onlyPercentage ? ' style="display: none !important;"' : '';
1165
- const heightDisplay = !pluginOptions.showHeightInput ? ' style="display: none !important;"' : '';
1166
- const ratioDisplay = !pluginOptions.showRatioOption ? ' style="display: none !important;"' : '';
1167
- const onlyWidthDisplay = !onlyPercentage && !pluginOptions.showHeightInput && !pluginOptions.showRatioOption ? ' style="display: none !important;"' : '';
1168
- html += /*html*/ `
1169
- <div class="se-modal-form">
1170
- <div class="se-modal-size-text">
1171
- <label class="size-w">${lang.width}</label>
1172
- <label class="se-modal-size-x">&nbsp;</label>
1173
- <label class="size-h"${heightDisplay}>${lang.height}</label>
1174
- <label class="size-h"${ratioDisplay}>(${lang.ratio})</label>
1175
- </div>
1176
- <input class="se-input-control _se_size_x" placeholder="100%"${onlyPercentage ? ' type="number" min="1"' : 'type="text"'}${onlyPercentage ? ' max="100"' : ''}/>
1177
- <label class="se-modal-size-x"${onlyWidthDisplay}>${onlyPercentage ? '%' : 'x'}</label>
1178
- <input class="se-input-control _se_size_y" placeholder="${pluginOptions.defaultRatio * 100}%"
1179
- ${onlyPercentage ? ' type="number" min="1"' : 'type="text"'}${onlyPercentage ? ' max="100"' : ''}${heightDisplay}/>
1180
- <select class="se-input-select se-modal-ratio" title="${lang.ratio}" aria-label="${lang.ratio}"${ratioDisplay}>
1181
- ${!heightDisplay ? '<option value=""> - </option>' : ''}
1182
- ${ratioList.map((ratioOption) => `<option value="${ratioOption.value}"${ratio.toString() === ratioOption.value.toString() ? ' selected' : ''}>${ratioOption.name}</option>`).join('')}
1183
- </select>
1184
- <button type="button" title="${lang.revert}" aria-label="${lang.revert}" class="se-btn se-modal-btn-revert">${icons.revert}</button>
1185
- </div>
1186
- <div class="se-modal-form se-modal-form-footer"${onlyPercentDisplay}${onlyWidthDisplay}>
1187
- <label>
1188
- <input type="checkbox" class="se-modal-btn-check _se_check_proportion" />&nbsp;
1189
- <span>${lang.proportion}</span>
1190
- </label>
1191
- </div>`;
1192
- }
1193
-
1194
- html += /*html*/ `
1195
- </div>
1196
- <div class="se-modal-footer">
1197
- <div class="se-figure-align">
1198
- <label><input type="radio" name="suneditor_video_radio" class="se-modal-btn-radio" value="none" checked>${lang.basic}</label>
1199
- <label><input type="radio" name="suneditor_video_radio" class="se-modal-btn-radio" value="left">${lang.left}</label>
1200
- <label><input type="radio" name="suneditor_video_radio" class="se-modal-btn-radio" value="center">${lang.center}</label>
1201
- <label><input type="radio" name="suneditor_video_radio" class="se-modal-btn-radio" value="right">${lang.right}</label>
1202
- </div>
1203
- <button type="submit" class="se-btn-primary" title="${lang.submitButton}" aria-label="${lang.submitButton}"><span>${lang.submitButton}</span></button>
1204
- </div>
1205
- </form>`;
1206
-
1207
- const content = dom.utils.createElement('DIV', { class: 'se-modal-content' }, html);
1208
-
1209
- return {
1210
- html: content,
1211
- alignForm: content.querySelector('.se-figure-align'),
1212
- fileModalWrapper: content.querySelector('.se-flex-input-wrapper'),
1213
- videoInputFile: content.querySelector('.__se__file_input'),
1214
- videoUrlFile: content.querySelector('.se-input-url'),
1215
- previewSrc: content.querySelector('.se-link-preview'),
1216
- galleryButton: content.querySelector('.__se__gallery'),
1217
- proportion: content.querySelector('._se_check_proportion'),
1218
- frameRatioOption: content.querySelector('.se-modal-ratio'),
1219
- inputX: content.querySelector('._se_size_x'),
1220
- inputY: content.querySelector('._se_size_y'),
1221
- revertBtn: content.querySelector('.se-modal-btn-revert'),
1222
- fileRemoveBtn: content.querySelector('.se-file-remove')
1223
- };
1224
- }
1225
-
1226
- export default Video;
1
+ import EditorInjector from '../../editorInjector';
2
+ import { Modal, Figure, FileManager } from '../../modules';
3
+ import { dom, numbers, env, converter, keyCodeMap } from '../../helper';
4
+ const { NO_EVENT } = env;
5
+
6
+ /**
7
+ * @typedef {import('../../events').VideoInfo} VideoInfo_video
8
+ */
9
+
10
+ /**
11
+ * @typedef {import('../../modules/Figure').FigureControls} FigureControls_video
12
+ */
13
+
14
+ /**
15
+ * @typedef {Object} VideoPluginOptions
16
+ * @property {boolean} [canResize=true] - Whether the video element can be resized.
17
+ * @property {boolean} [showHeightInput=true] - Whether to display the height input field.
18
+ * @property {string} [defaultWidth] - The default width of the video element. If a number is provided, "px" will be appended.
19
+ * @property {string} [defaultHeight] - The default height of the video element. If a number is provided, "px" will be appended.
20
+ * @property {boolean} [percentageOnlySize=false] - Whether to allow only percentage-based sizing.
21
+ * @property {boolean} [createFileInput=false] - Whether to create a file input element for video uploads.
22
+ * @property {boolean} [createUrlInput=true] - Whether to create a URL input element for video embedding.
23
+ * @property {string} [uploadUrl] - The URL endpoint for video file uploads.
24
+ * @property {Object<string, string>} [uploadHeaders] - Additional headers to include in the video upload request.
25
+ * @property {number} [uploadSizeLimit] - The total upload size limit for videos in bytes.
26
+ * @property {number} [uploadSingleSizeLimit] - The single file upload size limit for videos in bytes.
27
+ * @property {boolean} [allowMultiple=false] - Whether multiple video uploads are allowed.
28
+ * @property {string} [acceptedFormats="video/*"] - Accepted file formats for video uploads.
29
+ * @property {number} [defaultRatio=0.5625] - The default aspect ratio for the video (e.g., 16:9 is 0.5625).
30
+ * @property {boolean} [showRatioOption=true] - Whether to display the ratio option in the modal.
31
+ * @property {Array} [ratioOptions] - Custom ratio options for video resizing.
32
+ * @property {Object<string, string>} [videoTagAttributes] - Additional attributes to set on the video tag.
33
+ * @property {Object<string, string>} [iframeTagAttributes] - Additional attributes to set on the iframe tag.
34
+ * @property {string} [query_youtube=""] - Additional query parameters for YouTube embedding.
35
+ * @property {string} [query_vimeo=""] - Additional query parameters for Vimeo embedding.
36
+ * @property {Object<string, {pattern: RegExp, action: (url: string) => string, tag: string}>} [embedQuery] - Custom query objects for additional embedding services.
37
+ * @property {Array<RegExp>} [urlPatterns] - Additional URL patterns for video embedding.
38
+ * @property {Array<string>} [extensions] - Additional file extensions to be recognized for video uploads.
39
+ * @property {FigureControls_video} [controls] - Figure controls.
40
+ */
41
+
42
+ /**
43
+ * @class
44
+ * @description Video plugin.
45
+ * - This plugin provides video embedding functionality within the editor.
46
+ * - It also supports embedding from popular video services
47
+ */
48
+ class Video extends EditorInjector {
49
+ static key = 'video';
50
+ static type = 'modal';
51
+ static className = '';
52
+ /**
53
+ * @this {Video}
54
+ * @param {HTMLElement} node - The node to check.
55
+ * @returns {HTMLElement|null} Returns a node if the node is a valid component.
56
+ */
57
+ static component(node) {
58
+ if (/^(VIDEO)$/i.test(node?.nodeName)) {
59
+ return node;
60
+ } else if (/^(IFRAME)$/i.test(node?.nodeName)) {
61
+ return this.checkContentType(/** @type {HTMLIFrameElement} */ (node).src) ? node : null;
62
+ }
63
+ return null;
64
+ }
65
+
66
+ /**
67
+ * @constructor
68
+ * @param {__se__EditorCore} editor - The root editor instance
69
+ * @param {VideoPluginOptions} pluginOptions
70
+ */
71
+ constructor(editor, pluginOptions) {
72
+ // plugin bisic properties
73
+ super(editor);
74
+ this.title = this.lang.video;
75
+ this.icon = 'video';
76
+
77
+ // define plugin options
78
+ this.pluginOptions = {
79
+ canResize: pluginOptions.canResize === undefined ? true : pluginOptions.canResize,
80
+ showHeightInput: pluginOptions.showHeightInput === undefined ? true : !!pluginOptions.showHeightInput,
81
+ defaultWidth: !pluginOptions.defaultWidth || !numbers.get(pluginOptions.defaultWidth, 0) ? '' : numbers.is(pluginOptions.defaultWidth) ? pluginOptions.defaultWidth + 'px' : pluginOptions.defaultWidth,
82
+ defaultHeight: !pluginOptions.defaultHeight || !numbers.get(pluginOptions.defaultHeight, 0) ? '' : numbers.is(pluginOptions.defaultHeight) ? pluginOptions.defaultHeight + 'px' : pluginOptions.defaultHeight,
83
+ percentageOnlySize: !!pluginOptions.percentageOnlySize,
84
+ createFileInput: !!pluginOptions.createFileInput,
85
+ createUrlInput: pluginOptions.createUrlInput === undefined || !pluginOptions.createFileInput ? true : pluginOptions.createUrlInput,
86
+ uploadUrl: typeof pluginOptions.uploadUrl === 'string' ? pluginOptions.uploadUrl : null,
87
+ uploadHeaders: pluginOptions.uploadHeaders || null,
88
+ uploadSizeLimit: numbers.get(pluginOptions.uploadSizeLimit, 0),
89
+ uploadSingleSizeLimit: numbers.get(pluginOptions.uploadSingleSizeLimit, 0),
90
+ allowMultiple: !!pluginOptions.allowMultiple,
91
+ acceptedFormats: typeof pluginOptions.acceptedFormats !== 'string' || pluginOptions.acceptedFormats.trim() === '*' ? 'video/*' : pluginOptions.acceptedFormats.trim() || 'video/*',
92
+ defaultRatio: numbers.get(pluginOptions.defaultRatio, 4) || 0.5625,
93
+ showRatioOption: pluginOptions.showRatioOption === undefined ? true : !!pluginOptions.showRatioOption,
94
+ ratioOptions: !pluginOptions.ratioOptions ? null : pluginOptions.ratioOptions,
95
+ videoTagAttributes: pluginOptions.videoTagAttributes || null,
96
+ iframeTagAttributes: pluginOptions.iframeTagAttributes || null,
97
+ query_youtube: pluginOptions.query_youtube || '',
98
+ query_vimeo: pluginOptions.query_vimeo || ''
99
+ };
100
+
101
+ // create HTML
102
+ const sizeUnit = this.pluginOptions.percentageOnlySize ? '%' : 'px';
103
+ const modalEl = CreateHTML_modal(editor, this.pluginOptions);
104
+ const figureControls = pluginOptions.controls || !this.pluginOptions.canResize ? [['align', 'edit', 'copy', 'remove']] : [['resize_auto,75,50', 'align', 'edit', 'revert', 'copy', 'remove']];
105
+
106
+ // show align
107
+ if (!figureControls.some((subArray) => subArray.includes('align'))) modalEl.alignForm.style.display = 'none';
108
+
109
+ // modules
110
+ const defaultRatio = this.pluginOptions.defaultRatio * 100 + '%';
111
+ this.modal = new Modal(this, modalEl.html);
112
+ this.figure = new Figure(this, figureControls, { sizeUnit: sizeUnit, autoRatio: { current: defaultRatio, default: defaultRatio } });
113
+ this.fileManager = new FileManager(this, {
114
+ query: 'iframe, video',
115
+ loadHandler: this.events.onVideoLoad,
116
+ eventHandler: this.events.onVideoAction
117
+ });
118
+
119
+ // members
120
+ this.fileModalWrapper = modalEl.fileModalWrapper;
121
+ this.videoInputFile = modalEl.videoInputFile;
122
+ this.videoUrlFile = modalEl.videoUrlFile;
123
+ this.focusElement = this.videoUrlFile || this.videoInputFile;
124
+ this.previewSrc = modalEl.previewSrc;
125
+ this._linkValue = '';
126
+ this._align = 'none';
127
+ this._frameRatio = defaultRatio;
128
+ this._defaultRatio = defaultRatio;
129
+ this._defaultSizeX = '100%';
130
+ this._defaultSizeY = this.pluginOptions.defaultRatio * 100 + '%';
131
+ this.sizeUnit = sizeUnit;
132
+ this.proportion = null;
133
+ this.frameRatioOption = null;
134
+ this.inputX = null;
135
+ this.inputY = null;
136
+ this._element = null;
137
+ this._cover = null;
138
+ this._container = null;
139
+ this._ratio = { w: 1, h: 1 };
140
+ this._origin_w = this.pluginOptions.defaultWidth === '100%' ? '' : this.pluginOptions.defaultWidth;
141
+ this._origin_h = this.pluginOptions.defaultHeight === defaultRatio ? '' : this.pluginOptions.defaultHeight;
142
+ this._resizing = this.pluginOptions.canResize;
143
+ this._onlyPercentage = this.pluginOptions.percentageOnlySize;
144
+ this._nonResizing = !this._resizing || !this.pluginOptions.showHeightInput || this._onlyPercentage;
145
+ this.query = {
146
+ youtube: {
147
+ pattern: /youtu\.?be/i,
148
+ action: (url) => {
149
+ url = this.convertUrlYoutube(url);
150
+ return converter.addUrlQuery(url, this.pluginOptions.query_youtube);
151
+ },
152
+ tag: 'iframe'
153
+ },
154
+ vimeo: {
155
+ pattern: /vimeo\.com/i,
156
+ action: (url) => {
157
+ url = this.convertUrlVimeo(url);
158
+ return converter.addUrlQuery(url, this.pluginOptions.query_vimeo);
159
+ },
160
+ tag: 'iframe'
161
+ },
162
+ ...pluginOptions.embedQuery
163
+ };
164
+
165
+ const urlPatterns = [];
166
+ for (const key in this.query) {
167
+ urlPatterns.push(this.query[key].pattern);
168
+ }
169
+ this.extensions = ['.mp4', '.avi', '.mov', '.webm', '.flv', '.mkv', '.m4v', '.ogv'].concat(this.pluginOptions.extensions || []);
170
+ this.urlPatterns = urlPatterns
171
+ .concat([
172
+ /youtu\.?be/,
173
+ /vimeo\.com\//,
174
+ /dailymotion\.com\/video\//,
175
+ /facebook\.com\/.+\/videos\//,
176
+ /facebook\.com\/watch\/\?v=/,
177
+ /twitter\.com\/.+\/status\//,
178
+ /twitch\.tv\/videos\//,
179
+ /twitch\.tv\/[^/]+$/,
180
+ /tiktok\.com\/@[^/]+\/video\//,
181
+ /instagram\.com\/p\//,
182
+ /instagram\.com\/tv\//,
183
+ /instagram\.com\/reel\//,
184
+ /linkedin\.com\/posts\//,
185
+ /\.(wistia\.com|wi\.st)\/(medias|embed)\//,
186
+ /loom\.com\/share\//
187
+ ])
188
+ .concat(pluginOptions.urlPatterns || []);
189
+
190
+ const galleryButton = modalEl.galleryButton;
191
+ if (galleryButton) this.eventManager.addEvent(galleryButton, 'click', this.#OpenGallery.bind(this));
192
+
193
+ // init
194
+ if (this.videoInputFile) this.eventManager.addEvent(modalEl.fileRemoveBtn, 'click', this.#RemoveSelectedFiles.bind(this));
195
+ if (this.videoUrlFile) this.eventManager.addEvent(this.videoUrlFile, 'input', this.#OnLinkPreview.bind(this));
196
+ if (this.videoInputFile && this.videoUrlFile) this.eventManager.addEvent(this.videoInputFile, 'change', this.#OnfileInputChange.bind(this));
197
+
198
+ if (this._resizing) {
199
+ this.proportion = modalEl.proportion;
200
+ this.frameRatioOption = modalEl.frameRatioOption;
201
+ this.inputX = modalEl.inputX;
202
+ this.inputY = modalEl.inputY;
203
+ this.inputX.value = this.pluginOptions.defaultWidth;
204
+ this.inputY.value = this.pluginOptions.defaultHeight;
205
+
206
+ const ratioChange = this.#OnChangeRatio.bind(this);
207
+ this.eventManager.addEvent(this.inputX, 'keyup', this.#OnInputSize.bind(this, 'x'));
208
+ this.eventManager.addEvent(this.inputY, 'keyup', this.#OnInputSize.bind(this, 'y'));
209
+ this.eventManager.addEvent(this.inputX, 'change', ratioChange);
210
+ this.eventManager.addEvent(this.inputY, 'change', ratioChange);
211
+ this.eventManager.addEvent(this.proportion, 'change', ratioChange);
212
+ this.eventManager.addEvent(this.frameRatioOption, 'change', this.#SetRatio.bind(this));
213
+ this.eventManager.addEvent(modalEl.revertBtn, 'click', this.#OnClickRevert.bind(this));
214
+ }
215
+ }
216
+
217
+ /**
218
+ * @editorMethod Modules.Modal
219
+ * @description Executes the method that is called when a "Modal" module's is opened.
220
+ */
221
+ open() {
222
+ this.modal.open();
223
+ }
224
+
225
+ /**
226
+ * @editorMethod Modules.Controller(Figure)
227
+ * @description Executes the method that is called when a target component is edited.
228
+ */
229
+ edit() {
230
+ this.modal.open();
231
+ }
232
+
233
+ /**
234
+ * @editorMethod Modules.Modal
235
+ * @description Executes the method that is called when a plugin's modal is opened.
236
+ * @param {boolean} isUpdate "Indicates whether the modal is for editing an existing component (true) or registering a new one (false)."
237
+ */
238
+ on(isUpdate) {
239
+ if (!isUpdate) {
240
+ if (this._resizing) {
241
+ this.inputX.value = this._origin_w = this.pluginOptions.defaultWidth === this._defaultSizeX ? '' : this.pluginOptions.defaultWidth;
242
+ this.inputY.value = this._origin_h = this.pluginOptions.defaultHeight === this._defaultSizeY ? '' : this.pluginOptions.defaultHeight;
243
+ this.proportion.disabled = true;
244
+ }
245
+ if (this.videoInputFile && this.pluginOptions.allowMultiple) this.videoInputFile.setAttribute('multiple', 'multiple');
246
+ } else {
247
+ if (this.videoInputFile && this.pluginOptions.allowMultiple) this.videoInputFile.removeAttribute('multiple');
248
+ }
249
+
250
+ if (this._resizing) {
251
+ this._setRatioSelect(this._origin_h || this._defaultRatio);
252
+ }
253
+ }
254
+
255
+ /**
256
+ * @editorMethod Editor.EventManager
257
+ * @description Executes the event function of "paste" or "drop".
258
+ * @param {Object} params { frameContext, event, file }
259
+ * @param {__se__FrameContext} params.frameContext Frame context
260
+ * @param {ClipboardEvent} params.event Event object
261
+ * @param {File} params.file File object
262
+ * @returns {boolean} - If return false, the file upload will be canceled
263
+ */
264
+ onFilePasteAndDrop({ file }) {
265
+ if (!/^video/.test(file.type)) return;
266
+
267
+ this.submitFile([file]);
268
+ this.editor.focus();
269
+
270
+ return false;
271
+ }
272
+
273
+ /**
274
+ * @editorMethod Modules.Modal
275
+ * @description This function is called when a form within a modal window is "submit".
276
+ * @returns {Promise<boolean>} Success / failure
277
+ */
278
+ async modalAction() {
279
+ this._align = /** @type {HTMLInputElement} */ (this.modal.form.querySelector('input[name="suneditor_video_radio"]:checked')).value;
280
+
281
+ let result = false;
282
+ if (this.videoInputFile && this.videoInputFile.files.length > 0) {
283
+ result = await this.submitFile(this.videoInputFile.files);
284
+ } else if (this.videoUrlFile && this._linkValue.length > 0) {
285
+ result = await this.submitURL(this._linkValue);
286
+ }
287
+
288
+ if (result) this._w.setTimeout(this.component.select.bind(this.component, this._element, Video.key), 0);
289
+
290
+ return result;
291
+ }
292
+
293
+ /**
294
+ * @editorMethod Editor.core
295
+ * @description This method is used to validate and preserve the format of the component within the editor.
296
+ * - It ensures that the structure and attributes of the element are maintained and secure.
297
+ * - The method checks if the element is already wrapped in a valid container and updates its attributes if necessary.
298
+ * - If the element isn't properly contained, a new container is created to retain the format.
299
+ * @returns {{query: string, method: (element: HTMLIFrameElement|HTMLVideoElement) => void}} The format retention object containing the query and method to process the element.
300
+ * - query: The selector query to identify the relevant elements (in this case, 'audio').
301
+ * - method:The function to execute on the element to validate and preserve its format.
302
+ * - The function takes the element as an argument, checks if it is contained correctly, and applies necessary adjustments.
303
+ */
304
+ retainFormat() {
305
+ return {
306
+ query: 'iframe, video',
307
+ method: async (element) => {
308
+ if (/^(iframe)$/i.test(element?.nodeName)) {
309
+ if (!this.checkContentType(element.src)) return;
310
+ }
311
+
312
+ const figureInfo = Figure.GetContainer(element);
313
+ if (figureInfo && figureInfo.container && figureInfo.cover) return;
314
+
315
+ this._ready(element);
316
+ const line = this.format.getLine(element);
317
+ if (line) this._align = line.style.textAlign || line.style.float;
318
+
319
+ this._update(element);
320
+ }
321
+ };
322
+ }
323
+
324
+ /**
325
+ * @editorMethod Modules.Modal
326
+ * @description This function is called before the modal window is opened, but before it is closed.
327
+ */
328
+ init() {
329
+ Modal.OnChangeFile(this.fileModalWrapper, []);
330
+ if (this.videoInputFile) this.videoInputFile.value = '';
331
+ if (this.videoUrlFile) this._linkValue = this.previewSrc.textContent = this.videoUrlFile.value = '';
332
+ if (this.videoInputFile && this.videoUrlFile) {
333
+ this.videoUrlFile.disabled = false;
334
+ this.previewSrc.style.textDecoration = '';
335
+ }
336
+
337
+ /** @type {HTMLInputElement} */ (this.modal.form.querySelector('input[name="suneditor_video_radio"][value="none"]')).checked = true;
338
+ this._ratio = { w: 1, h: 1 };
339
+ this._nonResizing = false;
340
+
341
+ if (this._resizing) {
342
+ this.inputX.value = this.pluginOptions.defaultWidth === this._defaultSizeX ? '' : this.pluginOptions.defaultWidth;
343
+ this.inputY.value = this.pluginOptions.defaultHeight === this._defaultSizeY ? '' : this.pluginOptions.defaultHeight;
344
+ this.proportion.checked = false;
345
+ this.proportion.disabled = true;
346
+ this._setRatioSelect(this._defaultRatio);
347
+ }
348
+ }
349
+
350
+ /**
351
+ * @editorMethod Editor.component
352
+ * @description Executes the method that is called when a component of a plugin is selected.
353
+ * @param {HTMLIFrameElement|HTMLVideoElement} target Target component element
354
+ */
355
+ select(target) {
356
+ this._ready(target);
357
+ }
358
+
359
+ /**
360
+ * @private
361
+ * @description Prepares the component for selection.
362
+ * - Ensures that the controller is properly positioned and initialized.
363
+ * - Prevents duplicate event handling if the component is already selected.
364
+ * @param {HTMLIFrameElement|HTMLVideoElement} target - The selected element.
365
+ */
366
+ _ready(target) {
367
+ if (!target) return;
368
+ const figureInfo = this.figure.open(target, { nonResizing: this._nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, __fileManagerInfo: false });
369
+
370
+ this._element = target;
371
+ this._cover = figureInfo.cover;
372
+ this._container = figureInfo.container;
373
+ this._align = figureInfo.align;
374
+ target.style.float = '';
375
+
376
+ this._origin_w = String(figureInfo.width || figureInfo.originWidth || figureInfo.w || '');
377
+ this._origin_h = String(figureInfo.height || figureInfo.originHeight || figureInfo.h || '');
378
+
379
+ let w = figureInfo.width || figureInfo.w || this._origin_w || '';
380
+ const h = figureInfo.height || figureInfo.h || this._origin_h || '';
381
+
382
+ if (this.videoUrlFile) this._linkValue = this.previewSrc.textContent = this.videoUrlFile.value = this._element.src || this._element.querySelector('source')?.src || '';
383
+
384
+ /** @type {HTMLInputElement} */
385
+ const activeAlgin = this.modal.form.querySelector('input[name="suneditor_video_radio"][value="' + this._align + '"]') || this.modal.form.querySelector('input[name="suneditor_video_radio"][value="none"]');
386
+ activeAlgin.checked = true;
387
+
388
+ if (!this._resizing) return;
389
+
390
+ const percentageRotation = this._onlyPercentage && this.figure.isVertical;
391
+ if (this._onlyPercentage) {
392
+ w = numbers.get(w, 2);
393
+ if (w > 100) w = 100;
394
+ }
395
+ this.inputX.value = String(w === 'auto' ? '' : w);
396
+
397
+ if (!this._onlyPercentage) {
398
+ const infoH = percentageRotation ? '' : figureInfo.height;
399
+ this.inputY.value = String(infoH === 'auto' ? '' : infoH);
400
+ }
401
+
402
+ if (!this._setRatioSelect(h)) this.inputY.value = String(this._onlyPercentage ? numbers.get(h, 2) : h);
403
+
404
+ this.proportion.checked = true;
405
+ this.inputX.disabled = percentageRotation ? true : false;
406
+ this.inputY.disabled = percentageRotation ? true : false;
407
+ this.proportion.disabled = percentageRotation ? true : false;
408
+
409
+ this._ratio = this.proportion.checked ? figureInfo.ratio : { w: 1, h: 1 };
410
+ }
411
+
412
+ /**
413
+ * @editorMethod Editor.Component
414
+ * @description Method to delete a component of a plugin, called by the "FileManager", "Controller" module.
415
+ * @param {HTMLElement} target Target element
416
+ * @returns {Promise<void>}
417
+ */
418
+ async destroy(target) {
419
+ const targetEl = target || this._element;
420
+ const container = dom.query.getParentElement(targetEl, Figure.is) || targetEl;
421
+ const focusEl = container.previousElementSibling || container.nextElementSibling;
422
+ const emptyDiv = container.parentNode;
423
+
424
+ const message = await this.triggerEvent('onVideoDeleteBefore', { element: targetEl, container, align: this._align, url: this._linkValue });
425
+ if (message === false) return;
426
+
427
+ dom.utils.removeItem(container);
428
+ this.init();
429
+
430
+ if (emptyDiv !== this.editor.frameContext.get('wysiwyg')) {
431
+ this.nodeTransform.removeAllParents(
432
+ emptyDiv,
433
+ function (current) {
434
+ return current.childNodes.length === 0;
435
+ },
436
+ null
437
+ );
438
+ }
439
+
440
+ // focus
441
+ this.editor.focusEdge(focusEl);
442
+ this.history.push(false);
443
+ }
444
+
445
+ /**
446
+ * @description Checks if the given URL matches any of the defined URL patterns.
447
+ * @param {string} url - The URL to check.
448
+ * @returns {boolean} True if the URL matches a known pattern; otherwise, false.
449
+ */
450
+ checkContentType(url) {
451
+ url = url?.toLowerCase() || '';
452
+ if (this.extensions.some((ext) => url.endsWith(ext)) || this.urlPatterns.some((pattern) => pattern.test(url))) {
453
+ return true;
454
+ }
455
+
456
+ return false;
457
+ }
458
+
459
+ /**
460
+ * @description Finds and processes the URL for video by matching it against known service patterns.
461
+ * @param {string} url - The original URL.
462
+ * @returns {{origin: string, url: string, tag: string}|null} An object containing the original URL, the processed URL, and the tag type (e.g., 'iframe'),
463
+ * or null if no matching pattern is found.
464
+ */
465
+ findProcessUrl(url) {
466
+ const query = this.query;
467
+ for (const key in query) {
468
+ const service = query[key];
469
+ if (service.pattern.test(url)) {
470
+ return {
471
+ origin: url,
472
+ url: service.action(url),
473
+ tag: service.tag
474
+ };
475
+ }
476
+ }
477
+
478
+ return null;
479
+ }
480
+
481
+ /**
482
+ * @description Converts a YouTube URL into an embeddable URL.
483
+ * - If the URL does not start with "http", it prepends "https://". It also replaces "watch?v=" with the embed path.
484
+ * @param {string} url - The original YouTube URL.
485
+ * @returns {string} The converted YouTube embed URL.
486
+ */
487
+ convertUrlYoutube(url) {
488
+ if (!/^http/.test(url)) url = 'https://' + url;
489
+ url = url.replace('watch?v=', '');
490
+ if (!/^\/\/.+\/embed\//.test(url)) {
491
+ url = url.replace(url.match(/\/\/.+\//)[0], '//www.youtube.com/embed/').replace('&', '?&');
492
+ }
493
+ return url;
494
+ }
495
+
496
+ /**
497
+ * @description Converts a Vimeo URL into an embeddable URL.
498
+ * - Removes any trailing slash and extracts the video ID from the URL.
499
+ * @param {string} url - The original Vimeo URL.
500
+ * @returns {string} The converted Vimeo embed URL.
501
+ */
502
+ convertUrlVimeo(url) {
503
+ if (url.endsWith('/')) {
504
+ url = url.slice(0, -1);
505
+ }
506
+ url = 'https://player.vimeo.com/video/' + url.slice(url.lastIndexOf('/') + 1);
507
+ return url;
508
+ }
509
+
510
+ /**
511
+ * @description Adds query parameters to a URL.
512
+ * - If the URL already contains a query string, the provided query is appended with an "&".
513
+ * @param {string} url - The original URL.
514
+ * @param {string} query - The query string to append.
515
+ * @returns {string} The URL with the appended query parameters.
516
+ */
517
+ addQuery(url, query) {
518
+ if (query.length > 0) {
519
+ if (/\?/.test(url)) {
520
+ const splitUrl = url.split('?');
521
+ url = splitUrl[0] + '?' + query + '&' + splitUrl[1];
522
+ } else {
523
+ url += '?' + query;
524
+ }
525
+ }
526
+ return url;
527
+ }
528
+
529
+ /**
530
+ * @description Creates or updates a video embed component.
531
+ * - When updating, it replaces the existing element if necessary and applies the new source, size, and alignment.
532
+ * - When creating, it wraps the provided element in a figure container.
533
+ * @param {HTMLIFrameElement|HTMLVideoElement} oFrame - The existing video element (for update) or a newly created one.
534
+ * @param {string} src - The source URL for the video.
535
+ * @param {string} width - The desired width for the video element.
536
+ * @param {string} height - The desired height for the video element.
537
+ * @param {string} align - The alignment to apply to the video element (e.g., 'left', 'center', 'right').
538
+ * @param {boolean} isUpdate - Indicates whether this is an update to an existing component (true) or a new creation (false).
539
+ * @param {{name: string, size: number}} file - File metadata associated with the video
540
+ */
541
+ create(oFrame, src, width, height, align, isUpdate, file) {
542
+ let cover = null;
543
+ let container = null;
544
+
545
+ /** update */
546
+ if (isUpdate) {
547
+ oFrame = this._element;
548
+ if (oFrame.src !== src) {
549
+ const processUrl = this.findProcessUrl(src);
550
+ if (/^iframe$/i.test(processUrl?.tag) && !/^iframe$/i.test(oFrame.nodeName)) {
551
+ const newTag = this.createIframeTag();
552
+ newTag.src = src;
553
+ oFrame.replaceWith(newTag);
554
+ this._element = oFrame = newTag;
555
+ } else if (/^video$/i.test(processUrl?.tag) && !/^video$/i.test(oFrame.nodeName)) {
556
+ const newTag = this.createVideoTag();
557
+ newTag.src = src;
558
+ oFrame.replaceWith(newTag);
559
+ this._element = oFrame = newTag;
560
+ } else {
561
+ oFrame.src = src;
562
+ }
563
+ }
564
+ container = this._container;
565
+ cover = dom.query.getParentElement(oFrame, 'FIGURE');
566
+ } else {
567
+ /** create */
568
+ oFrame.src = src;
569
+ this._element = oFrame;
570
+ const figure = Figure.CreateContainer(oFrame, 'se-video-container');
571
+ cover = figure.cover;
572
+ container = figure.container;
573
+ }
574
+
575
+ /** rendering */
576
+ this._element = oFrame;
577
+ this._cover = cover;
578
+ this._container = container;
579
+ this.figure.open(oFrame, { nonResizing: this._nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, __fileManagerInfo: true });
580
+
581
+ width = width || this._defaultSizeX;
582
+ height = height || this._frameRatio;
583
+ const size = this.figure.getSize(oFrame);
584
+ const inputUpdate = size.w !== width || size.h !== height;
585
+ const changeSize = !isUpdate || inputUpdate;
586
+
587
+ // set size
588
+ if (changeSize) {
589
+ this._applySize(width, height);
590
+ }
591
+
592
+ // align
593
+ this.figure.setAlign(oFrame, align);
594
+
595
+ // select figure
596
+ // oFrame.onload = OnloadVideo.bind(this, oFrame);
597
+ this.fileManager.setFileData(oFrame, file);
598
+
599
+ if (!isUpdate) {
600
+ this.component.insert(container, { skipCharCount: false, skipSelection: true, skipHistory: false });
601
+ if (!this.options.get('componentAutoSelect')) {
602
+ const line = this.format.addLine(container, null);
603
+ if (line) this.selection.setRange(line, 0, line, 0);
604
+ }
605
+ return;
606
+ }
607
+
608
+ if (this._resizing && changeSize && this.figure.isVertical) this.figure.setTransform(oFrame, width, height, 0);
609
+ this.history.push(false);
610
+ }
611
+
612
+ /**
613
+ * @description Creates a new iframe element for video embedding.
614
+ * - Applies any additional properties provided and sets the necessary attributes for embedding.
615
+ * @param {Object<string, string>} [props] - An optional object containing properties to assign to the iframe.
616
+ * @returns {HTMLIFrameElement} The newly created iframe element.
617
+ */
618
+ createIframeTag(props) {
619
+ /** @type {HTMLIFrameElement} */
620
+ const iframeTag = dom.utils.createElement('IFRAME');
621
+ if (props) {
622
+ for (const key in props) {
623
+ iframeTag[key] = props[key];
624
+ }
625
+ }
626
+ this._setIframeAttrs(iframeTag);
627
+ return iframeTag;
628
+ }
629
+
630
+ /**
631
+ * @description Creates a new video element for video embedding.
632
+ * - Applies any additional properties provided and sets the necessary attributes.
633
+ * @param {Object<string, string>} [props] - An optional object containing properties to assign to the video element.
634
+ * @returns {HTMLVideoElement} The newly created video element.
635
+ */
636
+ createVideoTag(props) {
637
+ /** @type {HTMLVideoElement} */
638
+ const videoTag = dom.utils.createElement('VIDEO');
639
+ if (props) {
640
+ for (const key in props) {
641
+ videoTag[key] = props[key];
642
+ }
643
+ }
644
+ this._setTagAttrs(videoTag);
645
+ return videoTag;
646
+ }
647
+
648
+ /**
649
+ * @private
650
+ * @description Sets the size of the video element.
651
+ * @param {string|number} w - The width of the video.
652
+ * @param {string|number} h - The height of the video.
653
+ */
654
+ _applySize(w, h) {
655
+ if (!w) w = this.inputX?.value || this.pluginOptions.defaultWidth;
656
+ if (!h) h = this.inputY?.value || this.pluginOptions.defaultHeight;
657
+ if (this._onlyPercentage) {
658
+ if (!w) w = '100%';
659
+ else if (/%$/.test(w + '')) w += '%';
660
+ }
661
+ this.figure.setSize(w, h);
662
+ }
663
+
664
+ /**
665
+ * @private
666
+ * @description Retrieves video information including size and alignment.
667
+ * @returns {*} Video information object.
668
+ */
669
+ _getInfo() {
670
+ return {
671
+ inputWidth: this.inputX?.value || '',
672
+ inputHeight: this.inputY?.value || '',
673
+ align: this._align,
674
+ isUpdate: this.modal.isUpdate,
675
+ element: this._element
676
+ };
677
+ }
678
+
679
+ /**
680
+ * @description Create an "video" component using the provided files.
681
+ * @param {FileList|File[]} fileList File object list
682
+ * @returns {Promise<boolean>} If return false, the file upload will be canceled
683
+ */
684
+ async submitFile(fileList) {
685
+ if (fileList.length === 0) return;
686
+
687
+ let fileSize = 0;
688
+ const files = [];
689
+ const slngleSizeLimit = this.pluginOptions.uploadSingleSizeLimit;
690
+ for (let i = 0, len = fileList.length, f, s; i < len; i++) {
691
+ f = fileList[i];
692
+ if (!/video/i.test(f.type)) continue;
693
+
694
+ s = f.size;
695
+ if (slngleSizeLimit && slngleSizeLimit > s) {
696
+ const err = '[SUNEDITOR.videoUpload.fail] Size of uploadable single file: ' + slngleSizeLimit / 1000 + 'KB';
697
+ const message = await this.triggerEvent('onVideoUploadError', {
698
+ error: err,
699
+ limitSize: slngleSizeLimit,
700
+ uploadSize: s,
701
+ file: f
702
+ });
703
+
704
+ this.ui.alertOpen(message === NO_EVENT ? err : message || err, 'error');
705
+
706
+ return false;
707
+ }
708
+
709
+ files.push(f);
710
+ fileSize += s;
711
+ }
712
+
713
+ const limitSize = this.pluginOptions.uploadSizeLimit;
714
+ const currentSize = this.fileManager.getSize();
715
+ if (limitSize > 0 && fileSize + currentSize > limitSize) {
716
+ const err = '[SUNEDITOR.videoUpload.fail] Size of uploadable total videos: ' + limitSize / 1000 + 'KB';
717
+ const message = await this.triggerEvent('onVideoUploadError', { error: err, limitSize, currentSize, uploadSize: fileSize });
718
+
719
+ this.ui.alertOpen(message === NO_EVENT ? err : message || err, 'error');
720
+
721
+ return false;
722
+ }
723
+
724
+ const videoInfo = {
725
+ url: null,
726
+ files,
727
+ ...this._getInfo()
728
+ };
729
+
730
+ const handler = function (infos, newInfos) {
731
+ infos = newInfos || infos;
732
+ this._serverUpload(infos, infos.files);
733
+ }.bind(this, videoInfo);
734
+ // se-ts-ignore
735
+ this._serverUpload;
736
+
737
+ const result = await this.triggerEvent('onVideoUploadBefore', {
738
+ info: videoInfo,
739
+ handler
740
+ });
741
+
742
+ if (result === undefined) return true;
743
+ if (result === false) return false;
744
+ if (result !== null && typeof result === 'object') handler(result);
745
+
746
+ if (result === true || result === NO_EVENT) handler(null);
747
+ }
748
+
749
+ /**
750
+ * @description Create an "video" component using the provided url.
751
+ * @param {string} url File url
752
+ * @returns {Promise<boolean>} If return false, the file upload will be canceled
753
+ */
754
+ async submitURL(url) {
755
+ if (!url) url = this._linkValue;
756
+ if (!url) return false;
757
+
758
+ /** iframe source */
759
+ if (/^<iframe.*\/iframe>$/.test(url)) {
760
+ const oIframe = new DOMParser().parseFromString(url, 'text/html').querySelector('iframe');
761
+ url = oIframe.src;
762
+ if (url.length === 0) return false;
763
+ }
764
+
765
+ const processUrl = this.findProcessUrl(url);
766
+ if (processUrl) {
767
+ url = processUrl.url;
768
+ }
769
+
770
+ const file = { name: url.split('/').pop(), size: 0 };
771
+ const videoInfo = { url, files: file, ...this._getInfo(), process: processUrl };
772
+
773
+ const handler = function (infos, newInfos) {
774
+ infos = newInfos || infos;
775
+ this.create(this[/^iframe$/i.test(infos.process?.tag) ? 'createIframeTag' : 'createVideoTag'](), infos.url, infos.inputWidth, infos.inputHeight, infos.align, infos.isUpdate, infos.files);
776
+ }.bind(this, videoInfo);
777
+
778
+ const result = await this.triggerEvent('onVideoUploadBefore', {
779
+ info: videoInfo,
780
+ handler
781
+ });
782
+
783
+ if (result === undefined) return true;
784
+ if (result === false) return false;
785
+ if (result !== null && typeof result === 'object') handler(result);
786
+
787
+ if (result === true || result === NO_EVENT) handler(null);
788
+
789
+ return true;
790
+ }
791
+
792
+ /**
793
+ * @private
794
+ * @description Updates the video component within the editor.
795
+ * @param {HTMLIFrameElement|HTMLVideoElement} oFrame - The video element to update.
796
+ */
797
+ _update(oFrame) {
798
+ if (!oFrame) return;
799
+
800
+ if (/^video$/i.test(oFrame.nodeName)) {
801
+ this._setTagAttrs(/** @type {HTMLVideoElement} */ (oFrame));
802
+ } else if (/^iframe$/i.test(oFrame.nodeName)) {
803
+ this._setIframeAttrs(/** @type {HTMLIFrameElement} */ (oFrame));
804
+ }
805
+
806
+ let existElement = this.format.isBlock(oFrame.parentNode) || dom.check.isWysiwygFrame(oFrame.parentNode) ? oFrame : this.format.getLine(oFrame) || oFrame;
807
+
808
+ const prevFrame = oFrame;
809
+ const cloneFrame = /** @type {HTMLIFrameElement|HTMLVideoElement} */ (oFrame.cloneNode(true));
810
+ const figure = Figure.CreateContainer(cloneFrame, 'se-video-container');
811
+ const container = figure.container;
812
+
813
+ const figcaption = existElement.querySelector('figcaption');
814
+ let caption = null;
815
+ if (figcaption) {
816
+ caption = dom.utils.createElement('DIV');
817
+ caption.innerHTML = figcaption.innerHTML;
818
+ dom.utils.removeItem(figcaption);
819
+ }
820
+
821
+ // size
822
+ this.figure.open(cloneFrame, { nonResizing: this._nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, __fileManagerInfo: true });
823
+ const size = (cloneFrame.getAttribute('data-se-size') || ',').split(',');
824
+ this._applySize(size[0] || prevFrame.style.width || prevFrame.width || '', size[1] || prevFrame.style.height || prevFrame.height || '');
825
+
826
+ // align
827
+ const format = this.format.getLine(prevFrame);
828
+ if (format) this._align = format.style.textAlign || format.style.float;
829
+ this.figure.setAlign(cloneFrame, this._align);
830
+
831
+ if (dom.query.getParentElement(prevFrame, dom.check.isExcludeFormat)) {
832
+ prevFrame.replaceWith(container);
833
+ } else if (dom.check.isListCell(existElement)) {
834
+ const refer = dom.query.getParentElement(prevFrame, (current) => current.parentNode === existElement);
835
+ existElement.insertBefore(container, refer);
836
+ dom.utils.removeItem(prevFrame);
837
+ this.nodeTransform.removeEmptyNode(refer, null, true);
838
+ } else if (this.format.isLine(existElement)) {
839
+ const refer = dom.query.getParentElement(prevFrame, (current) => current.parentNode === existElement);
840
+ existElement = this.nodeTransform.split(existElement, refer);
841
+ existElement.parentNode.insertBefore(container, existElement);
842
+ dom.utils.removeItem(prevFrame);
843
+ this.nodeTransform.removeEmptyNode(existElement, null, true);
844
+ } else {
845
+ /** @type {Element} */ (existElement).replaceWith(container);
846
+ }
847
+
848
+ if (caption) existElement.parentNode.insertBefore(caption, container.nextElementSibling);
849
+
850
+ return cloneFrame;
851
+ }
852
+
853
+ /**
854
+ * @private
855
+ * @description Registers the uploaded video in the editor.
856
+ * @param {VideoInfo_video} info - Video information object.
857
+ * @param {Object<string, *>} response - Server response containing video data.
858
+ */
859
+ _register(info, response) {
860
+ const fileList = response.result;
861
+ const videoTag = this.createVideoTag();
862
+
863
+ for (let i = 0, len = fileList.length; i < len; i++) {
864
+ const ctag = info.isUpdate ? info.element : /** @type {HTMLIFrameElement|HTMLVideoElement} */ (videoTag.cloneNode(false));
865
+ this.create(ctag, fileList[i].url, info.inputWidth, info.inputHeight, info.align, info.isUpdate, {
866
+ name: fileList[i].name,
867
+ size: fileList[i].size
868
+ });
869
+ }
870
+ }
871
+
872
+ /**
873
+ * @private
874
+ * @description Uploads a video to the server using an external upload handler.
875
+ * @param {VideoInfo_video} info - Video information object.
876
+ * @param {FileList} files - The video files to upload.
877
+ */
878
+ _serverUpload(info, files) {
879
+ if (!files) return;
880
+
881
+ const videoUploadUrl = this.pluginOptions.uploadUrl;
882
+ if (typeof videoUploadUrl === 'string' && videoUploadUrl.length > 0) {
883
+ this.fileManager.upload(videoUploadUrl, this.pluginOptions.uploadHeaders, files, this.#UploadCallBack.bind(this, info), this._error.bind(this));
884
+ }
885
+ }
886
+
887
+ /**
888
+ * @private
889
+ * @description Sets attributes for the video tag.
890
+ * @param {HTMLVideoElement} element - The video element.
891
+ */
892
+ _setTagAttrs(element) {
893
+ element.setAttribute('controls', 'true');
894
+
895
+ const attrs = this.pluginOptions.videoTagAttributes;
896
+ if (!attrs) return;
897
+
898
+ for (const key in attrs) {
899
+ element.setAttribute(key, attrs[key]);
900
+ }
901
+ }
902
+
903
+ /**
904
+ * @private
905
+ * @description Sets attributes for the iframe tag.
906
+ * @param {HTMLIFrameElement} element - The iframe element.
907
+ */
908
+ _setIframeAttrs(element) {
909
+ element.frameBorder = '0';
910
+ element.allowFullscreen = true;
911
+
912
+ const attrs = this.pluginOptions.iframeTagAttributes;
913
+ if (!attrs) return;
914
+
915
+ for (const key in attrs) {
916
+ element.setAttribute(key, attrs[key]);
917
+ }
918
+ }
919
+
920
+ /**
921
+ * @private
922
+ * @description Selects a ratio option in the ratio dropdown.
923
+ * @param {string|number} value - The selected ratio value.
924
+ * @returns {boolean} Returns true if a ratio was selected.
925
+ */
926
+ _setRatioSelect(value) {
927
+ let ratioSelected = false;
928
+ const ratioOption = this.frameRatioOption.options;
929
+
930
+ if (/%$/.test(value + '') || this._onlyPercentage) value = numbers.get(value, 2) / 100 + '';
931
+ else if (!numbers.is(value) || Number(value) >= 1) value = '';
932
+
933
+ this.inputY.placeholder = '';
934
+ for (let i = 0, len = ratioOption.length; i < len; i++) {
935
+ if (ratioOption[i].value === value) {
936
+ ratioSelected = ratioOption[i].selected = true;
937
+ this.inputY.placeholder = !value ? '' : Number(value) * 100 + '%';
938
+ } else ratioOption[i].selected = false;
939
+ }
940
+
941
+ return ratioSelected;
942
+ }
943
+
944
+ /**
945
+ * @private
946
+ * @description Handles video upload errors.
947
+ * @param {Object<string, *>} response - The error response object.
948
+ * @returns {Promise<void>}
949
+ */
950
+ async _error(response) {
951
+ const message = await this.triggerEvent('onVideoUploadError', { error: response });
952
+ const err = message === NO_EVENT ? response.errorMessage : message || response.errorMessage;
953
+ this.ui.alertOpen(err, 'error');
954
+ console.error('[SUNEDITOR.plugin.video.error]', message);
955
+ }
956
+
957
+ /**
958
+ * @description Handles the callback function for video upload completion.
959
+ * @param {VideoInfo_video} info - Video information.
960
+ * @param {XMLHttpRequest} xmlHttp - The XMLHttpRequest object.
961
+ */
962
+ async #UploadCallBack(info, xmlHttp) {
963
+ if ((await this.triggerEvent('videoUploadHandler', { xmlHttp, info })) === NO_EVENT) {
964
+ const response = JSON.parse(xmlHttp.responseText);
965
+ if (response.errorMessage) {
966
+ this._error(response);
967
+ } else {
968
+ this._register(info, response);
969
+ }
970
+ }
971
+ }
972
+
973
+ /**
974
+ * @description Removes selected files from the file input.
975
+ */
976
+ #RemoveSelectedFiles() {
977
+ this.videoInputFile.value = '';
978
+ if (this.videoUrlFile) {
979
+ this.videoUrlFile.disabled = false;
980
+ this.previewSrc.style.textDecoration = '';
981
+ }
982
+
983
+ // inputFile check
984
+ Modal.OnChangeFile(this.fileModalWrapper, []);
985
+ }
986
+
987
+ /**
988
+ * @description Handles link preview input changes.
989
+ * @param {InputEvent} e - Event object
990
+ */
991
+ #OnLinkPreview(e) {
992
+ /** @type {HTMLInputElement} */
993
+ const eventTarget = dom.query.getEventTarget(e);
994
+ const value = eventTarget.value.trim();
995
+ if (/^<iframe.*\/iframe>$/.test(value)) {
996
+ this._linkValue = value;
997
+ this.previewSrc.textContent = '<IFrame :src=".."></IFrame>';
998
+ } else {
999
+ this._linkValue = this.previewSrc.textContent = !value
1000
+ ? ''
1001
+ : this.options.get('defaultUrlProtocol') && !value.includes('://') && value.indexOf('#') !== 0
1002
+ ? this.options.get('defaultUrlProtocol') + value
1003
+ : !value.includes('://')
1004
+ ? '/' + value
1005
+ : value;
1006
+ }
1007
+ }
1008
+
1009
+ /**
1010
+ * @description Opens the video gallery.
1011
+ */
1012
+ #OpenGallery() {
1013
+ this.plugins.videoGallery.open(this.#SetUrlInput.bind(this));
1014
+ }
1015
+
1016
+ /**
1017
+ * @description Sets the URL input value when selecting from the gallery.
1018
+ * @param {HTMLInputElement} target - The selected video element.
1019
+ */
1020
+ #SetUrlInput(target) {
1021
+ this._linkValue = this.previewSrc.textContent = this.videoUrlFile.value = target.getAttribute('data-command') || target.src;
1022
+ this.videoUrlFile.focus();
1023
+ }
1024
+
1025
+ /**
1026
+ * @param {InputEvent} e - Event object
1027
+ */
1028
+ #OnfileInputChange(e) {
1029
+ if (!this.videoInputFile.value) {
1030
+ this.videoUrlFile.disabled = false;
1031
+ this.previewSrc.style.textDecoration = '';
1032
+ } else {
1033
+ this.videoUrlFile.disabled = true;
1034
+ this.previewSrc.style.textDecoration = 'line-through';
1035
+ }
1036
+
1037
+ // inputFile check
1038
+ /** @type {HTMLInputElement} */
1039
+ const eventTarget = dom.query.getEventTarget(e);
1040
+ Modal.OnChangeFile(this.fileModalWrapper, eventTarget.files);
1041
+ }
1042
+
1043
+ #OnClickRevert() {
1044
+ if (this._onlyPercentage) {
1045
+ this.inputX.value = Number(this._origin_w) > 100 ? '100' : this._origin_w;
1046
+ } else {
1047
+ this.inputX.value = this._origin_w;
1048
+ this.inputY.value = this._origin_h;
1049
+ }
1050
+ }
1051
+
1052
+ /**
1053
+ * @param {InputEvent} e - Event object
1054
+ */
1055
+ #SetRatio(e) {
1056
+ /** @type {HTMLSelectElement} */
1057
+ const eventTarget = dom.query.getEventTarget(e);
1058
+ const value = eventTarget.options[eventTarget.selectedIndex].value;
1059
+ this._defaultSizeY = this.figure.autoRatio.current = this._frameRatio = !value ? this._defaultSizeY : Number(value) * 100 + '%';
1060
+ this.inputY.placeholder = !value ? '' : Number(value) * 100 + '%';
1061
+ this.inputY.value = '';
1062
+ }
1063
+
1064
+ #OnChangeRatio() {
1065
+ this._ratio = this.proportion.checked ? Figure.GetRatio(this.inputX.value, this.inputY.value, this.sizeUnit) : { w: 1, h: 1 };
1066
+ }
1067
+
1068
+ /**
1069
+ * @param {"x"|"y"} xy - x or y
1070
+ * @param {KeyboardEvent} e - Event object
1071
+ */
1072
+ #OnInputSize(xy, e) {
1073
+ if (keyCodeMap.isSpace(e.code)) {
1074
+ e.preventDefault();
1075
+ return;
1076
+ }
1077
+
1078
+ /** @type {HTMLInputElement} */
1079
+ const eventTarget = dom.query.getEventTarget(e);
1080
+ if (xy === 'x' && this._onlyPercentage && Number(eventTarget.value) > 100) {
1081
+ eventTarget.value = '100';
1082
+ } else if (this.proportion.checked) {
1083
+ const ratioSize = Figure.CalcRatio(this.inputX.value, this.inputY.value, this.sizeUnit, this._ratio);
1084
+ if (xy === 'x') {
1085
+ this.inputY.value = String(ratioSize.h);
1086
+ } else {
1087
+ this.inputX.value = String(ratioSize.w);
1088
+ }
1089
+ }
1090
+
1091
+ if (xy === 'y') {
1092
+ this._setRatioSelect(eventTarget.value || this._defaultRatio);
1093
+ }
1094
+ }
1095
+ }
1096
+
1097
+ /**
1098
+ * @typedef {object} ModalReturns_video
1099
+ * @property {HTMLElement} html
1100
+ * @property {HTMLElement} alignForm
1101
+ * @property {HTMLElement} fileModalWrapper
1102
+ * @property {HTMLInputElement} videoInputFile
1103
+ * @property {HTMLInputElement} videoUrlFile
1104
+ * @property {HTMLElement} previewSrc
1105
+ * @property {HTMLButtonElement} galleryButton
1106
+ * @property {HTMLInputElement} proportion
1107
+ * @property {HTMLSelectElement} frameRatioOption
1108
+ * @property {HTMLInputElement} inputX
1109
+ * @property {HTMLInputElement} inputY
1110
+ * @property {HTMLButtonElement} revertBtn
1111
+ * @property {HTMLButtonElement} fileRemoveBtn
1112
+ *
1113
+ * @param {__se__EditorCore} editor
1114
+ * @param {*} pluginOptions
1115
+ * @returns {ModalReturns_video}
1116
+ */
1117
+ function CreateHTML_modal({ lang, icons, plugins }, pluginOptions) {
1118
+ let html = /*html*/ `
1119
+ <form method="post" enctype="multipart/form-data">
1120
+ <div class="se-modal-header">
1121
+ <button type="button" data-command="close" class="se-btn se-close-btn" title="${lang.close}" aria-label="${lang.close}">
1122
+ ${icons.cancel}
1123
+ </button>
1124
+ <span class="se-modal-title">${lang.video_modal_title}</span>
1125
+ </div>
1126
+ <div class="se-modal-body">`;
1127
+
1128
+ if (pluginOptions.createFileInput) {
1129
+ html += /*html*/ `
1130
+ <div class="se-modal-form">
1131
+ <label>${lang.video_modal_file}</label>
1132
+ ${Modal.CreateFileInput({ lang, icons }, pluginOptions)}
1133
+ </div>`;
1134
+ }
1135
+
1136
+ if (pluginOptions.createUrlInput) {
1137
+ html += /*html*/ `
1138
+ <div class="se-modal-form">
1139
+ <label>${lang.video_modal_url}</label>
1140
+ <div class="se-modal-form-files">
1141
+ <input class="se-input-form se-input-url" type="text" data-focus />
1142
+ ${
1143
+ plugins.videoGallery
1144
+ ? `<button type="button" class="se-btn se-tooltip se-modal-files-edge-button __se__gallery" aria-label="${lang.videoGallery}">
1145
+ ${icons.video_gallery}
1146
+ ${dom.utils.createTooltipInner(lang.videoGallery)}
1147
+ </button>`
1148
+ : ''
1149
+ }
1150
+ </div>
1151
+ <pre class="se-link-preview"></pre>
1152
+ </div>`;
1153
+ }
1154
+
1155
+ if (pluginOptions.canResize) {
1156
+ const ratioList = pluginOptions.ratioOptions || [
1157
+ { name: '16:9', value: 0.5625 },
1158
+ { name: '4:3', value: 0.75 },
1159
+ { name: '21:9', value: 0.4285 },
1160
+ { name: '9:16', value: 1.78 }
1161
+ ];
1162
+ const ratio = pluginOptions.defaultRatio;
1163
+ const onlyPercentage = pluginOptions.percentageOnlySize;
1164
+ const onlyPercentDisplay = onlyPercentage ? ' style="display: none !important;"' : '';
1165
+ const heightDisplay = !pluginOptions.showHeightInput ? ' style="display: none !important;"' : '';
1166
+ const ratioDisplay = !pluginOptions.showRatioOption ? ' style="display: none !important;"' : '';
1167
+ const onlyWidthDisplay = !onlyPercentage && !pluginOptions.showHeightInput && !pluginOptions.showRatioOption ? ' style="display: none !important;"' : '';
1168
+ html += /*html*/ `
1169
+ <div class="se-modal-form">
1170
+ <div class="se-modal-size-text">
1171
+ <label class="size-w">${lang.width}</label>
1172
+ <label class="se-modal-size-x">&nbsp;</label>
1173
+ <label class="size-h"${heightDisplay}>${lang.height}</label>
1174
+ <label class="size-h"${ratioDisplay}>(${lang.ratio})</label>
1175
+ </div>
1176
+ <input class="se-input-control _se_size_x" placeholder="100%"${onlyPercentage ? ' type="number" min="1"' : 'type="text"'}${onlyPercentage ? ' max="100"' : ''}/>
1177
+ <label class="se-modal-size-x"${onlyWidthDisplay}>${onlyPercentage ? '%' : 'x'}</label>
1178
+ <input class="se-input-control _se_size_y" placeholder="${pluginOptions.defaultRatio * 100}%"
1179
+ ${onlyPercentage ? ' type="number" min="1"' : 'type="text"'}${onlyPercentage ? ' max="100"' : ''}${heightDisplay}/>
1180
+ <select class="se-input-select se-modal-ratio" title="${lang.ratio}" aria-label="${lang.ratio}"${ratioDisplay}>
1181
+ ${!heightDisplay ? '<option value=""> - </option>' : ''}
1182
+ ${ratioList.map((ratioOption) => `<option value="${ratioOption.value}"${ratio.toString() === ratioOption.value.toString() ? ' selected' : ''}>${ratioOption.name}</option>`).join('')}
1183
+ </select>
1184
+ <button type="button" title="${lang.revert}" aria-label="${lang.revert}" class="se-btn se-modal-btn-revert">${icons.revert}</button>
1185
+ </div>
1186
+ <div class="se-modal-form se-modal-form-footer"${onlyPercentDisplay}${onlyWidthDisplay}>
1187
+ <label>
1188
+ <input type="checkbox" class="se-modal-btn-check _se_check_proportion" />&nbsp;
1189
+ <span>${lang.proportion}</span>
1190
+ </label>
1191
+ </div>`;
1192
+ }
1193
+
1194
+ html += /*html*/ `
1195
+ </div>
1196
+ <div class="se-modal-footer">
1197
+ <div class="se-figure-align">
1198
+ <label><input type="radio" name="suneditor_video_radio" class="se-modal-btn-radio" value="none" checked>${lang.basic}</label>
1199
+ <label><input type="radio" name="suneditor_video_radio" class="se-modal-btn-radio" value="left">${lang.left}</label>
1200
+ <label><input type="radio" name="suneditor_video_radio" class="se-modal-btn-radio" value="center">${lang.center}</label>
1201
+ <label><input type="radio" name="suneditor_video_radio" class="se-modal-btn-radio" value="right">${lang.right}</label>
1202
+ </div>
1203
+ <button type="submit" class="se-btn-primary" title="${lang.submitButton}" aria-label="${lang.submitButton}"><span>${lang.submitButton}</span></button>
1204
+ </div>
1205
+ </form>`;
1206
+
1207
+ const content = dom.utils.createElement('DIV', { class: 'se-modal-content' }, html);
1208
+
1209
+ return {
1210
+ html: content,
1211
+ alignForm: content.querySelector('.se-figure-align'),
1212
+ fileModalWrapper: content.querySelector('.se-flex-input-wrapper'),
1213
+ videoInputFile: content.querySelector('.__se__file_input'),
1214
+ videoUrlFile: content.querySelector('.se-input-url'),
1215
+ previewSrc: content.querySelector('.se-link-preview'),
1216
+ galleryButton: content.querySelector('.__se__gallery'),
1217
+ proportion: content.querySelector('._se_check_proportion'),
1218
+ frameRatioOption: content.querySelector('.se-modal-ratio'),
1219
+ inputX: content.querySelector('._se_size_x'),
1220
+ inputY: content.querySelector('._se_size_y'),
1221
+ revertBtn: content.querySelector('.se-modal-btn-revert'),
1222
+ fileRemoveBtn: content.querySelector('.se-file-remove')
1223
+ };
1224
+ }
1225
+
1226
+ export default Video;