suneditor 3.0.0-alpha.9 → 3.0.0-beta.2

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 (315) hide show
  1. package/CONTRIBUTING.md +170 -22
  2. package/{LICENSE.txt → LICENSE} +9 -9
  3. package/README.md +168 -30
  4. package/dist/suneditor.min.css +1 -1
  5. package/dist/suneditor.min.js +1 -1
  6. package/package.json +47 -21
  7. package/src/assets/design/color.css +121 -0
  8. package/src/assets/design/index.css +3 -0
  9. package/src/assets/design/size.css +35 -0
  10. package/src/assets/design/typography.css +37 -0
  11. package/src/assets/icons/defaultIcons.js +232 -0
  12. package/src/assets/suneditor-contents.css +181 -46
  13. package/src/assets/suneditor.css +1403 -650
  14. package/src/core/base/eventHandlers/handler_toolbar.js +35 -14
  15. package/src/core/base/eventHandlers/handler_ww_clipboard.js +23 -4
  16. package/src/core/base/eventHandlers/handler_ww_dragDrop.js +49 -10
  17. package/src/core/base/eventHandlers/handler_ww_key_input.js +422 -224
  18. package/src/core/base/eventHandlers/handler_ww_mouse.js +83 -36
  19. package/src/core/base/eventManager.js +520 -179
  20. package/src/core/base/history.js +95 -41
  21. package/src/core/class/char.js +26 -11
  22. package/src/core/class/component.js +345 -137
  23. package/src/core/class/format.js +683 -519
  24. package/src/core/class/html.js +485 -305
  25. package/src/core/class/menu.js +133 -47
  26. package/src/core/class/nodeTransform.js +90 -71
  27. package/src/core/class/offset.js +408 -92
  28. package/src/core/class/selection.js +216 -106
  29. package/src/core/class/shortcuts.js +68 -8
  30. package/src/core/class/toolbar.js +106 -116
  31. package/src/core/class/ui.js +422 -0
  32. package/src/core/class/viewer.js +178 -74
  33. package/src/core/editor.js +496 -389
  34. package/src/core/section/actives.js +123 -27
  35. package/src/core/section/constructor.js +615 -206
  36. package/src/core/section/context.js +28 -23
  37. package/src/core/section/documentType.js +561 -0
  38. package/src/editorInjector/_classes.js +19 -5
  39. package/src/editorInjector/_core.js +71 -7
  40. package/src/editorInjector/index.js +63 -1
  41. package/src/events.js +622 -0
  42. package/src/helper/clipboard.js +59 -0
  43. package/src/helper/converter.js +202 -26
  44. package/src/helper/dom/domCheck.js +304 -0
  45. package/src/helper/dom/domQuery.js +669 -0
  46. package/src/helper/dom/domUtils.js +557 -0
  47. package/src/helper/dom/index.js +12 -0
  48. package/src/helper/env.js +46 -56
  49. package/src/helper/index.js +10 -4
  50. package/src/helper/keyCodeMap.js +183 -0
  51. package/src/helper/numbers.js +12 -8
  52. package/src/helper/unicode.js +9 -5
  53. package/src/langs/ckb.js +74 -4
  54. package/src/langs/cs.js +72 -2
  55. package/src/langs/da.js +73 -3
  56. package/src/langs/de.js +73 -4
  57. package/src/langs/en.js +23 -3
  58. package/src/langs/es.js +73 -4
  59. package/src/langs/fa.js +75 -3
  60. package/src/langs/fr.js +73 -3
  61. package/src/langs/he.js +73 -4
  62. package/src/langs/hu.js +230 -0
  63. package/src/langs/index.js +7 -3
  64. package/src/langs/it.js +70 -1
  65. package/src/langs/ja.js +72 -4
  66. package/src/langs/km.js +230 -0
  67. package/src/langs/ko.js +22 -2
  68. package/src/langs/lv.js +74 -5
  69. package/src/langs/nl.js +73 -4
  70. package/src/langs/pl.js +73 -4
  71. package/src/langs/pt_br.js +70 -1
  72. package/src/langs/ro.js +74 -5
  73. package/src/langs/ru.js +73 -4
  74. package/src/langs/se.js +73 -4
  75. package/src/langs/tr.js +73 -1
  76. package/src/langs/{ua.js → uk.js} +75 -6
  77. package/src/langs/ur.js +77 -8
  78. package/src/langs/zh_cn.js +74 -5
  79. package/src/modules/ApiManager.js +77 -54
  80. package/src/modules/Browser.js +667 -0
  81. package/src/modules/ColorPicker.js +162 -102
  82. package/src/modules/Controller.js +273 -142
  83. package/src/modules/Figure.js +925 -484
  84. package/src/modules/FileManager.js +121 -69
  85. package/src/modules/HueSlider.js +113 -61
  86. package/src/modules/Modal.js +291 -122
  87. package/src/modules/ModalAnchorEditor.js +383 -234
  88. package/src/modules/SelectMenu.js +270 -168
  89. package/src/modules/_DragHandle.js +2 -1
  90. package/src/modules/index.js +3 -3
  91. package/src/plugins/browser/audioGallery.js +83 -0
  92. package/src/plugins/browser/fileBrowser.js +103 -0
  93. package/src/plugins/browser/fileGallery.js +83 -0
  94. package/src/plugins/browser/imageGallery.js +81 -0
  95. package/src/plugins/browser/videoGallery.js +103 -0
  96. package/src/plugins/command/blockquote.js +40 -27
  97. package/src/plugins/command/exportPDF.js +134 -0
  98. package/src/plugins/command/fileUpload.js +229 -162
  99. package/src/plugins/command/list_bulleted.js +83 -47
  100. package/src/plugins/command/list_numbered.js +83 -47
  101. package/src/plugins/dropdown/align.js +66 -54
  102. package/src/plugins/dropdown/backgroundColor.js +63 -49
  103. package/src/plugins/dropdown/font.js +71 -47
  104. package/src/plugins/dropdown/fontColor.js +63 -48
  105. package/src/plugins/dropdown/formatBlock.js +70 -33
  106. package/src/plugins/dropdown/hr.js +92 -51
  107. package/src/plugins/dropdown/layout.js +37 -26
  108. package/src/plugins/dropdown/lineHeight.js +54 -38
  109. package/src/plugins/dropdown/list.js +60 -45
  110. package/src/plugins/dropdown/paragraphStyle.js +51 -30
  111. package/src/plugins/dropdown/table.js +2003 -813
  112. package/src/plugins/dropdown/template.js +38 -26
  113. package/src/plugins/dropdown/textStyle.js +43 -31
  114. package/src/plugins/field/mention.js +147 -86
  115. package/src/plugins/index.js +32 -6
  116. package/src/plugins/input/fontSize.js +161 -108
  117. package/src/plugins/input/pageNavigator.js +70 -0
  118. package/src/plugins/modal/audio.js +358 -173
  119. package/src/plugins/modal/drawing.js +531 -0
  120. package/src/plugins/modal/embed.js +886 -0
  121. package/src/plugins/modal/image.js +674 -362
  122. package/src/plugins/modal/link.js +100 -71
  123. package/src/plugins/modal/math.js +367 -167
  124. package/src/plugins/modal/video.js +691 -335
  125. package/src/plugins/popup/anchor.js +222 -0
  126. package/src/suneditor.js +50 -13
  127. package/src/themes/dark.css +122 -0
  128. package/src/typedef.js +130 -0
  129. package/types/assets/icons/defaultIcons.d.ts +153 -0
  130. package/types/core/base/eventHandlers/handler_toolbar.d.ts +41 -0
  131. package/types/core/base/eventHandlers/handler_ww_clipboard.d.ts +40 -0
  132. package/types/core/base/eventHandlers/handler_ww_dragDrop.d.ts +35 -0
  133. package/types/core/base/eventHandlers/handler_ww_key_input.d.ts +45 -0
  134. package/types/core/base/eventHandlers/handler_ww_mouse.d.ts +39 -0
  135. package/types/core/base/eventManager.d.ts +385 -0
  136. package/types/core/base/history.d.ts +81 -0
  137. package/types/core/class/char.d.ts +60 -0
  138. package/types/core/class/component.d.ts +212 -0
  139. package/types/core/class/format.d.ts +616 -0
  140. package/types/core/class/html.d.ts +422 -0
  141. package/types/core/class/menu.d.ts +126 -0
  142. package/types/core/class/nodeTransform.d.ts +93 -0
  143. package/types/core/class/offset.d.ts +522 -0
  144. package/types/core/class/selection.d.ts +188 -0
  145. package/types/core/class/shortcuts.d.ts +142 -0
  146. package/types/core/class/toolbar.d.ts +189 -0
  147. package/types/core/class/ui.d.ts +164 -0
  148. package/types/core/class/viewer.d.ts +140 -0
  149. package/types/core/editor.d.ts +610 -0
  150. package/types/core/section/actives.d.ts +46 -0
  151. package/types/core/section/constructor.d.ts +777 -0
  152. package/types/core/section/context.d.ts +45 -0
  153. package/types/core/section/documentType.d.ts +178 -0
  154. package/types/editorInjector/_classes.d.ts +41 -0
  155. package/types/editorInjector/_core.d.ts +92 -0
  156. package/types/editorInjector/index.d.ts +71 -0
  157. package/types/events.d.ts +273 -0
  158. package/types/helper/clipboard.d.ts +12 -0
  159. package/types/helper/converter.d.ts +197 -0
  160. package/types/helper/dom/domCheck.d.ts +189 -0
  161. package/types/helper/dom/domQuery.d.ts +223 -0
  162. package/types/helper/dom/domUtils.d.ts +226 -0
  163. package/types/helper/dom/index.d.ts +9 -0
  164. package/types/helper/env.d.ts +132 -0
  165. package/types/helper/index.d.ts +174 -0
  166. package/types/helper/keyCodeMap.d.ts +110 -0
  167. package/types/helper/numbers.d.ts +46 -0
  168. package/types/helper/unicode.d.ts +28 -0
  169. package/types/index.d.ts +120 -0
  170. package/{typings/Lang.d.ts → types/langs/_Lang.d.ts} +173 -103
  171. package/types/langs/ckb.d.ts +3 -0
  172. package/types/langs/cs.d.ts +3 -0
  173. package/types/langs/da.d.ts +3 -0
  174. package/types/langs/de.d.ts +3 -0
  175. package/types/langs/en.d.ts +3 -0
  176. package/types/langs/es.d.ts +3 -0
  177. package/types/langs/fa.d.ts +3 -0
  178. package/types/langs/fr.d.ts +3 -0
  179. package/types/langs/he.d.ts +3 -0
  180. package/types/langs/hu.d.ts +3 -0
  181. package/types/langs/index.d.ts +54 -0
  182. package/types/langs/it.d.ts +3 -0
  183. package/types/langs/ja.d.ts +3 -0
  184. package/types/langs/km.d.ts +3 -0
  185. package/types/langs/ko.d.ts +3 -0
  186. package/types/langs/lv.d.ts +3 -0
  187. package/types/langs/nl.d.ts +3 -0
  188. package/types/langs/pl.d.ts +3 -0
  189. package/types/langs/pt_br.d.ts +3 -0
  190. package/types/langs/ro.d.ts +3 -0
  191. package/types/langs/ru.d.ts +3 -0
  192. package/types/langs/se.d.ts +3 -0
  193. package/types/langs/tr.d.ts +3 -0
  194. package/types/langs/uk.d.ts +3 -0
  195. package/types/langs/ur.d.ts +3 -0
  196. package/types/langs/zh_cn.d.ts +3 -0
  197. package/types/modules/ApiManager.d.ts +125 -0
  198. package/types/modules/Browser.d.ts +326 -0
  199. package/types/modules/ColorPicker.d.ts +131 -0
  200. package/types/modules/Controller.d.ts +251 -0
  201. package/types/modules/Figure.d.ts +517 -0
  202. package/types/modules/FileManager.d.ts +202 -0
  203. package/types/modules/HueSlider.d.ts +136 -0
  204. package/types/modules/Modal.d.ts +111 -0
  205. package/types/modules/ModalAnchorEditor.d.ts +236 -0
  206. package/types/modules/SelectMenu.d.ts +194 -0
  207. package/types/modules/_DragHandle.d.ts +7 -0
  208. package/types/modules/index.d.ts +26 -0
  209. package/types/plugins/browser/audioGallery.d.ts +55 -0
  210. package/types/plugins/browser/fileBrowser.d.ts +64 -0
  211. package/types/plugins/browser/fileGallery.d.ts +55 -0
  212. package/types/plugins/browser/imageGallery.d.ts +51 -0
  213. package/types/plugins/browser/videoGallery.d.ts +57 -0
  214. package/types/plugins/command/blockquote.d.ts +28 -0
  215. package/types/plugins/command/exportPDF.d.ts +46 -0
  216. package/types/plugins/command/fileUpload.d.ts +156 -0
  217. package/types/plugins/command/list_bulleted.d.ts +46 -0
  218. package/types/plugins/command/list_numbered.d.ts +46 -0
  219. package/types/plugins/dropdown/align.d.ts +60 -0
  220. package/types/plugins/dropdown/backgroundColor.d.ts +63 -0
  221. package/types/plugins/dropdown/font.d.ts +54 -0
  222. package/types/plugins/dropdown/fontColor.d.ts +63 -0
  223. package/types/plugins/dropdown/formatBlock.d.ts +54 -0
  224. package/types/plugins/dropdown/hr.d.ts +71 -0
  225. package/types/plugins/dropdown/layout.d.ts +40 -0
  226. package/types/plugins/dropdown/lineHeight.d.ts +50 -0
  227. package/types/plugins/dropdown/list.d.ts +39 -0
  228. package/types/plugins/dropdown/paragraphStyle.d.ts +54 -0
  229. package/types/plugins/dropdown/table.d.ts +627 -0
  230. package/types/plugins/dropdown/template.d.ts +40 -0
  231. package/types/plugins/dropdown/textStyle.d.ts +41 -0
  232. package/types/plugins/field/mention.d.ts +102 -0
  233. package/types/plugins/index.d.ts +107 -0
  234. package/types/plugins/input/fontSize.d.ts +170 -0
  235. package/types/plugins/input/pageNavigator.d.ts +28 -0
  236. package/types/plugins/modal/audio.d.ts +269 -0
  237. package/types/plugins/modal/drawing.d.ts +246 -0
  238. package/types/plugins/modal/embed.d.ts +387 -0
  239. package/types/plugins/modal/image.d.ts +451 -0
  240. package/types/plugins/modal/link.d.ts +128 -0
  241. package/types/plugins/modal/math.d.ts +193 -0
  242. package/types/plugins/modal/video.d.ts +485 -0
  243. package/types/plugins/popup/anchor.d.ts +56 -0
  244. package/types/suneditor.d.ts +51 -0
  245. package/types/typedef.d.ts +233 -0
  246. package/.eslintignore +0 -7
  247. package/.eslintrc.json +0 -64
  248. package/src/assets/icons/_default.js +0 -194
  249. package/src/core/base/events.js +0 -320
  250. package/src/core/class/notice.js +0 -42
  251. package/src/helper/domUtils.js +0 -1177
  252. package/src/modules/FileBrowser.js +0 -271
  253. package/src/plugins/command/exportPdf.js +0 -168
  254. package/src/plugins/fileBrowser/imageGallery.js +0 -81
  255. package/src/themes/test.css +0 -61
  256. package/typings/CommandPlugin.d.ts +0 -8
  257. package/typings/DialogPlugin.d.ts +0 -20
  258. package/typings/FileBrowserPlugin.d.ts +0 -30
  259. package/typings/Module.d.ts +0 -15
  260. package/typings/Plugin.d.ts +0 -42
  261. package/typings/SubmenuPlugin.d.ts +0 -8
  262. package/typings/_classes.d.ts +0 -17
  263. package/typings/_colorPicker.d.ts +0 -60
  264. package/typings/_core.d.ts +0 -55
  265. package/typings/align.d.ts +0 -5
  266. package/typings/audio.d.ts +0 -5
  267. package/typings/backgroundColor.d.ts +0 -5
  268. package/typings/blockquote.d.ts +0 -5
  269. package/typings/char.d.ts +0 -39
  270. package/typings/component.d.ts +0 -38
  271. package/typings/context.d.ts +0 -39
  272. package/typings/converter.d.ts +0 -33
  273. package/typings/dialog.d.ts +0 -28
  274. package/typings/domUtils.d.ts +0 -361
  275. package/typings/editor.d.ts +0 -7
  276. package/typings/editor.ts +0 -542
  277. package/typings/env.d.ts +0 -70
  278. package/typings/eventManager.d.ts +0 -37
  279. package/typings/events.d.ts +0 -262
  280. package/typings/fileBrowser.d.ts +0 -42
  281. package/typings/fileManager.d.ts +0 -67
  282. package/typings/font.d.ts +0 -5
  283. package/typings/fontColor.d.ts +0 -5
  284. package/typings/fontSize.d.ts +0 -5
  285. package/typings/format.d.ts +0 -191
  286. package/typings/formatBlock.d.ts +0 -5
  287. package/typings/history.d.ts +0 -48
  288. package/typings/horizontalRule.d.ts +0 -5
  289. package/typings/image.d.ts +0 -5
  290. package/typings/imageGallery.d.ts +0 -5
  291. package/typings/index.d.ts +0 -21
  292. package/typings/index.modules.d.ts +0 -11
  293. package/typings/index.plugins.d.ts +0 -58
  294. package/typings/lineHeight.d.ts +0 -5
  295. package/typings/link.d.ts +0 -5
  296. package/typings/list.d.ts +0 -5
  297. package/typings/math.d.ts +0 -5
  298. package/typings/mediaContainer.d.ts +0 -25
  299. package/typings/mention.d.ts +0 -5
  300. package/typings/node.d.ts +0 -57
  301. package/typings/notice.d.ts +0 -16
  302. package/typings/numbers.d.ts +0 -29
  303. package/typings/offset.d.ts +0 -24
  304. package/typings/options.d.ts +0 -589
  305. package/typings/paragraphStyle.d.ts +0 -5
  306. package/typings/resizing.d.ts +0 -141
  307. package/typings/selection.d.ts +0 -94
  308. package/typings/shortcuts.d.ts +0 -13
  309. package/typings/suneditor.d.ts +0 -9
  310. package/typings/table.d.ts +0 -5
  311. package/typings/template.d.ts +0 -5
  312. package/typings/textStyle.d.ts +0 -5
  313. package/typings/toolbar.d.ts +0 -32
  314. package/typings/unicode.d.ts +0 -25
  315. package/typings/video.d.ts +0 -5
