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