@@ -1,188 +1,282 @@
1
1
  import EditorInjector from '../../editorInjector';
2
2
  import { Modal, Figure, FileManager } from '../../modules';
3
- import { domUtils, numbers, env } from '../../helper';
3
+ import { dom, numbers, env, converter, keyCodeMap } from '../../helper';
4
4
  const { NO_EVENT } = env;
5
5
 
6
- const Video = function (editor, pluginOptions) {
7
- // plugin bisic properties
8
- EditorInjector.call(this, editor);
9
- this.title = this.lang.video;
10
- this.icon = 'video';
11
-
12
- // define plugin options
13
- this.pluginOptions = {
14
- canResize: pluginOptions.canResize === undefined ? true : pluginOptions.canResize,
15
- showHeightInput: pluginOptions.showHeightInput === undefined ? true : !!pluginOptions.showHeightInput,
16
- defaultWidth: !pluginOptions.defaultWidth || !numbers.get(pluginOptions.defaultWidth, 0) ? '' : numbers.is(pluginOptions.defaultWidth) ? pluginOptions.defaultWidth + 'px' : pluginOptions.defaultWidth,
17
- defaultHeight: !pluginOptions.defaultHeight || !numbers.get(pluginOptions.defaultHeight, 0) ? '' : numbers.is(pluginOptions.defaultHeight) ? pluginOptions.defaultHeight + 'px' : pluginOptions.defaultHeight,
18
- percentageOnlySize: !!pluginOptions.percentageOnlySize,
19
- createFileInput: !!pluginOptions.createFileInput,
20
- createUrlInput: pluginOptions.createUrlInput === undefined || !pluginOptions.createFileInput ? true : pluginOptions.createUrlInput,
21
- uploadUrl: typeof pluginOptions.uploadUrl === 'string' ? pluginOptions.uploadUrl : null,
22
- uploadHeaders: pluginOptions.uploadHeaders || null,
23
- uploadSizeLimit: /\d+/.test(pluginOptions.uploadSizeLimit) ? numbers.get(pluginOptions.uploadSizeLimit, 0) : null,
24
- uploadSingleSizeLimit: /\d+/.test(pluginOptions.uploadSingleSizeLimit) ? numbers.get(pluginOptions.uploadSingleSizeLimit, 0) : null,
25
- allowMultiple: !!pluginOptions.allowMultiple,
26
- acceptedFormats: typeof pluginOptions.acceptedFormats !== 'string' || pluginOptions.acceptedFormats.trim() === '*' ? 'video/*' : pluginOptions.acceptedFormats.trim() || 'video/*',
27
- defaultRatio: numbers.get(pluginOptions.defaultRatio, 4) || 0.5625,
28
- showRatioOption: pluginOptions.showRatioOption === undefined ? true : !!pluginOptions.showRatioOption,
29
- ratioOptions: !pluginOptions.ratioOptions ? null : pluginOptions.ratioOptions,
30
- videoTagAttributes: pluginOptions.videoTagAttributes || null,
31
- iframeTagAttributes: pluginOptions.iframeTagAttributes || null
32
- };
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
+ }
33
65
 
34
- // create HTML
35
- const sizeUnit = this.pluginOptions.percentageOnlySize ? '%' : 'px';
36
- const modalEl = CreateHTML_modal(editor, this.pluginOptions);
37
- const figureControls =
38
- pluginOptions.controls || !this.pluginOptions.canResize
39
- ? [['mirror_h', 'mirror_v', 'align', 'revert', 'edit', 'remove']]
40
- : [
41
- ['resize_auto,75,50', 'rotate_l', 'rotate_r', 'mirror_h', 'mirror_v'],
42
- ['edit', 'align', 'revert', 'remove']
43
- ];
44
-
45
- // show align
46
- if (!figureControls.some((subArray) => subArray.includes('align'))) modalEl.querySelector('.se-figure-align').style.display = 'none';
47
-
48
- // modules
49
- const defaultRatio = this.pluginOptions.defaultRatio * 100 + '%';
50
- this.modal = new Modal(this, modalEl);
51
- this.figure = new Figure(this, figureControls, { sizeUnit: sizeUnit, autoRatio: { current: defaultRatio, default: defaultRatio } });
52
- this.fileManager = new FileManager(this, {
53
- query: 'iframe, video',
54
- loadHandler: this.events.onVideoLoad,
55
- eventHandler: this.events.onVideoAction
56
- });
57
-
58
- // members
59
- this.fileModalWrapper = modalEl.querySelector('.se-flex-input-wrapper');
60
- this.videoInputFile = modalEl.querySelector('.__se__file_input');
61
- this.videoUrlFile = modalEl.querySelector('.se-input-url');
62
- this.focusElement = this.videoUrlFile || this.videoInputFile;
63
- this.previewSrc = modalEl.querySelector('.se-link-preview');
64
- this._linkValue = '';
65
- this._align = 'none';
66
- this._videoRatio = defaultRatio;
67
- this._defaultRatio = defaultRatio;
68
- this._defaultSizeX = '100%';
69
- this._defaultSizeY = this.pluginOptions.defaultRatio * 100 + '%';
70
- this.sizeUnit = sizeUnit;
71
- this.proportion = {};
72
- this.videoRatioOption = {};
73
- this.inputX = {};
74
- this.inputY = {};
75
- this._element = null;
76
- this._cover = null;
77
- this._container = null;
78
- this._ratio = { w: 1, h: 1 };
79
- this._origin_w = this.pluginOptions.defaultWidth === '100%' ? '' : this.pluginOptions.defaultWidth;
80
- this._origin_h = this.pluginOptions.defaultHeight === defaultRatio ? '' : this.pluginOptions.defaultHeight;
81
- this._resizing = this.pluginOptions.canResize;
82
- this._onlyPercentage = this.pluginOptions.percentageOnlySize;
83
- this._nonResizing = !this._resizing || !this.pluginOptions.showHeightInput || this._onlyPercentage;
84
- this.query = {
85
- youtube: {
86
- pattern: /youtu\.?be/i,
87
- action: this.convertUrlYoutube.bind(this),
88
- tag: 'iframe'
89
- },
90
- vimeo: {
91
- pattern: /vimeo\.com/i,
92
- action: this.convertUrlVimeo.bind(this),
93
- tag: 'iframe'
94
- },
95
- ...pluginOptions.embedQuery
96
- };
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));
97
197
 
98
- // init
99
- if (this.videoInputFile) this.eventManager.addEvent(modalEl.querySelector('.se-file-remove'), 'click', RemoveSelectedFiles.bind(this));
100
- if (this.videoUrlFile) this.eventManager.addEvent(this.videoUrlFile, 'input', OnLinkPreview.bind(this));
101
- if (this.videoInputFile && this.videoUrlFile) this.eventManager.addEvent(this.videoInputFile, 'change', OnfileInputChange.bind(this));
102
-
103
- if (this._resizing) {
104
- this.proportion = modalEl.querySelector('._se_video_check_proportion');
105
- this.videoRatioOption = modalEl.querySelector('.se-video-ratio');
106
- this.inputX = modalEl.querySelector('._se_video_size_x');
107
- this.inputY = modalEl.querySelector('._se_video_size_y');
108
- this.inputX.value = this.pluginOptions.defaultWidth;
109
- this.inputY.value = this.pluginOptions.defaultHeight;
110
-
111
- const ratioChange = OnChangeRatio.bind(this);
112
- this.eventManager.addEvent(this.inputX, 'keyup', OnInputSize.bind(this, 'x'));
113
- this.eventManager.addEvent(this.inputY, 'keyup', OnInputSize.bind(this, 'y'));
114
- this.eventManager.addEvent(this.inputX, 'change', ratioChange);
115
- this.eventManager.addEvent(this.inputY, 'change', ratioChange);
116
- this.eventManager.addEvent(this.proportion, 'change', ratioChange);
117
- this.eventManager.addEvent(this.videoRatioOption, 'change', SetVideoRatio.bind(this));
118
- this.eventManager.addEvent(modalEl.querySelector('.se-modal-btn-revert'), 'click', OnClickRevert.bind(this));
119
- }
120
- };
121
-
122
- Video.key = 'video';
123
- Video.type = 'modal';
124
- Video.className = '';
125
- Video.component = function (node) {
126
- if (/^(VIDEO)$/i.test(node?.nodeName)) {
127
- return node;
128
- } else if (/^(IFRAME)$/i.test(node?.nodeName)) {
129
- return this.findProcessUrl(node.src) ? node : null;
130
- }
131
- return null;
132
- };
133
- Video.prototype = {
134
- /**
135
- * @override type = "modal"
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.
136
220
  */
137
221
  open() {
138
222
  this.modal.open();
139
- },
223
+ }
140
224
 
141
225
  /**
142
- * @override Figure
226
+ * @editorMethod Modules.Controller(Figure)
227
+ * @description Executes the method that is called when a target component is edited.
143
228
  */
144
229
  edit() {
145
230
  this.modal.open();
146
- },
231
+ }
147
232
 
148
233
  /**
149
- * @override modal
150
- * @param {boolean} isUpdate open state is update
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)."
151
237
  */
152
238
  on(isUpdate) {
153
239
  if (!isUpdate) {
154
- this.inputX.value = this._origin_w = this.pluginOptions.defaultWidth === this._defaultSizeX ? '' : this.pluginOptions.defaultWidth;
155
- this.inputY.value = this._origin_h = this.pluginOptions.defaultHeight === this._defaultSizeY ? '' : this.pluginOptions.defaultHeight;
156
- this.proportion.disabled = true;
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
+ }
157
245
  if (this.videoInputFile && this.pluginOptions.allowMultiple) this.videoInputFile.setAttribute('multiple', 'multiple');
158
246
  } else {
159
247
  if (this.videoInputFile && this.pluginOptions.allowMultiple) this.videoInputFile.removeAttribute('multiple');
160
248
  }
161
249
 
162
250
  if (this._resizing) {
163
- this._setVideoRatioSelect(this._origin_h || this._defaultRatio);
251
+ this._setRatioSelect(this._origin_h || this._defaultRatio);
164
252
  }
165
- },
253
+ }
166
254
 
167
255
  /**
168
- * @description On paste or drop
169
- * @param {*} params { frameContext, event, file }
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
170
263
  */
171
- onPastAndDrop({ file }) {
264
+ onFilePasteAndDrop({ file }) {
172
265
  if (!/^video/.test(file.type)) return;
173
266
 
174
267
  this.submitFile([file]);
175
268
  this.editor.focus();
176
269
 
177
270
  return false;
178
- },
271
+ }
179
272
 
180
273
  /**
181
- * @override modal
182
- * @returns {boolean | undefined}
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
183
277
  */
184
278
  async modalAction() {
185
- this._align = this.modal.form.querySelector('input[name="suneditor_video_radio"]:checked').value;
279
+ this._align = /** @type {HTMLInputElement} */ (this.modal.form.querySelector('input[name="suneditor_video_radio"]:checked')).value;
186
280
 
187
281
  let result = false;
188
282
  if (this.videoInputFile && this.videoInputFile.files.length > 0) {
@@ -191,43 +285,56 @@ Video.prototype = {
191
285
  result = await this.submitURL(this._linkValue);
192
286
  }
193
287
 
194
- if (result) this._w.setTimeout(this.component.select.bind(this.component, this._element, 'video'), 0);
288
+ if (result) this._w.setTimeout(this.component.select.bind(this.component, this._element, Video.key), 0);
195
289
 
196
290
  return result;
197
- },
291
+ }
198
292
 
199
293
  /**
200
- * @override core
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.
201
303
  */
202
304
  retainFormat() {
203
305
  return {
204
306
  query: 'iframe, video',
205
- method: (element) => {
307
+ method: async (element) => {
308
+ if (/^(iframe)$/i.test(element?.nodeName)) {
309
+ if (!this.checkContentType(element.src)) return;
310
+ }
311
+
206
312
  const figureInfo = Figure.GetContainer(element);
207
313
  if (figureInfo && figureInfo.container && figureInfo.cover) return;
208
314
 
209
- this.ready(element);
315
+ this._ready(element);
210
316
  const line = this.format.getLine(element);
211
317
  if (line) this._align = line.style.textAlign || line.style.float;
212
318
 
213
319
  this._update(element);
214
320
  }
215
321
  };
216
- },
322
+ }
217
323
 
218
324
  /**
219
- * @override modal
325
+ * @editorMethod Modules.Modal
326
+ * @description This function is called before the modal window is opened, but before it is closed.
220
327
  */
221
328
  init() {
222
329
  Modal.OnChangeFile(this.fileModalWrapper, []);
223
330
  if (this.videoInputFile) this.videoInputFile.value = '';
224
331
  if (this.videoUrlFile) this._linkValue = this.previewSrc.textContent = this.videoUrlFile.value = '';
225
332
  if (this.videoInputFile && this.videoUrlFile) {
226
- this.videoUrlFile.removeAttribute('disabled');
333
+ this.videoUrlFile.disabled = false;
227
334
  this.previewSrc.style.textDecoration = '';
228
335
  }
229
336
 
230
- this.modal.form.querySelector('input[name="suneditor_video_radio"][value="none"]').checked = true;
337
+ /** @type {HTMLInputElement} */ (this.modal.form.querySelector('input[name="suneditor_video_radio"][value="none"]')).checked = true;
231
338
  this._ratio = { w: 1, h: 1 };
232
339
  this._nonResizing = false;
233
340
 
@@ -236,24 +343,27 @@ Video.prototype = {
236
343
  this.inputY.value = this.pluginOptions.defaultHeight === this._defaultSizeY ? '' : this.pluginOptions.defaultHeight;
237
344
  this.proportion.checked = false;
238
345
  this.proportion.disabled = true;
239
- this._setVideoRatioSelect(this._defaultRatio);
346
+ this._setRatioSelect(this._defaultRatio);
240
347
  }
241
- },
348
+ }
242
349
 
243
350
  /**
244
- * @override component, fileManager
245
- * @description Called when a container is selected.
246
- * @param {Element} element Target element
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
247
354
  */
248
- select(element) {
249
- this.ready(element);
250
- },
355
+ select(target) {
356
+ this._ready(target);
357
+ }
251
358
 
252
359
  /**
253
- * @override fileManager, figure
254
- * @param {Element} target Target element
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.
255
365
  */
256
- ready(target) {
366
+ _ready(target) {
257
367
  if (!target) return;
258
368
  const figureInfo = this.figure.open(target, { nonResizing: this._nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, __fileManagerInfo: false });
259
369
 
@@ -263,14 +373,17 @@ Video.prototype = {
263
373
  this._align = figureInfo.align;
264
374
  target.style.float = '';
265
375
 
266
- this._origin_w = figureInfo.width || figureInfo.originWidth || figureInfo.w || '';
267
- this._origin_h = figureInfo.height || figureInfo.originHeight || figureInfo.h || '';
376
+ this._origin_w = String(figureInfo.width || figureInfo.originWidth || figureInfo.w || '');
377
+ this._origin_h = String(figureInfo.height || figureInfo.originHeight || figureInfo.h || '');
268
378
 
269
379
  let w = figureInfo.width || figureInfo.w || this._origin_w || '';
270
380
  const h = figureInfo.height || figureInfo.h || this._origin_h || '';
271
381
 
272
- if (this.videoUrlFile) this._linkValue = this.previewSrc.textContent = this.videoUrlFile.value = this._element.src || (this._element.querySelector('source') || '').src || '';
273
- (this.modal.form.querySelector('input[name="suneditor_video_radio"][value="' + this._align + '"]') || this.modal.form.querySelector('input[name="suneditor_video_radio"][value="none"]')).checked = true;
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;
274
387
 
275
388
  if (!this._resizing) return;
276
389
 
@@ -279,14 +392,14 @@ Video.prototype = {
279
392
  w = numbers.get(w, 2);
280
393
  if (w > 100) w = 100;
281
394
  }
282
- this.inputX.value = w === 'auto' ? '' : w;
395
+ this.inputX.value = String(w === 'auto' ? '' : w);
283
396
 
284
397
  if (!this._onlyPercentage) {
285
398
  const infoH = percentageRotation ? '' : figureInfo.height;
286
- this.inputY.value = infoH === 'auto' ? '' : infoH;
399
+ this.inputY.value = String(infoH === 'auto' ? '' : infoH);
287
400
  }
288
401
 
289
- if (!this._setVideoRatioSelect(h)) this.inputY.value = this._onlyPercentage ? numbers.get(h, 2) : h;
402
+ if (!this._setRatioSelect(h)) this.inputY.value = String(this._onlyPercentage ? numbers.get(h, 2) : h);
290
403
 
291
404
  this.proportion.checked = true;
292
405
  this.inputX.disabled = percentageRotation ? true : false;
@@ -294,21 +407,24 @@ Video.prototype = {
294
407
  this.proportion.disabled = percentageRotation ? true : false;
295
408
 
296
409
  this._ratio = this.proportion.checked ? figureInfo.ratio : { w: 1, h: 1 };
297
- },
410
+ }
298
411
 
299
412
  /**
300
- * @override fileManager
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>}
301
417
  */
302
- async destroy(element) {
303
- const targetEl = element || this._element;
304
- const container = domUtils.getParentElement(targetEl, Figure.__is) || targetEl;
418
+ async destroy(target) {
419
+ const targetEl = target || this._element;
420
+ const container = dom.query.getParentElement(targetEl, Figure.is) || targetEl;
305
421
  const focusEl = container.previousElementSibling || container.nextElementSibling;
306
422
  const emptyDiv = container.parentNode;
307
423
 
308
- const message = await this.triggerEvent('onVideoDeleteBefore', { target: targetEl, container, align: this._align, url: this._linkValue });
424
+ const message = await this.triggerEvent('onVideoDeleteBefore', { element: targetEl, container, align: this._align, url: this._linkValue });
309
425
  if (message === false) return;
310
426
 
311
- domUtils.removeItem(container);
427
+ dom.utils.removeItem(container);
312
428
  this.init();
313
429
 
314
430
  if (emptyDiv !== this.editor.frameContext.get('wysiwyg')) {
@@ -324,8 +440,28 @@ Video.prototype = {
324
440
  // focus
325
441
  this.editor.focusEdge(focusEl);
326
442
  this.history.push(false);
327
- },
443
+ }
328
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
+ */
329
465
  findProcessUrl(url) {
330
466
  const query = this.query;
331
467
  for (const key in query) {
@@ -340,8 +476,14 @@ Video.prototype = {
340
476
  }
341
477
 
342
478
  return null;
343
- },
479
+ }
344
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
+ */
345
487
  convertUrlYoutube(url) {
346
488
  if (!/^http/.test(url)) url = 'https://' + url;
347
489
  url = url.replace('watch?v=', '');
@@ -349,26 +491,53 @@ Video.prototype = {
349
491
  url = url.replace(url.match(/\/\/.+\//)[0], '//www.youtube.com/embed/').replace('&', '?&');
350
492
  }
351
493
  return url;
352
- },
494
+ }
353
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
+ */
354
502
  convertUrlVimeo(url) {
355
503
  if (url.endsWith('/')) {
356
504
  url = url.slice(0, -1);
357
505
  }
358
506
  url = 'https://player.vimeo.com/video/' + url.slice(url.lastIndexOf('/') + 1);
359
507
  return url;
360
- },
508
+ }
361
509
 
362
- applySize(w, h) {
363
- if (!w) w = this.inputX.value || this.pluginOptions.defaultWidth;
364
- if (!h) h = this.inputY.value || this.pluginOptions.defaultHeight;
365
- if (this._onlyPercentage) {
366
- if (!w) w = '100%';
367
- else if (/%$/.test(w)) w += '%';
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
+ }
368
525
  }
369
- this.figure.setSize(w, h);
370
- },
526
+ return url;
527
+ }
371
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
+ */
372
541
  create(oFrame, src, width, height, align, isUpdate, file) {
373
542
  let cover = null;
374
543
  let container = null;
@@ -381,19 +550,19 @@ Video.prototype = {
381
550
  if (/^iframe$/i.test(processUrl?.tag) && !/^iframe$/i.test(oFrame.nodeName)) {
382
551
  const newTag = this.createIframeTag();
383
552
  newTag.src = src;
384
- oFrame.parentNode.replaceChild(newTag, oFrame);
553
+ oFrame.replaceWith(newTag);
385
554
  this._element = oFrame = newTag;
386
555
  } else if (/^video$/i.test(processUrl?.tag) && !/^video$/i.test(oFrame.nodeName)) {
387
556
  const newTag = this.createVideoTag();
388
557
  newTag.src = src;
389
- oFrame.parentNode.replaceChild(newTag, oFrame);
558
+ oFrame.replaceWith(newTag);
390
559
  this._element = oFrame = newTag;
391
560
  } else {
392
561
  oFrame.src = src;
393
562
  }
394
563
  }
395
564
  container = this._container;
396
- cover = domUtils.getParentElement(oFrame, 'FIGURE');
565
+ cover = dom.query.getParentElement(oFrame, 'FIGURE');
397
566
  } else {
398
567
  /** create */
399
568
  oFrame.src = src;
@@ -410,14 +579,14 @@ Video.prototype = {
410
579
  this.figure.open(oFrame, { nonResizing: this._nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, __fileManagerInfo: true });
411
580
 
412
581
  width = width || this._defaultSizeX;
413
- height = height || this._videoRatio;
582
+ height = height || this._frameRatio;
414
583
  const size = this.figure.getSize(oFrame);
415
584
  const inputUpdate = size.w !== width || size.h !== height;
416
585
  const changeSize = !isUpdate || inputUpdate;
417
586
 
418
587
  // set size
419
588
  if (changeSize) {
420
- this.applySize(width, height);
589
+ this._applySize(width, height);
421
590
  }
422
591
 
423
592
  // align
@@ -425,11 +594,10 @@ Video.prototype = {
425
594
 
426
595
  // select figure
427
596
  // oFrame.onload = OnloadVideo.bind(this, oFrame);
428
-
429
597
  this.fileManager.setFileData(oFrame, file);
430
598
 
431
599
  if (!isUpdate) {
432
- this.component.insert(container, false, true);
600
+ this.component.insert(container, { skipCharCount: false, skipSelection: true, skipHistory: false });
433
601
  if (!this.options.get('componentAutoSelect')) {
434
602
  const line = this.format.addLine(container, null);
435
603
  if (line) this.selection.setRange(line, 0, line, 0);
@@ -439,36 +607,86 @@ Video.prototype = {
439
607
 
440
608
  if (this._resizing && changeSize && this.figure.isVertical) this.figure.setTransform(oFrame, width, height, 0);
441
609
  this.history.push(false);
442
- },
610
+ }
443
611
 
444
- createIframeTag() {
445
- const iframeTag = domUtils.createElement('IFRAME');
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
+ }
446
626
  this._setIframeAttrs(iframeTag);
447
627
  return iframeTag;
448
- },
628
+ }
449
629
 
450
- createVideoTag: function () {
451
- const videoTag = domUtils.createElement('VIDEO');
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
+ }
452
644
  this._setTagAttrs(videoTag);
453
645
  return videoTag;
454
- },
646
+ }
455
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
+ */
456
669
  _getInfo() {
457
670
  return {
458
- inputWidth: this.inputX.value,
459
- inputHeight: this.inputY.value,
671
+ inputWidth: this.inputX?.value || '',
672
+ inputHeight: this.inputY?.value || '',
460
673
  align: this._align,
461
674
  isUpdate: this.modal.isUpdate,
462
675
  element: this._element
463
676
  };
464
- },
677
+ }
465
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
+ */
466
684
  async submitFile(fileList) {
467
685
  if (fileList.length === 0) return;
468
686
 
469
687
  let fileSize = 0;
470
688
  const files = [];
471
- const slngleSizeLimit = this.uploadSingleSizeLimit;
689
+ const slngleSizeLimit = this.pluginOptions.uploadSingleSizeLimit;
472
690
  for (let i = 0, len = fileList.length, f, s; i < len; i++) {
473
691
  f = fileList[i];
474
692
  if (!/video/i.test(f.type)) continue;
@@ -483,7 +701,7 @@ Video.prototype = {
483
701
  file: f
484
702
  });
485
703
 
486
- this.notice.open(message === NO_EVENT ? err : message || err);
704
+ this.ui.alertOpen(message === NO_EVENT ? err : message || err, 'error');
487
705
 
488
706
  return false;
489
707
  }
@@ -498,7 +716,7 @@ Video.prototype = {
498
716
  const err = '[SUNEDITOR.videoUpload.fail] Size of uploadable total videos: ' + limitSize / 1000 + 'KB';
499
717
  const message = await this.triggerEvent('onVideoUploadError', { error: err, limitSize, currentSize, uploadSize: fileSize });
500
718
 
501
- this.notice.open(message === NO_EVENT ? err : message || err);
719
+ this.ui.alertOpen(message === NO_EVENT ? err : message || err, 'error');
502
720
 
503
721
  return false;
504
722
  }
@@ -513,9 +731,11 @@ Video.prototype = {
513
731
  infos = newInfos || infos;
514
732
  this._serverUpload(infos, infos.files);
515
733
  }.bind(this, videoInfo);
734
+ // se-ts-ignore
735
+ this._serverUpload;
516
736
 
517
737
  const result = await this.triggerEvent('onVideoUploadBefore', {
518
- ...videoInfo,
738
+ info: videoInfo,
519
739
  handler
520
740
  });
521
741
 
@@ -524,8 +744,13 @@ Video.prototype = {
524
744
  if (result !== null && typeof result === 'object') handler(result);
525
745
 
526
746
  if (result === true || result === NO_EVENT) handler(null);
527
- },
747
+ }
528
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
+ */
529
754
  async submitURL(url) {
530
755
  if (!url) url = this._linkValue;
531
756
  if (!url) return false;
@@ -551,7 +776,7 @@ Video.prototype = {
551
776
  }.bind(this, videoInfo);
552
777
 
553
778
  const result = await this.triggerEvent('onVideoUploadBefore', {
554
- ...videoInfo,
779
+ info: videoInfo,
555
780
  handler
556
781
  });
557
782
 
@@ -562,87 +787,110 @@ Video.prototype = {
562
787
  if (result === true || result === NO_EVENT) handler(null);
563
788
 
564
789
  return true;
565
- },
790
+ }
566
791
 
792
+ /**
793
+ * @private
794
+ * @description Updates the video component within the editor.
795
+ * @param {HTMLIFrameElement|HTMLVideoElement} oFrame - The video element to update.
796
+ */
567
797
  _update(oFrame) {
568
798
  if (!oFrame) return;
569
799
 
570
800
  if (/^video$/i.test(oFrame.nodeName)) {
571
- this._setTagAttrs(oFrame);
801
+ this._setTagAttrs(/** @type {HTMLVideoElement} */ (oFrame));
572
802
  } else if (/^iframe$/i.test(oFrame.nodeName)) {
573
- this._setIframeAttrs(oFrame);
803
+ this._setIframeAttrs(/** @type {HTMLIFrameElement} */ (oFrame));
574
804
  }
575
805
 
576
- let existElement = this.format.isBlock(oFrame.parentNode) || domUtils.isWysiwygFrame(oFrame.parentNode) ? oFrame : this.format.getLine(oFrame) || oFrame;
806
+ let existElement = this.format.isBlock(oFrame.parentNode) || dom.check.isWysiwygFrame(oFrame.parentNode) ? oFrame : this.format.getLine(oFrame) || oFrame;
577
807
 
578
808
  const prevFrame = oFrame;
579
- oFrame = oFrame.cloneNode(true);
580
- const figure = Figure.CreateContainer(oFrame, 'se-video-container');
809
+ const cloneFrame = /** @type {HTMLIFrameElement|HTMLVideoElement} */ (oFrame.cloneNode(true));
810
+ const figure = Figure.CreateContainer(cloneFrame, 'se-video-container');
581
811
  const container = figure.container;
582
812
 
583
813
  const figcaption = existElement.querySelector('figcaption');
584
814
  let caption = null;
585
815
  if (figcaption) {
586
- caption = domUtils.createElement('DIV');
816
+ caption = dom.utils.createElement('DIV');
587
817
  caption.innerHTML = figcaption.innerHTML;
588
- domUtils.removeItem(figcaption);
818
+ dom.utils.removeItem(figcaption);
589
819
  }
590
820
 
591
821
  // size
592
- this.figure.open(oFrame, { nonResizing: this._nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, __fileManagerInfo: true });
593
- const size = (oFrame.getAttribute('data-se-size') || ',').split(',');
594
- this.applySize(size[0] || prevFrame.style.width || prevFrame.width || '', size[1] || prevFrame.style.height || prevFrame.height || '');
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 || '');
595
825
 
596
826
  // align
597
827
  const format = this.format.getLine(prevFrame);
598
828
  if (format) this._align = format.style.textAlign || format.style.float;
599
- this.figure.setAlign(oFrame, this._align);
829
+ this.figure.setAlign(cloneFrame, this._align);
600
830
 
601
- if (domUtils.getParentElement(prevFrame, domUtils.isExcludeFormat)) {
602
- prevFrame.parentNode.replaceChild(container, prevFrame);
603
- } else if (domUtils.isListCell(existElement)) {
604
- const refer = domUtils.getParentElement(prevFrame, (current) => current.parentNode === existElement);
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);
605
835
  existElement.insertBefore(container, refer);
606
- domUtils.removeItem(prevFrame);
836
+ dom.utils.removeItem(prevFrame);
607
837
  this.nodeTransform.removeEmptyNode(refer, null, true);
608
- } else if (this.format.isLineOnly(existElement)) {
609
- const refer = domUtils.getParentElement(prevFrame, (current) => current.parentNode === existElement);
838
+ } else if (this.format.isLine(existElement)) {
839
+ const refer = dom.query.getParentElement(prevFrame, (current) => current.parentNode === existElement);
610
840
  existElement = this.nodeTransform.split(existElement, refer);
611
841
  existElement.parentNode.insertBefore(container, existElement);
612
- domUtils.removeItem(prevFrame);
842
+ dom.utils.removeItem(prevFrame);
613
843
  this.nodeTransform.removeEmptyNode(existElement, null, true);
614
844
  } else {
615
- existElement.parentNode.replaceChild(container, existElement);
845
+ /** @type {Element} */ (existElement).replaceWith(container);
616
846
  }
617
847
 
618
848
  if (caption) existElement.parentNode.insertBefore(caption, container.nextElementSibling);
619
849
 
620
- return oFrame;
621
- },
850
+ return cloneFrame;
851
+ }
622
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
+ */
623
859
  _register(info, response) {
624
860
  const fileList = response.result;
625
861
  const videoTag = this.createVideoTag();
626
862
 
627
863
  for (let i = 0, len = fileList.length; i < len; i++) {
628
- this.create(info.isUpdate ? info.element : videoTag.cloneNode(false), fileList[i].url, info.inputWidth, info.inputHeight, info.align, info.isUpdate, {
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, {
629
866
  name: fileList[i].name,
630
867
  size: fileList[i].size
631
868
  });
632
869
  }
633
- },
870
+ }
634
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
+ */
635
878
  _serverUpload(info, files) {
636
879
  if (!files) return;
637
880
 
638
881
  const videoUploadUrl = this.pluginOptions.uploadUrl;
639
882
  if (typeof videoUploadUrl === 'string' && videoUploadUrl.length > 0) {
640
- this.fileManager.upload(videoUploadUrl, this.pluginOptions.uploadHeaders, files, UploadCallBack.bind(this, info), this._error.bind(this));
883
+ this.fileManager.upload(videoUploadUrl, this.pluginOptions.uploadHeaders, files, this.#UploadCallBack.bind(this, info), this._error.bind(this));
641
884
  }
642
- },
885
+ }
643
886
 
887
+ /**
888
+ * @private
889
+ * @description Sets attributes for the video tag.
890
+ * @param {HTMLVideoElement} element - The video element.
891
+ */
644
892
  _setTagAttrs(element) {
645
- element.setAttribute('controls', true);
893
+ element.setAttribute('controls', 'true');
646
894
 
647
895
  const attrs = this.pluginOptions.videoTagAttributes;
648
896
  if (!attrs) return;
@@ -650,8 +898,13 @@ Video.prototype = {
650
898
  for (const key in attrs) {
651
899
  element.setAttribute(key, attrs[key]);
652
900
  }
653
- },
901
+ }
654
902
 
903
+ /**
904
+ * @private
905
+ * @description Sets attributes for the iframe tag.
906
+ * @param {HTMLIFrameElement} element - The iframe element.
907
+ */
655
908
  _setIframeAttrs(element) {
656
909
  element.frameBorder = '0';
657
910
  element.allowFullscreen = true;
@@ -662,130 +915,206 @@ Video.prototype = {
662
915
  for (const key in attrs) {
663
916
  element.setAttribute(key, attrs[key]);
664
917
  }
665
- },
918
+ }
666
919
 
667
- _setVideoRatioSelect(value) {
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) {
668
927
  let ratioSelected = false;
669
- const ratioOption = this.videoRatioOption.options;
928
+ const ratioOption = this.frameRatioOption.options;
670
929
 
671
- if (/%$/.test(value) || this._onlyPercentage) value = numbers.get(value, 2) / 100 + '';
672
- else if (!numbers.is(value) || value * 1 >= 1) value = '';
930
+ if (/%$/.test(value + '') || this._onlyPercentage) value = numbers.get(value, 2) / 100 + '';
931
+ else if (!numbers.is(value) || Number(value) >= 1) value = '';
673
932
 
674
933
  this.inputY.placeholder = '';
675
934
  for (let i = 0, len = ratioOption.length; i < len; i++) {
676
935
  if (ratioOption[i].value === value) {
677
936
  ratioSelected = ratioOption[i].selected = true;
678
- this.inputY.placeholder = !value ? '' : value * 100 + '%';
937
+ this.inputY.placeholder = !value ? '' : Number(value) * 100 + '%';
679
938
  } else ratioOption[i].selected = false;
680
939
  }
681
940
 
682
941
  return ratioSelected;
683
- },
942
+ }
684
943
 
944
+ /**
945
+ * @private
946
+ * @description Handles video upload errors.
947
+ * @param {Object<string, *>} response - The error response object.
948
+ * @returns {Promise<void>}
949
+ */
685
950
  async _error(response) {
686
951
  const message = await this.triggerEvent('onVideoUploadError', { error: response });
687
952
  const err = message === NO_EVENT ? response.errorMessage : message || response.errorMessage;
688
- this.notice.open(err);
953
+ this.ui.alertOpen(err, 'error');
689
954
  console.error('[SUNEDITOR.plugin.video.error]', message);
690
- },
691
-
692
- constructor: Video
693
- };
955
+ }
694
956
 
695
- async function UploadCallBack(info, xmlHttp) {
696
- if ((await this.triggerEvent('videoUploadHandler', { xmlHttp, info })) === NO_EVENT) {
697
- const response = JSON.parse(xmlHttp.responseText);
698
- if (response.errorMessage) {
699
- this._error(response);
700
- } else {
701
- this._register(info, response);
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
+ }
702
970
  }
703
971
  }
704
- }
705
972
 
706
- function RemoveSelectedFiles() {
707
- this.videoInputFile.value = '';
708
- if (this.videoUrlFile) {
709
- this.videoUrlFile.removeAttribute('disabled');
710
- this.previewSrc.style.textDecoration = '';
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, []);
711
985
  }
712
986
 
713
- // inputFile check
714
- Modal.OnChangeFile(this.fileModalWrapper, []);
715
- }
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
+ }
716
1008
 
717
- function OnLinkPreview(e) {
718
- const value = e.target.value.trim();
719
- if (/^<iframe.*\/iframe>$/.test(value)) {
720
- this._linkValue = value;
721
- this.previewSrc.textContent = '<IFrame :src=".."></IFrame>';
722
- } else {
723
- this._linkValue = this.previewSrc.textContent = !value
724
- ? ''
725
- : this.options.get('defaultUrlProtocol') && !value.includes('://') && value.indexOf('#') !== 0
726
- ? this.options.get('defaultUrlProtocol') + value
727
- : !value.includes('://')
728
- ? '/' + value
729
- : value;
1009
+ /**
1010
+ * @description Opens the video gallery.
1011
+ */
1012
+ #OpenGallery() {
1013
+ this.plugins.videoGallery.open(this.#SetUrlInput.bind(this));
730
1014
  }
731
- }
732
1015
 
733
- function OnfileInputChange({ target }) {
734
- if (!this.videoInputFile.value) {
735
- this.videoUrlFile.removeAttribute('disabled');
736
- this.previewSrc.style.textDecoration = '';
737
- } else {
738
- this.videoUrlFile.setAttribute('disabled', true);
739
- this.previewSrc.style.textDecoration = 'line-through';
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();
740
1023
  }
741
1024
 
742
- // inputFile check
743
- Modal.OnChangeFile(this.fileModalWrapper, target.files);
744
- }
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
+ }
745
1036
 
746
- function OnClickRevert() {
747
- if (this._onlyPercentage) {
748
- this.inputX.value = this._origin_w > 100 ? 100 : this._origin_w;
749
- } else {
750
- this.inputX.value = this._origin_w;
751
- this.inputY.value = this._origin_h;
1037
+ // inputFile check
1038
+ /** @type {HTMLInputElement} */
1039
+ const eventTarget = dom.query.getEventTarget(e);
1040
+ Modal.OnChangeFile(this.fileModalWrapper, eventTarget.files);
752
1041
  }
753
- }
754
1042
 
755
- function SetVideoRatio(e) {
756
- const value = e.target.options[e.target.selectedIndex].value;
757
- this._defaultSizeY = this.figure.autoRatio.current = this._videoRatio = !value ? this._defaultSizeY : value * 100 + '%';
758
- this.inputY.placeholder = !value ? '' : value * 100 + '%';
759
- this.inputY.value = '';
760
- }
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
+ }
761
1051
 
762
- function OnChangeRatio() {
763
- this._ratio = this.proportion.checked ? Figure.GetRatio(this.inputX.value, this.inputY.value, this.sizeUnit) : { w: 1, h: 1 };
764
- }
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
+ }
765
1063
 
766
- function OnInputSize(xy, e) {
767
- if (e.keyCode === 32) {
768
- e.preventDefault();
769
- return;
1064
+ #OnChangeRatio() {
1065
+ this._ratio = this.proportion.checked ? Figure.GetRatio(this.inputX.value, this.inputY.value, this.sizeUnit) : { w: 1, h: 1 };
770
1066
  }
771
1067
 
772
- if (xy === 'x' && this._onlyPercentage && e.target.value > 100) {
773
- e.target.value = 100;
774
- } else if (this.proportion.checked) {
775
- const ratioSize = Figure.CalcRatio(this.inputX.value, this.inputY.value, this.sizeUnit, this._ratio);
776
- if (xy === 'x') {
777
- this.inputY.value = ratioSize.h;
778
- } else {
779
- this.inputX.value = ratioSize.w;
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
+ }
780
1089
  }
781
- }
782
1090
 
783
- if (xy === 'y') {
784
- this._setVideoRatioSelect(e.target.value || this._defaultRatio);
1091
+ if (xy === 'y') {
1092
+ this._setRatioSelect(eventTarget.value || this._defaultRatio);
1093
+ }
785
1094
  }
786
1095
  }
787
1096
 
788
- function CreateHTML_modal({ lang, icons }, pluginOptions) {
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) {
789
1118
  let html = /*html*/ `
790
1119
  <form method="post" enctype="multipart/form-data">
791
1120
  <div class="se-modal-header">
@@ -808,7 +1137,17 @@ function CreateHTML_modal({ lang, icons }, pluginOptions) {
808
1137
  html += /*html*/ `
809
1138
  <div class="se-modal-form">
810
1139
  <label>${lang.video_modal_url}</label>
811
- <input class="se-input-form se-input-url" type="text" data-focus />
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>
812
1151
  <pre class="se-link-preview"></pre>
813
1152
  </div>`;
814
1153
  }
@@ -817,7 +1156,8 @@ function CreateHTML_modal({ lang, icons }, pluginOptions) {
817
1156
  const ratioList = pluginOptions.ratioOptions || [
818
1157
  { name: '16:9', value: 0.5625 },
819
1158
  { name: '4:3', value: 0.75 },
820
- { name: '21:9', value: 0.4285 }
1159
+ { name: '21:9', value: 0.4285 },
1160
+ { name: '9:16', value: 1.78 }
821
1161
  ];
822
1162
  const ratio = pluginOptions.defaultRatio;
823
1163
  const onlyPercentage = pluginOptions.percentageOnlySize;
@@ -833,11 +1173,11 @@ function CreateHTML_modal({ lang, icons }, pluginOptions) {
833
1173
  <label class="size-h"${heightDisplay}>${lang.height}</label>
834
1174
  <label class="size-h"${ratioDisplay}>(${lang.ratio})</label>
835
1175
  </div>
836
- <input class="se-input-control _se_video_size_x" placeholder="100%"${onlyPercentage ? ' type="number" min="1"' : 'type="text"'}${onlyPercentage ? ' max="100"' : ''}/>
1176
+ <input class="se-input-control _se_size_x" placeholder="100%"${onlyPercentage ? ' type="number" min="1"' : 'type="text"'}${onlyPercentage ? ' max="100"' : ''}/>
837
1177
  <label class="se-modal-size-x"${onlyWidthDisplay}>${onlyPercentage ? '%' : 'x'}</label>
838
- <input class="se-input-control _se_video_size_y" placeholder="${pluginOptions.defaultRatio * 100}%"
1178
+ <input class="se-input-control _se_size_y" placeholder="${pluginOptions.defaultRatio * 100}%"
839
1179
  ${onlyPercentage ? ' type="number" min="1"' : 'type="text"'}${onlyPercentage ? ' max="100"' : ''}${heightDisplay}/>
840
- <select class="se-input-select se-video-ratio" title="${lang.ratio}" aria-label="${lang.ratio}"${ratioDisplay}>
1180
+ <select class="se-input-select se-modal-ratio" title="${lang.ratio}" aria-label="${lang.ratio}"${ratioDisplay}>
841
1181
  ${!heightDisplay ? '<option value=""> - </option>' : ''}
842
1182
  ${ratioList.map((ratioOption) => `<option value="${ratioOption.value}"${ratio.toString() === ratioOption.value.toString() ? ' selected' : ''}>${ratioOption.name}</option>`).join('')}
843
1183
  </select>
@@ -845,7 +1185,7 @@ function CreateHTML_modal({ lang, icons }, pluginOptions) {
845
1185
  </div>
846
1186
  <div class="se-modal-form se-modal-form-footer"${onlyPercentDisplay}${onlyWidthDisplay}>
847
1187
  <label>
848
- <input type="checkbox" class="se-modal-btn-check _se_video_check_proportion" />&nbsp;
1188
+ <input type="checkbox" class="se-modal-btn-check _se_check_proportion" />&nbsp;
849
1189
  <span>${lang.proportion}</span>
850
1190
  </label>
851
1191
  </div>`;
@@ -864,7 +1204,23 @@ function CreateHTML_modal({ lang, icons }, pluginOptions) {
864
1204
  </div>
865
1205
  </form>`;
866
1206
 
867
- return domUtils.createElement('DIV', { class: 'se-modal-content' }, html);
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
+ };
868
1224
  }
869
1225
 
870
1226
  export default Video;